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

hot reload added/removed pages

This commit is contained in:
nkzawa 2016-10-24 16:22:15 +09:00
parent 301cd26235
commit 7ec46d512b
5 changed files with 254 additions and 8 deletions

View file

@ -1,5 +1,5 @@
import 'react-hot-loader/patch'
import 'webpack-dev-server/client?http://localhost:3030'
import './webpack-dev-client?http://localhost:3030'
import * as next from './next'
module.exports = next

View file

@ -0,0 +1,155 @@
/* 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) {
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

@ -0,0 +1,39 @@
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

@ -59,6 +59,26 @@ export default class Router {
}
}
async reload (route) {
delete this.components[route]
if (route !== this.route) return
let data
let props
try {
data = await this.fetchComponent(route)
if (route !== this.route) {
props = await this.getInitialProps(data.Component, data.ctx)
}
} catch (err) {
if (err.cancelled) return false
throw err
}
this.notify({ ...data, props })
}
back () {
window.history.back()
}

View file

@ -9,7 +9,8 @@ export default class HotReloader {
this.server = null
this.stats = null
this.compilationErrors = null
this.prevAssets = {}
this.prevAssets = null
this.prevEntryChunkNames = null
}
async start () {
@ -23,21 +24,44 @@ export default class HotReloader {
compiler.plugin('after-emit', (compilation, callback) => {
const { assets } = compilation
for (const f of Object.keys(assets)) {
deleteCache(assets[f].existsAt)
}
for (const f of Object.keys(this.prevAssets)) {
if (!assets[f]) {
deleteCache(this.prevAssets[f].existsAt)
if (this.prevAssets) {
for (const f of Object.keys(assets)) {
deleteCache(assets[f].existsAt)
}
for (const f of Object.keys(this.prevAssets)) {
if (!assets[f]) {
deleteCache(this.prevAssets[f].existsAt)
}
}
}
this.prevAssets = assets
callback()
})
compiler.plugin('done', (stats) => {
this.stats = stats
this.compilationErrors = null
const entryChunkNames = new Set(stats.compilation.chunks
.filter((c) => c.entry)
.map((c) => c.name))
if (this.prevEntryChunkNames) {
const added = diff(entryChunkNames, this.prevEntryChunkNames)
const removed = diff(this.prevEntryChunkNames, entryChunkNames)
for (const n of new Set([...added, ...removed])) {
const m = n.match(/^bundles\/pages(\/.+?)(?:\/index)?\.js$/)
if (!m) {
console.error('Unexpected chunk name: ' + n)
continue
}
this.send('reload', m[1])
}
}
this.prevEntryChunkNames = entryChunkNames
})
this.server = new WebpackDevServer(compiler, {
@ -98,6 +122,10 @@ export default class HotReloader {
return this.compilationErrors
}
send (type, data) {
this.server.sockWrite(this.server.sockets, type, data)
}
get fileSystem () {
return this.server.middleware.fileSystem
}
@ -107,3 +135,7 @@ function deleteCache (path) {
delete require.cache[path]
delete read.cache[path]
}
function diff (a, b) {
return new Set([...a].filter((v) => !b.has(v)))
}