Skip to content

Commit 7ec46d5

Browse files
committed
hot reload added/removed pages
1 parent 301cd26 commit 7ec46d5

File tree

5 files changed

+254
-8
lines changed

5 files changed

+254
-8
lines changed

client/next-dev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'react-hot-loader/patch'
2-
import 'webpack-dev-server/client?http://localhost:3030'
2+
import './webpack-dev-client?http://localhost:3030'
33
import * as next from './next'
44

55
module.exports = next

client/webpack-dev-client/index.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/* global __resourceQuery, next */
2+
3+
// Based on 'webpack-dev-server/client'
4+
5+
import url from 'url'
6+
import stripAnsi from 'strip-ansi'
7+
import socket from './socket'
8+
9+
function getCurrentScriptSource () {
10+
// `document.currentScript` is the most accurate way to find the current script,
11+
// but is not supported in all browsers.
12+
if (document.currentScript) {
13+
return document.currentScript.getAttribute('src')
14+
}
15+
// Fall back to getting all scripts in the document.
16+
const scriptElements = document.scripts || []
17+
const currentScript = scriptElements[scriptElements.length - 1]
18+
if (currentScript) {
19+
return currentScript.getAttribute('src')
20+
}
21+
// Fail as there was no script to use.
22+
throw new Error('[WDS] Failed to get current script source')
23+
}
24+
25+
let urlParts
26+
if (typeof __resourceQuery === 'string' && __resourceQuery) {
27+
// If this bundle is inlined, use the resource query to get the correct url.
28+
urlParts = url.parse(__resourceQuery.substr(1))
29+
} else {
30+
// Else, get the url from the <script> this file was called with.
31+
let scriptHost = getCurrentScriptSource()
32+
scriptHost = scriptHost.replace(/\/[^\/]+$/, '')
33+
urlParts = url.parse(scriptHost || '/', false, true)
34+
}
35+
36+
let hot = false
37+
let initial = true
38+
let currentHash = ''
39+
let logLevel = 'info'
40+
41+
function log (level, msg) {
42+
if (logLevel === 'info' && level === 'info') {
43+
return console.log(msg)
44+
}
45+
if (['info', 'warning'].indexOf(logLevel) >= 0 && level === 'warning') {
46+
return console.warn(msg)
47+
}
48+
if (['info', 'warning', 'error'].indexOf(logLevel) >= 0 && level === 'error') {
49+
return console.error(msg)
50+
}
51+
}
52+
53+
const onSocketMsg = {
54+
hot () {
55+
hot = true
56+
log('info', '[WDS] Hot Module Replacement enabled.')
57+
},
58+
invalid () {
59+
log('info', '[WDS] App updated. Recompiling...')
60+
},
61+
hash (hash) {
62+
currentHash = hash
63+
},
64+
'still-ok': () => {
65+
log('info', '[WDS] Nothing changed.')
66+
},
67+
'log-level': (level) => {
68+
logLevel = level
69+
},
70+
ok () {
71+
if (initial) {
72+
initial = false
73+
return
74+
}
75+
reloadApp()
76+
},
77+
warnings (warnings) {
78+
log('info', '[WDS] Warnings while compiling.')
79+
for (let i = 0; i < warnings.length; i++) {
80+
console.warn(stripAnsi(warnings[i]))
81+
}
82+
if (initial) {
83+
initial = false
84+
return
85+
}
86+
reloadApp()
87+
},
88+
errors (errors) {
89+
log('info', '[WDS] Errors while compiling.')
90+
for (let i = 0; i < errors.length; i++) {
91+
console.error(stripAnsi(errors[i]))
92+
}
93+
if (initial) {
94+
initial = false
95+
return
96+
}
97+
reloadApp()
98+
},
99+
'proxy-error': (errors) => {
100+
log('info', '[WDS] Proxy error.')
101+
for (let i = 0; i < errors.length; i++) {
102+
log('error', stripAnsi(errors[i]))
103+
}
104+
if (initial) {
105+
initial = false
106+
return
107+
}
108+
},
109+
reload (route) {
110+
next.router.reload(route)
111+
},
112+
close () {
113+
log('error', '[WDS] Disconnected!')
114+
}
115+
}
116+
117+
let hostname = urlParts.hostname
118+
let protocol = urlParts.protocol
119+
120+
if (urlParts.hostname === '0.0.0.0') {
121+
// why do we need this check?
122+
// hostname n/a for file protocol (example, when using electron, ionic)
123+
// see: https://github.com/webpack/webpack-dev-server/pull/384
124+
if (window.location.hostname && !!~window.location.protocol.indexOf('http')) {
125+
hostname = window.location.hostname
126+
}
127+
}
128+
129+
// `hostname` can be empty when the script path is relative. In that case, specifying
130+
// a protocol would result in an invalid URL.
131+
// When https is used in the app, secure websockets are always necessary
132+
// because the browser doesn't accept non-secure websockets.
133+
if (hostname && (window.location.protocol === 'https:' || urlParts.hostname === '0.0.0.0')) {
134+
protocol = window.location.protocol
135+
}
136+
137+
const socketUrl = url.format({
138+
protocol,
139+
auth: urlParts.auth,
140+
hostname,
141+
port: (urlParts.port === '0') ? window.location.port : urlParts.port,
142+
pathname: urlParts.path == null || urlParts.path === '/' ? '/sockjs-node' : urlParts.path
143+
})
144+
145+
socket(socketUrl, onSocketMsg)
146+
147+
function reloadApp () {
148+
if (hot) {
149+
log('info', '[WDS] App hot update...')
150+
window.postMessage('webpackHotUpdate' + currentHash, '*')
151+
} else {
152+
log('info', '[WDS] App updated. Reloading...')
153+
window.location.reload()
154+
}
155+
}

client/webpack-dev-client/socket.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import SockJS from 'sockjs-client'
2+
3+
let retries = 0
4+
let sock = null
5+
6+
export default function socket (url, handlers) {
7+
sock = new SockJS(url)
8+
9+
sock.onopen = () => {
10+
retries = 0
11+
}
12+
13+
sock.onclose = () => {
14+
if (retries === 0) handlers.close()
15+
16+
// Try to reconnect.
17+
sock = null
18+
19+
// After 10 retries stop trying, to prevent logspam.
20+
if (retries <= 10) {
21+
// Exponentially increase timeout to reconnect.
22+
// Respectfully copied from the package `got`.
23+
const retryInMs = 1000 * Math.pow(2, retries) + Math.random() * 100
24+
retries += 1
25+
26+
setTimeout(() => {
27+
socket(url, handlers)
28+
}, retryInMs)
29+
}
30+
}
31+
32+
sock.onmessage = (e) => {
33+
// This assumes that all data sent via the websocket is JSON.
34+
const msg = JSON.parse(e.data)
35+
if (handlers[msg.type]) {
36+
handlers[msg.type](msg.data)
37+
}
38+
}
39+
}

lib/router.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ export default class Router {
5959
}
6060
}
6161

62+
async reload (route) {
63+
delete this.components[route]
64+
65+
if (route !== this.route) return
66+
67+
let data
68+
let props
69+
try {
70+
data = await this.fetchComponent(route)
71+
if (route !== this.route) {
72+
props = await this.getInitialProps(data.Component, data.ctx)
73+
}
74+
} catch (err) {
75+
if (err.cancelled) return false
76+
throw err
77+
}
78+
79+
this.notify({ ...data, props })
80+
}
81+
6282
back () {
6383
window.history.back()
6484
}

server/hot-reloader.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export default class HotReloader {
99
this.server = null
1010
this.stats = null
1111
this.compilationErrors = null
12-
this.prevAssets = {}
12+
this.prevAssets = null
13+
this.prevEntryChunkNames = null
1314
}
1415

1516
async start () {
@@ -23,21 +24,44 @@ export default class HotReloader {
2324

2425
compiler.plugin('after-emit', (compilation, callback) => {
2526
const { assets } = compilation
26-
for (const f of Object.keys(assets)) {
27-
deleteCache(assets[f].existsAt)
28-
}
29-
for (const f of Object.keys(this.prevAssets)) {
30-
if (!assets[f]) {
31-
deleteCache(this.prevAssets[f].existsAt)
27+
28+
if (this.prevAssets) {
29+
for (const f of Object.keys(assets)) {
30+
deleteCache(assets[f].existsAt)
31+
}
32+
for (const f of Object.keys(this.prevAssets)) {
33+
if (!assets[f]) {
34+
deleteCache(this.prevAssets[f].existsAt)
35+
}
3236
}
3337
}
3438
this.prevAssets = assets
39+
3540
callback()
3641
})
3742

3843
compiler.plugin('done', (stats) => {
3944
this.stats = stats
4045
this.compilationErrors = null
46+
47+
const entryChunkNames = new Set(stats.compilation.chunks
48+
.filter((c) => c.entry)
49+
.map((c) => c.name))
50+
51+
if (this.prevEntryChunkNames) {
52+
const added = diff(entryChunkNames, this.prevEntryChunkNames)
53+
const removed = diff(this.prevEntryChunkNames, entryChunkNames)
54+
55+
for (const n of new Set([...added, ...removed])) {
56+
const m = n.match(/^bundles\/pages(\/.+?)(?:\/index)?\.js$/)
57+
if (!m) {
58+
console.error('Unexpected chunk name: ' + n)
59+
continue
60+
}
61+
this.send('reload', m[1])
62+
}
63+
}
64+
this.prevEntryChunkNames = entryChunkNames
4165
})
4266

4367
this.server = new WebpackDevServer(compiler, {
@@ -98,6 +122,10 @@ export default class HotReloader {
98122
return this.compilationErrors
99123
}
100124

125+
send (type, data) {
126+
this.server.sockWrite(this.server.sockets, type, data)
127+
}
128+
101129
get fileSystem () {
102130
return this.server.middleware.fileSystem
103131
}
@@ -107,3 +135,7 @@ function deleteCache (path) {
107135
delete require.cache[path]
108136
delete read.cache[path]
109137
}
138+
139+
function diff (a, b) {
140+
return new Set([...a].filter((v) => !b.has(v)))
141+
}

0 commit comments

Comments
 (0)