|
7 | 7 | * LICENSE.txt file in the root directory of this source tree.
|
8 | 8 | */
|
9 | 9 |
|
| 10 | +import path from 'path'; |
| 11 | +import express from 'express'; |
10 | 12 | import browserSync from 'browser-sync';
|
11 | 13 | import webpack from 'webpack';
|
| 14 | +import logApplyResult from 'webpack/hot/log-apply-result'; |
12 | 15 | import webpackDevMiddleware from 'webpack-dev-middleware';
|
13 | 16 | 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'; |
20 | 18 | import webpackConfig from './webpack.config';
|
| 19 | +import run, { format } from './run'; |
21 | 20 | import clean from './clean';
|
22 |
| -import copy from './copy'; |
23 | 21 |
|
24 | 22 | const isDebug = !process.argv.includes('--release');
|
25 |
| -process.argv.push('--watch'); |
26 | 23 |
|
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; |
28 | 57 |
|
29 | 58 | /**
|
30 | 59 | * Launches a development web server with "live reload" functionality -
|
31 | 60 | * synchronizing URLs, interactions and code changes across multiple devices.
|
32 | 61 | */
|
33 | 62 | 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 |
34 | 97 | 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); |
56 | 103 |
|
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 | + })); |
62 | 110 |
|
63 |
| - // Pretty colored output |
64 |
| - stats: clientConfig.stats, |
| 111 | + // https://github.com/glenjamin/webpack-hot-middleware |
| 112 | + server.use(webpackHotMiddleware(clientCompiler, { log: false })); |
65 | 113 |
|
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 | + } |
68 | 150 | });
|
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 | + } |
103 | 160 | });
|
| 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; |
104 | 188 | }
|
105 | 189 |
|
106 | 190 | export default start;
|
0 commit comments