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:
commit
6ad1e23167
|
@ -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
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
37
client/webpack-hot-middleware-client.js
Normal file
37
client/webpack-hot-middleware-client.js
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
13
examples/shared-modules/README.md
Normal file
13
examples/shared-modules/README.md
Normal 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
|
||||||
|
```
|
19
examples/shared-modules/components/Counter.js
Normal file
19
examples/shared-modules/components/Counter.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
20
examples/shared-modules/components/Header.js
Normal file
20
examples/shared-modules/components/Header.js
Normal 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>
|
||||||
|
)
|
19
examples/shared-modules/package.json
Normal file
19
examples/shared-modules/package.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
11
examples/shared-modules/pages/about.js
Normal file
11
examples/shared-modules/pages/about.js
Normal 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>
|
||||||
|
)
|
11
examples/shared-modules/pages/index.js
Normal file
11
examples/shared-modules/pages/index.js
Normal 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>
|
||||||
|
)
|
|
@ -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': {
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
package.json
20
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
133
server/index.js
133
server/index.js
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
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, 200, req.url, ctx)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const _err = this.getCompilationError('/_error')
|
const compilationErr2 = this.getCompilationError('/_error')
|
||||||
if (_err) {
|
if (compilationErr2) {
|
||||||
res.statusCode = 500
|
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: compilationErr2 })
|
||||||
html = await render('/_error-debug', { ...ctx, err: _err }, opts)
|
return
|
||||||
} else {
|
}
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
res.statusCode = 404
|
if (err.code !== 'ENOENT') {
|
||||||
} else {
|
console.error(err)
|
||||||
console.error(err)
|
const url = this.dev ? '/_error-debug' : '/_error'
|
||||||
res.statusCode = 500
|
await this.doRender(res, 500, url, { ...ctx, err })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.doRender(res, 404, '/_error', { ...ctx, err })
|
||||||
|
} catch (err2) {
|
||||||
|
if (this.dev) {
|
||||||
|
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: err2 })
|
||||||
|
} else {
|
||||||
|
throw err2
|
||||||
}
|
}
|
||||||
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
|
|
||||||
|
|
||||||
const err = this.getCompilationError(req.url)
|
|
||||||
if (err) {
|
|
||||||
res.statusCode = 500
|
|
||||||
json = await renderJSON('/_error-debug.json', opts)
|
|
||||||
json = { ...json, err: errorToJSON(err) }
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
json = await renderJSON(req.url, opts)
|
await this.doRenderJSON(res, 200, req.url)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const _err = this.getCompilationError('/_error.json')
|
const compilationErr2 = this.getCompilationError('/_error.json')
|
||||||
if (_err) {
|
if (compilationErr2) {
|
||||||
res.statusCode = 500
|
await this.doRenderJSON(res, 500, '/_error-debug.json', compilationErr2)
|
||||||
json = await renderJSON('/_error-debug.json', opts)
|
return
|
||||||
json = { ...json, err: errorToJSON(_err) }
|
}
|
||||||
} else {
|
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
res.statusCode = 404
|
await this.doRenderJSON(res, 404, '/_error.json')
|
||||||
} else {
|
} else {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
res.statusCode = 500
|
await this.doRenderJSON(res, 500, '/_error.json')
|
||||||
}
|
|
||||||
json = await renderJSON('/_error.json', opts)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
Loading…
Reference in a new issue