UNPKG

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