webpack-dev-server 4.3.0 → 4.6.0

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
@@ -6,7 +6,7 @@ const url = require("url");
6
6
  const util = require("util");
7
7
  const fs = require("graceful-fs");
8
8
  const ipaddr = require("ipaddr.js");
9
- const internalIp = require("internal-ip");
9
+ const defaultGateway = require("default-gateway");
10
10
  const express = require("express");
11
11
  const { validate } = require("schema-utils");
12
12
  const schema = require("./options.json");
@@ -32,24 +32,24 @@ class Server {
32
32
 
33
33
  this.options = options;
34
34
  this.staticWatchers = [];
35
+ this.listeners = [];
35
36
  // Keep track of websocket proxies for external websocket upgrade.
36
37
  this.webSocketProxies = [];
37
38
  this.sockets = [];
38
39
  this.compiler = compiler;
40
+ this.currentHash = null;
39
41
  }
40
42
 
41
43
  static get DEFAULT_STATS() {
42
44
  return {
43
45
  all: false,
44
46
  hash: true,
45
- assets: true,
46
47
  warnings: true,
47
48
  errors: true,
48
49
  errorDetails: false,
49
50
  };
50
51
  }
51
52
 
52
- // eslint-disable-next-line class-methods-use-this
53
53
  static isAbsoluteURL(URL) {
54
54
  // Don't match Windows paths `c:\`
55
55
  if (/^[a-zA-Z]:\\/.test(URL)) {
@@ -61,13 +61,54 @@ class Server {
61
61
  return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL);
62
62
  }
63
63
 
64
+ static findIp(gateway) {
65
+ const gatewayIp = ipaddr.parse(gateway);
66
+
67
+ // Look for the matching interface in all local interfaces.
68
+ for (const addresses of Object.values(os.networkInterfaces())) {
69
+ for (const { cidr } of addresses) {
70
+ const net = ipaddr.parseCIDR(cidr);
71
+
72
+ if (
73
+ net[0] &&
74
+ net[0].kind() === gatewayIp.kind() &&
75
+ gatewayIp.match(net)
76
+ ) {
77
+ return net[0].toString();
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ static async internalIP(family) {
84
+ try {
85
+ const { gateway } = await defaultGateway[family]();
86
+ return Server.findIp(gateway);
87
+ } catch {
88
+ // ignore
89
+ }
90
+ }
91
+
92
+ static internalIPSync(family) {
93
+ try {
94
+ const { gateway } = defaultGateway[family].sync();
95
+ return Server.findIp(gateway);
96
+ } catch {
97
+ // ignore
98
+ }
99
+ }
100
+
64
101
  static async getHostname(hostname) {
65
102
  if (hostname === "local-ip") {
66
- return (await internalIp.v4()) || (await internalIp.v6()) || "0.0.0.0";
103
+ return (
104
+ (await Server.internalIP("v4")) ||
105
+ (await Server.internalIP("v6")) ||
106
+ "0.0.0.0"
107
+ );
67
108
  } else if (hostname === "local-ipv4") {
68
- return (await internalIp.v4()) || "0.0.0.0";
109
+ return (await Server.internalIP("v4")) || "0.0.0.0";
69
110
  } else if (hostname === "local-ipv6") {
70
- return (await internalIp.v6()) || "::";
111
+ return (await Server.internalIP("v6")) || "::";
71
112
  }
72
113
 
73
114
  return hostname;
@@ -155,7 +196,7 @@ class Server {
155
196
  if (typeof this.options.client.webSocketURL.protocol !== "undefined") {
156
197
  protocol = this.options.client.webSocketURL.protocol;
157
198
  } else {
158
- protocol = this.options.https ? "wss:" : "ws:";
199
+ protocol = this.options.server.type === "http" ? "ws:" : "wss:";
159
200
  }
160
201
 
161
202
  searchParams.set("protocol", protocol);
@@ -258,6 +299,10 @@ class Server {
258
299
  searchParams.set("logging", this.options.client.logging);
259
300
  }
260
301
 
302
+ if (typeof this.options.client.reconnect !== "undefined") {
303
+ searchParams.set("reconnect", this.options.client.reconnect);
304
+ }
305
+
261
306
  webSocketURL = searchParams.toString();
262
307
  }
263
308
 
@@ -390,7 +435,6 @@ class Server {
390
435
  return this.compiler.options;
391
436
  }
392
437
 
393
- // eslint-disable-next-line class-methods-use-this
394
438
  async normalizeOptions() {
395
439
  const { options } = this;
396
440
 
@@ -400,14 +444,125 @@ class Server {
400
444
 
401
445
  const compilerOptions = this.getCompilerOptions();
402
446
  // TODO remove `{}` after drop webpack v4 support
403
- const watchOptions = compilerOptions.watchOptions || {};
404
- const defaultOptionsForStatic = {
405
- directory: path.join(process.cwd(), "public"),
406
- staticOptions: {},
407
- publicPath: ["/"],
408
- serveIndex: { icons: true },
409
- // Respect options from compiler watchOptions
410
- watch: watchOptions,
447
+ const compilerWatchOptions = compilerOptions.watchOptions || {};
448
+ const getWatchOptions = (watchOptions = {}) => {
449
+ const getPolling = () => {
450
+ if (typeof watchOptions.usePolling !== "undefined") {
451
+ return watchOptions.usePolling;
452
+ }
453
+
454
+ if (typeof watchOptions.poll !== "undefined") {
455
+ return Boolean(watchOptions.poll);
456
+ }
457
+
458
+ if (typeof compilerWatchOptions.poll !== "undefined") {
459
+ return Boolean(compilerWatchOptions.poll);
460
+ }
461
+
462
+ return false;
463
+ };
464
+ const getInterval = () => {
465
+ if (typeof watchOptions.interval !== "undefined") {
466
+ return watchOptions.interval;
467
+ }
468
+
469
+ if (typeof watchOptions.poll === "number") {
470
+ return watchOptions.poll;
471
+ }
472
+
473
+ if (typeof compilerWatchOptions.poll === "number") {
474
+ return compilerWatchOptions.poll;
475
+ }
476
+ };
477
+
478
+ const usePolling = getPolling();
479
+ const interval = getInterval();
480
+ const { poll, ...rest } = watchOptions;
481
+
482
+ return {
483
+ ignoreInitial: true,
484
+ persistent: true,
485
+ followSymlinks: false,
486
+ atomic: false,
487
+ alwaysStat: true,
488
+ ignorePermissionErrors: true,
489
+ // Respect options from compiler watchOptions
490
+ usePolling,
491
+ interval,
492
+ ignored: watchOptions.ignored,
493
+ // TODO: we respect these options for all watch options and allow developers to pass them to chokidar, but chokidar doesn't have these options maybe we need revisit that in future
494
+ ...rest,
495
+ };
496
+ };
497
+ const getStaticItem = (optionsForStatic) => {
498
+ const getDefaultStaticOptions = () => {
499
+ return {
500
+ directory: path.join(process.cwd(), "public"),
501
+ staticOptions: {},
502
+ publicPath: ["/"],
503
+ serveIndex: { icons: true },
504
+ watch: getWatchOptions(),
505
+ };
506
+ };
507
+
508
+ let item;
509
+
510
+ if (typeof optionsForStatic === "undefined") {
511
+ item = getDefaultStaticOptions();
512
+ } else if (typeof optionsForStatic === "string") {
513
+ item = {
514
+ ...getDefaultStaticOptions(),
515
+ directory: optionsForStatic,
516
+ };
517
+ } else {
518
+ const def = getDefaultStaticOptions();
519
+
520
+ item = {
521
+ directory:
522
+ typeof optionsForStatic.directory !== "undefined"
523
+ ? optionsForStatic.directory
524
+ : def.directory,
525
+ // TODO: do merge in the next major release
526
+ staticOptions:
527
+ typeof optionsForStatic.staticOptions !== "undefined"
528
+ ? optionsForStatic.staticOptions
529
+ : def.staticOptions,
530
+ publicPath:
531
+ typeof optionsForStatic.publicPath !== "undefined"
532
+ ? optionsForStatic.publicPath
533
+ : def.publicPath,
534
+ // TODO: do merge in the next major release
535
+ serveIndex:
536
+ // eslint-disable-next-line no-nested-ternary
537
+ typeof optionsForStatic.serveIndex !== "undefined"
538
+ ? typeof optionsForStatic.serveIndex === "boolean" &&
539
+ optionsForStatic.serveIndex
540
+ ? def.serveIndex
541
+ : optionsForStatic.serveIndex
542
+ : def.serveIndex,
543
+ watch:
544
+ // eslint-disable-next-line no-nested-ternary
545
+ typeof optionsForStatic.watch !== "undefined"
546
+ ? // eslint-disable-next-line no-nested-ternary
547
+ typeof optionsForStatic.watch === "boolean"
548
+ ? optionsForStatic.watch
549
+ ? def.watch
550
+ : false
551
+ : getWatchOptions(optionsForStatic.watch)
552
+ : def.watch,
553
+ };
554
+ }
555
+
556
+ if (Server.isAbsoluteURL(item.directory)) {
557
+ throw new Error("Using a URL as static.directory is not supported");
558
+ }
559
+
560
+ // ensure that publicPath is an array
561
+ if (typeof item.publicPath === "string") {
562
+ item.publicPath = [item.publicPath];
563
+ }
564
+
565
+ return item;
411
566
  };
412
567
 
413
568
  if (typeof options.allowedHosts === "undefined") {
@@ -474,6 +629,14 @@ class Server {
474
629
  };
475
630
  }
476
631
 
632
+ if (typeof options.client.reconnect === "undefined") {
633
+ options.client.reconnect = 10;
634
+ } else if (options.client.reconnect === true) {
635
+ options.client.reconnect = Infinity;
636
+ } else if (options.client.reconnect === false) {
637
+ options.client.reconnect = 0;
638
+ }
639
+
477
640
  // Respect infrastructureLogging.level
478
641
  if (typeof options.client.logging === "undefined") {
479
642
  options.client.logging = compilerOptions.infrastructureLogging
@@ -508,23 +671,62 @@ class Server {
508
671
  ? options.hot
509
672
  : true;
510
673
 
511
- // if the user enables http2, we can safely enable https
512
- if ((options.http2 && !options.https) || options.https === true) {
513
- options.https = {
514
- requestCert: false,
674
+ const isHTTPs = Boolean(options.https);
675
+ const isSPDY = Boolean(options.http2);
676
+
677
+ if (isHTTPs || isSPDY) {
678
+ // TODO: remove in the next major release
679
+ util.deprecate(
680
+ () => {},
681
+ `'${
682
+ isHTTPs ? "https" : "http2"
683
+ }' option is deprecated. Please use the 'server' option.`,
684
+ `DEP_WEBPACK_DEV_SERVER_${isHTTPs ? "HTTPS" : "HTTP2"}`
685
+ )();
686
+ }
687
+
688
+ options.server = {
689
+ type:
690
+ // eslint-disable-next-line no-nested-ternary
691
+ typeof options.server === "string"
692
+ ? options.server
693
+ : // eslint-disable-next-line no-nested-ternary
694
+ typeof (options.server || {}).type === "string"
695
+ ? options.server.type
696
+ : // eslint-disable-next-line no-nested-ternary
697
+ isSPDY
698
+ ? "spdy"
699
+ : isHTTPs
700
+ ? "https"
701
+ : "http",
702
+ options: {
703
+ ...options.https,
704
+ ...(options.server || {}).options,
705
+ },
706
+ };
707
+
708
+ if (
709
+ options.server.type === "spdy" &&
710
+ typeof options.server.options.spdy === "undefined"
711
+ ) {
712
+ options.server.options.spdy = {
713
+ protocols: ["h2", "http/1.1"],
515
714
  };
516
715
  }
517
716
 
518
- // https option
519
- if (options.https) {
717
+ if (options.server.type === "https" || options.server.type === "spdy") {
718
+ if (typeof options.server.options.requestCert === "undefined") {
719
+ options.server.options.requestCert = false;
720
+ }
721
+
520
722
  // TODO remove the `cacert` option in favor `ca` in the next major release
521
723
  for (const property of ["cacert", "ca", "cert", "crl", "key", "pfx"]) {
522
- if (typeof options.https[property] === "undefined") {
724
+ if (typeof options.server.options[property] === "undefined") {
523
725
  // eslint-disable-next-line no-continue
524
726
  continue;
525
727
  }
526
728
 
527
- const value = options.https[property];
729
+ const value = options.server.options[property];
528
730
  const readFile = (item) => {
529
731
  if (
530
732
  Buffer.isBuffer(item) ||
@@ -547,14 +749,14 @@ class Server {
547
749
  }
548
750
  };
549
751
 
550
- options.https[property] = Array.isArray(value)
752
+ options.server.options[property] = Array.isArray(value)
551
753
  ? value.map((item) => readFile(item))
552
754
  : readFile(value);
553
755
  }
554
756
 
555
757
  let fakeCert;
556
758
 
557
- if (!options.https.key || !options.https.cert) {
759
+ if (!options.server.options.key || !options.server.options.cert) {
558
760
  const certificateDir = Server.findCacheDir();
559
761
  const certificatePath = path.join(certificateDir, "server.pem");
560
762
  let certificateExists;
@@ -577,7 +779,7 @@ class Server {
577
779
  const del = require("del");
578
780
 
579
781
  this.logger.info(
580
- "SSL Certificate is more than 30 days old. Removing..."
782
+ "SSL certificate is more than 30 days old. Removing..."
581
783
  );
582
784
 
583
785
  await del([certificatePath], { force: true });
@@ -587,7 +789,7 @@ class Server {
587
789
  }
588
790
 
589
791
  if (!certificateExists) {
590
- this.logger.info("Generating SSL Certificate...");
792
+ this.logger.info("Generating SSL certificate...");
591
793
 
592
794
  const selfsigned = require("selfsigned");
593
795
  const attributes = [{ name: "commonName", value: "localhost" }];
@@ -669,20 +871,20 @@ class Server {
669
871
  this.logger.info(`SSL certificate: ${certificatePath}`);
670
872
  }
671
873
 
672
- if (options.https.cacert) {
673
- if (options.https.ca) {
874
+ if (options.server.options.cacert) {
875
+ if (options.server.options.ca) {
674
876
  this.logger.warn(
675
- "Do not specify 'https.ca' and 'https.cacert' options together, the 'https.ca' option will be used."
877
+ "Do not specify 'ca' and 'cacert' options together, the 'ca' option will be used."
676
878
  );
677
879
  } else {
678
- options.https.ca = options.https.cacert;
880
+ options.server.options.ca = options.server.options.cacert;
679
881
  }
680
882
 
681
- delete options.https.cacert;
883
+ delete options.server.options.cacert;
682
884
  }
683
885
 
684
- options.https.key = options.https.key || fakeCert;
685
- options.https.cert = options.https.cert || fakeCert;
886
+ options.server.options.key = options.server.options.key || fakeCert;
887
+ options.server.options.cert = options.server.options.cert || fakeCert;
686
888
  }
687
889
 
688
890
  if (typeof options.ipc === "boolean") {
@@ -830,50 +1032,27 @@ class Server {
830
1032
  }
831
1033
 
832
1034
  if (typeof options.static === "undefined") {
833
- options.static = [defaultOptionsForStatic];
1035
+ options.static = [getStaticItem()];
834
1036
  } else if (typeof options.static === "boolean") {
835
- options.static = options.static ? [defaultOptionsForStatic] : false;
1037
+ options.static = options.static ? [getStaticItem()] : false;
836
1038
  } else if (typeof options.static === "string") {
837
- options.static = [
838
- { ...defaultOptionsForStatic, directory: options.static },
839
- ];
1039
+ options.static = [getStaticItem(options.static)];
840
1040
  } else if (Array.isArray(options.static)) {
841
1041
  options.static = options.static.map((item) => {
842
1042
  if (typeof item === "string") {
843
- return { ...defaultOptionsForStatic, directory: item };
1043
+ return getStaticItem(item);
844
1044
  }
845
1045
 
846
- return { ...defaultOptionsForStatic, ...item };
1046
+ return getStaticItem(item);
847
1047
  });
848
1048
  } else {
849
- options.static = [{ ...defaultOptionsForStatic, ...options.static }];
850
- }
851
-
852
- if (options.static) {
853
- options.static.forEach((staticOption) => {
854
- if (Server.isAbsoluteURL(staticOption.directory)) {
855
- throw new Error("Using a URL as static.directory is not supported");
856
- }
857
-
858
- // ensure that publicPath is an array
859
- if (typeof staticOption.publicPath === "string") {
860
- staticOption.publicPath = [staticOption.publicPath];
861
- }
862
-
863
- // ensure that watch is an object if true
864
- if (staticOption.watch === true) {
865
- staticOption.watch = defaultOptionsForStatic.watch;
866
- }
867
-
868
- // ensure that serveIndex is an object if true
869
- if (staticOption.serveIndex === true) {
870
- staticOption.serveIndex = defaultOptionsForStatic.serveIndex;
871
- }
872
- });
1049
+ options.static = [getStaticItem(options.static)];
873
1050
  }
874
1051
 
875
1052
  if (typeof options.watchFiles === "string") {
876
- options.watchFiles = [{ paths: options.watchFiles, options: {} }];
1053
+ options.watchFiles = [
1054
+ { paths: options.watchFiles, options: getWatchOptions() },
1055
+ ];
877
1056
  } else if (
878
1057
  typeof options.watchFiles === "object" &&
879
1058
  options.watchFiles !== null &&
@@ -882,16 +1061,19 @@ class Server {
882
1061
  options.watchFiles = [
883
1062
  {
884
1063
  paths: options.watchFiles.paths,
885
- options: options.watchFiles.options || {},
1064
+ options: getWatchOptions(options.watchFiles.options || {}),
886
1065
  },
887
1066
  ];
888
1067
  } else if (Array.isArray(options.watchFiles)) {
889
1068
  options.watchFiles = options.watchFiles.map((item) => {
890
1069
  if (typeof item === "string") {
891
- return { paths: item, options: {} };
1070
+ return { paths: item, options: getWatchOptions() };
892
1071
  }
893
1072
 
894
- return { paths: item.paths, options: item.options || {} };
1073
+ return {
1074
+ paths: item.paths,
1075
+ options: getWatchOptions(item.options || {}),
1076
+ };
895
1077
  });
896
1078
  } else {
897
1079
  options.watchFiles = [];
@@ -1066,7 +1248,6 @@ class Server {
1066
1248
  if (this.options.webSocketServer) {
1067
1249
  const compilers = this.compiler.compilers || [this.compiler];
1068
1250
 
1069
- // eslint-disable-next-line no-shadow
1070
1251
  compilers.forEach((compiler) => {
1071
1252
  this.addAdditionalEntries(compiler);
1072
1253
 
@@ -1117,15 +1298,11 @@ class Server {
1117
1298
 
1118
1299
  let needForceShutdown = false;
1119
1300
 
1120
- const exitProcess = () => {
1121
- // eslint-disable-next-line no-process-exit
1122
- process.exit();
1123
- };
1124
-
1125
1301
  signals.forEach((signal) => {
1126
- process.on(signal, () => {
1302
+ const listener = () => {
1127
1303
  if (needForceShutdown) {
1128
- exitProcess();
1304
+ // eslint-disable-next-line no-process-exit
1305
+ process.exit();
1129
1306
  }
1130
1307
 
1131
1308
  this.logger.info(
@@ -1136,19 +1313,26 @@ class Server {
1136
1313
 
1137
1314
  this.stopCallback(() => {
1138
1315
  if (typeof this.compiler.close === "function") {
1139
- this.compiler.close(exitProcess);
1316
+ this.compiler.close(() => {
1317
+ // eslint-disable-next-line no-process-exit
1318
+ process.exit();
1319
+ });
1140
1320
  } else {
1141
- exitProcess();
1321
+ // eslint-disable-next-line no-process-exit
1322
+ process.exit();
1142
1323
  }
1143
1324
  });
1144
- });
1325
+ };
1326
+
1327
+ this.listeners.push({ name: signal, listener });
1328
+
1329
+ process.on(signal, listener);
1145
1330
  });
1146
1331
  }
1147
1332
 
1148
1333
  // Proxy WebSocket without the initial http request
1149
1334
  // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
1150
- // eslint-disable-next-line func-names
1151
- this.webSocketProxies.forEach(function (webSocketProxy) {
1335
+ this.webSocketProxies.forEach((webSocketProxy) => {
1152
1336
  this.server.on("upgrade", webSocketProxy.upgrade);
1153
1337
  }, this);
1154
1338
  }
@@ -1547,28 +1731,11 @@ class Server {
1547
1731
  }
1548
1732
 
1549
1733
  createServer() {
1550
- if (this.options.https) {
1551
- if (this.options.http2) {
1552
- // TODO: we need to replace spdy with http2 which is an internal module
1553
- this.server = require("spdy").createServer(
1554
- {
1555
- ...this.options.https,
1556
- spdy: {
1557
- protocols: ["h2", "http/1.1"],
1558
- },
1559
- },
1560
- this.app
1561
- );
1562
- } else {
1563
- const https = require("https");
1564
-
1565
- this.server = https.createServer(this.options.https, this.app);
1566
- }
1567
- } else {
1568
- const http = require("http");
1569
-
1570
- this.server = http.createServer(this.app);
1571
- }
1734
+ // eslint-disable-next-line import/no-dynamic-require
1735
+ this.server = require(this.options.server.type).createServer(
1736
+ this.options.server.options,
1737
+ this.app
1738
+ );
1572
1739
 
1573
1740
  this.server.on("connection", (socket) => {
1574
1741
  // Add socket to list
@@ -1585,6 +1752,7 @@ class Server {
1585
1752
  });
1586
1753
  }
1587
1754
 
1755
+ // TODO: remove `--web-socket-server` in favor of `--web-socket-server-type`
1588
1756
  createWebSocketServer() {
1589
1757
  this.webSocketServer = new (this.getServerTransport())(this);
1590
1758
  this.webSocketServer.implementation.on("connection", (client, request) => {
@@ -1610,7 +1778,9 @@ class Server {
1610
1778
  ) {
1611
1779
  this.sendMessage([client], "error", "Invalid Host/Origin header");
1612
1780
 
1613
- client.terminate();
1781
+ // With https enabled, the sendMessage above is encrypted asynchronously so not yet sent
1782
+ // Terminate would prevent it sending, so use close to allow it to be sent
1783
+ client.close();
1614
1784
 
1615
1785
  return;
1616
1786
  }
@@ -1627,6 +1797,10 @@ class Server {
1627
1797
  this.sendMessage([client], "progress", this.options.client.progress);
1628
1798
  }
1629
1799
 
1800
+ if (this.options.client && this.options.client.reconnect) {
1801
+ this.sendMessage([client], "reconnect", this.options.client.reconnect);
1802
+ }
1803
+
1630
1804
  if (this.options.client && this.options.client.overlay) {
1631
1805
  this.sendMessage([client], "overlay", this.options.client.overlay);
1632
1806
  }
@@ -1674,22 +1848,25 @@ class Server {
1674
1848
  );
1675
1849
  }
1676
1850
 
1677
- runBonjour() {
1678
- const bonjour = require("bonjour")();
1851
+ stopBonjour(callback = () => {}) {
1852
+ this.bonjour.unpublishAll(() => {
1853
+ this.bonjour.destroy();
1679
1854
 
1680
- bonjour.publish({
1855
+ if (callback) {
1856
+ callback();
1857
+ }
1858
+ });
1859
+ }
1860
+
1861
+ runBonjour() {
1862
+ this.bonjour = require("bonjour")();
1863
+ this.bonjour.publish({
1681
1864
  name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
1682
1865
  port: this.options.port,
1683
- type: this.options.https ? "https" : "http",
1866
+ type: this.options.server.type === "http" ? "http" : "https",
1684
1867
  subtypes: ["webpack"],
1685
1868
  ...this.options.bonjour,
1686
1869
  });
1687
-
1688
- process.on("exit", () => {
1689
- bonjour.unpublishAll(() => {
1690
- bonjour.destroy();
1691
- });
1692
- });
1693
1870
  }
1694
1871
 
1695
1872
  logStatus() {
@@ -1731,7 +1908,7 @@ class Server {
1731
1908
  if (this.options.ipc) {
1732
1909
  this.logger.info(`Project is running at: "${this.server.address()}"`);
1733
1910
  } else {
1734
- const protocol = this.options.https ? "https" : "http";
1911
+ const protocol = this.options.server.type === "http" ? "http" : "https";
1735
1912
  const { address, port } = this.server.address();
1736
1913
  const prettyPrintURL = (newHostname) =>
1737
1914
  url.format({ protocol, hostname: newHostname, port, pathname: "/" });
@@ -1766,13 +1943,13 @@ class Server {
1766
1943
  if (parsedIP.range() === "unspecified") {
1767
1944
  localhost = prettyPrintURL("localhost");
1768
1945
 
1769
- const networkIPv4 = internalIp.v4.sync();
1946
+ const networkIPv4 = Server.internalIPSync("v4");
1770
1947
 
1771
1948
  if (networkIPv4) {
1772
1949
  networkUrlIPv4 = prettyPrintURL(networkIPv4);
1773
1950
  }
1774
1951
 
1775
- const networkIPv6 = internalIp.v6.sync();
1952
+ const networkIPv6 = Server.internalIPSync("v6");
1776
1953
 
1777
1954
  if (networkIPv6) {
1778
1955
  networkUrlIPv6 = prettyPrintURL(networkIPv6);
@@ -1850,7 +2027,9 @@ class Server {
1850
2027
 
1851
2028
  if (this.options.bonjour) {
1852
2029
  const bonjourProtocol =
1853
- this.options.bonjour.type || this.options.https ? "https" : "http";
2030
+ this.options.bonjour.type || this.options.server.type === "http"
2031
+ ? "http"
2032
+ : "https";
1854
2033
 
1855
2034
  this.logger.info(
1856
2035
  `Broadcasting "${bonjourProtocol}" with subtype of "webpack" via ZeroConf DNS (Bonjour)`
@@ -1969,14 +2148,14 @@ class Server {
1969
2148
  }
1970
2149
 
1971
2150
  // eslint-disable-next-line class-methods-use-this
1972
- sendMessage(clients, type, data) {
1973
- clients.forEach((client) => {
2151
+ sendMessage(clients, type, data, params) {
2152
+ for (const client of clients) {
1974
2153
  // `sockjs` uses `1` to indicate client is ready to accept data
1975
2154
  // `ws` uses `WebSocket.OPEN`, but it is mean `1` too
1976
2155
  if (client.readyState === 1) {
1977
- client.send(JSON.stringify({ type, data }));
2156
+ client.send(JSON.stringify({ type, data, params }));
1978
2157
  }
1979
- });
2158
+ }
1980
2159
  }
1981
2160
 
1982
2161
  serveMagicHtml(req, res, next) {
@@ -2011,8 +2190,7 @@ class Server {
2011
2190
  stats &&
2012
2191
  (!stats.errors || stats.errors.length === 0) &&
2013
2192
  (!stats.warnings || stats.warnings.length === 0) &&
2014
- stats.assets &&
2015
- stats.assets.every((asset) => !asset.emitted);
2193
+ this.currentHash === stats.hash;
2016
2194
 
2017
2195
  if (shouldEmit) {
2018
2196
  this.sendMessage(clients, "still-ok");
@@ -2020,11 +2198,20 @@ class Server {
2020
2198
  return;
2021
2199
  }
2022
2200
 
2201
+ this.currentHash = stats.hash;
2023
2202
  this.sendMessage(clients, "hash", stats.hash);
2024
2203
 
2025
2204
  if (stats.errors.length > 0 || stats.warnings.length > 0) {
2205
+ const hasErrors = stats.errors.length > 0;
2206
+
2026
2207
  if (stats.warnings.length > 0) {
2027
- this.sendMessage(clients, "warnings", stats.warnings);
2208
+ let params;
2209
+
2210
+ if (hasErrors) {
2211
+ params = { preventReloading: true };
2212
+ }
2213
+
2214
+ this.sendMessage(clients, "warnings", stats.warnings, params);
2028
2215
  }
2029
2216
 
2030
2217
  if (stats.errors.length > 0) {
@@ -2036,37 +2223,8 @@ class Server {
2036
2223
  }
2037
2224
 
2038
2225
  watchFiles(watchPath, watchOptions) {
2039
- // duplicate the same massaging of options that watchpack performs
2040
- // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
2041
- // this isn't an elegant solution, but we'll improve it in the future
2042
- // eslint-disable-next-line no-undefined
2043
- const usePolling =
2044
- typeof watchOptions.usePolling !== "undefined"
2045
- ? watchOptions.usePolling
2046
- : Boolean(watchOptions.poll);
2047
- const interval =
2048
- // eslint-disable-next-line no-nested-ternary
2049
- typeof watchOptions.interval !== "undefined"
2050
- ? watchOptions.interval
2051
- : typeof watchOptions.poll === "number"
2052
- ? watchOptions.poll
2053
- : // eslint-disable-next-line no-undefined
2054
- undefined;
2055
-
2056
- const finalWatchOptions = {
2057
- ignoreInitial: true,
2058
- persistent: true,
2059
- followSymlinks: false,
2060
- atomic: false,
2061
- alwaysStat: true,
2062
- ignorePermissionErrors: true,
2063
- ignored: watchOptions.ignored,
2064
- usePolling,
2065
- interval,
2066
- };
2067
-
2068
2226
  const chokidar = require("chokidar");
2069
- const watcher = chokidar.watch(watchPath, finalWatchOptions);
2227
+ const watcher = chokidar.watch(watchPath, watchOptions);
2070
2228
 
2071
2229
  // disabling refreshing on changing the content
2072
2230
  if (this.options.liveReload) {
@@ -2158,11 +2316,21 @@ class Server {
2158
2316
  }
2159
2317
  }
2160
2318
 
2161
- startCallback(callback) {
2162
- this.start().then(() => callback(null), callback);
2319
+ startCallback(callback = () => {}) {
2320
+ this.start()
2321
+ .then(() => callback(null), callback)
2322
+ .catch(callback);
2163
2323
  }
2164
2324
 
2165
2325
  async stop() {
2326
+ if (this.bonjour) {
2327
+ await new Promise((resolve) => {
2328
+ this.stopBonjour(() => {
2329
+ resolve();
2330
+ });
2331
+ });
2332
+ }
2333
+
2166
2334
  this.webSocketProxies = [];
2167
2335
 
2168
2336
  await Promise.all(this.staticWatchers.map((watcher) => watcher.close()));
@@ -2216,10 +2384,18 @@ class Server {
2216
2384
  this.middleware = null;
2217
2385
  }
2218
2386
  }
2387
+
2388
+ // We add listeners to signals when creating a new Server instance
2389
+ // So ensure they are removed to prevent EventEmitter memory leak warnings
2390
+ for (const item of this.listeners) {
2391
+ process.removeListener(item.name, item.listener);
2392
+ }
2219
2393
  }
2220
2394
 
2221
- stopCallback(callback) {
2222
- this.stop().then(() => callback(null), callback);
2395
+ stopCallback(callback = () => {}) {
2396
+ this.stop()
2397
+ .then(() => callback(null), callback)
2398
+ .catch(callback);
2223
2399
  }
2224
2400
 
2225
2401
  // TODO remove in the next major release