1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Merge branch 'master' into pr/259

This commit is contained in:
nkzawa 2016-12-02 10:35:07 +09:00
commit 6ad1e23167
26 changed files with 426 additions and 380 deletions

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Zeit, Inc. developers Copyright (c) 2016 Zeit, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -190,13 +190,17 @@ import React from 'react'
export default class Error extends React.Component { export default class Error extends React.Component {
static getInitialProps ({ res, xhr }) { static getInitialProps ({ res, xhr }) {
const statusCode = res ? res.statusCode : xhr.status const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
return { statusCode } return { statusCode }
} }
render () { render () {
return ( return (
<p>An error { this.props.statusCode } occurred</p> <p>{
this.props.statusCode
? `An error ${this.props.statusCode} occurred on server`
: 'An error occurred on client'
]</p>
) )
} }
} }

View file

@ -1,7 +1,6 @@
import 'react-hot-loader/patch' import 'react-hot-loader/patch'
import './webpack-dev-client?http://localhost:3030'
import * as next from './next' import * as next from './next'
import { requireModule } from '../lib/eval-script'
module.exports = next
window.next = next window.next = next
module.exports = requireModule

View file

@ -4,19 +4,33 @@ import HeadManager from './head-manager'
import { rehydrate } from '../lib/css' import { rehydrate } from '../lib/css'
import Router from '../lib/router' import Router from '../lib/router'
import App from '../lib/app' import App from '../lib/app'
import evalScript from '../lib/eval-script' import evalScript, { requireModule } from '../lib/eval-script'
const { const {
__NEXT_DATA__: { component, props, ids, err } __NEXT_DATA__: { component, errorComponent, props, ids, err }
} = window } = window
const Component = evalScript(component).default document.addEventListener('DOMContentLoaded', () => {
const Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default
export const router = new Router(window.location.href, { Component, ctx: { err } }) const router = new Router(window.location.href, {
Component,
ErrorComponent,
ctx: { err }
})
const headManager = new HeadManager() // This it to support error handling in the dev time with hot code reload.
const container = document.getElementById('__next') if (window.next) {
const appProps = { Component, props, router, headManager } window.next.router = router
}
rehydrate(ids) const headManager = new HeadManager()
render(createElement(App, appProps), container) const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager }
rehydrate(ids)
render(createElement(App, appProps), container)
})
module.exports = requireModule

View file

@ -1,167 +0,0 @@
/* global __resourceQuery, next */
// Based on 'webpack-dev-server/client'
import url from 'url'
import stripAnsi from 'strip-ansi'
import socket from './socket'
function getCurrentScriptSource () {
// `document.currentScript` is the most accurate way to find the current script,
// but is not supported in all browsers.
if (document.currentScript) {
return document.currentScript.getAttribute('src')
}
// Fall back to getting all scripts in the document.
const scriptElements = document.scripts || []
const currentScript = scriptElements[scriptElements.length - 1]
if (currentScript) {
return currentScript.getAttribute('src')
}
// Fail as there was no script to use.
throw new Error('[WDS] Failed to get current script source')
}
let urlParts
if (typeof __resourceQuery === 'string' && __resourceQuery) {
// If this bundle is inlined, use the resource query to get the correct url.
urlParts = url.parse(__resourceQuery.substr(1))
} else {
// Else, get the url from the <script> this file was called with.
let scriptHost = getCurrentScriptSource()
scriptHost = scriptHost.replace(/\/[^\/]+$/, '')
urlParts = url.parse(scriptHost || '/', false, true)
}
let hot = false
let initial = true
let currentHash = ''
let logLevel = 'info'
function log (level, msg) {
if (logLevel === 'info' && level === 'info') {
return console.log(msg)
}
if (['info', 'warning'].indexOf(logLevel) >= 0 && level === 'warning') {
return console.warn(msg)
}
if (['info', 'warning', 'error'].indexOf(logLevel) >= 0 && level === 'error') {
return console.error(msg)
}
}
const onSocketMsg = {
hot () {
hot = true
log('info', '[WDS] Hot Module Replacement enabled.')
},
invalid () {
log('info', '[WDS] App updated. Recompiling...')
},
hash (hash) {
currentHash = hash
},
'still-ok': () => {
log('info', '[WDS] Nothing changed.')
},
'log-level': (level) => {
logLevel = level
},
ok () {
if (initial) {
initial = false
return
}
reloadApp()
},
warnings (warnings) {
log('info', '[WDS] Warnings while compiling.')
for (let i = 0; i < warnings.length; i++) {
console.warn(stripAnsi(warnings[i]))
}
if (initial) {
initial = false
return
}
reloadApp()
},
errors (errors) {
log('info', '[WDS] Errors while compiling.')
for (let i = 0; i < errors.length; i++) {
console.error(stripAnsi(errors[i]))
}
if (initial) {
initial = false
return
}
reloadApp()
},
'proxy-error': (errors) => {
log('info', '[WDS] Proxy error.')
for (let i = 0; i < errors.length; i++) {
log('error', stripAnsi(errors[i]))
}
if (initial) {
initial = false
return
}
},
reload (route) {
if (route === '/_error') {
for (const r of Object.keys(next.router.components)) {
const { Component } = next.router.components[r]
if (Component.__route === '/_error-debug') {
// reload all '/_error-debug'
// which are expected to be errors of '/_error' routes
next.router.reload(r)
}
}
return
}
next.router.reload(route)
},
close () {
log('error', '[WDS] Disconnected!')
}
}
let hostname = urlParts.hostname
let protocol = urlParts.protocol
if (urlParts.hostname === '0.0.0.0') {
// why do we need this check?
// hostname n/a for file protocol (example, when using electron, ionic)
// see: https://github.com/webpack/webpack-dev-server/pull/384
if (window.location.hostname && !!~window.location.protocol.indexOf('http')) {
hostname = window.location.hostname
}
}
// `hostname` can be empty when the script path is relative. In that case, specifying
// a protocol would result in an invalid URL.
// When https is used in the app, secure websockets are always necessary
// because the browser doesn't accept non-secure websockets.
if (hostname && (window.location.protocol === 'https:' || urlParts.hostname === '0.0.0.0')) {
protocol = window.location.protocol
}
const socketUrl = url.format({
protocol,
auth: urlParts.auth,
hostname,
port: (urlParts.port === '0') ? window.location.port : urlParts.port,
pathname: urlParts.path == null || urlParts.path === '/' ? '/sockjs-node' : urlParts.path
})
socket(socketUrl, onSocketMsg)
function reloadApp () {
if (hot) {
log('info', '[WDS] App hot update...')
window.postMessage('webpackHotUpdate' + currentHash, '*')
} else {
log('info', '[WDS] App updated. Reloading...')
window.location.reload()
}
}

View file

@ -1,39 +0,0 @@
import SockJS from 'sockjs-client'
let retries = 0
let sock = null
export default function socket (url, handlers) {
sock = new SockJS(url)
sock.onopen = () => {
retries = 0
}
sock.onclose = () => {
if (retries === 0) handlers.close()
// Try to reconnect.
sock = null
// After 10 retries stop trying, to prevent logspam.
if (retries <= 10) {
// Exponentially increase timeout to reconnect.
// Respectfully copied from the package `got`.
const retryInMs = 1000 * Math.pow(2, retries) + Math.random() * 100
retries += 1
setTimeout(() => {
socket(url, handlers)
}, retryInMs)
}
}
sock.onmessage = (e) => {
// This assumes that all data sent via the websocket is JSON.
const msg = JSON.parse(e.data)
if (handlers[msg.type]) {
handlers[msg.type](msg.data)
}
}
}

View file

@ -0,0 +1,37 @@
/* global next */
import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true'
const handlers = {
reload (route) {
if (route === '/_error') {
for (const r of Object.keys(next.router.components)) {
const { Component } = next.router.components[r]
if (Component.__route === '/_error-debug') {
// reload all '/_error-debug'
// which are expected to be errors of '/_error' routes
next.router.reload(r)
}
}
return
}
next.router.reload(route)
},
change (route) {
const { Component } = next.router.components[route] || {}
if (Component && Component.__route === '/_error-debug') {
// reload to recover from runtime errors
next.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)
}
})

View file

@ -0,0 +1,13 @@
# Example app using shared modules
This example features:
* An app with two pages which has a common Counter component
* That Counter component maintain the counter inside its module.
## How to run it
```sh
npm install
npm run dev
```

View file

@ -0,0 +1,19 @@
import React from 'react'
let count = 0
export default class Counter extends React.Component {
add () {
count += 1
this.forceUpdate()
}
render () {
return (
<div>
<p>Count is: {count}</p>
<button onClick={() => this.add()}>Add</button>
</div>
)
}
}

View file

@ -0,0 +1,20 @@
import React from 'react'
import Link from 'next/link'
const styles = {
a: {
marginRight: 10
}
}
export default () => (
<div>
<Link href='/'>
<a style={styles.a} >Home</a>
</Link>
<Link href='/about'>
<a style={styles.a} >About</a>
</Link>
</div>
)

View file

@ -0,0 +1,19 @@
{
"name": "shared-modules",
"version": "1.0.0",
"description": "This example features:",
"main": "index.js",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "*"
},
"author": "",
"license": "ISC",
"next": {
"cdn": false
}
}

View file

@ -0,0 +1,11 @@
import React from 'react'
import Header from '../components/Header'
import Counter from '../components/Counter'
export default () => (
<div>
<Header />
<p>This is the about page.</p>
<Counter />
</div>
)

View file

@ -0,0 +1,11 @@
import React from 'react'
import Header from '../components/Header'
import Counter from '../components/Counter'
export default () => (
<div>
<Header />
<p>HOME PAGE is here!</p>
<Counter />
</div>
)

View file

@ -81,7 +81,7 @@ gulp.task('build-dev-client', ['compile-lib', 'compile-client'], () => {
.src('dist/client/next-dev.js') .src('dist/client/next-dev.js')
.pipe(webpack({ .pipe(webpack({
quiet: true, quiet: true,
output: { filename: 'next-dev.bundle.js' }, output: { filename: 'next-dev.bundle.js', libraryTarget: 'var', library: 'require' },
module: { module: {
loaders: [ loaders: [
{ {
@ -106,7 +106,7 @@ gulp.task('build-client', ['compile-lib', 'compile-client'], () => {
.src('dist/client/next.js') .src('dist/client/next.js')
.pipe(webpack({ .pipe(webpack({
quiet: true, quiet: true,
output: { filename: 'next.bundle.js' }, output: { filename: 'next.bundle.js', libraryTarget: 'var', library: 'require' },
plugins: [ plugins: [
new webpack.webpack.DefinePlugin({ new webpack.webpack.DefinePlugin({
'process.env': { 'process.env': {

View file

@ -12,8 +12,11 @@ export default ({ head, css, html, data, dev, staticMarkup, cdn }) => {
</head> </head>
<body> <body>
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} /> <div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
{staticMarkup ? null : <script dangerouslySetInnerHTML={{ __html: '__NEXT_DATA__ = ' + htmlescape(data) }} />} {staticMarkup ? null : <script dangerouslySetInnerHTML={{
__html: `__NEXT_DATA__ =${htmlescape(data)}; module={};`
}} />}
{staticMarkup ? null : createClientScript({ dev, cdn })} {staticMarkup ? null : createClientScript({ dev, cdn })}
<script type='text/javascript' src='/_next/commons.js' />
</body> </body>
</html> </html>
} }
@ -32,7 +35,6 @@ function createClientScript ({ dev, cdn }) {
load('https://cdn.zeit.co/next.js/${pkg.version}/next.min.js', function (err) { load('https://cdn.zeit.co/next.js/${pkg.version}/next.min.js', function (err) {
if (err) load('/_next/next.bundle.js') if (err) load('/_next/next.bundle.js')
}) })
function load (src, fn) { function load (src, fn) {
fn = fn || function () {} fn = fn || function () {}
var script = document.createElement('script') var script = document.createElement('script')

View file

@ -14,6 +14,17 @@ const modules = new Map([
['next/head', Head] ['next/head', Head]
]) ])
function require (moduleName) {
const name = moduleName
const m = modules.get(name)
if (m) return m
throw new Error(`Module "${moduleName}" is not exists in the bundle`)
}
export const requireModule = require
/** /**
* IMPORTANT: This module is compiled *without* `use strict` * IMPORTANT: This module is compiled *without* `use strict`
* so that when we `eval` a dependency below, we don't enforce * so that when we `eval` a dependency below, we don't enforce
@ -28,10 +39,6 @@ const modules = new Map([
export default function evalScript (script) { export default function evalScript (script) {
const module = { exports: {} } const module = { exports: {} }
const require = function (path) { // eslint-disable-line no-unused-vars
return modules.get(path)
}
// don't use Function() here since it changes source locations
eval(script) // eslint-disable-line no-eval eval(script) // eslint-disable-line no-eval
return module.exports return module.exports
} }

View file

@ -3,15 +3,16 @@ import evalScript from './eval-script'
import shallowEquals from './shallow-equals' import shallowEquals from './shallow-equals'
export default class Router { export default class Router {
constructor (url, initialData) { constructor (url, { Component, ErrorComponent, ctx } = {}) {
const parsed = parse(url, true) const parsed = parse(url, true)
// represents the current component key // represents the current component key
this.route = toRoute(parsed.pathname) this.route = toRoute(parsed.pathname)
// set up the component cache (by route keys) // set up the component cache (by route keys)
this.components = { [this.route]: initialData } this.components = { [this.route]: { Component, ctx } }
this.ErrorComponent = ErrorComponent
this.pathname = parsed.pathname this.pathname = parsed.pathname
this.query = parsed.query this.query = parsed.query
this.subscriptions = new Set() this.subscriptions = new Set()
@ -38,13 +39,19 @@ export default class Router {
this.route = route this.route = route
this.set(getURL(), { ...data, props }) this.set(getURL(), { ...data, props })
}) })
.catch((err) => { .catch(async (err) => {
if (err.cancelled) return if (err.cancelled) return
// the only way we can appropriately handle const data = { Component: this.ErrorComponent, ctx: { err } }
// this failure is deferring to the browser const ctx = { ...data.ctx, pathname, query }
// since the URL has already changed const props = await this.getInitialProps(data.Component, ctx)
window.location.reload()
this.route = route
this.set(getURL(), { ...data, props })
console.error(err)
})
.catch((err) => {
console.error(err)
}) })
} }
@ -67,16 +74,25 @@ export default class Router {
let data let data
let props let props
let _err
try { try {
data = await this.fetchComponent(route) data = await this.fetchComponent(route)
const ctx = { ...data.ctx, pathname, query } const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx) props = await this.getInitialProps(data.Component, ctx)
} catch (err) { } catch (err) {
if (err.cancelled) return false if (err.cancelled) return false
throw err
data = { Component: this.ErrorComponent, ctx: { err } }
const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx)
_err = err
console.error(err)
} }
this.notify({ ...data, props }) this.notify({ ...data, props })
if (_err) throw _err
} }
back () { back () {
@ -100,13 +116,20 @@ export default class Router {
let data let data
let props let props
let _err
try { try {
data = await this.fetchComponent(route) data = await this.fetchComponent(route)
const ctx = { ...data.ctx, pathname, query } const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx) props = await this.getInitialProps(data.Component, ctx)
} catch (err) { } catch (err) {
if (err.cancelled) return false if (err.cancelled) return false
throw err
data = { Component: this.ErrorComponent, ctx: { err } }
const ctx = { ...data.ctx, pathname, query }
props = await this.getInitialProps(data.Component, ctx)
_err = err
console.error(err)
} }
if (getURL() !== url) { if (getURL() !== url) {
@ -115,6 +138,9 @@ export default class Router {
this.route = route this.route = route
this.set(url, { ...data, props }) this.set(url, { ...data, props })
if (_err) throw _err
return true return true
} }

View file

@ -55,7 +55,8 @@
"babel-core": "6.18.2", "babel-core": "6.18.2",
"babel-generator": "6.19.0", "babel-generator": "6.19.0",
"babel-loader": "6.2.8", "babel-loader": "6.2.8",
"babel-plugin-module-resolver": "2.3.0", "babel-plugin-module-resolver": "2.4.0",
"babel-plugin-react-require": "^3.0.0",
"babel-plugin-transform-async-to-generator": "6.16.0", "babel-plugin-transform-async-to-generator": "6.16.0",
"babel-plugin-transform-class-properties": "6.19.0", "babel-plugin-transform-class-properties": "6.19.0",
"babel-plugin-transform-object-rest-spread": "6.19.0", "babel-plugin-transform-object-rest-spread": "6.19.0",
@ -65,30 +66,33 @@
"babel-runtime": "6.18.0", "babel-runtime": "6.18.0",
"cross-spawn": "5.0.1", "cross-spawn": "5.0.1",
"del": "2.2.2", "del": "2.2.2",
"glamor": "2.18.2", "friendly-errors-webpack-plugin": "1.1.0",
"glamor": "2.20.8",
"glob-promise": "2.0.0", "glob-promise": "2.0.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"is-windows-bash": "1.0.2",
"json-loader": "0.5.4", "json-loader": "0.5.4",
"loader-utils": "0.2.16", "loader-utils": "0.2.16",
"minimist": "1.2.0", "minimist": "1.2.0",
"mkdirp-then": "1.2.0", "mkdirp-then": "1.2.0",
"mz": "2.5.0", "mz": "2.6.0",
"path-match": "1.2.4", "path-match": "1.2.4",
"react": "15.4.0", "react": "15.4.1",
"react-dom": "15.4.0", "react-dom": "15.4.1",
"react-hot-loader": "3.0.0-beta.6", "react-hot-loader": "3.0.0-beta.6",
"read-pkg-up": "2.0.0", "read-pkg-up": "2.0.0",
"send": "0.14.1", "send": "0.14.1",
"sockjs-client": "1.1.1",
"strip-ansi": "3.0.1", "strip-ansi": "3.0.1",
"url": "0.11.0", "url": "0.11.0",
"webpack": "1.13.3", "webpack": "1.13.3",
"webpack-dev-server": "1.16.2", "webpack-dev-middleware": "1.8.4",
"webpack-hot-middleware": "2.13.2",
"write-file-webpack-plugin": "3.4.2" "write-file-webpack-plugin": "3.4.2"
}, },
"devDependencies": { "devDependencies": {
"ava": "0.17.0", "ava": "0.17.0",
"babel-eslint": "7.1.1", "babel-eslint": "7.1.1",
"babel-plugin-istanbul": "3.0.0",
"babel-plugin-transform-remove-strict-mode": "0.0.2", "babel-plugin-transform-remove-strict-mode": "0.0.2",
"benchmark": "2.1.2", "benchmark": "2.1.2",
"coveralls": "2.11.15", "coveralls": "2.11.15",
@ -103,7 +107,7 @@
"husky": "0.11.9", "husky": "0.11.9",
"nyc": "10.0.0", "nyc": "10.0.0",
"run-sequence": "1.2.2", "run-sequence": "1.2.2",
"standard": "8.5.0", "standard": "8.6.0",
"webpack-stream": "3.2.0" "webpack-stream": "3.2.0"
} }
} }

View file

@ -5,12 +5,12 @@ import style from 'next/css'
export default class ErrorDebug extends React.Component { export default class ErrorDebug extends React.Component {
static getInitialProps ({ err }) { static getInitialProps ({ err }) {
const { message, module } = err const { name, message, stack, module } = err
return { message, path: module.rawRequest } return { name, message, stack, path: module ? module.rawRequest : null }
} }
render () { render () {
const { message, path } = this.props const { name, message, stack, path } = this.props
return <div className={styles.errorDebug}> return <div className={styles.errorDebug}>
<Head> <Head>
@ -21,8 +21,12 @@ export default class ErrorDebug extends React.Component {
} }
`}} /> `}} />
</Head> </Head>
<div className={styles.heading}>Error in {path}</div> {path ? <div className={styles.heading}>Error in {path}</div> : null}
<pre className={styles.message} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} /> {
name === 'ModuleBuildError'
? <pre className={styles.message} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
: <pre className={styles.message}>{stack}</pre>
}
</div> </div>
} }
} }

View file

@ -1,19 +1,21 @@
import React from 'react' import React from 'react'
import style, { merge } from 'next/css' import style from 'next/css'
export default class Error extends React.Component { export default class Error extends React.Component {
static getInitialProps ({ res, xhr }) { static getInitialProps ({ res, xhr }) {
const statusCode = res ? res.statusCode : xhr.status const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
return { statusCode } return { statusCode }
} }
render () { render () {
const { statusCode } = this.props const { statusCode } = this.props
const title = statusCode === 404 ? 'This page could not be found' : 'Internal Server Error' const title = statusCode === 404
? 'This page could not be found'
: (statusCode ? 'Internal Server Error' : 'An unexpected error has occurred')
return <div className={merge(styles.error, styles['error_' + statusCode])}> return <div className={styles.error}>
<div className={styles.text}> <div className={styles.text}>
<h1 className={styles.h1}>{statusCode}</h1> {statusCode ? <h1 className={styles.h1}>{statusCode}</h1> : null}
<div className={styles.desc}> <div className={styles.desc}>
<h2 className={styles.h2}>{title}.</h2> <h2 className={styles.h2}>{title}.</h2>
</div> </div>

View file

@ -35,7 +35,7 @@ export default class WatchPagesPlugin {
if (compiler.hasEntry(name)) return if (compiler.hasEntry(name)) return
const entries = ['webpack/hot/dev-server', f] const entries = ['next/dist/client/webpack-hot-middleware-client', f]
compiler.addEntry(entries, name) compiler.addEntry(entries, name)
}) })
@ -47,7 +47,7 @@ export default class WatchPagesPlugin {
if (name === errorPageName) { if (name === errorPageName) {
compiler.addEntry([ compiler.addEntry([
'webpack/hot/dev-server', 'next/dist/client/webpack-hot-middleware-client',
join(__dirname, '..', '..', '..', 'pages', '_error.js') join(__dirname, '..', '..', '..', 'pages', '_error.js')
], name) ], name)
} }

View file

@ -7,6 +7,7 @@ import WatchPagesPlugin from './plugins/watch-pages-plugin'
import WatchRemoveEventPlugin from './plugins/watch-remove-event-plugin' import WatchRemoveEventPlugin from './plugins/watch-remove-event-plugin'
import DynamicEntryPlugin from './plugins/dynamic-entry-plugin' import DynamicEntryPlugin from './plugins/dynamic-entry-plugin'
import DetachPlugin from './plugins/detach-plugin' import DetachPlugin from './plugins/detach-plugin'
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
export default async function createCompiler (dir, { hotReload = false, dev = false } = {}) { export default async function createCompiler (dir, { hotReload = false, dev = false } = {}) {
dir = resolve(dir) dir = resolve(dir)
@ -14,7 +15,7 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
const pages = await glob('pages/**/*.js', { cwd: dir }) const pages = await glob('pages/**/*.js', { cwd: dir })
const entry = {} const entry = {}
const defaultEntries = hotReload ? ['webpack/hot/dev-server'] : [] const defaultEntries = hotReload ? ['next/dist/client/webpack-hot-middleware-client'] : []
for (const p of pages) { for (const p of pages) {
entry[join('bundles', p)] = defaultEntries.concat(['./' + p]) entry[join('bundles', p)] = defaultEntries.concat(['./' + p])
} }
@ -39,6 +40,10 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
log: false, log: false,
// required not to cache removed files // required not to cache removed files
useHashIndex: false useHashIndex: false
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: 'commons.js'
}) })
] ]
@ -56,30 +61,22 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
if (hotReload) { if (hotReload) {
plugins.push( plugins.push(
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new DetachPlugin(), new DetachPlugin(),
new DynamicEntryPlugin(), new DynamicEntryPlugin(),
new UnlinkFilePlugin(), new UnlinkFilePlugin(),
new WatchRemoveEventPlugin(), new WatchRemoveEventPlugin(),
new WatchPagesPlugin(dir) new WatchPagesPlugin(dir),
new FriendlyErrorsWebpackPlugin()
) )
} }
const babelRuntimePath = require.resolve('babel-runtime/package') const babelRuntimePath = require.resolve('babel-runtime/package')
.replace(/[\\\/]package\.json$/, '') .replace(/[\\/]package\.json$/, '')
const loaders = [{ const loaders = (hotReload ? [{
test: /\.js$/,
loader: 'emit-file-loader',
include: [dir, nextPagesDir],
exclude (str) {
return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0
},
query: {
name: 'dist/[path][name].[ext]'
}
}]
.concat(hotReload ? [{
test: /\.js$/, test: /\.js$/,
loader: 'hot-self-accept-loader', loader: 'hot-self-accept-loader',
include: [ include: [
@ -88,6 +85,19 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
] ]
}] : []) }] : [])
.concat([{ .concat([{
test: /\.json$/,
loader: 'json-loader'
}, {
test: /\.(js|json)$/,
loader: 'emit-file-loader',
include: [dir, nextPagesDir],
exclude (str) {
return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0
},
query: {
name: 'dist/[path][name].[ext]'
}
}, {
loader: 'babel', loader: 'babel',
include: nextPagesDir, include: nextPagesDir,
query: { query: {
@ -107,11 +117,12 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
loader: 'babel', loader: 'babel',
include: [dir, nextPagesDir], include: [dir, nextPagesDir],
exclude (str) { exclude (str) {
return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0 return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0 && str.indexOf(dir) !== 0
}, },
query: { query: {
presets: ['es2015', 'react'], presets: ['es2015', 'react'],
plugins: [ plugins: [
require.resolve('babel-plugin-react-require'),
require.resolve('babel-plugin-transform-async-to-generator'), require.resolve('babel-plugin-transform-async-to-generator'),
require.resolve('babel-plugin-transform-object-rest-spread'), require.resolve('babel-plugin-transform-object-rest-spread'),
require.resolve('babel-plugin-transform-class-properties'), require.resolve('babel-plugin-transform-class-properties'),
@ -144,7 +155,7 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
path: join(dir, '.next'), path: join(dir, '.next'),
filename: '[name]', filename: '[name]',
libraryTarget: 'commonjs2', libraryTarget: 'commonjs2',
publicPath: hotReload ? 'http://localhost:3030/' : null publicPath: hotReload ? '/_webpack/' : null
}, },
externals: [ externals: [
'react', 'react',
@ -173,9 +184,6 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
}, },
plugins, plugins,
module: { module: {
preLoaders: [
{ test: /\.json$/, loader: 'json-loader' }
],
loaders loaders
}, },
customInterpolateName: function (url, name, opts) { customInterpolateName: function (url, name, opts) {

View file

@ -1,5 +1,7 @@
import { join, relative, sep } from 'path' import { join, relative, sep } from 'path'
import WebpackDevServer from 'webpack-dev-server' import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import isWindowsBash from 'is-windows-bash'
import webpack from './build/webpack' import webpack from './build/webpack'
import read from './read' import read from './read'
@ -7,22 +9,35 @@ export default class HotReloader {
constructor (dir, dev = false) { constructor (dir, dev = false) {
this.dir = dir this.dir = dir
this.dev = dev this.dev = dev
this.server = null this.middlewares = []
this.webpackDevMiddleware = null
this.webpackHotMiddleware = null
this.initialized = false this.initialized = false
this.stats = null this.stats = null
this.compilationErrors = null this.compilationErrors = null
this.prevAssets = null this.prevAssets = null
this.prevChunkNames = null this.prevChunkNames = null
this.prevFailedChunkNames = null this.prevFailedChunkNames = null
this.prevChunkHashes = null
}
async run (req, res) {
for (const fn of this.middlewares) {
await new Promise((resolve, reject) => {
fn(req, res, (err) => {
if (err) reject(err)
resolve()
})
})
}
} }
async start () { async start () {
await this.prepareServer() await this.prepareMiddlewares()
this.stats = await this.waitUntilValid() this.stats = await this.waitUntilValid()
await this.listen()
} }
async prepareServer () { async prepareMiddlewares () {
const compiler = await webpack(this.dir, { hotReload: true, dev: this.dev }) const compiler = await webpack(this.dir, { hotReload: true, dev: this.dev })
compiler.plugin('after-emit', (compilation, callback) => { compiler.plugin('after-emit', (compilation, callback) => {
@ -53,6 +68,8 @@ export default class HotReloader {
.reduce((a, b) => a.concat(b), []) .reduce((a, b) => a.concat(b), [])
.map((c) => c.name)) .map((c) => c.name))
const chunkHashes = new Map(compilation.chunks.map((c) => [c.name, c.hash]))
if (this.initialized) { if (this.initialized) {
// detect chunks which have to be replaced with a new template // detect chunks which have to be replaced with a new template
// e.g, pages/index.js <-> pages/_error.js // e.g, pages/index.js <-> pages/_error.js
@ -70,6 +87,16 @@ export default class HotReloader {
const route = toRoute(relative(rootDir, n)) const route = toRoute(relative(rootDir, n))
this.send('reload', route) this.send('reload', route)
} }
for (const [n, hash] of chunkHashes) {
if (!this.prevChunkHashes.has(n)) continue
if (this.prevChunkHashes.get(n) === hash) continue
const route = toRoute(relative(rootDir, n))
// notify change to recover from runtime errors
this.send('change', route)
}
} }
this.initialized = true this.initialized = true
@ -77,44 +104,36 @@ export default class HotReloader {
this.compilationErrors = null this.compilationErrors = null
this.prevChunkNames = chunkNames this.prevChunkNames = chunkNames
this.prevFailedChunkNames = failedChunkNames this.prevFailedChunkNames = failedChunkNames
this.prevChunkHashes = chunkHashes
}) })
this.server = new WebpackDevServer(compiler, { const windowsSettings = isWindowsBash() ? {
publicPath: '/', lazy: false,
hot: true, watchOptions: {
noInfo: true, aggregateTimeout: 300,
clientLogLevel: 'warning', poll: true
stats: {
assets: false,
children: false,
chunks: false,
color: false,
errors: true,
errorDetails: false,
hash: false,
modules: false,
publicPath: false,
reasons: false,
source: false,
timings: false,
version: false,
warnings: false
} }
} : {}
this.webpackDevMiddleware = webpackDevMiddleware(compiler, {
publicPath: '/_webpack/',
noInfo: true,
quiet: true,
clientLogLevel: 'warning',
...windowsSettings
}) })
this.webpackHotMiddleware = webpackHotMiddleware(compiler, { log: false })
this.middlewares = [
this.webpackDevMiddleware,
this.webpackHotMiddleware
]
} }
waitUntilValid () { waitUntilValid () {
return new Promise((resolve) => { return new Promise((resolve) => {
this.server.middleware.waitUntilValid(resolve) this.webpackDevMiddleware.waitUntilValid(resolve)
})
}
listen () {
return new Promise((resolve, reject) => {
this.server.listen(3030, (err) => {
if (err) return reject(err)
resolve()
})
}) })
} }
@ -141,8 +160,8 @@ export default class HotReloader {
return this.compilationErrors return this.compilationErrors
} }
send (type, data) { send (action, ...args) {
this.server.sockWrite(this.server.sockets, type, data) this.webpackHotMiddleware.publish({ action, data: args })
} }
} }

View file

@ -18,7 +18,7 @@ export default class Server {
this.run(req, res) this.run(req, res)
.catch((err) => { .catch((err) => {
console.error(err) console.error(err)
res.status(500) res.statusCode = 500
res.end('error') res.end('error')
}) })
}) })
@ -40,6 +40,11 @@ export default class Server {
} }
defineRoutes () { defineRoutes () {
this.router.get('/_next/commons.js', async (req, res, params) => {
const p = join(this.dir, '.next/commons.js')
await this.serveStatic(req, res, p)
})
this.router.get('/_next/:path+', async (req, res, params) => { this.router.get('/_next/:path+', async (req, res, params) => {
const p = join(__dirname, '..', 'client', ...(params.path || [])) const p = join(__dirname, '..', 'client', ...(params.path || []))
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p)
@ -60,6 +65,10 @@ export default class Server {
} }
async run (req, res) { async run (req, res) {
if (this.hotReloader) {
await this.hotReloader.run(req, res)
}
const fn = this.router.match(req, res) const fn = this.router.match(req, res)
if (fn) { if (fn) {
await fn() await fn()
@ -69,96 +78,112 @@ export default class Server {
} }
async render (req, res) { async render (req, res) {
const { dir, dev } = this
const { pathname, query } = parse(req.url, true) const { pathname, query } = parse(req.url, true)
const ctx = { req, res, pathname, query } const ctx = { req, res, pathname, query }
const opts = { dir, dev }
let html const compilationErr = this.getCompilationError(req.url)
if (compilationErr) {
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: compilationErr })
return
}
try {
await this.doRender(res, 200, req.url, ctx)
} catch (err) {
const compilationErr2 = this.getCompilationError('/_error')
if (compilationErr2) {
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: compilationErr2 })
return
}
if (err.code !== 'ENOENT') {
console.error(err)
const url = this.dev ? '/_error-debug' : '/_error'
await this.doRender(res, 500, url, { ...ctx, err })
return
}
const err = this.getCompilationError(req.url)
if (err) {
res.statusCode = 500
html = await render('/_error-debug', { ...ctx, err }, opts)
} else {
try { try {
html = await render(req.url, ctx, opts) await this.doRender(res, 404, '/_error', { ...ctx, err })
} catch (err) { } catch (err2) {
const _err = this.getCompilationError('/_error') if (this.dev) {
if (_err) { await this.doRender(res, 500, '/_error-debug', { ...ctx, err: err2 })
res.statusCode = 500
html = await render('/_error-debug', { ...ctx, err: _err }, opts)
} else { } else {
if (err.code === 'ENOENT') { throw err2
res.statusCode = 404
} else {
console.error(err)
res.statusCode = 500
}
html = await render('/_error', { ...ctx, err }, opts)
} }
} }
} }
}
async doRender (res, statusCode, url, ctx) {
const { dir, dev } = this
// need to set statusCode before `render`
// since it can be used on getInitialProps
res.statusCode = statusCode
const html = await render(url, ctx, { dir, dev })
sendHTML(res, html) sendHTML(res, html)
} }
async renderJSON (req, res) { async renderJSON (req, res) {
const { dir } = this const compilationErr = this.getCompilationError(req.url)
const opts = { dir } if (compilationErr) {
await this.doRenderJSON(res, 500, '/_error-debug.json', compilationErr)
return
}
let json try {
await this.doRenderJSON(res, 200, req.url)
const err = this.getCompilationError(req.url) } catch (err) {
if (err) { const compilationErr2 = this.getCompilationError('/_error.json')
res.statusCode = 500 if (compilationErr2) {
json = await renderJSON('/_error-debug.json', opts) await this.doRenderJSON(res, 500, '/_error-debug.json', compilationErr2)
json = { ...json, err: errorToJSON(err) } return
} else {
try {
json = await renderJSON(req.url, opts)
} catch (err) {
const _err = this.getCompilationError('/_error.json')
if (_err) {
res.statusCode = 500
json = await renderJSON('/_error-debug.json', opts)
json = { ...json, err: errorToJSON(_err) }
} else {
if (err.code === 'ENOENT') {
res.statusCode = 404
} else {
console.error(err)
res.statusCode = 500
}
json = await renderJSON('/_error.json', opts)
}
} }
if (err.code === 'ENOENT') {
await this.doRenderJSON(res, 404, '/_error.json')
} else {
console.error(err)
await this.doRenderJSON(res, 500, '/_error.json')
}
}
}
async doRenderJSON (res, statusCode, url, err) {
const { dir } = this
const json = await renderJSON(url, { dir })
if (err) {
json.err = errorToJSON(err)
} }
const data = JSON.stringify(json) const data = JSON.stringify(json)
res.setHeader('Content-Type', 'application/json') res.setHeader('Content-Type', 'application/json')
res.setHeader('Content-Length', Buffer.byteLength(data)) res.setHeader('Content-Length', Buffer.byteLength(data))
res.statusCode = statusCode
res.end(data) res.end(data)
} }
async render404 (req, res) { async render404 (req, res) {
const { dir, dev } = this
const { pathname, query } = parse(req.url, true) const { pathname, query } = parse(req.url, true)
const ctx = { req, res, pathname, query } const ctx = { req, res, pathname, query }
const opts = { dir, dev }
let html const compilationErr = this.getCompilationError('/_error')
if (compilationErr) {
const err = this.getCompilationError('/_error') await this.doRender(res, 500, '/_error-debug', { ...ctx, err: compilationErr })
if (err) { return
res.statusCode = 500
html = await render('/_error-debug', { ...ctx, err }, opts)
} else {
res.statusCode = 404
html = await render('/_error', ctx, opts)
} }
sendHTML(res, html) try {
await this.doRender(res, 404, '/_error', ctx)
} catch (err) {
if (this.dev) {
await this.doRender(res, 500, '/_error-debug', { ...ctx, err })
} else {
throw err
}
}
} }
serveStatic (req, res, path) { serveStatic (req, res, path) {

View file

@ -20,8 +20,15 @@ export async function render (url, ctx = {}, {
const mod = await requireModule(join(dir, '.next', 'dist', 'pages', path)) const mod = await requireModule(join(dir, '.next', 'dist', 'pages', path))
const Component = mod.default || mod const Component = mod.default || mod
const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {}) const [
const component = await read(join(dir, '.next', 'bundles', 'pages', path)) props,
component,
errorComponent
] = await Promise.all([
Component.getInitialProps ? Component.getInitialProps(ctx) : {},
read(join(dir, '.next', 'bundles', 'pages', path)),
read(join(dir, '.next', 'bundles', 'pages', dev ? '_error-debug' : '_error'))
])
const { html, css, ids } = renderStatic(() => { const { html, css, ids } = renderStatic(() => {
const app = createElement(App, { const app = createElement(App, {
@ -42,6 +49,7 @@ export async function render (url, ctx = {}, {
css, css,
data: { data: {
component, component,
errorComponent,
props, props,
ids: ids, ids: ids,
err: (ctx.err && dev) ? errorToJSON(ctx.err) : null err: (ctx.err && dev) ? errorToJSON(ctx.err) : null

View file

@ -27,8 +27,8 @@ test('header helper renders header information', async t => {
test('css helper renders styles', async t => { test('css helper renders styles', async t => {
const html = await render('/css') const html = await render('/css')
t.true(html.includes('.css-im3wl1')) t.regex(html, /\.css-\w+/)
t.true(html.includes('<div class="css-im3wl1">This is red</div>')) t.regex(html, /<div class="css-\w+">This is red<\/div>/)
}) })
test('renders properties populated asynchronously', async t => { test('renders properties populated asynchronously', async t => {