webpack-dev-server 3.1.14 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/Server.js CHANGED
@@ -4,10 +4,7 @@
4
4
  import/order,
5
5
  no-shadow,
6
6
  no-undefined,
7
- func-names,
8
- multiline-ternary,
9
- array-bracket-spacing,
10
- space-before-function-paren
7
+ func-names
11
8
  */
12
9
  const fs = require('fs');
13
10
  const path = require('path');
@@ -17,7 +14,6 @@ const tls = require('tls');
17
14
  const url = require('url');
18
15
  const http = require('http');
19
16
  const https = require('https');
20
- const spdy = require('spdy');
21
17
  const sockjs = require('sockjs');
22
18
 
23
19
  const semver = require('semver');
@@ -37,6 +33,7 @@ const historyApiFallback = require('connect-history-api-fallback');
37
33
  const webpack = require('webpack');
38
34
  const webpackDevMiddleware = require('webpack-dev-middleware');
39
35
 
36
+ const updateCompiler = require('./utils/updateCompiler');
40
37
  const createLogger = require('./utils/createLogger');
41
38
  const createCertificate = require('./utils/createCertificate');
42
39
 
@@ -53,7 +50,11 @@ const schema = require('./options.json');
53
50
  SockjsSession.prototype.decorateConnection = function(req) {
54
51
  decorateConnection.call(this, req);
55
52
  const connection = this.connection;
56
- if (connection.headers && !('origin' in connection.headers) && 'origin' in req.headers) {
53
+ if (
54
+ connection.headers &&
55
+ !('origin' in connection.headers) &&
56
+ 'origin' in req.headers
57
+ ) {
57
58
  connection.headers.origin = req.headers.origin;
58
59
  }
59
60
  };
@@ -68,849 +69,963 @@ if (semver.satisfies(process.version, '8.6.0 - 9')) {
68
69
  tls.DEFAULT_ECDH_CURVE = 'auto';
69
70
  }
70
71
 
71
- const STATS = {
72
- all: false,
73
- hash: true,
74
- assets: true,
75
- warnings: true,
76
- errors: true,
77
- errorDetails: false
78
- };
72
+ class Server {
73
+ static get DEFAULT_STATS() {
74
+ return {
75
+ all: false,
76
+ hash: true,
77
+ assets: true,
78
+ warnings: true,
79
+ errors: true,
80
+ errorDetails: false,
81
+ };
82
+ }
79
83
 
80
- function Server (compiler, options = {}, _log) {
81
- this.log = _log || createLogger(options);
84
+ constructor(compiler, options = {}, _log) {
85
+ this.log = _log || createLogger(options);
82
86
 
83
- validateOptions(schema, options, 'webpack Dev Server');
87
+ validateOptions(schema, options, 'webpack Dev Server');
84
88
 
85
- if (options.lazy && !options.filename) {
86
- throw new Error("'filename' option must be set in lazy mode.");
87
- }
89
+ if (options.lazy && !options.filename) {
90
+ throw new Error("'filename' option must be set in lazy mode.");
91
+ }
88
92
 
89
- this.hot = options.hot || options.hotOnly;
90
- this.headers = options.headers;
91
- this.progress = options.progress;
93
+ // if the user enables http2, we can safely enable https
94
+ if (options.http2 && !options.https) {
95
+ options.https = true;
96
+ }
92
97
 
93
- this.clientOverlay = options.overlay;
94
- this.clientLogLevel = options.clientLogLevel;
98
+ updateCompiler(compiler, options);
95
99
 
96
- this.publicHost = options.public;
97
- this.allowedHosts = options.allowedHosts;
98
- this.disableHostCheck = !!options.disableHostCheck;
100
+ this.originalStats =
101
+ options.stats && Object.keys(options.stats).length ? options.stats : {};
99
102
 
100
- this.sockets = [];
103
+ this.hot = options.hot || options.hotOnly;
104
+ this.headers = options.headers;
105
+ this.progress = options.progress;
101
106
 
102
- this.watchOptions = options.watchOptions || {};
103
- this.contentBaseWatchers = [];
107
+ this.serveIndex = options.serveIndex;
104
108
 
105
- // Listening for events
106
- const invalidPlugin = () => {
107
- this.sockWrite(this.sockets, 'invalid');
108
- };
109
+ this.clientOverlay = options.overlay;
110
+ this.clientLogLevel = options.clientLogLevel;
109
111
 
110
- if (this.progress) {
111
- const progressPlugin = new webpack.ProgressPlugin(
112
- (percent, msg, addInfo) => {
113
- percent = Math.floor(percent * 100);
112
+ this.publicHost = options.public;
113
+ this.allowedHosts = options.allowedHosts;
114
+ this.disableHostCheck = !!options.disableHostCheck;
114
115
 
115
- if (percent === 100) {
116
- msg = 'Compilation completed';
117
- }
116
+ this.sockets = [];
118
117
 
119
- if (addInfo) {
120
- msg = `${msg} (${addInfo})`;
121
- }
118
+ this.watchOptions = options.watchOptions || {};
119
+ this.contentBaseWatchers = [];
120
+ // Replace leading and trailing slashes to normalize path
121
+ this.sockPath = `/${
122
+ options.sockPath
123
+ ? options.sockPath.replace(/^\/|\/$/g, '')
124
+ : 'sockjs-node'
125
+ }`;
122
126
 
123
- this.sockWrite(this.sockets, 'progress-update', { percent, msg });
124
- }
125
- );
127
+ // Listening for events
128
+ const invalidPlugin = () => {
129
+ this.sockWrite(this.sockets, 'invalid');
130
+ };
126
131
 
127
- progressPlugin.apply(compiler);
128
- }
132
+ if (this.progress) {
133
+ const progressPlugin = new webpack.ProgressPlugin(
134
+ (percent, msg, addInfo) => {
135
+ percent = Math.floor(percent * 100);
129
136
 
130
- const addHooks = (compiler) => {
131
- const { compile, invalid, done } = compiler.hooks;
137
+ if (percent === 100) {
138
+ msg = 'Compilation completed';
139
+ }
132
140
 
133
- compile.tap('webpack-dev-server', invalidPlugin);
134
- invalid.tap('webpack-dev-server', invalidPlugin);
135
- done.tap('webpack-dev-server', (stats) => {
136
- this._sendStats(this.sockets, stats.toJson(STATS));
137
- this._stats = stats;
138
- });
139
- };
141
+ if (addInfo) {
142
+ msg = `${msg} (${addInfo})`;
143
+ }
140
144
 
141
- if (compiler.compilers) {
142
- compiler.compilers.forEach(addHooks);
143
- } else {
144
- addHooks(compiler);
145
- }
145
+ this.sockWrite(this.sockets, 'progress-update', { percent, msg });
146
+ }
147
+ );
146
148
 
147
- // Init express server
148
- // eslint-disable-next-line
149
- const app = this.app = new express();
149
+ progressPlugin.apply(compiler);
150
+ }
150
151
 
151
- // ref: https://github.com/webpack/webpack-dev-server/issues/1575
152
- // remove this when send@^0.16.3
153
- express.static.mime.types.wasm = 'application/wasm';
152
+ const addHooks = (compiler) => {
153
+ const { compile, invalid, done } = compiler.hooks;
154
154
 
155
- app.all('*', (req, res, next) => {
156
- if (this.checkHost(req.headers)) {
157
- return next();
155
+ compile.tap('webpack-dev-server', invalidPlugin);
156
+ invalid.tap('webpack-dev-server', invalidPlugin);
157
+ done.tap('webpack-dev-server', (stats) => {
158
+ this._sendStats(this.sockets, this.getStats(stats));
159
+ this._stats = stats;
160
+ });
161
+ };
162
+
163
+ if (compiler.compilers) {
164
+ compiler.compilers.forEach(addHooks);
165
+ } else {
166
+ addHooks(compiler);
158
167
  }
159
168
 
160
- res.send('Invalid Host header');
161
- });
169
+ // Init express server
170
+ // eslint-disable-next-line
171
+ const app = (this.app = new express());
162
172
 
163
- const wdmOptions = { logLevel: this.log.options.level };
173
+ // ref: https://github.com/webpack/webpack-dev-server/issues/1575
174
+ // ref: https://github.com/webpack/webpack-dev-server/issues/1724
175
+ // remove this when send@^0.16.3
176
+ if (express.static && express.static.mime && express.static.mime.types) {
177
+ express.static.mime.types.wasm = 'application/wasm';
178
+ }
164
179
 
165
- // middleware for serving webpack bundle
166
- this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));
180
+ app.all('*', (req, res, next) => {
181
+ if (this.checkHost(req.headers)) {
182
+ return next();
183
+ }
167
184
 
168
- app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
169
- res.setHeader('Content-Type', 'application/javascript');
185
+ res.send('Invalid Host header');
186
+ });
170
187
 
171
- fs.createReadStream(
172
- path.join(__dirname, '..', 'client', 'live.bundle.js')
173
- ).pipe(res);
174
- });
188
+ const wdmOptions = { logLevel: this.log.options.level };
175
189
 
176
- app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
177
- res.setHeader('Content-Type', 'application/javascript');
190
+ // middleware for serving webpack bundle
191
+ this.middleware = webpackDevMiddleware(
192
+ compiler,
193
+ Object.assign({}, options, wdmOptions)
194
+ );
178
195
 
179
- fs.createReadStream(
180
- path.join(__dirname, '..', 'client', 'sockjs.bundle.js')
181
- ).pipe(res);
182
- });
196
+ app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
197
+ res.setHeader('Content-Type', 'application/javascript');
183
198
 
184
- app.get('/webpack-dev-server.js', (req, res) => {
185
- res.setHeader('Content-Type', 'application/javascript');
199
+ fs.createReadStream(
200
+ path.join(__dirname, '..', 'client', 'live.bundle.js')
201
+ ).pipe(res);
202
+ });
186
203
 
187
- fs.createReadStream(
188
- path.join(__dirname, '..', 'client', 'index.bundle.js')
189
- ).pipe(res);
190
- });
204
+ app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
205
+ res.setHeader('Content-Type', 'application/javascript');
191
206
 
192
- app.get('/webpack-dev-server/*', (req, res) => {
193
- res.setHeader('Content-Type', 'text/html');
207
+ fs.createReadStream(
208
+ path.join(__dirname, '..', 'client', 'sockjs.bundle.js')
209
+ ).pipe(res);
210
+ });
194
211
 
195
- fs.createReadStream(
196
- path.join(__dirname, '..', 'client', 'live.html')
197
- ).pipe(res);
198
- });
212
+ app.get('/webpack-dev-server.js', (req, res) => {
213
+ res.setHeader('Content-Type', 'application/javascript');
199
214
 
200
- app.get('/webpack-dev-server', (req, res) => {
201
- res.setHeader('Content-Type', 'text/html');
215
+ fs.createReadStream(
216
+ path.join(__dirname, '..', 'client', 'index.bundle.js')
217
+ ).pipe(res);
218
+ });
202
219
 
203
- res.write(
204
- '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
205
- );
220
+ app.get('/webpack-dev-server/*', (req, res) => {
221
+ res.setHeader('Content-Type', 'text/html');
206
222
 
207
- const outputPath = this.middleware.getFilenameFromUrl(
208
- options.publicPath || '/'
209
- );
223
+ fs.createReadStream(
224
+ path.join(__dirname, '..', 'client', 'live.html')
225
+ ).pipe(res);
226
+ });
227
+
228
+ app.get('/webpack-dev-server', (req, res) => {
229
+ res.setHeader('Content-Type', 'text/html');
210
230
 
211
- const filesystem = this.middleware.fileSystem;
231
+ res.write(
232
+ '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
233
+ );
212
234
 
213
- function writeDirectory(baseUrl, basePath) {
214
- const content = filesystem.readdirSync(basePath);
235
+ const outputPath = this.middleware.getFilenameFromUrl(
236
+ options.publicPath || '/'
237
+ );
215
238
 
216
- res.write('<ul>');
239
+ const filesystem = this.middleware.fileSystem;
217
240
 
218
- content.forEach((item) => {
219
- const p = `${basePath}/${item}`;
241
+ function writeDirectory(baseUrl, basePath) {
242
+ const content = filesystem.readdirSync(basePath);
220
243
 
221
- if (filesystem.statSync(p).isFile()) {
222
- res.write('<li><a href="');
223
- res.write(baseUrl + item);
224
- res.write('">');
225
- res.write(item);
226
- res.write('</a></li>');
244
+ res.write('<ul>');
227
245
 
228
- if (/\.js$/.test(item)) {
229
- const html = item.substr(0, item.length - 3);
246
+ content.forEach((item) => {
247
+ const p = `${basePath}/${item}`;
230
248
 
249
+ if (filesystem.statSync(p).isFile()) {
231
250
  res.write('<li><a href="');
232
- res.write(baseUrl + html);
251
+ res.write(baseUrl + item);
233
252
  res.write('">');
234
- res.write(html);
235
- res.write('</a> (magic html for ');
236
253
  res.write(item);
237
- res.write(') (<a href="');
238
- res.write(
239
- baseUrl.replace(
240
- // eslint-disable-next-line
241
- /(^(https?:\/\/[^\/]+)?\/)/,
242
- '$1webpack-dev-server/'
243
- ) + html
244
- );
245
- res.write('">webpack-dev-server</a>)</li>');
246
- }
247
- } else {
248
- res.write('<li>');
249
- res.write(item);
250
- res.write('<br>');
254
+ res.write('</a></li>');
255
+
256
+ if (/\.js$/.test(item)) {
257
+ const html = item.substr(0, item.length - 3);
258
+
259
+ res.write('<li><a href="');
260
+ res.write(baseUrl + html);
261
+ res.write('">');
262
+ res.write(html);
263
+ res.write('</a> (magic html for ');
264
+ res.write(item);
265
+ res.write(') (<a href="');
266
+ res.write(
267
+ baseUrl.replace(
268
+ // eslint-disable-next-line
269
+ /(^(https?:\/\/[^\/]+)?\/)/,
270
+ '$1webpack-dev-server/'
271
+ ) + html
272
+ );
273
+ res.write('">webpack-dev-server</a>)</li>');
274
+ }
275
+ } else {
276
+ res.write('<li>');
277
+ res.write(item);
278
+ res.write('<br>');
251
279
 
252
- writeDirectory(`${baseUrl + item}/`, p);
280
+ writeDirectory(`${baseUrl + item}/`, p);
253
281
 
254
- res.write('</li>');
255
- }
256
- });
282
+ res.write('</li>');
283
+ }
284
+ });
257
285
 
258
- res.write('</ul>');
259
- }
286
+ res.write('</ul>');
287
+ }
260
288
 
261
- writeDirectory(options.publicPath || '/', outputPath);
289
+ writeDirectory(options.publicPath || '/', outputPath);
262
290
 
263
- res.end('</body></html>');
264
- });
291
+ res.end('</body></html>');
292
+ });
265
293
 
266
- let contentBase;
294
+ let contentBase;
267
295
 
268
- if (options.contentBase !== undefined) {
269
- contentBase = options.contentBase;
270
- } else {
271
- contentBase = process.cwd();
272
- }
296
+ if (options.contentBase !== undefined) {
297
+ contentBase = options.contentBase;
298
+ } else {
299
+ contentBase = process.cwd();
300
+ }
273
301
 
274
- // Keep track of websocket proxies for external websocket upgrade.
275
- const websocketProxies = [];
302
+ // Keep track of websocket proxies for external websocket upgrade.
303
+ const websocketProxies = [];
276
304
 
277
- const features = {
278
- compress: () => {
279
- if (options.compress) {
280
- // Enable gzip compression.
281
- app.use(compress());
282
- }
283
- },
284
- proxy: () => {
285
- if (options.proxy) {
286
- /**
287
- * Assume a proxy configuration specified as:
288
- * proxy: {
289
- * 'context': { options }
290
- * }
291
- * OR
292
- * proxy: {
293
- * 'context': 'target'
294
- * }
295
- */
296
- if (!Array.isArray(options.proxy)) {
297
- options.proxy = Object.keys(options.proxy).map((context) => {
298
- let proxyOptions;
299
- // For backwards compatibility reasons.
300
- const correctedContext = context
301
- .replace(/^\*$/, '**')
302
- .replace(/\/\*$/, '');
303
-
304
- if (typeof options.proxy[context] === 'string') {
305
- proxyOptions = {
306
- context: correctedContext,
307
- target: options.proxy[context]
308
- };
305
+ const features = {
306
+ compress: () => {
307
+ if (options.compress) {
308
+ // Enable gzip compression.
309
+ app.use(compress());
310
+ }
311
+ },
312
+ proxy: () => {
313
+ if (options.proxy) {
314
+ /**
315
+ * Assume a proxy configuration specified as:
316
+ * proxy: {
317
+ * 'context': { options }
318
+ * }
319
+ * OR
320
+ * proxy: {
321
+ * 'context': 'target'
322
+ * }
323
+ */
324
+ if (!Array.isArray(options.proxy)) {
325
+ if (Object.prototype.hasOwnProperty.call(options.proxy, 'target')) {
326
+ options.proxy = [options.proxy];
309
327
  } else {
310
- proxyOptions = Object.assign({}, options.proxy[context]);
311
- proxyOptions.context = correctedContext;
328
+ options.proxy = Object.keys(options.proxy).map((context) => {
329
+ let proxyOptions;
330
+ // For backwards compatibility reasons.
331
+ const correctedContext = context
332
+ .replace(/^\*$/, '**')
333
+ .replace(/\/\*$/, '');
334
+
335
+ if (typeof options.proxy[context] === 'string') {
336
+ proxyOptions = {
337
+ context: correctedContext,
338
+ target: options.proxy[context],
339
+ };
340
+ } else {
341
+ proxyOptions = Object.assign({}, options.proxy[context]);
342
+ proxyOptions.context = correctedContext;
343
+ }
344
+
345
+ proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
346
+
347
+ return proxyOptions;
348
+ });
312
349
  }
313
-
314
- proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
315
-
316
- return proxyOptions;
317
- });
318
- }
319
-
320
- const getProxyMiddleware = (proxyConfig) => {
321
- const context = proxyConfig.context || proxyConfig.path;
322
- // It is possible to use the `bypass` method without a `target`.
323
- // However, the proxy middleware has no use in this case, and will fail to instantiate.
324
- if (proxyConfig.target) {
325
- return httpProxyMiddleware(context, proxyConfig);
326
- }
327
- };
328
- /**
329
- * Assume a proxy configuration specified as:
330
- * proxy: [
331
- * {
332
- * context: ...,
333
- * ...options...
334
- * },
335
- * // or:
336
- * function() {
337
- * return {
338
- * context: ...,
339
- * ...options...
340
- * };
341
- * }
342
- * ]
343
- */
344
- options.proxy.forEach((proxyConfigOrCallback) => {
345
- let proxyConfig;
346
- let proxyMiddleware;
347
-
348
- if (typeof proxyConfigOrCallback === 'function') {
349
- proxyConfig = proxyConfigOrCallback();
350
- } else {
351
- proxyConfig = proxyConfigOrCallback;
352
350
  }
353
351
 
354
- proxyMiddleware = getProxyMiddleware(proxyConfig);
355
-
356
- if (proxyConfig.ws) {
357
- websocketProxies.push(proxyMiddleware);
358
- }
352
+ const getProxyMiddleware = (proxyConfig) => {
353
+ const context = proxyConfig.context || proxyConfig.path;
354
+ // It is possible to use the `bypass` method without a `target`.
355
+ // However, the proxy middleware has no use in this case, and will fail to instantiate.
356
+ if (proxyConfig.target) {
357
+ return httpProxyMiddleware(context, proxyConfig);
358
+ }
359
+ };
360
+ /**
361
+ * Assume a proxy configuration specified as:
362
+ * proxy: [
363
+ * {
364
+ * context: ...,
365
+ * ...options...
366
+ * },
367
+ * // or:
368
+ * function() {
369
+ * return {
370
+ * context: ...,
371
+ * ...options...
372
+ * };
373
+ * }
374
+ * ]
375
+ */
376
+ options.proxy.forEach((proxyConfigOrCallback) => {
377
+ let proxyConfig;
378
+ let proxyMiddleware;
359
379
 
360
- app.use((req, res, next) => {
361
380
  if (typeof proxyConfigOrCallback === 'function') {
362
- const newProxyConfig = proxyConfigOrCallback();
363
-
364
- if (newProxyConfig !== proxyConfig) {
365
- proxyConfig = newProxyConfig;
366
- proxyMiddleware = getProxyMiddleware(proxyConfig);
367
- }
381
+ proxyConfig = proxyConfigOrCallback();
382
+ } else {
383
+ proxyConfig = proxyConfigOrCallback;
368
384
  }
369
385
 
370
- const bypass = typeof proxyConfig.bypass === 'function';
386
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
371
387
 
372
- const bypassUrl = (bypass && proxyConfig.bypass(req, res, proxyConfig)) || false;
388
+ if (proxyConfig.ws) {
389
+ websocketProxies.push(proxyMiddleware);
390
+ }
373
391
 
374
- if (bypassUrl) {
375
- req.url = bypassUrl;
392
+ app.use((req, res, next) => {
393
+ if (typeof proxyConfigOrCallback === 'function') {
394
+ const newProxyConfig = proxyConfigOrCallback();
376
395
 
377
- next();
378
- } else if (proxyMiddleware) {
379
- return proxyMiddleware(req, res, next);
380
- } else {
381
- next();
382
- }
383
- });
384
- });
385
- }
386
- },
387
- historyApiFallback: () => {
388
- if (options.historyApiFallback) {
389
- const fallback = typeof options.historyApiFallback === 'object'
390
- ? options.historyApiFallback
391
- : null;
392
- // Fall back to /index.html if nothing else matches.
393
- app.use(historyApiFallback(fallback));
394
- }
395
- },
396
- contentBaseFiles: () => {
397
- if (Array.isArray(contentBase)) {
398
- contentBase.forEach((item) => {
399
- app.get('*', express.static(item));
400
- });
401
- } else if (/^(https?:)?\/\//.test(contentBase)) {
402
- this.log.warn(
403
- 'Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
404
- );
396
+ if (newProxyConfig !== proxyConfig) {
397
+ proxyConfig = newProxyConfig;
398
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
399
+ }
400
+ }
405
401
 
406
- this.log.warn(
407
- 'proxy: {\n\t"*": "<your current contentBase configuration>"\n}'
408
- );
409
- // Redirect every request to contentBase
410
- app.get('*', (req, res) => {
411
- res.writeHead(302, {
412
- Location: contentBase + req.path + (req._parsedUrl.search || '')
402
+ // - Check if we have a bypass function defined
403
+ // - In case the bypass function is defined we'll retrieve the
404
+ // bypassUrl from it otherwise byPassUrl would be null
405
+ const isByPassFuncDefined =
406
+ typeof proxyConfig.bypass === 'function';
407
+ const bypassUrl = isByPassFuncDefined
408
+ ? proxyConfig.bypass(req, res, proxyConfig)
409
+ : null;
410
+
411
+ if (typeof bypassUrl === 'boolean') {
412
+ // skip the proxy
413
+ req.url = null;
414
+ next();
415
+ } else if (typeof bypassUrl === 'string') {
416
+ // byPass to that url
417
+ req.url = bypassUrl;
418
+ next();
419
+ } else if (proxyMiddleware) {
420
+ return proxyMiddleware(req, res, next);
421
+ } else {
422
+ next();
423
+ }
424
+ });
425
+ });
426
+ }
427
+ },
428
+ historyApiFallback: () => {
429
+ if (options.historyApiFallback) {
430
+ const fallback =
431
+ typeof options.historyApiFallback === 'object'
432
+ ? options.historyApiFallback
433
+ : null;
434
+ // Fall back to /index.html if nothing else matches.
435
+ app.use(historyApiFallback(fallback));
436
+ }
437
+ },
438
+ contentBaseFiles: () => {
439
+ if (Array.isArray(contentBase)) {
440
+ contentBase.forEach((item) => {
441
+ app.get('*', express.static(item));
413
442
  });
443
+ } else if (/^(https?:)?\/\//.test(contentBase)) {
444
+ this.log.warn(
445
+ 'Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
446
+ );
447
+
448
+ this.log.warn(
449
+ 'proxy: {\n\t"*": "<your current contentBase configuration>"\n}'
450
+ );
451
+ // Redirect every request to contentBase
452
+ app.get('*', (req, res) => {
453
+ res.writeHead(302, {
454
+ Location: contentBase + req.path + (req._parsedUrl.search || ''),
455
+ });
456
+
457
+ res.end();
458
+ });
459
+ } else if (typeof contentBase === 'number') {
460
+ this.log.warn(
461
+ 'Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
462
+ );
463
+
464
+ this.log.warn(
465
+ 'proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'
466
+ );
467
+ // Redirect every request to the port contentBase
468
+ app.get('*', (req, res) => {
469
+ res.writeHead(302, {
470
+ Location: `//localhost:${contentBase}${req.path}${req._parsedUrl
471
+ .search || ''}`,
472
+ });
473
+
474
+ res.end();
475
+ });
476
+ } else {
477
+ // route content request
478
+ app.get('*', express.static(contentBase, options.staticOptions));
479
+ }
480
+ },
481
+ contentBaseIndex: () => {
482
+ if (Array.isArray(contentBase)) {
483
+ contentBase.forEach((item) => {
484
+ app.get('*', serveIndex(item));
485
+ });
486
+ } else if (
487
+ !/^(https?:)?\/\//.test(contentBase) &&
488
+ typeof contentBase !== 'number'
489
+ ) {
490
+ app.get('*', serveIndex(contentBase));
491
+ }
492
+ },
493
+ watchContentBase: () => {
494
+ if (
495
+ /^(https?:)?\/\//.test(contentBase) ||
496
+ typeof contentBase === 'number'
497
+ ) {
498
+ throw new Error('Watching remote files is not supported.');
499
+ } else if (Array.isArray(contentBase)) {
500
+ contentBase.forEach((item) => {
501
+ this._watch(item);
502
+ });
503
+ } else {
504
+ this._watch(contentBase);
505
+ }
506
+ },
507
+ before: () => {
508
+ if (typeof options.before === 'function') {
509
+ options.before(app, this, compiler);
510
+ }
511
+ },
512
+ middleware: () => {
513
+ // include our middleware to ensure
514
+ // it is able to handle '/index.html' request after redirect
515
+ app.use(this.middleware);
516
+ },
517
+ after: () => {
518
+ if (typeof options.after === 'function') {
519
+ options.after(app, this, compiler);
520
+ }
521
+ },
522
+ headers: () => {
523
+ app.all('*', this.setContentHeaders.bind(this));
524
+ },
525
+ magicHtml: () => {
526
+ app.get('*', this.serveMagicHtml.bind(this));
527
+ },
528
+ setup: () => {
529
+ if (typeof options.setup === 'function') {
530
+ this.log.warn(
531
+ 'The `setup` option is deprecated and will be removed in v4. Please update your config to use `before`'
532
+ );
533
+
534
+ options.setup(app, this);
535
+ }
536
+ },
537
+ };
414
538
 
415
- res.end();
416
- });
417
- } else if (typeof contentBase === 'number') {
418
- this.log.warn(
419
- 'Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
420
- );
539
+ const defaultFeatures = ['setup', 'before', 'headers', 'middleware'];
421
540
 
422
- this.log.warn(
423
- 'proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'
424
- );
425
- // Redirect every request to the port contentBase
426
- app.get('*', (req, res) => {
427
- res.writeHead(302, {
428
- Location: `//localhost:${contentBase}${req.path}${req._parsedUrl.search || ''}`
429
- });
541
+ if (options.proxy) {
542
+ defaultFeatures.push('proxy', 'middleware');
543
+ }
430
544
 
431
- res.end();
432
- });
433
- } else {
434
- // route content request
435
- app.get('*', express.static(contentBase, options.staticOptions));
436
- }
437
- },
438
- contentBaseIndex: () => {
439
- if (Array.isArray(contentBase)) {
440
- contentBase.forEach((item) => {
441
- app.get('*', serveIndex(item));
442
- });
443
- } else if (
444
- !/^(https?:)?\/\//.test(contentBase) &&
445
- typeof contentBase !== 'number'
446
- ) {
447
- app.get('*', serveIndex(contentBase));
448
- }
449
- },
450
- watchContentBase: () => {
451
- if (
452
- /^(https?:)?\/\//.test(contentBase) ||
453
- typeof contentBase === 'number'
454
- ) {
455
- throw new Error('Watching remote files is not supported.');
456
- } else if (Array.isArray(contentBase)) {
457
- contentBase.forEach((item) => {
458
- this._watch(item);
459
- });
460
- } else {
461
- this._watch(contentBase);
462
- }
463
- },
464
- before: () => {
465
- if (typeof options.before === 'function') {
466
- options.before(app, this);
467
- }
468
- },
469
- middleware: () => {
470
- // include our middleware to ensure
471
- // it is able to handle '/index.html' request after redirect
472
- app.use(this.middleware);
473
- },
474
- after: () => {
475
- if (typeof options.after === 'function') {
476
- options.after(app, this);
477
- }
478
- },
479
- headers: () => {
480
- app.all('*', this.setContentHeaders.bind(this));
481
- },
482
- magicHtml: () => {
483
- app.get('*', this.serveMagicHtml.bind(this));
484
- },
485
- setup: () => {
486
- if (typeof options.setup === 'function') {
487
- this.log.warn(
488
- 'The `setup` option is deprecated and will be removed in v3. Please update your config to use `before`'
489
- );
545
+ if (contentBase !== false) {
546
+ defaultFeatures.push('contentBaseFiles');
547
+ }
490
548
 
491
- options.setup(app, this);
549
+ if (options.watchContentBase) {
550
+ defaultFeatures.push('watchContentBase');
551
+ }
552
+
553
+ if (options.historyApiFallback) {
554
+ defaultFeatures.push('historyApiFallback', 'middleware');
555
+
556
+ if (contentBase !== false) {
557
+ defaultFeatures.push('contentBaseFiles');
492
558
  }
493
559
  }
494
- };
495
560
 
496
- const defaultFeatures = [
497
- 'setup',
498
- 'before',
499
- 'headers',
500
- 'middleware'
501
- ];
561
+ defaultFeatures.push('magicHtml');
502
562
 
503
- if (options.proxy) {
504
- defaultFeatures.push('proxy', 'middleware');
505
- }
563
+ // checking if it's set to true or not set (Default : undefined => true)
564
+ this.serveIndex = this.serveIndex || this.serveIndex === undefined;
506
565
 
507
- if (contentBase !== false) {
508
- defaultFeatures.push('contentBaseFiles');
509
- }
566
+ const shouldHandleServeIndex = contentBase && this.serveIndex;
510
567
 
511
- if (options.watchContentBase) {
512
- defaultFeatures.push('watchContentBase');
513
- }
568
+ if (shouldHandleServeIndex) {
569
+ defaultFeatures.push('contentBaseIndex');
570
+ }
514
571
 
515
- if (options.historyApiFallback) {
516
- defaultFeatures.push('historyApiFallback', 'middleware');
572
+ // compress is placed last and uses unshift so that it will be the first middleware used
573
+ if (options.compress) {
574
+ defaultFeatures.unshift('compress');
575
+ }
517
576
 
518
- if (contentBase !== false) {
519
- defaultFeatures.push('contentBaseFiles');
577
+ if (options.after) {
578
+ defaultFeatures.push('after');
520
579
  }
521
- }
522
580
 
523
- defaultFeatures.push('magicHtml');
581
+ (options.features || defaultFeatures).forEach((feature) => {
582
+ features[feature]();
583
+ });
524
584
 
525
- if (contentBase !== false) {
526
- defaultFeatures.push('contentBaseIndex');
527
- }
528
- // compress is placed last and uses unshift so that it will be the first middleware used
529
- if (options.compress) {
530
- defaultFeatures.unshift('compress');
531
- }
585
+ if (options.https) {
586
+ // for keep supporting CLI parameters
587
+ if (typeof options.https === 'boolean') {
588
+ options.https = {
589
+ ca: options.ca,
590
+ pfx: options.pfx,
591
+ key: options.key,
592
+ cert: options.cert,
593
+ passphrase: options.pfxPassphrase,
594
+ requestCert: options.requestCert || false,
595
+ };
596
+ }
532
597
 
533
- if (options.after) {
534
- defaultFeatures.push('after');
535
- }
598
+ for (const property of ['ca', 'pfx', 'key', 'cert']) {
599
+ const value = options.https[property];
600
+ const isBuffer = value instanceof Buffer;
536
601
 
537
- (options.features || defaultFeatures).forEach((feature) => {
538
- features[feature]();
539
- });
540
-
541
- if (options.https) {
542
- // for keep supporting CLI parameters
543
- if (typeof options.https === 'boolean') {
544
- options.https = {
545
- ca: options.ca,
546
- pfx: options.pfx,
547
- key: options.key,
548
- cert: options.cert,
549
- passphrase: options.pfxPassphrase,
550
- requestCert: options.requestCert || false
551
- };
552
- }
602
+ if (value && !isBuffer) {
603
+ let stats = null;
553
604
 
554
- let fakeCert;
605
+ try {
606
+ stats = fs.lstatSync(value).isFile();
607
+ } catch (error) {
608
+ // ignore error
609
+ }
555
610
 
556
- if (!options.https.key || !options.https.cert) {
557
- // Use a self-signed certificate if no certificate was configured.
558
- // Cycle certs every 24 hours
559
- const certPath = path.join(__dirname, '../ssl/server.pem');
611
+ if (stats) {
612
+ // It is file
613
+ options.https[property] = fs.readFileSync(path.resolve(value));
614
+ } else {
615
+ options.https[property] = value;
616
+ }
617
+ }
618
+ }
560
619
 
561
- let certExists = fs.existsSync(certPath);
620
+ let fakeCert;
562
621
 
563
- if (certExists) {
564
- const certTtl = 1000 * 60 * 60 * 24;
565
- const certStat = fs.statSync(certPath);
622
+ if (!options.https.key || !options.https.cert) {
623
+ // Use a self-signed certificate if no certificate was configured.
624
+ // Cycle certs every 24 hours
625
+ const certPath = path.join(__dirname, '../ssl/server.pem');
566
626
 
567
- const now = new Date();
627
+ let certExists = fs.existsSync(certPath);
568
628
 
569
- // cert is more than 30 days old, kill it with fire
570
- if ((now - certStat.ctime) / certTtl > 30) {
571
- this.log.info('SSL Certificate is more than 30 days old. Removing.');
629
+ if (certExists) {
630
+ const certTtl = 1000 * 60 * 60 * 24;
631
+ const certStat = fs.statSync(certPath);
572
632
 
573
- del.sync([certPath], { force: true });
633
+ const now = new Date();
574
634
 
575
- certExists = false;
635
+ // cert is more than 30 days old, kill it with fire
636
+ if ((now - certStat.ctime) / certTtl > 30) {
637
+ this.log.info(
638
+ 'SSL Certificate is more than 30 days old. Removing.'
639
+ );
640
+
641
+ del.sync([certPath], { force: true });
642
+
643
+ certExists = false;
644
+ }
576
645
  }
577
- }
578
646
 
579
- if (!certExists) {
580
- this.log.info('Generating SSL Certificate');
647
+ if (!certExists) {
648
+ this.log.info('Generating SSL Certificate');
581
649
 
582
- const attrs = [
583
- { name: 'commonName', value: 'localhost' }
584
- ];
650
+ const attrs = [{ name: 'commonName', value: 'localhost' }];
585
651
 
586
- const pems = createCertificate(attrs);
652
+ const pems = createCertificate(attrs);
587
653
 
588
- fs.writeFileSync(
589
- certPath,
590
- pems.private + pems.cert,
591
- { encoding: 'utf-8' }
592
- );
654
+ fs.writeFileSync(certPath, pems.private + pems.cert, {
655
+ encoding: 'utf8',
656
+ });
657
+ }
658
+
659
+ fakeCert = fs.readFileSync(certPath);
593
660
  }
594
661
 
595
- fakeCert = fs.readFileSync(certPath);
596
- }
662
+ options.https.key = options.https.key || fakeCert;
663
+ options.https.cert = options.https.cert || fakeCert;
597
664
 
598
- options.https.key = options.https.key || fakeCert;
599
- options.https.cert = options.https.cert || fakeCert;
665
+ // Only prevent HTTP/2 if http2 is explicitly set to false
666
+ const isHttp2 = options.http2 !== false;
600
667
 
601
- if (!options.https.spdy) {
602
- options.https.spdy = {
603
- protocols: ['h2', 'http/1.1']
604
- };
605
- }
668
+ // note that options.spdy never existed. The user was able
669
+ // to set options.https.spdy before, though it was not in the
670
+ // docs. Keep options.https.spdy if the user sets it for
671
+ // backwards compatability, but log a deprecation warning.
672
+ if (options.https.spdy) {
673
+ // for backwards compatability: if options.https.spdy was passed in before,
674
+ // it was not altered in any way
675
+ this.log.warn(
676
+ 'Providing custom spdy server options is deprecated and will be removed in the next major version.'
677
+ );
678
+ } else {
679
+ // if the normal https server gets this option, it will not affect it.
680
+ options.https.spdy = {
681
+ protocols: ['h2', 'http/1.1'],
682
+ };
683
+ }
606
684
 
607
- // `spdy` is effectively unmaintained, and as a consequence of an
608
- // implementation that extensively relies on Node’s non-public APIs, broken
609
- // on Node 10 and above. In those cases, only https will be used for now.
610
- // Once express supports Node's built-in HTTP/2 support, migrating over to
611
- // that should be the best way to go.
612
- // The relevant issues are:
613
- // - https://github.com/nodejs/node/issues/21665
614
- // - https://github.com/webpack/webpack-dev-server/issues/1449
615
- // - https://github.com/expressjs/express/issues/3388
616
- if (semver.gte(process.version, '10.0.0')) {
617
- this.listeningApp = https.createServer(options.https, app);
685
+ // `spdy` is effectively unmaintained, and as a consequence of an
686
+ // implementation that extensively relies on Node’s non-public APIs, broken
687
+ // on Node 10 and above. In those cases, only https will be used for now.
688
+ // Once express supports Node's built-in HTTP/2 support, migrating over to
689
+ // that should be the best way to go.
690
+ // The relevant issues are:
691
+ // - https://github.com/nodejs/node/issues/21665
692
+ // - https://github.com/webpack/webpack-dev-server/issues/1449
693
+ // - https://github.com/expressjs/express/issues/3388
694
+ if (semver.gte(process.version, '10.0.0') || !isHttp2) {
695
+ if (options.http2) {
696
+ // the user explicitly requested http2 but is not getting it because
697
+ // of the node version.
698
+ this.log.warn(
699
+ 'HTTP/2 is currently unsupported for Node 10.0.0 and above, but will be supported once Express supports it'
700
+ );
701
+ }
702
+ this.listeningApp = https.createServer(options.https, app);
703
+ } else {
704
+ /* eslint-disable global-require */
705
+ // The relevant issues are:
706
+ // https://github.com/spdy-http2/node-spdy/issues/350
707
+ // https://github.com/webpack/webpack-dev-server/issues/1592
708
+ this.listeningApp = require('spdy').createServer(options.https, app);
709
+ /* eslint-enable global-require */
710
+ }
618
711
  } else {
619
- this.listeningApp = spdy.createServer(options.https, app);
712
+ this.listeningApp = http.createServer(app);
620
713
  }
621
- } else {
622
- this.listeningApp = http.createServer(app);
623
- }
624
714
 
625
- killable(this.listeningApp);
715
+ killable(this.listeningApp);
626
716
 
627
- // Proxy websockets without the initial http request
628
- // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
629
- websocketProxies.forEach(function (wsProxy) {
630
- this.listeningApp.on('upgrade', wsProxy.upgrade);
631
- }, this);
632
- }
717
+ // Proxy websockets without the initial http request
718
+ // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
719
+ websocketProxies.forEach(function(wsProxy) {
720
+ this.listeningApp.on('upgrade', wsProxy.upgrade);
721
+ }, this);
722
+ }
633
723
 
634
- Server.prototype.use = function () {
635
- // eslint-disable-next-line
636
- this.app.use.apply(this.app, arguments);
637
- };
724
+ getStats(statsObj) {
725
+ const stats = Server.DEFAULT_STATS;
638
726
 
639
- Server.prototype.setContentHeaders = function (req, res, next) {
640
- if (this.headers) {
641
- for (const name in this.headers) { // eslint-disable-line
642
- res.setHeader(name, this.headers[name]);
727
+ if (this.originalStats.warningsFilter) {
728
+ stats.warningsFilter = this.originalStats.warningsFilter;
643
729
  }
644
- }
645
730
 
646
- next();
647
- };
731
+ return statsObj.toJson(stats);
732
+ }
648
733
 
649
- Server.prototype.checkHost = function (headers, headerToCheck) {
650
- // allow user to opt-out this security check, at own risk
651
- if (this.disableHostCheck) {
652
- return true;
734
+ use() {
735
+ // eslint-disable-next-line
736
+ this.app.use.apply(this.app, arguments);
653
737
  }
654
738
 
655
- if (!headerToCheck) headerToCheck = 'host';
656
- // get the Host header and extract hostname
657
- // we don't care about port not matching
658
- const hostHeader = headers[headerToCheck];
739
+ setContentHeaders(req, res, next) {
740
+ if (this.headers) {
741
+ // eslint-disable-next-line
742
+ for (const name in this.headers) {
743
+ // eslint-disable-line
744
+ res.setHeader(name, this.headers[name]);
745
+ }
746
+ }
659
747
 
660
- if (!hostHeader) {
661
- return false;
748
+ next();
662
749
  }
663
750
 
664
- // use the node url-parser to retrieve the hostname from the host-header.
665
- const hostname = url.parse(
666
- // if hostHeader doesn't have scheme, add // for parsing.
667
- /^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
668
- false,
669
- true
670
- ).hostname;
671
- // always allow requests with explicit IPv4 or IPv6-address.
672
- // A note on IPv6 addresses:
673
- // hostHeader will always contain the brackets denoting
674
- // an IPv6-address in URLs,
675
- // these are removed from the hostname in url.parse(),
676
- // so we have the pure IPv6-address in hostname.
677
- if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) {
678
- return true;
751
+ checkHost(headers) {
752
+ return this.checkHeaders(headers, 'host');
679
753
  }
680
- // always allow localhost host, for convience
681
- if (hostname === 'localhost') {
682
- return true;
754
+
755
+ checkOrigin(headers) {
756
+ return this.checkHeaders(headers, 'origin');
683
757
  }
684
- // allow if hostname is in allowedHosts
685
- if (this.allowedHosts && this.allowedHosts.length) {
686
- for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
687
- const allowedHost = this.allowedHosts[hostIdx];
688
-
689
- if (allowedHost === hostname) return true;
690
-
691
- // support "." as a subdomain wildcard
692
- // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
693
- if (allowedHost[0] === '.') {
694
- // "example.com"
695
- if (hostname === allowedHost.substring(1)) {
696
- return true;
697
- }
698
- // "*.example.com"
699
- if (hostname.endsWith(allowedHost)) {
700
- return true;
701
- }
702
- }
758
+
759
+ checkHeaders(headers, headerToCheck) {
760
+ // allow user to opt-out this security check, at own risk
761
+ if (this.disableHostCheck) {
762
+ return true;
703
763
  }
704
- }
705
764
 
706
- // allow hostname of listening adress
707
- if (hostname === this.hostname) {
708
- return true;
709
- }
765
+ if (!headerToCheck) {
766
+ headerToCheck = 'host';
767
+ }
710
768
 
711
- // also allow public hostname if provided
712
- if (typeof this.publicHost === 'string') {
713
- const idxPublic = this.publicHost.indexOf(':');
769
+ // get the Host header and extract hostname
770
+ // we don't care about port not matching
771
+ const hostHeader = headers[headerToCheck];
714
772
 
715
- const publicHostname = idxPublic >= 0
716
- ? this.publicHost.substr(0, idxPublic)
717
- : this.publicHost;
773
+ if (!hostHeader) {
774
+ return false;
775
+ }
718
776
 
719
- if (hostname === publicHostname) {
777
+ // use the node url-parser to retrieve the hostname from the host-header.
778
+ const hostname = url.parse(
779
+ // if hostHeader doesn't have scheme, add // for parsing.
780
+ /^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
781
+ false,
782
+ true
783
+ ).hostname;
784
+ // always allow requests with explicit IPv4 or IPv6-address.
785
+ // A note on IPv6 addresses:
786
+ // hostHeader will always contain the brackets denoting
787
+ // an IPv6-address in URLs,
788
+ // these are removed from the hostname in url.parse(),
789
+ // so we have the pure IPv6-address in hostname.
790
+ if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) {
720
791
  return true;
721
792
  }
722
- }
793
+ // always allow localhost host, for convience
794
+ if (hostname === 'localhost') {
795
+ return true;
796
+ }
797
+ // allow if hostname is in allowedHosts
798
+ if (this.allowedHosts && this.allowedHosts.length) {
799
+ for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
800
+ const allowedHost = this.allowedHosts[hostIdx];
723
801
 
724
- // disallow
725
- return false;
726
- };
727
-
728
- // delegate listen call and init sockjs
729
- Server.prototype.listen = function (port, hostname, fn) {
730
- this.hostname = hostname;
731
-
732
- const returnValue = this.listeningApp.listen(port, hostname, (err) => {
733
- const socket = sockjs.createServer({
734
- // Use provided up-to-date sockjs-client
735
- sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
736
- // Limit useless logs
737
- log: (severity, line) => {
738
- if (severity === 'error') {
739
- this.log.error(line);
740
- } else {
741
- this.log.debug(line);
802
+ if (allowedHost === hostname) {
803
+ return true;
742
804
  }
743
- }
744
- });
745
805
 
746
- socket.on('connection', (connection) => {
747
- if (!connection) {
748
- return;
806
+ // support "." as a subdomain wildcard
807
+ // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
808
+ if (allowedHost[0] === '.') {
809
+ // "example.com"
810
+ if (hostname === allowedHost.substring(1)) {
811
+ return true;
812
+ }
813
+ // "*.example.com"
814
+ if (hostname.endsWith(allowedHost)) {
815
+ return true;
816
+ }
817
+ }
749
818
  }
819
+ }
820
+
821
+ // allow hostname of listening adress
822
+ if (hostname === this.hostname) {
823
+ return true;
824
+ }
750
825
 
751
- if (!this.checkHost(connection.headers) || !this.checkHost(connection.headers, 'origin')) {
752
- this.sockWrite([ connection ], 'error', 'Invalid Host/Origin header');
826
+ // also allow public hostname if provided
827
+ if (typeof this.publicHost === 'string') {
828
+ const idxPublic = this.publicHost.indexOf(':');
753
829
 
754
- connection.close();
830
+ const publicHostname =
831
+ idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
755
832
 
756
- return;
833
+ if (hostname === publicHostname) {
834
+ return true;
757
835
  }
836
+ }
758
837
 
759
- this.sockets.push(connection);
838
+ // disallow
839
+ return false;
840
+ }
760
841
 
761
- connection.on('close', () => {
762
- const idx = this.sockets.indexOf(connection);
842
+ // delegate listen call and init sockjs
843
+ listen(port, hostname, fn) {
844
+ this.hostname = hostname;
845
+
846
+ return this.listeningApp.listen(port, hostname, (err) => {
847
+ const socket = sockjs.createServer({
848
+ // Use provided up-to-date sockjs-client
849
+ sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
850
+ // Limit useless logs
851
+ log: (severity, line) => {
852
+ if (severity === 'error') {
853
+ this.log.error(line);
854
+ } else {
855
+ this.log.debug(line);
856
+ }
857
+ },
858
+ });
763
859
 
764
- if (idx >= 0) {
765
- this.sockets.splice(idx, 1);
860
+ socket.on('connection', (connection) => {
861
+ if (!connection) {
862
+ return;
766
863
  }
767
- });
768
864
 
769
- if (this.hot) {
770
- this.sockWrite([ connection ], 'hot');
771
- }
865
+ if (
866
+ !this.checkHost(connection.headers) ||
867
+ !this.checkOrigin(connection.headers)
868
+ ) {
869
+ this.sockWrite([connection], 'error', 'Invalid Host/Origin header');
772
870
 
773
- if (this.progress) {
774
- this.sockWrite([ connection ], 'progress', this.progress);
775
- }
871
+ connection.close();
776
872
 
777
- if (this.clientOverlay) {
778
- this.sockWrite([ connection ], 'overlay', this.clientOverlay);
779
- }
873
+ return;
874
+ }
780
875
 
781
- if (this.clientLogLevel) {
782
- this.sockWrite([ connection ], 'log-level', this.clientLogLevel);
783
- }
876
+ this.sockets.push(connection);
784
877
 
785
- if (!this._stats) {
786
- return;
787
- }
878
+ connection.on('close', () => {
879
+ const idx = this.sockets.indexOf(connection);
788
880
 
789
- this._sendStats([ connection ], this._stats.toJson(STATS), true);
790
- });
881
+ if (idx >= 0) {
882
+ this.sockets.splice(idx, 1);
883
+ }
884
+ });
791
885
 
792
- socket.installHandlers(this.listeningApp, {
793
- prefix: '/sockjs-node'
794
- });
886
+ if (this.hot) {
887
+ this.sockWrite([connection], 'hot');
888
+ }
795
889
 
796
- if (fn) {
797
- fn.call(this.listeningApp, err);
798
- }
799
- });
890
+ if (this.progress) {
891
+ this.sockWrite([connection], 'progress', this.progress);
892
+ }
800
893
 
801
- return returnValue;
802
- };
894
+ if (this.clientOverlay) {
895
+ this.sockWrite([connection], 'overlay', this.clientOverlay);
896
+ }
803
897
 
804
- Server.prototype.close = function (cb) {
805
- this.sockets.forEach((socket) => {
806
- socket.close();
807
- });
898
+ if (this.clientLogLevel) {
899
+ this.sockWrite([connection], 'log-level', this.clientLogLevel);
900
+ }
808
901
 
809
- this.sockets = [];
902
+ if (!this._stats) {
903
+ return;
904
+ }
810
905
 
811
- this.contentBaseWatchers.forEach((watcher) => {
812
- watcher.close();
813
- });
906
+ this._sendStats([connection], this.getStats(this._stats), true);
907
+ });
814
908
 
815
- this.contentBaseWatchers = [];
909
+ socket.installHandlers(this.listeningApp, {
910
+ prefix: this.sockPath,
911
+ });
816
912
 
817
- this.listeningApp.kill(() => {
818
- this.middleware.close(cb);
819
- });
820
- };
913
+ if (fn) {
914
+ fn.call(this.listeningApp, err);
915
+ }
916
+ });
917
+ }
821
918
 
822
- Server.prototype.sockWrite = function (sockets, type, data) {
823
- sockets.forEach((socket) => {
824
- socket.write(
825
- JSON.stringify({ type, data })
826
- );
827
- });
828
- };
919
+ close(cb) {
920
+ this.sockets.forEach((socket) => {
921
+ socket.close();
922
+ });
829
923
 
830
- Server.prototype.serveMagicHtml = function (req, res, next) {
831
- const _path = req.path;
924
+ this.sockets = [];
832
925
 
833
- try {
834
- const isFile = this.middleware.fileSystem.statSync(
835
- this.middleware.getFilenameFromUrl(`${_path}.js`)
836
- ).isFile();
926
+ this.contentBaseWatchers.forEach((watcher) => {
927
+ watcher.close();
928
+ });
837
929
 
838
- if (!isFile) {
839
- return next();
840
- }
841
- // Serve a page that executes the javascript
842
- res.write(
843
- '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="'
844
- );
845
- res.write(_path);
846
- res.write('.js');
847
- res.write(req._parsedUrl.search || '');
930
+ this.contentBaseWatchers = [];
848
931
 
849
- res.end('"></script></body></html>');
850
- } catch (err) {
851
- return next();
932
+ this.listeningApp.kill(() => {
933
+ this.middleware.close(cb);
934
+ });
852
935
  }
853
- };
854
-
855
- // send stats to a socket or multiple sockets
856
- Server.prototype._sendStats = function (sockets, stats, force) {
857
- if (
858
- !force &&
859
- stats &&
860
- (!stats.errors || stats.errors.length === 0) &&
861
- stats.assets &&
862
- stats.assets.every(asset => !asset.emitted)
863
- ) {
864
- return this.sockWrite(sockets, 'still-ok');
936
+
937
+ // eslint-disable-next-line
938
+ sockWrite(sockets, type, data) {
939
+ sockets.forEach((socket) => {
940
+ socket.write(JSON.stringify({ type, data }));
941
+ });
865
942
  }
866
943
 
867
- this.sockWrite(sockets, 'hash', stats.hash);
944
+ serveMagicHtml(req, res, next) {
945
+ const _path = req.path;
868
946
 
869
- if (stats.errors.length > 0) {
870
- this.sockWrite(sockets, 'errors', stats.errors);
871
- } else if (stats.warnings.length > 0) {
872
- this.sockWrite(sockets, 'warnings', stats.warnings);
873
- } else {
874
- this.sockWrite(sockets, 'ok');
947
+ try {
948
+ const isFile = this.middleware.fileSystem
949
+ .statSync(this.middleware.getFilenameFromUrl(`${_path}.js`))
950
+ .isFile();
951
+
952
+ if (!isFile) {
953
+ return next();
954
+ }
955
+ // Serve a page that executes the javascript
956
+ res.write(
957
+ '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="'
958
+ );
959
+ res.write(_path);
960
+ res.write('.js');
961
+ res.write(req._parsedUrl.search || '');
962
+
963
+ res.end('"></script></body></html>');
964
+ } catch (err) {
965
+ return next();
966
+ }
875
967
  }
876
- };
877
-
878
- Server.prototype._watch = function (watchPath) {
879
- // duplicate the same massaging of options that watchpack performs
880
- // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
881
- // this isn't an elegant solution, but we'll improve it in the future
882
- const usePolling = this.watchOptions.poll ? true : undefined;
883
- const interval = typeof this.watchOptions.poll === 'number'
884
- ? this.watchOptions.poll
885
- : undefined;
886
-
887
- const options = {
888
- ignoreInitial: true,
889
- persistent: true,
890
- followSymlinks: false,
891
- depth: 0,
892
- atomic: false,
893
- alwaysStat: true,
894
- ignorePermissionErrors: true,
895
- ignored: this.watchOptions.ignored,
896
- usePolling,
897
- interval
898
- };
899
968
 
900
- const watcher = chokidar.watch(watchPath, options);
969
+ // send stats to a socket or multiple sockets
970
+ _sendStats(sockets, stats, force) {
971
+ if (
972
+ !force &&
973
+ stats &&
974
+ (!stats.errors || stats.errors.length === 0) &&
975
+ stats.assets &&
976
+ stats.assets.every((asset) => !asset.emitted)
977
+ ) {
978
+ return this.sockWrite(sockets, 'still-ok');
979
+ }
901
980
 
902
- watcher.on('change', () => {
903
- this.sockWrite(this.sockets, 'content-changed');
904
- });
981
+ this.sockWrite(sockets, 'hash', stats.hash);
905
982
 
906
- this.contentBaseWatchers.push(watcher);
907
- };
983
+ if (stats.errors.length > 0) {
984
+ this.sockWrite(sockets, 'errors', stats.errors);
985
+ } else if (stats.warnings.length > 0) {
986
+ this.sockWrite(sockets, 'warnings', stats.warnings);
987
+ } else {
988
+ this.sockWrite(sockets, 'ok');
989
+ }
990
+ }
991
+
992
+ _watch(watchPath) {
993
+ // duplicate the same massaging of options that watchpack performs
994
+ // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
995
+ // this isn't an elegant solution, but we'll improve it in the future
996
+ const usePolling = this.watchOptions.poll ? true : undefined;
997
+ const interval =
998
+ typeof this.watchOptions.poll === 'number'
999
+ ? this.watchOptions.poll
1000
+ : undefined;
1001
+
1002
+ const options = {
1003
+ ignoreInitial: true,
1004
+ persistent: true,
1005
+ followSymlinks: false,
1006
+ atomic: false,
1007
+ alwaysStat: true,
1008
+ ignorePermissionErrors: true,
1009
+ ignored: this.watchOptions.ignored,
1010
+ usePolling,
1011
+ interval,
1012
+ };
1013
+
1014
+ const watcher = chokidar.watch(watchPath, options);
1015
+
1016
+ watcher.on('change', () => {
1017
+ this.sockWrite(this.sockets, 'content-changed');
1018
+ });
908
1019
 
909
- Server.prototype.invalidate = function () {
910
- if (this.middleware) {
911
- this.middleware.invalidate();
1020
+ this.contentBaseWatchers.push(watcher);
912
1021
  }
913
- };
1022
+
1023
+ invalidate() {
1024
+ if (this.middleware) {
1025
+ this.middleware.invalidate();
1026
+ }
1027
+ }
1028
+ }
914
1029
 
915
1030
  // Export this logic,
916
1031
  // so that other implementations,