webpack-dev-server 2.11.0 → 2.11.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
@@ -1,706 +1,706 @@
1
- 'use strict';
2
-
3
- /* eslint func-names: off */
4
- require('./polyfills');
5
-
6
- const fs = require('fs');
7
- const http = require('http');
8
- const path = require('path');
9
- const url = require('url');
10
- const chokidar = require('chokidar');
11
- const compress = require('compression');
12
- const del = require('del');
13
- const express = require('express');
14
- const httpProxyMiddleware = require('http-proxy-middleware');
15
- const ip = require('ip');
16
- const killable = require('killable');
17
- const serveIndex = require('serve-index');
18
- const historyApiFallback = require('connect-history-api-fallback');
19
- const selfsigned = require('selfsigned');
20
- const sockjs = require('sockjs');
21
- const spdy = require('spdy');
22
- const webpack = require('webpack');
23
- const webpackDevMiddleware = require('webpack-dev-middleware');
24
- const OptionsValidationError = require('./OptionsValidationError');
25
- const optionsSchema = require('./optionsSchema.json');
26
-
27
- const clientStats = { errorDetails: false };
28
- const log = console.log; // eslint-disable-line no-console
29
-
30
- function Server(compiler, options) {
31
- // Default options
32
- if (!options) options = {};
33
-
34
- const validationErrors = webpack.validateSchema(optionsSchema, options);
35
- if (validationErrors.length) {
36
- throw new OptionsValidationError(validationErrors);
37
- }
38
-
39
- if (options.lazy && !options.filename) {
40
- throw new Error("'filename' option must be set in lazy mode.");
41
- }
42
-
43
- this.hot = options.hot || options.hotOnly;
44
- this.headers = options.headers;
45
- this.clientLogLevel = options.clientLogLevel;
46
- this.clientOverlay = options.overlay;
47
- this.progress = options.progress;
48
- this.disableHostCheck = !!options.disableHostCheck;
49
- this.publicHost = options.public;
50
- this.allowedHosts = options.allowedHosts;
51
- this.sockets = [];
52
- this.contentBaseWatchers = [];
53
- this.watchOptions = options.watchOptions || {};
54
-
55
- // Listening for events
56
- const invalidPlugin = () => {
57
- this.sockWrite(this.sockets, 'invalid');
58
- };
59
- if (this.progress) {
60
- const progressPlugin = new webpack.ProgressPlugin((percent, msg, addInfo) => {
61
- percent = Math.floor(percent * 100);
62
- if (percent === 100) msg = 'Compilation completed';
63
- if (addInfo) msg = `${msg} (${addInfo})`;
64
- this.sockWrite(this.sockets, 'progress-update', { percent, msg });
65
- });
66
- compiler.apply(progressPlugin);
67
- }
68
- compiler.plugin('compile', invalidPlugin);
69
- compiler.plugin('invalid', invalidPlugin);
70
- compiler.plugin('done', (stats) => {
71
- this._sendStats(this.sockets, stats.toJson(clientStats));
72
- this._stats = stats;
73
- });
74
-
75
- // Init express server
76
- const app = this.app = new express(); // eslint-disable-line
77
-
78
- app.all('*', (req, res, next) => { // eslint-disable-line
79
- if (this.checkHost(req.headers)) { return next(); }
80
- res.send('Invalid Host header');
81
- });
82
-
83
- const wdmOptions = {};
84
-
85
- if (options.quiet === true) {
86
- wdmOptions.logLevel = 'silent';
87
- }
88
- if (options.noInfo === true) {
89
- wdmOptions.logLevel = 'warn';
90
- }
91
- // middleware for serving webpack bundle
92
- this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));
93
-
94
- app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
95
- res.setHeader('Content-Type', 'application/javascript');
96
- fs.createReadStream(path.join(__dirname, '..', 'client', 'live.bundle.js')).pipe(res);
97
- });
98
-
99
- app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
100
- res.setHeader('Content-Type', 'application/javascript');
101
- fs.createReadStream(path.join(__dirname, '..', 'client', 'sockjs.bundle.js')).pipe(res);
102
- });
103
-
104
- app.get('/webpack-dev-server.js', (req, res) => {
105
- res.setHeader('Content-Type', 'application/javascript');
106
- fs.createReadStream(path.join(__dirname, '..', 'client', 'index.bundle.js')).pipe(res);
107
- });
108
-
109
- app.get('/webpack-dev-server/*', (req, res) => {
110
- res.setHeader('Content-Type', 'text/html');
111
- fs.createReadStream(path.join(__dirname, '..', 'client', 'live.html')).pipe(res);
112
- });
113
-
114
- app.get('/webpack-dev-server', (req, res) => {
115
- res.setHeader('Content-Type', 'text/html');
116
- res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>');
117
- const outputPath = this.middleware.getFilenameFromUrl(options.publicPath || '/');
118
- const filesystem = this.middleware.fileSystem;
119
-
120
- function writeDirectory(baseUrl, basePath) {
121
- const content = filesystem.readdirSync(basePath);
122
- res.write('<ul>');
123
- content.forEach((item) => {
124
- const p = `${basePath}/${item}`;
125
- if (filesystem.statSync(p).isFile()) {
126
- res.write('<li><a href="');
127
- res.write(baseUrl + item);
128
- res.write('">');
129
- res.write(item);
130
- res.write('</a></li>');
131
- if (/\.js$/.test(item)) {
132
- const htmlItem = item.substr(0, item.length - 3);
133
- res.write('<li><a href="');
134
- res.write(baseUrl + htmlItem);
135
- res.write('">');
136
- res.write(htmlItem);
137
- res.write('</a> (magic html for ');
138
- res.write(item);
139
- res.write(') (<a href="');
140
- res.write(baseUrl.replace(/(^(https?:\/\/[^\/]+)?\/)/, "$1webpack-dev-server/") + htmlItem); // eslint-disable-line
141
- res.write('">webpack-dev-server</a>)</li>');
142
- }
143
- } else {
144
- res.write('<li>');
145
- res.write(item);
146
- res.write('<br>');
147
- writeDirectory(`${baseUrl + item}/`, p);
148
- res.write('</li>');
149
- }
150
- });
151
- res.write('</ul>');
152
- }
153
- writeDirectory(options.publicPath || '/', outputPath);
154
- res.end('</body></html>');
155
- });
156
-
157
- let contentBase;
158
- if (options.contentBase !== undefined) { // eslint-disable-line
159
- contentBase = options.contentBase; // eslint-disable-line
160
- } else {
161
- contentBase = process.cwd();
162
- }
163
-
164
- // Keep track of websocket proxies for external websocket upgrade.
165
- const websocketProxies = [];
166
-
167
- const features = {
168
- compress() {
169
- if (options.compress) {
170
- // Enable gzip compression.
171
- app.use(compress());
172
- }
173
- },
174
-
175
- proxy() {
176
- if (options.proxy) {
177
- /**
178
- * Assume a proxy configuration specified as:
179
- * proxy: {
180
- * 'context': { options }
181
- * }
182
- * OR
183
- * proxy: {
184
- * 'context': 'target'
185
- * }
186
- */
187
- if (!Array.isArray(options.proxy)) {
188
- options.proxy = Object.keys(options.proxy).map((context) => {
189
- let proxyOptions;
190
- // For backwards compatibility reasons.
191
- const correctedContext = context.replace(/^\*$/, '**').replace(/\/\*$/, '');
192
-
193
- if (typeof options.proxy[context] === 'string') {
194
- proxyOptions = {
195
- context: correctedContext,
196
- target: options.proxy[context]
197
- };
198
- } else {
199
- proxyOptions = Object.assign({}, options.proxy[context]);
200
- proxyOptions.context = correctedContext;
201
- }
202
- proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
203
-
204
- return proxyOptions;
205
- });
206
- }
207
-
208
- const getProxyMiddleware = (proxyConfig) => {
209
- const context = proxyConfig.context || proxyConfig.path;
210
-
211
- // It is possible to use the `bypass` method without a `target`.
212
- // However, the proxy middleware has no use in this case, and will fail to instantiate.
213
- if (proxyConfig.target) {
214
- return httpProxyMiddleware(context, proxyConfig);
215
- }
216
- };
217
-
218
- /**
219
- * Assume a proxy configuration specified as:
220
- * proxy: [
221
- * {
222
- * context: ...,
223
- * ...options...
224
- * },
225
- * // or:
226
- * function() {
227
- * return {
228
- * context: ...,
229
- * ...options...
230
- * };
231
- * }
232
- * ]
233
- */
234
- options.proxy.forEach((proxyConfigOrCallback) => {
235
- let proxyConfig;
236
- let proxyMiddleware;
237
-
238
- if (typeof proxyConfigOrCallback === 'function') {
239
- proxyConfig = proxyConfigOrCallback();
240
- } else {
241
- proxyConfig = proxyConfigOrCallback;
242
- }
243
-
244
- proxyMiddleware = getProxyMiddleware(proxyConfig);
245
- if (proxyConfig.ws) {
246
- websocketProxies.push(proxyMiddleware);
247
- }
248
-
249
- app.use((req, res, next) => {
250
- if (typeof proxyConfigOrCallback === 'function') {
251
- const newProxyConfig = proxyConfigOrCallback();
252
- if (newProxyConfig !== proxyConfig) {
253
- proxyConfig = newProxyConfig;
254
- proxyMiddleware = getProxyMiddleware(proxyConfig);
255
- }
256
- }
257
- const bypass = typeof proxyConfig.bypass === 'function';
258
- // eslint-disable-next-line
259
- const bypassUrl = bypass && proxyConfig.bypass(req, res, proxyConfig) || false;
260
-
261
- if (bypassUrl) {
262
- req.url = bypassUrl;
263
- next();
264
- } else if (proxyMiddleware) {
265
- return proxyMiddleware(req, res, next);
266
- } else {
267
- next();
268
- }
269
- });
270
- });
271
- }
272
- },
273
-
274
- historyApiFallback() {
275
- if (options.historyApiFallback) {
276
- // Fall back to /index.html if nothing else matches.
277
- app.use(historyApiFallback(typeof options.historyApiFallback === 'object' ? options.historyApiFallback : null));
278
- }
279
- },
280
-
281
- contentBaseFiles() {
282
- if (Array.isArray(contentBase)) {
283
- contentBase.forEach((item) => {
284
- app.get('*', express.static(item));
285
- });
286
- } else if (/^(https?:)?\/\//.test(contentBase)) {
287
- log('Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.');
288
- log('proxy: {\n\t"*": "<your current contentBase configuration>"\n}'); // eslint-disable-line quotes
289
- // Redirect every request to contentBase
290
- app.get('*', (req, res) => {
291
- res.writeHead(302, {
292
- Location: contentBase + req.path + (req._parsedUrl.search || '')
293
- });
294
- res.end();
295
- });
296
- } else if (typeof contentBase === 'number') {
297
- log('Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.');
298
- log('proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'); // eslint-disable-line quotes
299
- // Redirect every request to the port contentBase
300
- app.get('*', (req, res) => {
301
- res.writeHead(302, {
302
- Location: `//localhost:${contentBase}${req.path}${req._parsedUrl.search || ''}`
303
- });
304
- res.end();
305
- });
306
- } else {
307
- // route content request
308
- app.get('*', express.static(contentBase, options.staticOptions));
309
- }
310
- },
311
-
312
- contentBaseIndex() {
313
- if (Array.isArray(contentBase)) {
314
- contentBase.forEach((item) => {
315
- app.get('*', serveIndex(item));
316
- });
317
- } else if (!/^(https?:)?\/\//.test(contentBase) && typeof contentBase !== 'number') {
318
- app.get('*', serveIndex(contentBase));
319
- }
320
- },
321
-
322
- watchContentBase: () => {
323
- if (/^(https?:)?\/\//.test(contentBase) || typeof contentBase === 'number') {
324
- throw new Error('Watching remote files is not supported.');
325
- } else if (Array.isArray(contentBase)) {
326
- contentBase.forEach((item) => {
327
- this._watch(item);
328
- });
329
- } else {
330
- this._watch(contentBase);
331
- }
332
- },
333
-
334
- before: () => {
335
- if (typeof options.before === 'function') {
336
- options.before(app, this);
337
- }
338
- },
339
-
340
- middleware: () => {
341
- // include our middleware to ensure it is able to handle '/index.html' request after redirect
342
- app.use(this.middleware);
343
- },
344
-
345
- after: () => {
346
- if (typeof options.after === 'function') { options.after(app, this); }
347
- },
348
-
349
- headers: () => {
350
- app.all('*', this.setContentHeaders.bind(this));
351
- },
352
-
353
- magicHtml: () => {
354
- app.get('*', this.serveMagicHtml.bind(this));
355
- },
356
-
357
- setup: () => {
358
- if (typeof options.setup === 'function') {
359
- log('The `setup` option is deprecated and will be removed in v3. Please update your config to use `before`');
360
- options.setup(app, this);
361
- }
362
- }
363
- };
364
-
365
- const defaultFeatures = ['before', 'setup', 'headers', 'middleware'];
366
- if (options.proxy) { defaultFeatures.push('proxy', 'middleware'); }
367
- if (contentBase !== false) { defaultFeatures.push('contentBaseFiles'); }
368
- if (options.watchContentBase) { defaultFeatures.push('watchContentBase'); }
369
- if (options.historyApiFallback) {
370
- defaultFeatures.push('historyApiFallback', 'middleware');
371
- if (contentBase !== false) { defaultFeatures.push('contentBaseFiles'); }
372
- }
373
- defaultFeatures.push('magicHtml');
374
- if (contentBase !== false) { defaultFeatures.push('contentBaseIndex'); }
375
- // compress is placed last and uses unshift so that it will be the first middleware used
376
- if (options.compress) { defaultFeatures.unshift('compress'); }
377
- if (options.after) { defaultFeatures.push('after'); }
378
-
379
- (options.features || defaultFeatures).forEach((feature) => {
380
- features[feature]();
381
- });
382
-
383
- if (options.https) {
384
- // for keep supporting CLI parameters
385
- if (typeof options.https === 'boolean') {
386
- options.https = {
387
- key: options.key,
388
- cert: options.cert,
389
- ca: options.ca,
390
- pfx: options.pfx,
391
- passphrase: options.pfxPassphrase,
392
- requestCert: options.requestCert || false
393
- };
394
- }
395
-
396
- let fakeCert;
397
- if (!options.https.key || !options.https.cert) {
398
- // Use a self-signed certificate if no certificate was configured.
399
- // Cycle certs every 24 hours
400
- const certPath = path.join(__dirname, '../ssl/server.pem');
401
- let certExists = fs.existsSync(certPath);
402
-
403
- if (certExists) {
404
- const certStat = fs.statSync(certPath);
405
- const certTtl = 1000 * 60 * 60 * 24;
406
- const now = new Date();
407
-
408
- // cert is more than 30 days old, kill it with fire
409
- if ((now - certStat.ctime) / certTtl > 30) {
410
- log('SSL Certificate is more than 30 days old. Removing.');
411
- del.sync([certPath], { force: true });
412
- certExists = false;
413
- }
414
- }
415
-
416
- if (!certExists) {
417
- log('Generating SSL Certificate');
418
- const attrs = [{ name: 'commonName', value: 'localhost' }];
419
- const pems = selfsigned.generate(attrs, {
420
- algorithm: 'sha256',
421
- days: 30,
422
- keySize: 2048,
423
- extensions: [{
424
- name: 'basicConstraints',
425
- cA: true
426
- }, {
427
- name: 'keyUsage',
428
- keyCertSign: true,
429
- digitalSignature: true,
430
- nonRepudiation: true,
431
- keyEncipherment: true,
432
- dataEncipherment: true
433
- }, {
434
- name: 'subjectAltName',
435
- altNames: [
436
- {
437
- // type 2 is DNS
438
- type: 2,
439
- value: 'localhost'
440
- },
441
- {
442
- type: 2,
443
- value: 'localhost.localdomain'
444
- },
445
- {
446
- type: 2,
447
- value: 'lvh.me'
448
- },
449
- {
450
- type: 2,
451
- value: '*.lvh.me'
452
- },
453
- {
454
- type: 2,
455
- value: '[::1]'
456
- },
457
- {
458
- // type 7 is IP
459
- type: 7,
460
- ip: '127.0.0.1'
461
- },
462
- {
463
- type: 7,
464
- ip: 'fe80::1'
465
- }
466
- ]
467
- }]
468
- });
469
-
470
- fs.writeFileSync(certPath, pems.private + pems.cert, { encoding: 'utf-8' });
471
- }
472
- fakeCert = fs.readFileSync(certPath);
473
- }
474
-
475
- options.https.key = options.https.key || fakeCert;
476
- options.https.cert = options.https.cert || fakeCert;
477
-
478
- if (!options.https.spdy) {
479
- options.https.spdy = {
480
- protocols: ['h2', 'http/1.1']
481
- };
482
- }
483
-
484
- this.listeningApp = spdy.createServer(options.https, app);
485
- } else {
486
- this.listeningApp = http.createServer(app);
487
- }
488
-
489
- killable(this.listeningApp);
490
-
491
- // Proxy websockets without the initial http request
492
- // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
493
- websocketProxies.forEach(function (wsProxy) {
494
- this.listeningApp.on('upgrade', wsProxy.upgrade);
495
- }, this);
496
- }
497
-
498
- Server.prototype.use = function () {
499
- // eslint-disable-next-line
500
- this.app.use.apply(this.app, arguments);
501
- };
502
-
503
- Server.prototype.setContentHeaders = function (req, res, next) {
504
- if (this.headers) {
505
- for (const name in this.headers) { // eslint-disable-line
506
- res.setHeader(name, this.headers[name]);
507
- }
508
- }
509
-
510
- next();
511
- };
512
-
513
- Server.prototype.checkHost = function (headers) {
514
- // allow user to opt-out this security check, at own risk
515
- if (this.disableHostCheck) return true;
516
-
517
- // get the Host header and extract hostname
518
- // we don't care about port not matching
519
- const hostHeader = headers.host;
520
- if (!hostHeader) return false;
521
-
522
- // use the node url-parser to retrieve the hostname from the host-header.
523
- const hostname = url.parse(`//${hostHeader}`, false, true).hostname;
524
-
525
- // always allow requests with explicit IPv4 or IPv6-address.
526
- // A note on IPv6 addresses: hostHeader will always contain the brackets denoting
527
- // an IPv6-address in URLs, these are removed from the hostname in url.parse(),
528
- // so we have the pure IPv6-address in hostname.
529
- if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) return true;
530
-
531
- // always allow localhost host, for convience
532
- if (hostname === 'localhost') return true;
533
-
534
- // allow if hostname is in allowedHosts
535
- if (this.allowedHosts && this.allowedHosts.length) {
536
- for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
537
- const allowedHost = this.allowedHosts[hostIdx];
538
- if (allowedHost === hostname) return true;
539
-
540
- // support "." as a subdomain wildcard
541
- // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
542
- if (allowedHost[0] === '.') {
543
- // "example.com"
544
- if (hostname === allowedHost.substring(1)) return true;
545
- // "*.example.com"
546
- if (hostname.endsWith(allowedHost)) return true;
547
- }
548
- }
549
- }
550
-
551
- // allow hostname of listening adress
552
- if (hostname === this.listenHostname) return true;
553
-
554
- // also allow public hostname if provided
555
- if (typeof this.publicHost === 'string') {
556
- const idxPublic = this.publicHost.indexOf(':');
557
- const publicHostname = idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
558
- if (hostname === publicHostname) return true;
559
- }
560
-
561
- // disallow
562
- return false;
563
- };
564
-
565
- // delegate listen call and init sockjs
566
- Server.prototype.listen = function (port, hostname, fn) {
567
- this.listenHostname = hostname;
568
- // eslint-disable-next-line
569
-
570
- const returnValue = this.listeningApp.listen(port, hostname, (err) => {
571
- const sockServer = sockjs.createServer({
572
- // Use provided up-to-date sockjs-client
573
- sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
574
- // Limit useless logs
575
- log(severity, line) {
576
- if (severity === 'error') {
577
- log(line);
578
- }
579
- }
580
- });
581
-
582
- sockServer.on('connection', (conn) => {
583
- if (!conn) return;
584
- if (!this.checkHost(conn.headers)) {
585
- this.sockWrite([conn], 'error', 'Invalid Host header');
586
- conn.close();
587
- return;
588
- }
589
- this.sockets.push(conn);
590
-
591
- conn.on('close', () => {
592
- const connIndex = this.sockets.indexOf(conn);
593
- if (connIndex >= 0) {
594
- this.sockets.splice(connIndex, 1);
595
- }
596
- });
597
-
598
- if (this.clientLogLevel) { this.sockWrite([conn], 'log-level', this.clientLogLevel); }
599
-
600
- if (this.progress) { this.sockWrite([conn], 'progress', this.progress); }
601
-
602
- if (this.clientOverlay) { this.sockWrite([conn], 'overlay', this.clientOverlay); }
603
-
604
- if (this.hot) this.sockWrite([conn], 'hot');
605
-
606
- if (!this._stats) return;
607
- this._sendStats([conn], this._stats.toJson(clientStats), true);
608
- });
609
-
610
- sockServer.installHandlers(this.listeningApp, {
611
- prefix: '/sockjs-node'
612
- });
613
-
614
- if (fn) {
615
- fn.call(this.listeningApp, err);
616
- }
617
- });
618
-
619
- return returnValue;
620
- };
621
-
622
- Server.prototype.close = function (callback) {
623
- this.sockets.forEach((sock) => {
624
- sock.close();
625
- });
626
- this.sockets = [];
627
-
628
- this.contentBaseWatchers.forEach((watcher) => {
629
- watcher.close();
630
- });
631
- this.contentBaseWatchers = [];
632
-
633
- this.listeningApp.kill(() => {
634
- this.middleware.close(callback);
635
- });
636
- };
637
-
638
- Server.prototype.sockWrite = function (sockets, type, data) {
639
- sockets.forEach((sock) => {
640
- sock.write(JSON.stringify({
641
- type,
642
- data
643
- }));
644
- });
645
- };
646
-
647
- Server.prototype.serveMagicHtml = function (req, res, next) {
648
- const _path = req.path;
649
- try {
650
- if (!this.middleware.fileSystem.statSync(this.middleware.getFilenameFromUrl(`${_path}.js`)).isFile()) { return next(); }
651
- // Serve a page that executes the javascript
652
- res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="');
653
- res.write(_path);
654
- res.write('.js');
655
- res.write(req._parsedUrl.search || '');
656
- res.end('"></script></body></html>');
657
- } catch (e) {
658
- return next();
659
- }
660
- };
661
-
662
- // send stats to a socket or multiple sockets
663
- Server.prototype._sendStats = function (sockets, stats, force) {
664
- if (!force &&
665
- stats &&
666
- (!stats.errors || stats.errors.length === 0) &&
667
- stats.assets &&
668
- stats.assets.every(asset => !asset.emitted)
669
- ) { return this.sockWrite(sockets, 'still-ok'); }
670
- this.sockWrite(sockets, 'hash', stats.hash);
671
- if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
672
- };
673
-
674
- Server.prototype._watch = function (watchPath) {
675
- // duplicate the same massaging of options that watchpack performs
676
- // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
677
- // this isn't an elegant solution, but we'll improve it in the future
678
- const usePolling = this.watchOptions.poll ? true : undefined; // eslint-disable-line no-undefined
679
- const interval = typeof this.watchOptions.poll === 'number' ? this.watchOptions.poll : undefined; // eslint-disable-line no-undefined
680
- const options = {
681
- ignoreInitial: true,
682
- persistent: true,
683
- followSymlinks: false,
684
- depth: 0,
685
- atomic: false,
686
- alwaysStat: true,
687
- ignorePermissionErrors: true,
688
- ignored: this.watchOptions.ignored,
689
- usePolling,
690
- interval
691
- };
692
- const watcher = chokidar.watch(watchPath, options).on('change', () => {
693
- this.sockWrite(this.sockets, 'content-changed');
694
- });
695
-
696
- this.contentBaseWatchers.push(watcher);
697
- };
698
-
699
- Server.prototype.invalidate = function () {
700
- if (this.middleware) this.middleware.invalidate();
701
- };
702
-
703
- // Export this logic, so that other implementations, like task-runners can use it
704
- Server.addDevServerEntrypoints = require('./util/addDevServerEntrypoints');
705
-
706
- module.exports = Server;
1
+ 'use strict';
2
+
3
+ /* eslint func-names: off */
4
+ require('./polyfills');
5
+
6
+ const fs = require('fs');
7
+ const http = require('http');
8
+ const path = require('path');
9
+ const url = require('url');
10
+ const chokidar = require('chokidar');
11
+ const compress = require('compression');
12
+ const del = require('del');
13
+ const express = require('express');
14
+ const httpProxyMiddleware = require('http-proxy-middleware');
15
+ const ip = require('ip');
16
+ const killable = require('killable');
17
+ const serveIndex = require('serve-index');
18
+ const historyApiFallback = require('connect-history-api-fallback');
19
+ const selfsigned = require('selfsigned');
20
+ const sockjs = require('sockjs');
21
+ const spdy = require('spdy');
22
+ const webpack = require('webpack');
23
+ const webpackDevMiddleware = require('webpack-dev-middleware');
24
+ const OptionsValidationError = require('./OptionsValidationError');
25
+ const optionsSchema = require('./optionsSchema.json');
26
+
27
+ const clientStats = { errorDetails: false };
28
+ const log = console.log; // eslint-disable-line no-console
29
+
30
+ function Server(compiler, options) {
31
+ // Default options
32
+ if (!options) options = {};
33
+
34
+ const validationErrors = webpack.validateSchema(optionsSchema, options);
35
+ if (validationErrors.length) {
36
+ throw new OptionsValidationError(validationErrors);
37
+ }
38
+
39
+ if (options.lazy && !options.filename) {
40
+ throw new Error("'filename' option must be set in lazy mode.");
41
+ }
42
+
43
+ this.hot = options.hot || options.hotOnly;
44
+ this.headers = options.headers;
45
+ this.clientLogLevel = options.clientLogLevel;
46
+ this.clientOverlay = options.overlay;
47
+ this.progress = options.progress;
48
+ this.disableHostCheck = !!options.disableHostCheck;
49
+ this.publicHost = options.public;
50
+ this.allowedHosts = options.allowedHosts;
51
+ this.sockets = [];
52
+ this.contentBaseWatchers = [];
53
+ this.watchOptions = options.watchOptions || {};
54
+
55
+ // Listening for events
56
+ const invalidPlugin = () => {
57
+ this.sockWrite(this.sockets, 'invalid');
58
+ };
59
+ if (this.progress) {
60
+ const progressPlugin = new webpack.ProgressPlugin((percent, msg, addInfo) => {
61
+ percent = Math.floor(percent * 100);
62
+ if (percent === 100) msg = 'Compilation completed';
63
+ if (addInfo) msg = `${msg} (${addInfo})`;
64
+ this.sockWrite(this.sockets, 'progress-update', { percent, msg });
65
+ });
66
+ compiler.apply(progressPlugin);
67
+ }
68
+ compiler.plugin('compile', invalidPlugin);
69
+ compiler.plugin('invalid', invalidPlugin);
70
+ compiler.plugin('done', (stats) => {
71
+ this._sendStats(this.sockets, stats.toJson(clientStats));
72
+ this._stats = stats;
73
+ });
74
+
75
+ // Init express server
76
+ const app = this.app = new express(); // eslint-disable-line
77
+
78
+ app.all('*', (req, res, next) => { // eslint-disable-line
79
+ if (this.checkHost(req.headers)) { return next(); }
80
+ res.send('Invalid Host header');
81
+ });
82
+
83
+ const wdmOptions = {};
84
+
85
+ if (options.quiet === true) {
86
+ wdmOptions.logLevel = 'silent';
87
+ }
88
+ if (options.noInfo === true) {
89
+ wdmOptions.logLevel = 'warn';
90
+ }
91
+ // middleware for serving webpack bundle
92
+ this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));
93
+
94
+ app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => {
95
+ res.setHeader('Content-Type', 'application/javascript');
96
+ fs.createReadStream(path.join(__dirname, '..', 'client', 'live.bundle.js')).pipe(res);
97
+ });
98
+
99
+ app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
100
+ res.setHeader('Content-Type', 'application/javascript');
101
+ fs.createReadStream(path.join(__dirname, '..', 'client', 'sockjs.bundle.js')).pipe(res);
102
+ });
103
+
104
+ app.get('/webpack-dev-server.js', (req, res) => {
105
+ res.setHeader('Content-Type', 'application/javascript');
106
+ fs.createReadStream(path.join(__dirname, '..', 'client', 'index.bundle.js')).pipe(res);
107
+ });
108
+
109
+ app.get('/webpack-dev-server/*', (req, res) => {
110
+ res.setHeader('Content-Type', 'text/html');
111
+ fs.createReadStream(path.join(__dirname, '..', 'client', 'live.html')).pipe(res);
112
+ });
113
+
114
+ app.get('/webpack-dev-server', (req, res) => {
115
+ res.setHeader('Content-Type', 'text/html');
116
+ res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>');
117
+ const outputPath = this.middleware.getFilenameFromUrl(options.publicPath || '/');
118
+ const filesystem = this.middleware.fileSystem;
119
+
120
+ function writeDirectory(baseUrl, basePath) {
121
+ const content = filesystem.readdirSync(basePath);
122
+ res.write('<ul>');
123
+ content.forEach((item) => {
124
+ const p = `${basePath}/${item}`;
125
+ if (filesystem.statSync(p).isFile()) {
126
+ res.write('<li><a href="');
127
+ res.write(baseUrl + item);
128
+ res.write('">');
129
+ res.write(item);
130
+ res.write('</a></li>');
131
+ if (/\.js$/.test(item)) {
132
+ const htmlItem = item.substr(0, item.length - 3);
133
+ res.write('<li><a href="');
134
+ res.write(baseUrl + htmlItem);
135
+ res.write('">');
136
+ res.write(htmlItem);
137
+ res.write('</a> (magic html for ');
138
+ res.write(item);
139
+ res.write(') (<a href="');
140
+ res.write(baseUrl.replace(/(^(https?:\/\/[^\/]+)?\/)/, "$1webpack-dev-server/") + htmlItem); // eslint-disable-line
141
+ res.write('">webpack-dev-server</a>)</li>');
142
+ }
143
+ } else {
144
+ res.write('<li>');
145
+ res.write(item);
146
+ res.write('<br>');
147
+ writeDirectory(`${baseUrl + item}/`, p);
148
+ res.write('</li>');
149
+ }
150
+ });
151
+ res.write('</ul>');
152
+ }
153
+ writeDirectory(options.publicPath || '/', outputPath);
154
+ res.end('</body></html>');
155
+ });
156
+
157
+ let contentBase;
158
+ if (options.contentBase !== undefined) { // eslint-disable-line
159
+ contentBase = options.contentBase; // eslint-disable-line
160
+ } else {
161
+ contentBase = process.cwd();
162
+ }
163
+
164
+ // Keep track of websocket proxies for external websocket upgrade.
165
+ const websocketProxies = [];
166
+
167
+ const features = {
168
+ compress() {
169
+ if (options.compress) {
170
+ // Enable gzip compression.
171
+ app.use(compress());
172
+ }
173
+ },
174
+
175
+ proxy() {
176
+ if (options.proxy) {
177
+ /**
178
+ * Assume a proxy configuration specified as:
179
+ * proxy: {
180
+ * 'context': { options }
181
+ * }
182
+ * OR
183
+ * proxy: {
184
+ * 'context': 'target'
185
+ * }
186
+ */
187
+ if (!Array.isArray(options.proxy)) {
188
+ options.proxy = Object.keys(options.proxy).map((context) => {
189
+ let proxyOptions;
190
+ // For backwards compatibility reasons.
191
+ const correctedContext = context.replace(/^\*$/, '**').replace(/\/\*$/, '');
192
+
193
+ if (typeof options.proxy[context] === 'string') {
194
+ proxyOptions = {
195
+ context: correctedContext,
196
+ target: options.proxy[context]
197
+ };
198
+ } else {
199
+ proxyOptions = Object.assign({}, options.proxy[context]);
200
+ proxyOptions.context = correctedContext;
201
+ }
202
+ proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
203
+
204
+ return proxyOptions;
205
+ });
206
+ }
207
+
208
+ const getProxyMiddleware = (proxyConfig) => {
209
+ const context = proxyConfig.context || proxyConfig.path;
210
+
211
+ // It is possible to use the `bypass` method without a `target`.
212
+ // However, the proxy middleware has no use in this case, and will fail to instantiate.
213
+ if (proxyConfig.target) {
214
+ return httpProxyMiddleware(context, proxyConfig);
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Assume a proxy configuration specified as:
220
+ * proxy: [
221
+ * {
222
+ * context: ...,
223
+ * ...options...
224
+ * },
225
+ * // or:
226
+ * function() {
227
+ * return {
228
+ * context: ...,
229
+ * ...options...
230
+ * };
231
+ * }
232
+ * ]
233
+ */
234
+ options.proxy.forEach((proxyConfigOrCallback) => {
235
+ let proxyConfig;
236
+ let proxyMiddleware;
237
+
238
+ if (typeof proxyConfigOrCallback === 'function') {
239
+ proxyConfig = proxyConfigOrCallback();
240
+ } else {
241
+ proxyConfig = proxyConfigOrCallback;
242
+ }
243
+
244
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
245
+ if (proxyConfig.ws) {
246
+ websocketProxies.push(proxyMiddleware);
247
+ }
248
+
249
+ app.use((req, res, next) => {
250
+ if (typeof proxyConfigOrCallback === 'function') {
251
+ const newProxyConfig = proxyConfigOrCallback();
252
+ if (newProxyConfig !== proxyConfig) {
253
+ proxyConfig = newProxyConfig;
254
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
255
+ }
256
+ }
257
+ const bypass = typeof proxyConfig.bypass === 'function';
258
+ // eslint-disable-next-line
259
+ const bypassUrl = bypass && proxyConfig.bypass(req, res, proxyConfig) || false;
260
+
261
+ if (bypassUrl) {
262
+ req.url = bypassUrl;
263
+ next();
264
+ } else if (proxyMiddleware) {
265
+ return proxyMiddleware(req, res, next);
266
+ } else {
267
+ next();
268
+ }
269
+ });
270
+ });
271
+ }
272
+ },
273
+
274
+ historyApiFallback() {
275
+ if (options.historyApiFallback) {
276
+ // Fall back to /index.html if nothing else matches.
277
+ app.use(historyApiFallback(typeof options.historyApiFallback === 'object' ? options.historyApiFallback : null));
278
+ }
279
+ },
280
+
281
+ contentBaseFiles() {
282
+ if (Array.isArray(contentBase)) {
283
+ contentBase.forEach((item) => {
284
+ app.get('*', express.static(item));
285
+ });
286
+ } else if (/^(https?:)?\/\//.test(contentBase)) {
287
+ log('Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.');
288
+ log('proxy: {\n\t"*": "<your current contentBase configuration>"\n}'); // eslint-disable-line quotes
289
+ // Redirect every request to contentBase
290
+ app.get('*', (req, res) => {
291
+ res.writeHead(302, {
292
+ Location: contentBase + req.path + (req._parsedUrl.search || '')
293
+ });
294
+ res.end();
295
+ });
296
+ } else if (typeof contentBase === 'number') {
297
+ log('Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.');
298
+ log('proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'); // eslint-disable-line quotes
299
+ // Redirect every request to the port contentBase
300
+ app.get('*', (req, res) => {
301
+ res.writeHead(302, {
302
+ Location: `//localhost:${contentBase}${req.path}${req._parsedUrl.search || ''}`
303
+ });
304
+ res.end();
305
+ });
306
+ } else {
307
+ // route content request
308
+ app.get('*', express.static(contentBase, options.staticOptions));
309
+ }
310
+ },
311
+
312
+ contentBaseIndex() {
313
+ if (Array.isArray(contentBase)) {
314
+ contentBase.forEach((item) => {
315
+ app.get('*', serveIndex(item));
316
+ });
317
+ } else if (!/^(https?:)?\/\//.test(contentBase) && typeof contentBase !== 'number') {
318
+ app.get('*', serveIndex(contentBase));
319
+ }
320
+ },
321
+
322
+ watchContentBase: () => {
323
+ if (/^(https?:)?\/\//.test(contentBase) || typeof contentBase === 'number') {
324
+ throw new Error('Watching remote files is not supported.');
325
+ } else if (Array.isArray(contentBase)) {
326
+ contentBase.forEach((item) => {
327
+ this._watch(item);
328
+ });
329
+ } else {
330
+ this._watch(contentBase);
331
+ }
332
+ },
333
+
334
+ before: () => {
335
+ if (typeof options.before === 'function') {
336
+ options.before(app, this);
337
+ }
338
+ },
339
+
340
+ middleware: () => {
341
+ // include our middleware to ensure it is able to handle '/index.html' request after redirect
342
+ app.use(this.middleware);
343
+ },
344
+
345
+ after: () => {
346
+ if (typeof options.after === 'function') { options.after(app, this); }
347
+ },
348
+
349
+ headers: () => {
350
+ app.all('*', this.setContentHeaders.bind(this));
351
+ },
352
+
353
+ magicHtml: () => {
354
+ app.get('*', this.serveMagicHtml.bind(this));
355
+ },
356
+
357
+ setup: () => {
358
+ if (typeof options.setup === 'function') {
359
+ log('The `setup` option is deprecated and will be removed in v3. Please update your config to use `before`');
360
+ options.setup(app, this);
361
+ }
362
+ }
363
+ };
364
+
365
+ const defaultFeatures = ['before', 'setup', 'headers', 'middleware'];
366
+ if (options.proxy) { defaultFeatures.push('proxy', 'middleware'); }
367
+ if (contentBase !== false) { defaultFeatures.push('contentBaseFiles'); }
368
+ if (options.watchContentBase) { defaultFeatures.push('watchContentBase'); }
369
+ if (options.historyApiFallback) {
370
+ defaultFeatures.push('historyApiFallback', 'middleware');
371
+ if (contentBase !== false) { defaultFeatures.push('contentBaseFiles'); }
372
+ }
373
+ defaultFeatures.push('magicHtml');
374
+ if (contentBase !== false) { defaultFeatures.push('contentBaseIndex'); }
375
+ // compress is placed last and uses unshift so that it will be the first middleware used
376
+ if (options.compress) { defaultFeatures.unshift('compress'); }
377
+ if (options.after) { defaultFeatures.push('after'); }
378
+
379
+ (options.features || defaultFeatures).forEach((feature) => {
380
+ features[feature]();
381
+ });
382
+
383
+ if (options.https) {
384
+ // for keep supporting CLI parameters
385
+ if (typeof options.https === 'boolean') {
386
+ options.https = {
387
+ key: options.key,
388
+ cert: options.cert,
389
+ ca: options.ca,
390
+ pfx: options.pfx,
391
+ passphrase: options.pfxPassphrase,
392
+ requestCert: options.requestCert || false
393
+ };
394
+ }
395
+
396
+ let fakeCert;
397
+ if (!options.https.key || !options.https.cert) {
398
+ // Use a self-signed certificate if no certificate was configured.
399
+ // Cycle certs every 24 hours
400
+ const certPath = path.join(__dirname, '../ssl/server.pem');
401
+ let certExists = fs.existsSync(certPath);
402
+
403
+ if (certExists) {
404
+ const certStat = fs.statSync(certPath);
405
+ const certTtl = 1000 * 60 * 60 * 24;
406
+ const now = new Date();
407
+
408
+ // cert is more than 30 days old, kill it with fire
409
+ if ((now - certStat.ctime) / certTtl > 30) {
410
+ log('SSL Certificate is more than 30 days old. Removing.');
411
+ del.sync([certPath], { force: true });
412
+ certExists = false;
413
+ }
414
+ }
415
+
416
+ if (!certExists) {
417
+ log('Generating SSL Certificate');
418
+ const attrs = [{ name: 'commonName', value: 'localhost' }];
419
+ const pems = selfsigned.generate(attrs, {
420
+ algorithm: 'sha256',
421
+ days: 30,
422
+ keySize: 2048,
423
+ extensions: [{
424
+ name: 'basicConstraints',
425
+ cA: true
426
+ }, {
427
+ name: 'keyUsage',
428
+ keyCertSign: true,
429
+ digitalSignature: true,
430
+ nonRepudiation: true,
431
+ keyEncipherment: true,
432
+ dataEncipherment: true
433
+ }, {
434
+ name: 'subjectAltName',
435
+ altNames: [
436
+ {
437
+ // type 2 is DNS
438
+ type: 2,
439
+ value: 'localhost'
440
+ },
441
+ {
442
+ type: 2,
443
+ value: 'localhost.localdomain'
444
+ },
445
+ {
446
+ type: 2,
447
+ value: 'lvh.me'
448
+ },
449
+ {
450
+ type: 2,
451
+ value: '*.lvh.me'
452
+ },
453
+ {
454
+ type: 2,
455
+ value: '[::1]'
456
+ },
457
+ {
458
+ // type 7 is IP
459
+ type: 7,
460
+ ip: '127.0.0.1'
461
+ },
462
+ {
463
+ type: 7,
464
+ ip: 'fe80::1'
465
+ }
466
+ ]
467
+ }]
468
+ });
469
+
470
+ fs.writeFileSync(certPath, pems.private + pems.cert, { encoding: 'utf-8' });
471
+ }
472
+ fakeCert = fs.readFileSync(certPath);
473
+ }
474
+
475
+ options.https.key = options.https.key || fakeCert;
476
+ options.https.cert = options.https.cert || fakeCert;
477
+
478
+ if (!options.https.spdy) {
479
+ options.https.spdy = {
480
+ protocols: ['h2', 'http/1.1']
481
+ };
482
+ }
483
+
484
+ this.listeningApp = spdy.createServer(options.https, app);
485
+ } else {
486
+ this.listeningApp = http.createServer(app);
487
+ }
488
+
489
+ killable(this.listeningApp);
490
+
491
+ // Proxy websockets without the initial http request
492
+ // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
493
+ websocketProxies.forEach(function (wsProxy) {
494
+ this.listeningApp.on('upgrade', wsProxy.upgrade);
495
+ }, this);
496
+ }
497
+
498
+ Server.prototype.use = function () {
499
+ // eslint-disable-next-line
500
+ this.app.use.apply(this.app, arguments);
501
+ };
502
+
503
+ Server.prototype.setContentHeaders = function (req, res, next) {
504
+ if (this.headers) {
505
+ for (const name in this.headers) { // eslint-disable-line
506
+ res.setHeader(name, this.headers[name]);
507
+ }
508
+ }
509
+
510
+ next();
511
+ };
512
+
513
+ Server.prototype.checkHost = function (headers) {
514
+ // allow user to opt-out this security check, at own risk
515
+ if (this.disableHostCheck) return true;
516
+
517
+ // get the Host header and extract hostname
518
+ // we don't care about port not matching
519
+ const hostHeader = headers.host;
520
+ if (!hostHeader) return false;
521
+
522
+ // use the node url-parser to retrieve the hostname from the host-header.
523
+ const hostname = url.parse(`//${hostHeader}`, false, true).hostname;
524
+
525
+ // always allow requests with explicit IPv4 or IPv6-address.
526
+ // A note on IPv6 addresses: hostHeader will always contain the brackets denoting
527
+ // an IPv6-address in URLs, these are removed from the hostname in url.parse(),
528
+ // so we have the pure IPv6-address in hostname.
529
+ if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) return true;
530
+
531
+ // always allow localhost host, for convience
532
+ if (hostname === 'localhost') return true;
533
+
534
+ // allow if hostname is in allowedHosts
535
+ if (this.allowedHosts && this.allowedHosts.length) {
536
+ for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
537
+ const allowedHost = this.allowedHosts[hostIdx];
538
+ if (allowedHost === hostname) return true;
539
+
540
+ // support "." as a subdomain wildcard
541
+ // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
542
+ if (allowedHost[0] === '.') {
543
+ // "example.com"
544
+ if (hostname === allowedHost.substring(1)) return true;
545
+ // "*.example.com"
546
+ if (hostname.endsWith(allowedHost)) return true;
547
+ }
548
+ }
549
+ }
550
+
551
+ // allow hostname of listening adress
552
+ if (hostname === this.listenHostname) return true;
553
+
554
+ // also allow public hostname if provided
555
+ if (typeof this.publicHost === 'string') {
556
+ const idxPublic = this.publicHost.indexOf(':');
557
+ const publicHostname = idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
558
+ if (hostname === publicHostname) return true;
559
+ }
560
+
561
+ // disallow
562
+ return false;
563
+ };
564
+
565
+ // delegate listen call and init sockjs
566
+ Server.prototype.listen = function (port, hostname, fn) {
567
+ this.listenHostname = hostname;
568
+ // eslint-disable-next-line
569
+
570
+ const returnValue = this.listeningApp.listen(port, hostname, (err) => {
571
+ const sockServer = sockjs.createServer({
572
+ // Use provided up-to-date sockjs-client
573
+ sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
574
+ // Limit useless logs
575
+ log(severity, line) {
576
+ if (severity === 'error') {
577
+ log(line);
578
+ }
579
+ }
580
+ });
581
+
582
+ sockServer.on('connection', (conn) => {
583
+ if (!conn) return;
584
+ if (!this.checkHost(conn.headers)) {
585
+ this.sockWrite([conn], 'error', 'Invalid Host header');
586
+ conn.close();
587
+ return;
588
+ }
589
+ this.sockets.push(conn);
590
+
591
+ conn.on('close', () => {
592
+ const connIndex = this.sockets.indexOf(conn);
593
+ if (connIndex >= 0) {
594
+ this.sockets.splice(connIndex, 1);
595
+ }
596
+ });
597
+
598
+ if (this.clientLogLevel) { this.sockWrite([conn], 'log-level', this.clientLogLevel); }
599
+
600
+ if (this.progress) { this.sockWrite([conn], 'progress', this.progress); }
601
+
602
+ if (this.clientOverlay) { this.sockWrite([conn], 'overlay', this.clientOverlay); }
603
+
604
+ if (this.hot) this.sockWrite([conn], 'hot');
605
+
606
+ if (!this._stats) return;
607
+ this._sendStats([conn], this._stats.toJson(clientStats), true);
608
+ });
609
+
610
+ sockServer.installHandlers(this.listeningApp, {
611
+ prefix: '/sockjs-node'
612
+ });
613
+
614
+ if (fn) {
615
+ fn.call(this.listeningApp, err);
616
+ }
617
+ });
618
+
619
+ return returnValue;
620
+ };
621
+
622
+ Server.prototype.close = function (callback) {
623
+ this.sockets.forEach((sock) => {
624
+ sock.close();
625
+ });
626
+ this.sockets = [];
627
+
628
+ this.contentBaseWatchers.forEach((watcher) => {
629
+ watcher.close();
630
+ });
631
+ this.contentBaseWatchers = [];
632
+
633
+ this.listeningApp.kill(() => {
634
+ this.middleware.close(callback);
635
+ });
636
+ };
637
+
638
+ Server.prototype.sockWrite = function (sockets, type, data) {
639
+ sockets.forEach((sock) => {
640
+ sock.write(JSON.stringify({
641
+ type,
642
+ data
643
+ }));
644
+ });
645
+ };
646
+
647
+ Server.prototype.serveMagicHtml = function (req, res, next) {
648
+ const _path = req.path;
649
+ try {
650
+ if (!this.middleware.fileSystem.statSync(this.middleware.getFilenameFromUrl(`${_path}.js`)).isFile()) { return next(); }
651
+ // Serve a page that executes the javascript
652
+ res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="');
653
+ res.write(_path);
654
+ res.write('.js');
655
+ res.write(req._parsedUrl.search || '');
656
+ res.end('"></script></body></html>');
657
+ } catch (e) {
658
+ return next();
659
+ }
660
+ };
661
+
662
+ // send stats to a socket or multiple sockets
663
+ Server.prototype._sendStats = function (sockets, stats, force) {
664
+ if (!force &&
665
+ stats &&
666
+ (!stats.errors || stats.errors.length === 0) &&
667
+ stats.assets &&
668
+ stats.assets.every(asset => !asset.emitted)
669
+ ) { return this.sockWrite(sockets, 'still-ok'); }
670
+ this.sockWrite(sockets, 'hash', stats.hash);
671
+ if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
672
+ };
673
+
674
+ Server.prototype._watch = function (watchPath) {
675
+ // duplicate the same massaging of options that watchpack performs
676
+ // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
677
+ // this isn't an elegant solution, but we'll improve it in the future
678
+ const usePolling = this.watchOptions.poll ? true : undefined; // eslint-disable-line no-undefined
679
+ const interval = typeof this.watchOptions.poll === 'number' ? this.watchOptions.poll : undefined; // eslint-disable-line no-undefined
680
+ const options = {
681
+ ignoreInitial: true,
682
+ persistent: true,
683
+ followSymlinks: false,
684
+ depth: 0,
685
+ atomic: false,
686
+ alwaysStat: true,
687
+ ignorePermissionErrors: true,
688
+ ignored: this.watchOptions.ignored,
689
+ usePolling,
690
+ interval
691
+ };
692
+ const watcher = chokidar.watch(watchPath, options).on('change', () => {
693
+ this.sockWrite(this.sockets, 'content-changed');
694
+ });
695
+
696
+ this.contentBaseWatchers.push(watcher);
697
+ };
698
+
699
+ Server.prototype.invalidate = function () {
700
+ if (this.middleware) this.middleware.invalidate();
701
+ };
702
+
703
+ // Export this logic, so that other implementations, like task-runners can use it
704
+ Server.addDevServerEntrypoints = require('./util/addDevServerEntrypoints');
705
+
706
+ module.exports = Server;