Skip to content

Commit 9b64871

Browse files
frenzzyokendoken
authored andcommitted
Isomorphic Hot Module Replacement (#1317)
1 parent a9a0021 commit 9b64871

File tree

7 files changed

+329
-216
lines changed

7 files changed

+329
-216
lines changed

docs/getting-started.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ This command will build the app from the source files (`/src`) into the output
6767
Node.js server (`node build/server.js`) and [Browsersync](https://browsersync.io/)
6868
with [HMR](https://webpack.github.io/docs/hot-module-replacement) on top of it.
6969

70-
> [http://localhost:3000/](http://localhost:3000/) — Node.js server (`build/server.js`)<br>
70+
> [http://localhost:3000/](http://localhost:3000/) — Node.js server (`build/server.js`)
71+
with Browsersync and HMR enabled<br>
7172
> [http://localhost:3000/graphql](http://localhost:3000/graphql) — GraphQL server and IDE<br>
72-
> [http://localhost:3001/](http://localhost:3001/) — BrowserSync proxy with HMR, React Hot Transform<br>
73-
> [http://localhost:3002/](http://localhost:3002/) — BrowserSync control panel (UI)
73+
> [http://localhost:3001/](http://localhost:3001/) — Browsersync control panel (UI)
7474
7575
Now you can open your web app in a browser, on mobile devices and start
7676
hacking. Whenever you modify any of the source files inside the `/src` folder,

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@
116116
"pre-commit": "^1.2.2",
117117
"raw-loader": "^0.5.1",
118118
"react-deep-force-update": "^2.0.1",
119-
"react-dev-utils": "^3.0.0",
120119
"react-error-overlay": "^1.0.7",
121120
"react-hot-loader": "^3.0.0-beta.7",
122121
"react-test-renderer": "^15.5.4",
@@ -132,8 +131,7 @@
132131
"webpack-bundle-analyzer": "^2.8.2",
133132
"webpack-dev-middleware": "^1.10.2",
134133
"webpack-hot-middleware": "^2.18.0",
135-
"webpack-node-externals": "^1.6.0",
136-
"write-file-webpack-plugin": "^4.1.0"
134+
"webpack-node-externals": "^1.6.0"
137135
},
138136
"babel": {
139137
"presets": [

src/server.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,21 @@ app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
230230
//
231231
// Launch the server
232232
// -----------------------------------------------------------------------------
233-
models.sync().catch(err => console.error(err.stack)).then(() => {
234-
app.listen(config.port, () => {
235-
console.info(`The server is running at http://localhost:${config.port}/`);
233+
const promise = models.sync().catch(err => console.error(err.stack));
234+
if (!module.hot) {
235+
promise.then(() => {
236+
app.listen(config.port, () => {
237+
console.info(`The server is running at http://localhost:${config.port}/`);
238+
});
236239
});
237-
});
240+
}
241+
242+
//
243+
// Hot Module Replacement
244+
// -----------------------------------------------------------------------------
245+
if (module.hot) {
246+
app.hot = module.hot;
247+
module.hot.accept('./components/App');
248+
}
249+
250+
export default app;

tools/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Flag | Description
3030
`--analyze` | Launches [Webpack Bundle Analyzer](https://github.com/th0r/webpack-bundle-analyzer)
3131
`--static` | Renders [specified routes](./render.js#L15) as static html files
3232
`--docker` | Build an image from a Dockerfile
33+
`--silent` | Do not open the default browser
3334

3435
For example:
3536

tools/start.js

Lines changed: 157 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,100 +7,184 @@
77
* LICENSE.txt file in the root directory of this source tree.
88
*/
99

10+
import path from 'path';
11+
import express from 'express';
1012
import browserSync from 'browser-sync';
1113
import webpack from 'webpack';
14+
import logApplyResult from 'webpack/hot/log-apply-result';
1215
import webpackDevMiddleware from 'webpack-dev-middleware';
1316
import webpackHotMiddleware from 'webpack-hot-middleware';
14-
import WriteFilePlugin from 'write-file-webpack-plugin';
15-
import url from 'url';
16-
import querystring from 'querystring';
17-
import launchEditor from 'react-dev-utils/launchEditor';
18-
import run from './run';
19-
import runServer from './runServer';
17+
import createLaunchEditorMiddleware from 'react-error-overlay/middleware';
2018
import webpackConfig from './webpack.config';
19+
import run, { format } from './run';
2120
import clean from './clean';
22-
import copy from './copy';
2321

2422
const isDebug = !process.argv.includes('--release');
25-
process.argv.push('--watch');
2623

27-
const [clientConfig, serverConfig] = webpackConfig;
24+
// https://webpack.js.org/configuration/watch/#watchoptions
25+
const watchOptions = {
26+
// Watching may not work with NFS and machines in VirtualBox
27+
// Uncomment next line if it's your case (use true or interval in milliseconds)
28+
// poll: true,
29+
30+
// Decrease CPU or memory usage in some file systems
31+
// ignored: /node_modules/,
32+
};
33+
34+
function createCompilationPromise(name, compiler, config) {
35+
return new Promise((resolve, reject) => {
36+
let timeStart = new Date();
37+
compiler.plugin('compile', () => {
38+
timeStart = new Date();
39+
console.info(`[${format(timeStart)}] Compiling '${name}'...`);
40+
});
41+
compiler.plugin('done', (stats) => {
42+
console.info(stats.toString(config.stats));
43+
const timeEnd = new Date();
44+
const time = timeEnd.getTime() - timeStart.getTime();
45+
if (stats.hasErrors()) {
46+
console.info(`[${format(timeEnd)}] Failed to compile '${name}' after ${time} ms`);
47+
reject(new Error('Compilation failed!'));
48+
} else {
49+
console.info(`[${format(timeEnd)}] Finished '${name}' compilation after ${time} ms`);
50+
resolve(stats);
51+
}
52+
});
53+
});
54+
}
55+
56+
let server;
2857

2958
/**
3059
* Launches a development web server with "live reload" functionality -
3160
* synchronizing URLs, interactions and code changes across multiple devices.
3261
*/
3362
async function start() {
63+
if (server) return server;
64+
server = express();
65+
server.use(createLaunchEditorMiddleware());
66+
server.use(express.static(path.resolve(__dirname, '../public')));
67+
68+
// Configure client-side hot module replacement
69+
const clientConfig = webpackConfig.find(config => config.name === 'client');
70+
clientConfig.entry.client = [
71+
'react-error-overlay',
72+
'react-hot-loader/patch',
73+
'webpack-hot-middleware/client?name=client&reload=true',
74+
].concat(clientConfig.entry.client).sort((a, b) => b.includes('polyfill') - a.includes('polyfill'));
75+
clientConfig.output.filename = clientConfig.output.filename.replace('chunkhash', 'hash');
76+
clientConfig.output.chunkFilename = clientConfig.output.chunkFilename.replace('chunkhash', 'hash');
77+
clientConfig.module.rules = clientConfig.module.rules.filter(x => x.loader !== 'null-loader');
78+
const { query } = clientConfig.module.rules.find(x => x.loader === 'babel-loader');
79+
query.plugins = ['react-hot-loader/babel'].concat(query.plugins || []);
80+
clientConfig.plugins.push(
81+
new webpack.HotModuleReplacementPlugin(),
82+
new webpack.NoEmitOnErrorsPlugin(),
83+
);
84+
85+
// Configure server-side hot module replacement
86+
const serverConfig = webpackConfig.find(config => config.name === 'server');
87+
serverConfig.output.hotUpdateMainFilename = 'updates/[hash].hot-update.json';
88+
serverConfig.output.hotUpdateChunkFilename = 'updates/[id].[hash].hot-update.js';
89+
serverConfig.module.rules = serverConfig.module.rules.filter(x => x.loader !== 'null-loader');
90+
serverConfig.plugins.push(
91+
new webpack.HotModuleReplacementPlugin(),
92+
new webpack.NoEmitOnErrorsPlugin(),
93+
new webpack.NamedModulesPlugin(),
94+
);
95+
96+
// Configure compilation
3497
await run(clean);
35-
await run(copy);
36-
await new Promise((resolve) => {
37-
// Save the server-side bundle files to the file system after compilation
38-
// https://github.com/webpack/webpack-dev-server/issues/62
39-
serverConfig.plugins.push(new WriteFilePlugin({ log: false }));
40-
41-
// Hot Module Replacement (HMR) + React Hot Reload
42-
if (isDebug) {
43-
clientConfig.entry.client = [...new Set([
44-
'babel-polyfill',
45-
'react-error-overlay',
46-
'react-hot-loader/patch',
47-
'webpack-hot-middleware/client',
48-
].concat(clientConfig.entry.client))];
49-
clientConfig.output.filename = clientConfig.output.filename.replace('[chunkhash', '[hash');
50-
clientConfig.output.chunkFilename = clientConfig.output.chunkFilename.replace('[chunkhash', '[hash');
51-
const { query } = clientConfig.module.rules.find(x => x.loader === 'babel-loader');
52-
query.plugins = ['react-hot-loader/babel'].concat(query.plugins || []);
53-
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
54-
clientConfig.plugins.push(new webpack.NoEmitOnErrorsPlugin());
55-
}
98+
const multiCompiler = webpack(webpackConfig);
99+
const clientCompiler = multiCompiler.compilers.find(compiler => compiler.name === 'client');
100+
const serverCompiler = multiCompiler.compilers.find(compiler => compiler.name === 'server');
101+
const clientPromise = createCompilationPromise('client', clientCompiler, clientConfig);
102+
const serverPromise = createCompilationPromise('server', serverCompiler, serverConfig);
56103

57-
const bundler = webpack(webpackConfig);
58-
const wpMiddleware = webpackDevMiddleware(bundler, {
59-
// IMPORTANT: webpack middleware can't access config,
60-
// so we should provide publicPath by ourselves
61-
publicPath: clientConfig.output.publicPath,
104+
// https://github.com/webpack/webpack-dev-middleware
105+
server.use(webpackDevMiddleware(clientCompiler, {
106+
publicPath: clientConfig.output.publicPath,
107+
quiet: true,
108+
watchOptions,
109+
}));
62110

63-
// Pretty colored output
64-
stats: clientConfig.stats,
111+
// https://github.com/glenjamin/webpack-hot-middleware
112+
server.use(webpackHotMiddleware(clientCompiler, { log: false }));
65113

66-
// For other settings see
67-
// https://webpack.github.io/docs/webpack-dev-middleware
114+
let appPromise;
115+
let appPromiseResolve;
116+
let appPromiseIsResolved = true;
117+
serverCompiler.plugin('compile', () => {
118+
if (!appPromiseIsResolved) return;
119+
appPromiseIsResolved = false;
120+
appPromise = new Promise(resolve => (appPromiseResolve = resolve));
121+
});
122+
123+
let app;
124+
server.use((req, res) => {
125+
appPromise.then(() => app.handle(req, res)).catch(error => console.error(error));
126+
});
127+
128+
function checkForUpdate(fromUpdate) {
129+
return app.hot.check().then((updatedModules) => {
130+
if (updatedModules) {
131+
return app.hot.apply().then((renewedModules) => {
132+
logApplyResult(updatedModules, renewedModules);
133+
checkForUpdate(true);
134+
});
135+
}
136+
if (fromUpdate) {
137+
return console.info('[HMR] Update applied.');
138+
}
139+
return console.warn('[HMR] Cannot find update.');
140+
}).catch((error) => {
141+
if (['abort', 'fail'].includes(app.hot.status())) {
142+
console.warn('[HMR] Cannot apply update.');
143+
delete require.cache[require.resolve('../build/server')];
144+
// eslint-disable-next-line global-require, import/no-unresolved
145+
app = require('../build/server').default;
146+
console.warn('[HMR] App has been reloaded.');
147+
} else {
148+
console.warn(`[HMR] Update failed: ${error.stack || error.message}`);
149+
}
68150
});
69-
const hotMiddleware = webpackHotMiddleware(bundler.compilers[0]);
70-
71-
let handleBundleComplete = async () => {
72-
handleBundleComplete = stats => !stats.stats[1].compilation.errors.length && runServer();
73-
74-
const server = await runServer();
75-
const bs = browserSync.create();
76-
77-
bs.init({
78-
...isDebug ? {} : { notify: false, ui: false },
79-
80-
proxy: {
81-
target: server.host,
82-
middleware: [
83-
{
84-
// Keep this in sync with react-error-overlay
85-
route: '/__open-stack-frame-in-editor',
86-
handle(req, res) {
87-
const query = querystring.parse(url.parse(req.url).query);
88-
launchEditor(query.fileName, query.lineNumber);
89-
res.end();
90-
},
91-
},
92-
wpMiddleware,
93-
hotMiddleware,
94-
],
95-
proxyOptions: {
96-
xfwd: true,
97-
},
98-
},
99-
}, resolve);
100-
};
101-
102-
bundler.plugin('done', stats => handleBundleComplete(stats));
151+
}
152+
153+
serverCompiler.watch(watchOptions, (error, stats) => {
154+
if (app && !error && !stats.hasErrors()) {
155+
checkForUpdate().then(() => {
156+
appPromiseIsResolved = true;
157+
appPromiseResolve();
158+
});
159+
}
103160
});
161+
162+
// Wait until both client-side and server-side bundles are ready
163+
await clientPromise;
164+
await serverPromise;
165+
166+
const timeStart = new Date();
167+
console.info(`[${format(timeStart)}] Launching server...`);
168+
169+
// Load compiled src/server.js as a middleware
170+
// eslint-disable-next-line global-require, import/no-unresolved
171+
app = require('../build/server').default;
172+
appPromiseIsResolved = true;
173+
appPromiseResolve();
174+
175+
// Launch the development server with Browsersync and HMR
176+
await new Promise((resolve, reject) => browserSync.create().init({
177+
// https://www.browsersync.io/docs/options
178+
server: 'src/server.js',
179+
middleware: [server],
180+
open: !process.argv.includes('--silent'),
181+
...isDebug ? {} : { notify: false, ui: false },
182+
}, (error, bs) => (error ? reject(error) : resolve(bs))));
183+
184+
const timeEnd = new Date();
185+
const time = timeEnd.getTime() - timeStart.getTime();
186+
console.info(`[${format(timeEnd)}] Server launched after ${time} ms`);
187+
return server;
104188
}
105189

106190
export default start;

0 commit comments

Comments
 (0)