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:
parent
301cd26235
commit
7ec46d512b
|
@ -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
|
||||
|
|
155
client/webpack-dev-client/index.js
Normal file
155
client/webpack-dev-client/index.js
Normal 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()
|
||||
}
|
||||
}
|
39
client/webpack-dev-client/socket.js
Normal file
39
client/webpack-dev-client/socket.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue