webpack-dev-server 4.2.1 → 4.5.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
@@ -32,17 +32,18 @@ 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,
@@ -74,7 +75,7 @@ class Server {
74
75
  }
75
76
 
76
77
  static async getFreePort(port) {
77
- if (port && port !== "auto") {
78
+ if (typeof port !== "undefined" && port !== null && port !== "auto") {
78
79
  return port;
79
80
  }
80
81
 
@@ -155,7 +156,7 @@ class Server {
155
156
  if (typeof this.options.client.webSocketURL.protocol !== "undefined") {
156
157
  protocol = this.options.client.webSocketURL.protocol;
157
158
  } else {
158
- protocol = this.options.https ? "wss:" : "ws:";
159
+ protocol = this.options.server.type === "http" ? "ws:" : "wss:";
159
160
  }
160
161
 
161
162
  searchParams.set("protocol", protocol);
@@ -258,6 +259,10 @@ class Server {
258
259
  searchParams.set("logging", this.options.client.logging);
259
260
  }
260
261
 
262
+ if (typeof this.options.client.reconnect !== "undefined") {
263
+ searchParams.set("reconnect", this.options.client.reconnect);
264
+ }
265
+
261
266
  webSocketURL = searchParams.toString();
262
267
  }
263
268
 
@@ -474,6 +479,14 @@ class Server {
474
479
  };
475
480
  }
476
481
 
482
+ if (typeof options.client.reconnect === "undefined") {
483
+ options.client.reconnect = 10;
484
+ } else if (options.client.reconnect === true) {
485
+ options.client.reconnect = Infinity;
486
+ } else if (options.client.reconnect === false) {
487
+ options.client.reconnect = 0;
488
+ }
489
+
477
490
  // Respect infrastructureLogging.level
478
491
  if (typeof options.client.logging === "undefined") {
479
492
  options.client.logging = compilerOptions.infrastructureLogging
@@ -508,23 +521,62 @@ class Server {
508
521
  ? options.hot
509
522
  : true;
510
523
 
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,
524
+ const isHTTPs = Boolean(options.https);
525
+ const isSPDY = Boolean(options.http2);
526
+
527
+ if (isHTTPs || isSPDY) {
528
+ // TODO: remove in the next major release
529
+ util.deprecate(
530
+ () => {},
531
+ `'${
532
+ isHTTPs ? "https" : "http2"
533
+ }' option is deprecated. Please use the 'server' option.`,
534
+ `DEP_WEBPACK_DEV_SERVER_${isHTTPs ? "HTTPS" : "HTTP2"}`
535
+ )();
536
+ }
537
+
538
+ options.server = {
539
+ type:
540
+ // eslint-disable-next-line no-nested-ternary
541
+ typeof options.server === "string"
542
+ ? options.server
543
+ : // eslint-disable-next-line no-nested-ternary
544
+ typeof (options.server || {}).type === "string"
545
+ ? options.server.type
546
+ : // eslint-disable-next-line no-nested-ternary
547
+ isSPDY
548
+ ? "spdy"
549
+ : isHTTPs
550
+ ? "https"
551
+ : "http",
552
+ options: {
553
+ ...options.https,
554
+ ...(options.server || {}).options,
555
+ },
556
+ };
557
+
558
+ if (
559
+ options.server.type === "spdy" &&
560
+ typeof options.server.options.spdy === "undefined"
561
+ ) {
562
+ options.server.options.spdy = {
563
+ protocols: ["h2", "http/1.1"],
515
564
  };
516
565
  }
517
566
 
518
- // https option
519
- if (options.https) {
567
+ if (options.server.type === "https" || options.server.type === "spdy") {
568
+ if (typeof options.server.options.requestCert === "undefined") {
569
+ options.server.options.requestCert = false;
570
+ }
571
+
520
572
  // TODO remove the `cacert` option in favor `ca` in the next major release
521
573
  for (const property of ["cacert", "ca", "cert", "crl", "key", "pfx"]) {
522
- if (typeof options.https[property] === "undefined") {
574
+ if (typeof options.server.options[property] === "undefined") {
523
575
  // eslint-disable-next-line no-continue
524
576
  continue;
525
577
  }
526
578
 
527
- const value = options.https[property];
579
+ const value = options.server.options[property];
528
580
  const readFile = (item) => {
529
581
  if (
530
582
  Buffer.isBuffer(item) ||
@@ -547,14 +599,14 @@ class Server {
547
599
  }
548
600
  };
549
601
 
550
- options.https[property] = Array.isArray(value)
602
+ options.server.options[property] = Array.isArray(value)
551
603
  ? value.map((item) => readFile(item))
552
604
  : readFile(value);
553
605
  }
554
606
 
555
607
  let fakeCert;
556
608
 
557
- if (!options.https.key || !options.https.cert) {
609
+ if (!options.server.options.key || !options.server.options.cert) {
558
610
  const certificateDir = Server.findCacheDir();
559
611
  const certificatePath = path.join(certificateDir, "server.pem");
560
612
  let certificateExists;
@@ -577,7 +629,7 @@ class Server {
577
629
  const del = require("del");
578
630
 
579
631
  this.logger.info(
580
- "SSL Certificate is more than 30 days old. Removing..."
632
+ "SSL certificate is more than 30 days old. Removing..."
581
633
  );
582
634
 
583
635
  await del([certificatePath], { force: true });
@@ -587,7 +639,7 @@ class Server {
587
639
  }
588
640
 
589
641
  if (!certificateExists) {
590
- this.logger.info("Generating SSL Certificate...");
642
+ this.logger.info("Generating SSL certificate...");
591
643
 
592
644
  const selfsigned = require("selfsigned");
593
645
  const attributes = [{ name: "commonName", value: "localhost" }];
@@ -669,20 +721,20 @@ class Server {
669
721
  this.logger.info(`SSL certificate: ${certificatePath}`);
670
722
  }
671
723
 
672
- if (options.https.cacert) {
673
- if (options.https.ca) {
724
+ if (options.server.options.cacert) {
725
+ if (options.server.options.ca) {
674
726
  this.logger.warn(
675
- "Do not specify 'https.ca' and 'https.cacert' options together, the 'https.ca' option will be used."
727
+ "Do not specify 'ca' and 'cacert' options together, the 'ca' option will be used."
676
728
  );
677
729
  } else {
678
- options.https.ca = options.https.cacert;
730
+ options.server.options.ca = options.server.options.cacert;
679
731
  }
680
732
 
681
- delete options.https.cacert;
733
+ delete options.server.options.cacert;
682
734
  }
683
735
 
684
- options.https.key = options.https.key || fakeCert;
685
- options.https.cert = options.https.cert || fakeCert;
736
+ options.server.options.key = options.server.options.key || fakeCert;
737
+ options.server.options.cert = options.server.options.cert || fakeCert;
686
738
  }
687
739
 
688
740
  if (typeof options.ipc === "boolean") {
@@ -1063,42 +1115,43 @@ class Server {
1063
1115
  }
1064
1116
 
1065
1117
  async initialize() {
1066
- const compilers = this.compiler.compilers || [this.compiler];
1118
+ if (this.options.webSocketServer) {
1119
+ const compilers = this.compiler.compilers || [this.compiler];
1067
1120
 
1068
- // eslint-disable-next-line no-shadow
1069
- compilers.forEach((compiler) => {
1070
- this.addAdditionalEntries(compiler);
1121
+ // eslint-disable-next-line no-shadow
1122
+ compilers.forEach((compiler) => {
1123
+ this.addAdditionalEntries(compiler);
1071
1124
 
1072
- const webpack = compiler.webpack || require("webpack");
1125
+ const webpack = compiler.webpack || require("webpack");
1073
1126
 
1074
- const providePlugin = new webpack.ProvidePlugin({
1075
- __webpack_dev_server_client__: this.getClientTransport(),
1076
- });
1127
+ new webpack.ProvidePlugin({
1128
+ __webpack_dev_server_client__: this.getClientTransport(),
1129
+ }).apply(compiler);
1077
1130
 
1078
- providePlugin.apply(compiler);
1131
+ // TODO remove after drop webpack v4 support
1132
+ compiler.options.plugins = compiler.options.plugins || [];
1079
1133
 
1080
- // TODO remove after drop webpack v4 support
1081
- compiler.options.plugins = compiler.options.plugins || [];
1134
+ if (this.options.hot) {
1135
+ const HMRPluginExists = compiler.options.plugins.find(
1136
+ (p) => p.constructor === webpack.HotModuleReplacementPlugin
1137
+ );
1082
1138
 
1083
- if (this.options.hot) {
1084
- const HMRPluginExists = compiler.options.plugins.find(
1085
- (p) => p.constructor === webpack.HotModuleReplacementPlugin
1086
- );
1139
+ if (HMRPluginExists) {
1140
+ this.logger.warn(
1141
+ `"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.`
1142
+ );
1143
+ } else {
1144
+ // Apply the HMR plugin
1145
+ const plugin = new webpack.HotModuleReplacementPlugin();
1087
1146
 
1088
- if (HMRPluginExists) {
1089
- this.logger.warn(
1090
- `"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.`
1091
- );
1092
- } else {
1093
- // apply the HMR plugin
1094
- const plugin = new webpack.HotModuleReplacementPlugin();
1095
- plugin.apply(compiler);
1147
+ plugin.apply(compiler);
1148
+ }
1096
1149
  }
1097
- }
1098
- });
1150
+ });
1099
1151
 
1100
- if (this.options.client && this.options.client.progress) {
1101
- this.setupProgressPlugin();
1152
+ if (this.options.client && this.options.client.progress) {
1153
+ this.setupProgressPlugin();
1154
+ }
1102
1155
  }
1103
1156
 
1104
1157
  this.setupHooks();
@@ -1114,20 +1167,43 @@ class Server {
1114
1167
  if (this.options.setupExitSignals) {
1115
1168
  const signals = ["SIGINT", "SIGTERM"];
1116
1169
 
1170
+ let needForceShutdown = false;
1171
+
1117
1172
  signals.forEach((signal) => {
1118
- process.on(signal, () => {
1119
- this.stopCallback(() => {
1173
+ const listener = () => {
1174
+ if (needForceShutdown) {
1120
1175
  // eslint-disable-next-line no-process-exit
1121
1176
  process.exit();
1177
+ }
1178
+
1179
+ this.logger.info(
1180
+ "Gracefully shutting down. To force exit, press ^C again. Please wait..."
1181
+ );
1182
+
1183
+ needForceShutdown = true;
1184
+
1185
+ this.stopCallback(() => {
1186
+ if (typeof this.compiler.close === "function") {
1187
+ this.compiler.close(() => {
1188
+ // eslint-disable-next-line no-process-exit
1189
+ process.exit();
1190
+ });
1191
+ } else {
1192
+ // eslint-disable-next-line no-process-exit
1193
+ process.exit();
1194
+ }
1122
1195
  });
1123
- });
1196
+ };
1197
+
1198
+ this.listeners.push({ name: signal, listener });
1199
+
1200
+ process.on(signal, listener);
1124
1201
  });
1125
1202
  }
1126
1203
 
1127
1204
  // Proxy WebSocket without the initial http request
1128
1205
  // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
1129
- // eslint-disable-next-line func-names
1130
- this.webSocketProxies.forEach(function (webSocketProxy) {
1206
+ this.webSocketProxies.forEach((webSocketProxy) => {
1131
1207
  this.server.on("upgrade", webSocketProxy.upgrade);
1132
1208
  }, this);
1133
1209
  }
@@ -1526,28 +1602,11 @@ class Server {
1526
1602
  }
1527
1603
 
1528
1604
  createServer() {
1529
- if (this.options.https) {
1530
- if (this.options.http2) {
1531
- // TODO: we need to replace spdy with http2 which is an internal module
1532
- this.server = require("spdy").createServer(
1533
- {
1534
- ...this.options.https,
1535
- spdy: {
1536
- protocols: ["h2", "http/1.1"],
1537
- },
1538
- },
1539
- this.app
1540
- );
1541
- } else {
1542
- const https = require("https");
1543
-
1544
- this.server = https.createServer(this.options.https, this.app);
1545
- }
1546
- } else {
1547
- const http = require("http");
1548
-
1549
- this.server = http.createServer(this.app);
1550
- }
1605
+ // eslint-disable-next-line import/no-dynamic-require
1606
+ this.server = require(this.options.server.type).createServer(
1607
+ this.options.server.options,
1608
+ this.app
1609
+ );
1551
1610
 
1552
1611
  this.server.on("connection", (socket) => {
1553
1612
  // Add socket to list
@@ -1564,6 +1623,7 @@ class Server {
1564
1623
  });
1565
1624
  }
1566
1625
 
1626
+ // TODO: remove `--web-socket-server` in favor of `--web-socket-server-type`
1567
1627
  createWebSocketServer() {
1568
1628
  this.webSocketServer = new (this.getServerTransport())(this);
1569
1629
  this.webSocketServer.implementation.on("connection", (client, request) => {
@@ -1589,7 +1649,9 @@ class Server {
1589
1649
  ) {
1590
1650
  this.sendMessage([client], "error", "Invalid Host/Origin header");
1591
1651
 
1592
- client.terminate();
1652
+ // With https enabled, the sendMessage above is encrypted asynchronously so not yet sent
1653
+ // Terminate would prevent it sending, so use close to allow it to be sent
1654
+ client.close();
1593
1655
 
1594
1656
  return;
1595
1657
  }
@@ -1606,6 +1668,10 @@ class Server {
1606
1668
  this.sendMessage([client], "progress", this.options.client.progress);
1607
1669
  }
1608
1670
 
1671
+ if (this.options.client && this.options.client.reconnect) {
1672
+ this.sendMessage([client], "reconnect", this.options.client.reconnect);
1673
+ }
1674
+
1609
1675
  if (this.options.client && this.options.client.overlay) {
1610
1676
  this.sendMessage([client], "overlay", this.options.client.overlay);
1611
1677
  }
@@ -1653,26 +1719,29 @@ class Server {
1653
1719
  );
1654
1720
  }
1655
1721
 
1656
- runBonjour() {
1657
- const bonjour = require("bonjour")();
1722
+ stopBonjour(callback = () => {}) {
1723
+ this.bonjour.unpublishAll(() => {
1724
+ this.bonjour.destroy();
1725
+
1726
+ if (callback) {
1727
+ callback();
1728
+ }
1729
+ });
1730
+ }
1658
1731
 
1659
- bonjour.publish({
1732
+ runBonjour() {
1733
+ this.bonjour = require("bonjour")();
1734
+ this.bonjour.publish({
1660
1735
  name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
1661
1736
  port: this.options.port,
1662
- type: this.options.https ? "https" : "http",
1737
+ type: this.options.server.type === "http" ? "http" : "https",
1663
1738
  subtypes: ["webpack"],
1664
1739
  ...this.options.bonjour,
1665
1740
  });
1666
-
1667
- process.on("exit", () => {
1668
- bonjour.unpublishAll(() => {
1669
- bonjour.destroy();
1670
- });
1671
- });
1672
1741
  }
1673
1742
 
1674
1743
  logStatus() {
1675
- const colorette = require("colorette");
1744
+ const { isColorSupported, cyan, red } = require("colorette");
1676
1745
 
1677
1746
  const getColorsOption = (compilerOptions) => {
1678
1747
  let colorsEnabled;
@@ -1683,7 +1752,7 @@ class Server {
1683
1752
  ) {
1684
1753
  colorsEnabled = compilerOptions.stats;
1685
1754
  } else {
1686
- colorsEnabled = colorette.options.enabled;
1755
+ colorsEnabled = isColorSupported;
1687
1756
  }
1688
1757
 
1689
1758
  return colorsEnabled;
@@ -1692,14 +1761,14 @@ class Server {
1692
1761
  const colors = {
1693
1762
  info(useColor, msg) {
1694
1763
  if (useColor) {
1695
- return colorette.cyan(msg);
1764
+ return cyan(msg);
1696
1765
  }
1697
1766
 
1698
1767
  return msg;
1699
1768
  },
1700
1769
  error(useColor, msg) {
1701
1770
  if (useColor) {
1702
- return colorette.red(msg);
1771
+ return red(msg);
1703
1772
  }
1704
1773
 
1705
1774
  return msg;
@@ -1710,7 +1779,7 @@ class Server {
1710
1779
  if (this.options.ipc) {
1711
1780
  this.logger.info(`Project is running at: "${this.server.address()}"`);
1712
1781
  } else {
1713
- const protocol = this.options.https ? "https" : "http";
1782
+ const protocol = this.options.server.type === "http" ? "http" : "https";
1714
1783
  const { address, port } = this.server.address();
1715
1784
  const prettyPrintURL = (newHostname) =>
1716
1785
  url.format({ protocol, hostname: newHostname, port, pathname: "/" });
@@ -1829,7 +1898,9 @@ class Server {
1829
1898
 
1830
1899
  if (this.options.bonjour) {
1831
1900
  const bonjourProtocol =
1832
- this.options.bonjour.type || this.options.https ? "https" : "http";
1901
+ this.options.bonjour.type || this.options.server.type === "http"
1902
+ ? "http"
1903
+ : "https";
1833
1904
 
1834
1905
  this.logger.info(
1835
1906
  `Broadcasting "${bonjourProtocol}" with subtype of "webpack" via ZeroConf DNS (Bonjour)`
@@ -1845,10 +1916,19 @@ class Server {
1845
1916
  headers = headers(req, res, this.middleware.context);
1846
1917
  }
1847
1918
 
1848
- // eslint-disable-next-line guard-for-in
1849
- for (const name in headers) {
1850
- res.setHeader(name, headers[name]);
1919
+ const allHeaders = [];
1920
+
1921
+ if (!Array.isArray(headers)) {
1922
+ // eslint-disable-next-line guard-for-in
1923
+ for (const name in headers) {
1924
+ allHeaders.push({ key: name, value: headers[name] });
1925
+ }
1926
+ headers = allHeaders;
1851
1927
  }
1928
+
1929
+ headers.forEach((header) => {
1930
+ res.setHeader(header.key, header.value);
1931
+ });
1852
1932
  }
1853
1933
 
1854
1934
  next();
@@ -1940,13 +2020,13 @@ class Server {
1940
2020
 
1941
2021
  // eslint-disable-next-line class-methods-use-this
1942
2022
  sendMessage(clients, type, data) {
1943
- clients.forEach((client) => {
2023
+ for (const client of clients) {
1944
2024
  // `sockjs` uses `1` to indicate client is ready to accept data
1945
2025
  // `ws` uses `WebSocket.OPEN`, but it is mean `1` too
1946
2026
  if (client.readyState === 1) {
1947
2027
  client.send(JSON.stringify({ type, data }));
1948
2028
  }
1949
- });
2029
+ }
1950
2030
  }
1951
2031
 
1952
2032
  serveMagicHtml(req, res, next) {
@@ -1981,8 +2061,7 @@ class Server {
1981
2061
  stats &&
1982
2062
  (!stats.errors || stats.errors.length === 0) &&
1983
2063
  (!stats.warnings || stats.warnings.length === 0) &&
1984
- stats.assets &&
1985
- stats.assets.every((asset) => !asset.emitted);
2064
+ this.currentHash === stats.hash;
1986
2065
 
1987
2066
  if (shouldEmit) {
1988
2067
  this.sendMessage(clients, "still-ok");
@@ -1990,6 +2069,7 @@ class Server {
1990
2069
  return;
1991
2070
  }
1992
2071
 
2072
+ this.currentHash = stats.hash;
1993
2073
  this.sendMessage(clients, "hash", stats.hash);
1994
2074
 
1995
2075
  if (stats.errors.length > 0 || stats.warnings.length > 0) {
@@ -2036,7 +2116,6 @@ class Server {
2036
2116
  };
2037
2117
 
2038
2118
  const chokidar = require("chokidar");
2039
-
2040
2119
  const watcher = chokidar.watch(watchPath, finalWatchOptions);
2041
2120
 
2042
2121
  // disabling refreshing on changing the content
@@ -2129,11 +2208,21 @@ class Server {
2129
2208
  }
2130
2209
  }
2131
2210
 
2132
- startCallback(callback) {
2133
- this.start().then(() => callback(null), callback);
2211
+ startCallback(callback = () => {}) {
2212
+ this.start()
2213
+ .then(() => callback(null), callback)
2214
+ .catch(callback);
2134
2215
  }
2135
2216
 
2136
2217
  async stop() {
2218
+ if (this.bonjour) {
2219
+ await new Promise((resolve) => {
2220
+ this.stopBonjour(() => {
2221
+ resolve();
2222
+ });
2223
+ });
2224
+ }
2225
+
2137
2226
  this.webSocketProxies = [];
2138
2227
 
2139
2228
  await Promise.all(this.staticWatchers.map((watcher) => watcher.close()));
@@ -2187,10 +2276,18 @@ class Server {
2187
2276
  this.middleware = null;
2188
2277
  }
2189
2278
  }
2279
+
2280
+ // We add listeners to signals when creating a new Server instance
2281
+ // So ensure they are removed to prevent EventEmitter memory leak warnings
2282
+ for (const item of this.listeners) {
2283
+ process.removeListener(item.name, item.listener);
2284
+ }
2190
2285
  }
2191
2286
 
2192
- stopCallback(callback) {
2193
- this.stop().then(() => callback(null), callback);
2287
+ stopCallback(callback = () => {}) {
2288
+ this.stop()
2289
+ .then(() => callback(null), callback)
2290
+ .catch(callback);
2194
2291
  }
2195
2292
 
2196
2293
  // TODO remove in the next major release