webpack-dev-server 4.3.1 → 4.7.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,19 +6,219 @@ 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");
13
13
 
14
+ /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
15
+ /** @typedef {import("webpack").Compiler} Compiler */
16
+ /** @typedef {import("webpack").MultiCompiler} MultiCompiler */
17
+ /** @typedef {import("webpack").Configuration} WebpackConfiguration */
18
+ /** @typedef {import("webpack").StatsOptions} StatsOptions */
19
+ /** @typedef {import("webpack").StatsCompilation} StatsCompilation */
20
+ /** @typedef {import("webpack").Stats} Stats */
21
+ /** @typedef {import("webpack").MultiStats} MultiStats */
22
+ /** @typedef {import("os").NetworkInterfaceInfo} NetworkInterfaceInfo */
23
+ /** @typedef {import("express").Request} Request */
24
+ /** @typedef {import("express").Response} Response */
25
+ /** @typedef {import("express").NextFunction} NextFunction */
26
+ /** @typedef {import("express").RequestHandler} ExpressRequestHandler */
27
+ /** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */
28
+ /** @typedef {import("chokidar").WatchOptions} WatchOptions */
29
+ /** @typedef {import("chokidar").FSWatcher} FSWatcher */
30
+ /** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */
31
+ /** @typedef {import("bonjour").Bonjour} Bonjour */
32
+ /** @typedef {import("bonjour").BonjourOptions} BonjourOptions */
33
+ /** @typedef {import("http-proxy-middleware").RequestHandler} RequestHandler */
34
+ /** @typedef {import("http-proxy-middleware").Options} HttpProxyMiddlewareOptions */
35
+ /** @typedef {import("http-proxy-middleware").Filter} HttpProxyMiddlewareOptionsFilter */
36
+ /** @typedef {import("serve-index").Options} ServeIndexOptions */
37
+ /** @typedef {import("serve-static").ServeStaticOptions} ServeStaticOptions */
38
+ /** @typedef {import("ipaddr.js").IPv4} IPv4 */
39
+ /** @typedef {import("ipaddr.js").IPv6} IPv6 */
40
+ /** @typedef {import("net").Socket} Socket */
41
+ /** @typedef {import("http").IncomingMessage} IncomingMessage */
42
+ /** @typedef {import("open").Options} OpenOptions */
43
+
44
+ /** @typedef {import("https").ServerOptions & { spdy?: { plain?: boolean | undefined, ssl?: boolean | undefined, 'x-forwarded-for'?: string | undefined, protocol?: string | undefined, protocols?: string[] | undefined }}} ServerOptions */
45
+
46
+ /**
47
+ * @template Request, Response
48
+ * @typedef {import("webpack-dev-middleware").Options<Request, Response>} DevMiddlewareOptions
49
+ */
50
+
51
+ /**
52
+ * @template Request, Response
53
+ * @typedef {import("webpack-dev-middleware").Context<Request, Response>} DevMiddlewareContext
54
+ */
55
+
56
+ /**
57
+ * @typedef {"local-ip" | "local-ipv4" | "local-ipv6" | string} Host
58
+ */
59
+
60
+ /**
61
+ * @typedef {number | string | "auto"} Port
62
+ */
63
+
64
+ /**
65
+ * @typedef {Object} WatchFiles
66
+ * @property {string | string[]} paths
67
+ * @property {WatchOptions & { aggregateTimeout?: number, ignored?: string | RegExp | string[], poll?: number | boolean }} [options]
68
+ */
69
+
70
+ /**
71
+ * @typedef {Object} Static
72
+ * @property {string} [directory]
73
+ * @property {string | string[]} [publicPath]
74
+ * @property {boolean | ServeIndexOptions} [serveIndex]
75
+ * @property {ServeStaticOptions} [staticOptions]
76
+ * @property {boolean | WatchOptions & { aggregateTimeout?: number, ignored?: string | RegExp | string[], poll?: number | boolean }} [watch]
77
+ */
78
+
79
+ /**
80
+ * @typedef {Object} NormalizedStatic
81
+ * @property {string} directory
82
+ * @property {string[]} publicPath
83
+ * @property {false | ServeIndexOptions} serveIndex
84
+ * @property {ServeStaticOptions} staticOptions
85
+ * @property {false | WatchOptions} watch
86
+ */
87
+
88
+ /**
89
+ * @typedef {Object} ServerConfiguration
90
+ * @property {"http" | "https" | "spdy" | string} [type]
91
+ * @property {ServerOptions} [options]
92
+ */
93
+
94
+ /**
95
+ * @typedef {Object} WebSocketServerConfiguration
96
+ * @property {"sockjs" | "ws" | string | Function} [type]
97
+ * @property {Record<string, any>} [options]
98
+ */
99
+
100
+ /**
101
+ * @typedef {(import("ws").WebSocket | import("sockjs").Connection & { send: import("ws").WebSocket["send"], terminate: import("ws").WebSocket["terminate"], ping: import("ws").WebSocket["ping"] }) & { isAlive?: boolean }} ClientConnection
102
+ */
103
+
104
+ /**
105
+ * @typedef {import("ws").WebSocketServer | import("sockjs").Server & { close: import("ws").WebSocketServer["close"] }} WebSocketServer
106
+ */
107
+
108
+ /**
109
+ * @typedef {{ implementation: WebSocketServer, clients: ClientConnection[] }} WebSocketServerImplementation
110
+ */
111
+
112
+ /**
113
+ * @typedef {{ [url: string]: string | HttpProxyMiddlewareOptions }} ProxyConfigMap
114
+ */
115
+
116
+ /**
117
+ * @typedef {HttpProxyMiddlewareOptions[]} ProxyArray
118
+ */
119
+
120
+ /**
121
+ * @callback ByPass
122
+ * @param {Request} req
123
+ * @param {Response} res
124
+ * @param {ProxyConfigArray} proxyConfig
125
+ */
126
+
127
+ /**
128
+ * @typedef {{ path?: string | string[] | undefined, context?: string | string[] | HttpProxyMiddlewareOptionsFilter | undefined } & HttpProxyMiddlewareOptions & ByPass} ProxyConfigArray
129
+ */
130
+
131
+ /**
132
+ * @typedef {Object} OpenApp
133
+ * @property {string} [name]
134
+ * @property {string[]} [arguments]
135
+ */
136
+
137
+ /**
138
+ * @typedef {Object} Open
139
+ * @property {string | string[] | OpenApp} [app]
140
+ * @property {string | string[]} [target]
141
+ */
142
+
143
+ /**
144
+ * @typedef {Object} NormalizedOpen
145
+ * @property {string} target
146
+ * @property {import("open").Options} options
147
+ */
148
+
149
+ /**
150
+ * @typedef {Object} WebSocketURL
151
+ * @property {string} [hostname]
152
+ * @property {string} [password]
153
+ * @property {string} [pathname]
154
+ * @property {number | string} [port]
155
+ * @property {string} [protocol]
156
+ * @property {string} [username]
157
+ */
158
+
159
+ /**
160
+ * @typedef {Object} ClientConfiguration
161
+ * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
162
+ * @property {boolean | { warnings?: boolean, errors?: boolean }} [overlay]
163
+ * @property {boolean} [progress]
164
+ * @property {boolean | number} [reconnect]
165
+ * @property {"ws" | "sockjs" | string} [webSocketTransport]
166
+ * @property {string | WebSocketURL} [webSocketURL]
167
+ */
168
+
169
+ /**
170
+ * @typedef {Array<{ key: string; value: string }> | Record<string, string | string[]>} Headers
171
+ */
172
+
173
+ /**
174
+ * @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware
175
+ */
176
+
177
+ /**
178
+ * @typedef {Object} Configuration
179
+ * @property {boolean | string} [ipc]
180
+ * @property {Host} [host]
181
+ * @property {Port} [port]
182
+ * @property {boolean | "only"} [hot]
183
+ * @property {boolean} [liveReload]
184
+ * @property {DevMiddlewareOptions<Request, Response>} [devMiddleware]
185
+ * @property {boolean} [compress]
186
+ * @property {boolean} [magicHtml]
187
+ * @property {"auto" | "all" | string | string[]} [allowedHosts]
188
+ * @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback]
189
+ * @property {boolean} [setupExitSignals]
190
+ * @property {boolean | BonjourOptions} [bonjour]
191
+ * @property {string | string[] | WatchFiles | Array<string | WatchFiles>} [watchFiles]
192
+ * @property {boolean | string | Static | Array<string | Static>} [static]
193
+ * @property {boolean | ServerOptions} [https]
194
+ * @property {boolean} [http2]
195
+ * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server]
196
+ * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer]
197
+ * @property {ProxyConfigMap | ProxyConfigArray | ProxyArray} [proxy]
198
+ * @property {boolean | string | Open | Array<string | Open>} [open]
199
+ * @property {boolean} [setupExitSignals]
200
+ * @property {boolean | ClientConfiguration} [client]
201
+ * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext<Request, Response>) => Headers)} [headers]
202
+ * @property {(devServer: Server) => void} [onAfterSetupMiddleware]
203
+ * @property {(devServer: Server) => void} [onBeforeSetupMiddleware]
204
+ * @property {(devServer: Server) => void} [onListening]
205
+ * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares]
206
+ */
207
+
14
208
  if (!process.env.WEBPACK_SERVE) {
209
+ // TODO fix me in the next major release
210
+ // @ts-ignore
15
211
  process.env.WEBPACK_SERVE = true;
16
212
  }
17
213
 
18
214
  class Server {
215
+ /**
216
+ * @param {Configuration | Compiler | MultiCompiler} options
217
+ * @param {Compiler | MultiCompiler | Configuration} compiler
218
+ */
19
219
  constructor(options = {}, compiler) {
20
220
  // TODO: remove this after plugin support is published
21
- if (options.hooks) {
221
+ if (/** @type {Compiler | MultiCompiler} */ (options).hooks) {
22
222
  util.deprecate(
23
223
  () => {},
24
224
  "Using 'compiler' as the first argument is deprecated. Please use 'options' as the first argument and 'compiler' as the second argument.",
@@ -28,17 +228,65 @@ class Server {
28
228
  [options = {}, compiler] = [compiler, options];
29
229
  }
30
230
 
31
- validate(schema, options, "webpack Dev Server");
231
+ validate(/** @type {Schema} */ (schema), options, {
232
+ name: "Dev Server",
233
+ baseDataPath: "options",
234
+ });
32
235
 
33
- this.options = options;
236
+ this.compiler = /** @type {Compiler | MultiCompiler} */ (compiler);
237
+ /**
238
+ * @type {ReturnType<Compiler["getInfrastructureLogger"]>}
239
+ * */
240
+ this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
241
+ this.options = /** @type {Configuration} */ (options);
242
+ /**
243
+ * @type {FSWatcher[]}
244
+ */
34
245
  this.staticWatchers = [];
246
+ /**
247
+ * @private
248
+ * @type {{ name: string | symbol, listener: (...args: any[]) => void}[] }}
249
+ */
250
+ this.listeners = [];
35
251
  // Keep track of websocket proxies for external websocket upgrade.
252
+ /**
253
+ * @private
254
+ * @type {RequestHandler[]}
255
+ */
36
256
  this.webSocketProxies = [];
257
+ /**
258
+ * @type {Socket[]}
259
+ */
37
260
  this.sockets = [];
38
- this.compiler = compiler;
39
- this.currentHash = null;
261
+ /**
262
+ * @private
263
+ * @type {string | undefined}
264
+ */
265
+ // eslint-disable-next-line no-undefined
266
+ this.currentHash = undefined;
267
+ }
268
+
269
+ // TODO compatibility with webpack v4, remove it after drop
270
+ static get cli() {
271
+ return {
272
+ get getArguments() {
273
+ return () => require("../bin/cli-flags");
274
+ },
275
+ get processArguments() {
276
+ return require("../bin/process-arguments");
277
+ },
278
+ };
279
+ }
280
+
281
+ static get schema() {
282
+ return schema;
40
283
  }
41
284
 
285
+ /**
286
+ * @private
287
+ * @returns {StatsOptions}
288
+ * @constructor
289
+ */
42
290
  static get DEFAULT_STATS() {
43
291
  return {
44
292
  all: false,
@@ -49,7 +297,10 @@ class Server {
49
297
  };
50
298
  }
51
299
 
52
- // eslint-disable-next-line class-methods-use-this
300
+ /**
301
+ * @param {string} URL
302
+ * @returns {boolean}
303
+ */
53
304
  static isAbsoluteURL(URL) {
54
305
  // Don't match Windows paths `c:\`
55
306
  if (/^[a-zA-Z]:\\/.test(URL)) {
@@ -61,18 +312,81 @@ class Server {
61
312
  return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL);
62
313
  }
63
314
 
315
+ /**
316
+ * @param {string} gateway
317
+ * @returns {string | undefined}
318
+ */
319
+ static findIp(gateway) {
320
+ const gatewayIp = ipaddr.parse(gateway);
321
+
322
+ // Look for the matching interface in all local interfaces.
323
+ for (const addresses of Object.values(os.networkInterfaces())) {
324
+ for (const { cidr } of /** @type {NetworkInterfaceInfo[]} */ (
325
+ addresses
326
+ )) {
327
+ const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));
328
+
329
+ if (
330
+ net[0] &&
331
+ net[0].kind() === gatewayIp.kind() &&
332
+ gatewayIp.match(net)
333
+ ) {
334
+ return net[0].toString();
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+ /**
341
+ * @param {"v4" | "v6"} family
342
+ * @returns {Promise<string | undefined>}
343
+ */
344
+ static async internalIP(family) {
345
+ try {
346
+ const { gateway } = await defaultGateway[family]();
347
+ return Server.findIp(gateway);
348
+ } catch {
349
+ // ignore
350
+ }
351
+ }
352
+
353
+ /**
354
+ * @param {"v4" | "v6"} family
355
+ * @returns {string | undefined}
356
+ */
357
+ static internalIPSync(family) {
358
+ try {
359
+ const { gateway } = defaultGateway[family].sync();
360
+ return Server.findIp(gateway);
361
+ } catch {
362
+ // ignore
363
+ }
364
+ }
365
+
366
+ /**
367
+ * @param {Host} hostname
368
+ * @returns {Promise<string>}
369
+ */
64
370
  static async getHostname(hostname) {
65
371
  if (hostname === "local-ip") {
66
- return (await internalIp.v4()) || (await internalIp.v6()) || "0.0.0.0";
372
+ return (
373
+ (await Server.internalIP("v4")) ||
374
+ (await Server.internalIP("v6")) ||
375
+ "0.0.0.0"
376
+ );
67
377
  } else if (hostname === "local-ipv4") {
68
- return (await internalIp.v4()) || "0.0.0.0";
378
+ return (await Server.internalIP("v4")) || "0.0.0.0";
69
379
  } else if (hostname === "local-ipv6") {
70
- return (await internalIp.v6()) || "::";
380
+ return (await Server.internalIP("v6")) || "::";
71
381
  }
72
382
 
73
383
  return hostname;
74
384
  }
75
385
 
386
+ /**
387
+ * @param {Port} port
388
+ * @returns {Promise<number | string>}
389
+ */
76
390
  static async getFreePort(port) {
77
391
  if (typeof port !== "undefined" && port !== null && port !== "auto") {
78
392
  return port;
@@ -81,21 +395,32 @@ class Server {
81
395
  const pRetry = require("p-retry");
82
396
  const portfinder = require("portfinder");
83
397
 
84
- portfinder.basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT || 8080;
398
+ portfinder.basePort =
399
+ typeof process.env.WEBPACK_DEV_SERVER_BASE_PORT !== "undefined"
400
+ ? parseInt(process.env.WEBPACK_DEV_SERVER_BASE_PORT, 10)
401
+ : 8080;
85
402
 
86
403
  // Try to find unused port and listen on it for 3 times,
87
404
  // if port is not specified in options.
88
405
  const defaultPortRetry =
89
- parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10) || 3;
406
+ typeof process.env.WEBPACK_DEV_SERVER_PORT_RETRY !== "undefined"
407
+ ? parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10)
408
+ : 3;
90
409
 
91
410
  return pRetry(() => portfinder.getPortPromise(), {
92
411
  retries: defaultPortRetry,
93
412
  });
94
413
  }
95
414
 
415
+ /**
416
+ * @returns {string}
417
+ */
96
418
  static findCacheDir() {
97
419
  const cwd = process.cwd();
98
420
 
421
+ /**
422
+ * @type {string | undefined}
423
+ */
99
424
  let dir = cwd;
100
425
 
101
426
  for (;;) {
@@ -126,7 +451,14 @@ class Server {
126
451
  return path.resolve(dir, "node_modules/.cache/webpack-dev-server");
127
452
  }
128
453
 
454
+ /**
455
+ * @private
456
+ * @param {Compiler} compiler
457
+ */
129
458
  addAdditionalEntries(compiler) {
459
+ /**
460
+ * @type {string[]}
461
+ */
130
462
  const additionalEntries = [];
131
463
 
132
464
  const isWebTarget = compiler.options.externalsPresets
@@ -140,38 +472,44 @@ class Server {
140
472
  // eslint-disable-next-line no-undefined
141
473
  undefined,
142
474
  null,
143
- ].includes(compiler.options.target);
475
+ ].includes(/** @type {string} */ (compiler.options.target));
144
476
 
145
477
  // TODO maybe empty empty client
146
478
  if (this.options.client && isWebTarget) {
147
- let webSocketURL = "";
479
+ let webSocketURLStr = "";
480
+
148
481
  if (this.options.webSocketServer) {
482
+ const webSocketURL =
483
+ /** @type {WebSocketURL} */
484
+ (
485
+ /** @type {ClientConfiguration} */
486
+ (this.options.client).webSocketURL
487
+ );
488
+ const webSocketServer =
489
+ /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable<WebSocketServerConfiguration["options"]> }} */
490
+ (this.options.webSocketServer);
149
491
  const searchParams = new URLSearchParams();
150
492
 
151
- /** @type {"ws:" | "wss:" | "http:" | "https:" | "auto:"} */
493
+ /** @type {string} */
152
494
  let protocol;
153
495
 
154
496
  // We are proxying dev server and need to specify custom `hostname`
155
- if (typeof this.options.client.webSocketURL.protocol !== "undefined") {
156
- protocol = this.options.client.webSocketURL.protocol;
497
+ if (typeof webSocketURL.protocol !== "undefined") {
498
+ protocol = webSocketURL.protocol;
157
499
  } else {
158
- protocol = this.options.https ? "wss:" : "ws:";
500
+ protocol =
501
+ /** @type {ServerConfiguration} */
502
+ (this.options.server).type === "http" ? "ws:" : "wss:";
159
503
  }
160
504
 
161
505
  searchParams.set("protocol", protocol);
162
506
 
163
- if (typeof this.options.client.webSocketURL.username !== "undefined") {
164
- searchParams.set(
165
- "username",
166
- this.options.client.webSocketURL.username
167
- );
507
+ if (typeof webSocketURL.username !== "undefined") {
508
+ searchParams.set("username", webSocketURL.username);
168
509
  }
169
510
 
170
- if (typeof this.options.client.webSocketURL.password !== "undefined") {
171
- searchParams.set(
172
- "password",
173
- this.options.client.webSocketURL.password
174
- );
511
+ if (typeof webSocketURL.password !== "undefined") {
512
+ searchParams.set("password", webSocketURL.password);
175
513
  }
176
514
 
177
515
  /** @type {string} */
@@ -179,18 +517,18 @@ class Server {
179
517
 
180
518
  // SockJS is not supported server mode, so `hostname` and `port` can't specified, let's ignore them
181
519
  // TODO show warning about this
182
- const isSockJSType = this.options.webSocketServer.type === "sockjs";
520
+ const isSockJSType = webSocketServer.type === "sockjs";
183
521
 
184
522
  // We are proxying dev server and need to specify custom `hostname`
185
- if (typeof this.options.client.webSocketURL.hostname !== "undefined") {
186
- hostname = this.options.client.webSocketURL.hostname;
523
+ if (typeof webSocketURL.hostname !== "undefined") {
524
+ hostname = webSocketURL.hostname;
187
525
  }
188
526
  // Web socket server works on custom `hostname`, only for `ws` because `sock-js` is not support custom `hostname`
189
527
  else if (
190
- typeof this.options.webSocketServer.options.host !== "undefined" &&
528
+ typeof webSocketServer.options.host !== "undefined" &&
191
529
  !isSockJSType
192
530
  ) {
193
- hostname = this.options.webSocketServer.options.host;
531
+ hostname = webSocketServer.options.host;
194
532
  }
195
533
  // The `host` option is specified
196
534
  else if (typeof this.options.host !== "undefined") {
@@ -207,15 +545,15 @@ class Server {
207
545
  let port;
208
546
 
209
547
  // We are proxying dev server and need to specify custom `port`
210
- if (typeof this.options.client.webSocketURL.port !== "undefined") {
211
- port = this.options.client.webSocketURL.port;
548
+ if (typeof webSocketURL.port !== "undefined") {
549
+ port = webSocketURL.port;
212
550
  }
213
551
  // Web socket server works on custom `port`, only for `ws` because `sock-js` is not support custom `port`
214
552
  else if (
215
- typeof this.options.webSocketServer.options.port !== "undefined" &&
553
+ typeof webSocketServer.options.port !== "undefined" &&
216
554
  !isSockJSType
217
555
  ) {
218
- port = this.options.webSocketServer.options.port;
556
+ port = webSocketServer.options.port;
219
557
  }
220
558
  // The `port` option is specified
221
559
  else if (typeof this.options.port === "number") {
@@ -239,43 +577,47 @@ class Server {
239
577
  let pathname = "";
240
578
 
241
579
  // We are proxying dev server and need to specify custom `pathname`
242
- if (typeof this.options.client.webSocketURL.pathname !== "undefined") {
243
- pathname = this.options.client.webSocketURL.pathname;
580
+ if (typeof webSocketURL.pathname !== "undefined") {
581
+ pathname = webSocketURL.pathname;
244
582
  }
245
583
  // Web socket server works on custom `path`
246
584
  else if (
247
- typeof this.options.webSocketServer.options.prefix !== "undefined" ||
248
- typeof this.options.webSocketServer.options.path !== "undefined"
585
+ typeof webSocketServer.options.prefix !== "undefined" ||
586
+ typeof webSocketServer.options.path !== "undefined"
249
587
  ) {
250
588
  pathname =
251
- this.options.webSocketServer.options.prefix ||
252
- this.options.webSocketServer.options.path;
589
+ webSocketServer.options.prefix || webSocketServer.options.path;
253
590
  }
254
591
 
255
592
  searchParams.set("pathname", pathname);
256
593
 
257
- if (typeof this.options.client.logging !== "undefined") {
258
- searchParams.set("logging", this.options.client.logging);
594
+ const client = /** @type {ClientConfiguration} */ (this.options.client);
595
+
596
+ if (typeof client.logging !== "undefined") {
597
+ searchParams.set("logging", client.logging);
598
+ }
599
+
600
+ if (typeof client.reconnect !== "undefined") {
601
+ searchParams.set(
602
+ "reconnect",
603
+ typeof client.reconnect === "number"
604
+ ? String(client.reconnect)
605
+ : "10"
606
+ );
259
607
  }
260
608
 
261
- webSocketURL = searchParams.toString();
609
+ webSocketURLStr = searchParams.toString();
262
610
  }
263
611
 
264
612
  additionalEntries.push(
265
- `${require.resolve("../client/index.js")}?${webSocketURL}`
613
+ `${require.resolve("../client/index.js")}?${webSocketURLStr}`
266
614
  );
267
615
  }
268
616
 
269
- if (this.options.hot) {
270
- let hotEntry;
271
-
272
- if (this.options.hot === "only") {
273
- hotEntry = require.resolve("webpack/hot/only-dev-server");
274
- } else if (this.options.hot) {
275
- hotEntry = require.resolve("webpack/hot/dev-server");
276
- }
277
-
278
- additionalEntries.push(hotEntry);
617
+ if (this.options.hot === "only") {
618
+ additionalEntries.push(require.resolve("webpack/hot/only-dev-server"));
619
+ } else if (this.options.hot) {
620
+ additionalEntries.push(require.resolve("webpack/hot/dev-server"));
279
621
  }
280
622
 
281
623
  const webpack = compiler.webpack || require("webpack");
@@ -293,9 +635,9 @@ class Server {
293
635
  else {
294
636
  /**
295
637
  * prependEntry Method for webpack 4
296
- * @param {Entry} originalEntry
297
- * @param {Entry} newAdditionalEntries
298
- * @returns {Entry}
638
+ * @param {any} originalEntry
639
+ * @param {any} newAdditionalEntries
640
+ * @returns {any}
299
641
  */
300
642
  const prependEntry = (originalEntry, newAdditionalEntries) => {
301
643
  if (typeof originalEntry === "function") {
@@ -324,7 +666,7 @@ class Server {
324
666
 
325
667
  // in this case, entry is a string or an array.
326
668
  // make sure that we do not add duplicates.
327
- /** @type {Entry} */
669
+ /** @type {any} */
328
670
  const entriesClone = additionalEntries.slice(0);
329
671
 
330
672
  [].concat(originalEntry).forEach((newEntry) => {
@@ -341,73 +683,201 @@ class Server {
341
683
  additionalEntries
342
684
  );
343
685
  compiler.hooks.entryOption.call(
344
- compiler.options.context,
686
+ /** @type {string} */ (compiler.options.context),
345
687
  compiler.options.entry
346
688
  );
347
689
  }
348
690
  }
349
691
 
692
+ /**
693
+ * @private
694
+ * @returns {Compiler["options"]}
695
+ */
350
696
  getCompilerOptions() {
351
- if (typeof this.compiler.compilers !== "undefined") {
352
- if (this.compiler.compilers.length === 1) {
353
- return this.compiler.compilers[0].options;
697
+ if (
698
+ typeof (/** @type {MultiCompiler} */ (this.compiler).compilers) !==
699
+ "undefined"
700
+ ) {
701
+ if (/** @type {MultiCompiler} */ (this.compiler).compilers.length === 1) {
702
+ return (
703
+ /** @type {MultiCompiler} */
704
+ (this.compiler).compilers[0].options
705
+ );
354
706
  }
355
707
 
356
708
  // Configuration with the `devServer` options
357
- const compilerWithDevServer = this.compiler.compilers.find(
358
- (config) => config.options.devServer
359
- );
709
+ const compilerWithDevServer =
710
+ /** @type {MultiCompiler} */
711
+ (this.compiler).compilers.find((config) => config.options.devServer);
360
712
 
361
713
  if (compilerWithDevServer) {
362
714
  return compilerWithDevServer.options;
363
715
  }
364
716
 
365
717
  // Configuration with `web` preset
366
- const compilerWithWebPreset = this.compiler.compilers.find(
367
- (config) =>
368
- (config.options.externalsPresets &&
369
- config.options.externalsPresets.web) ||
370
- [
371
- "web",
372
- "webworker",
373
- "electron-preload",
374
- "electron-renderer",
375
- "node-webkit",
376
- // eslint-disable-next-line no-undefined
377
- undefined,
378
- null,
379
- ].includes(config.options.target)
380
- );
718
+ const compilerWithWebPreset =
719
+ /** @type {MultiCompiler} */
720
+ (this.compiler).compilers.find(
721
+ (config) =>
722
+ (config.options.externalsPresets &&
723
+ config.options.externalsPresets.web) ||
724
+ [
725
+ "web",
726
+ "webworker",
727
+ "electron-preload",
728
+ "electron-renderer",
729
+ "node-webkit",
730
+ // eslint-disable-next-line no-undefined
731
+ undefined,
732
+ null,
733
+ ].includes(/** @type {string} */ (config.options.target))
734
+ );
381
735
 
382
736
  if (compilerWithWebPreset) {
383
737
  return compilerWithWebPreset.options;
384
738
  }
385
739
 
386
740
  // Fallback
387
- return this.compiler.compilers[0].options;
741
+ return /** @type {MultiCompiler} */ (this.compiler).compilers[0].options;
388
742
  }
389
743
 
390
- return this.compiler.options;
744
+ return /** @type {Compiler} */ (this.compiler).options;
391
745
  }
392
746
 
393
- // eslint-disable-next-line class-methods-use-this
747
+ /**
748
+ * @private
749
+ * @returns {Promise<void>}
750
+ */
394
751
  async normalizeOptions() {
395
752
  const { options } = this;
396
-
397
- if (!this.logger) {
398
- this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
399
- }
400
-
401
753
  const compilerOptions = this.getCompilerOptions();
402
754
  // 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,
755
+ const compilerWatchOptions = compilerOptions.watchOptions || {};
756
+ /**
757
+ * @param {WatchOptions & WebpackConfiguration["watchOptions"]} watchOptions
758
+ * @returns {WatchOptions}
759
+ */
760
+ const getWatchOptions = (watchOptions = {}) => {
761
+ const getPolling = () => {
762
+ if (typeof watchOptions.usePolling !== "undefined") {
763
+ return watchOptions.usePolling;
764
+ }
765
+
766
+ if (typeof watchOptions.poll !== "undefined") {
767
+ return Boolean(watchOptions.poll);
768
+ }
769
+
770
+ if (typeof compilerWatchOptions.poll !== "undefined") {
771
+ return Boolean(compilerWatchOptions.poll);
772
+ }
773
+
774
+ return false;
775
+ };
776
+ const getInterval = () => {
777
+ if (typeof watchOptions.interval !== "undefined") {
778
+ return watchOptions.interval;
779
+ }
780
+
781
+ if (typeof watchOptions.poll === "number") {
782
+ return watchOptions.poll;
783
+ }
784
+
785
+ if (typeof compilerWatchOptions.poll === "number") {
786
+ return compilerWatchOptions.poll;
787
+ }
788
+ };
789
+
790
+ const usePolling = getPolling();
791
+ const interval = getInterval();
792
+ const { poll, ...rest } = watchOptions;
793
+
794
+ return {
795
+ ignoreInitial: true,
796
+ persistent: true,
797
+ followSymlinks: false,
798
+ atomic: false,
799
+ alwaysStat: true,
800
+ ignorePermissionErrors: true,
801
+ // Respect options from compiler watchOptions
802
+ usePolling,
803
+ interval,
804
+ ignored: watchOptions.ignored,
805
+ // 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
806
+ ...rest,
807
+ };
808
+ };
809
+ /**
810
+ * @param {string | Static | undefined} [optionsForStatic]
811
+ * @returns {NormalizedStatic}
812
+ */
813
+ const getStaticItem = (optionsForStatic) => {
814
+ const getDefaultStaticOptions = () => {
815
+ return {
816
+ directory: path.join(process.cwd(), "public"),
817
+ staticOptions: {},
818
+ publicPath: ["/"],
819
+ serveIndex: { icons: true },
820
+ watch: getWatchOptions(),
821
+ };
822
+ };
823
+
824
+ /** @type {NormalizedStatic} */
825
+ let item;
826
+
827
+ if (typeof optionsForStatic === "undefined") {
828
+ item = getDefaultStaticOptions();
829
+ } else if (typeof optionsForStatic === "string") {
830
+ item = {
831
+ ...getDefaultStaticOptions(),
832
+ directory: optionsForStatic,
833
+ };
834
+ } else {
835
+ const def = getDefaultStaticOptions();
836
+
837
+ item = {
838
+ directory:
839
+ typeof optionsForStatic.directory !== "undefined"
840
+ ? optionsForStatic.directory
841
+ : def.directory,
842
+ // TODO: do merge in the next major release
843
+ staticOptions:
844
+ typeof optionsForStatic.staticOptions !== "undefined"
845
+ ? optionsForStatic.staticOptions
846
+ : def.staticOptions,
847
+ publicPath:
848
+ // eslint-disable-next-line no-nested-ternary
849
+ typeof optionsForStatic.publicPath !== "undefined"
850
+ ? Array.isArray(optionsForStatic.publicPath)
851
+ ? optionsForStatic.publicPath
852
+ : [optionsForStatic.publicPath]
853
+ : def.publicPath,
854
+ // TODO: do merge in the next major release
855
+ serveIndex:
856
+ // eslint-disable-next-line no-nested-ternary
857
+ typeof optionsForStatic.serveIndex !== "undefined"
858
+ ? typeof optionsForStatic.serveIndex === "boolean" &&
859
+ optionsForStatic.serveIndex
860
+ ? def.serveIndex
861
+ : optionsForStatic.serveIndex
862
+ : def.serveIndex,
863
+ watch:
864
+ // eslint-disable-next-line no-nested-ternary
865
+ typeof optionsForStatic.watch !== "undefined"
866
+ ? // eslint-disable-next-line no-nested-ternary
867
+ typeof optionsForStatic.watch === "boolean"
868
+ ? optionsForStatic.watch
869
+ ? def.watch
870
+ : false
871
+ : getWatchOptions(optionsForStatic.watch)
872
+ : def.watch,
873
+ };
874
+ }
875
+
876
+ if (Server.isAbsoluteURL(item.directory)) {
877
+ throw new Error("Using a URL as static.directory is not supported");
878
+ }
879
+
880
+ return item;
411
881
  };
412
882
 
413
883
  if (typeof options.allowedHosts === "undefined") {
@@ -474,6 +944,14 @@ class Server {
474
944
  };
475
945
  }
476
946
 
947
+ if (typeof options.client.reconnect === "undefined") {
948
+ options.client.reconnect = 10;
949
+ } else if (options.client.reconnect === true) {
950
+ options.client.reconnect = Infinity;
951
+ } else if (options.client.reconnect === false) {
952
+ options.client.reconnect = 0;
953
+ }
954
+
477
955
  // Respect infrastructureLogging.level
478
956
  if (typeof options.client.logging === "undefined") {
479
957
  options.client.logging = compilerOptions.infrastructureLogging
@@ -508,23 +986,100 @@ class Server {
508
986
  ? options.hot
509
987
  : true;
510
988
 
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,
989
+ const isHTTPs = Boolean(options.https);
990
+ const isSPDY = Boolean(options.http2);
991
+
992
+ if (isHTTPs) {
993
+ // TODO: remove in the next major release
994
+ util.deprecate(
995
+ () => {},
996
+ "'https' option is deprecated. Please use the 'server' option.",
997
+ "DEP_WEBPACK_DEV_SERVER_HTTPS"
998
+ )();
999
+ }
1000
+
1001
+ if (isSPDY) {
1002
+ // TODO: remove in the next major release
1003
+ util.deprecate(
1004
+ () => {},
1005
+ "'http2' option is deprecated. Please use the 'server' option.",
1006
+ "DEP_WEBPACK_DEV_SERVER_HTTP2"
1007
+ )();
1008
+ }
1009
+
1010
+ options.server = {
1011
+ type:
1012
+ // eslint-disable-next-line no-nested-ternary
1013
+ typeof options.server === "string"
1014
+ ? options.server
1015
+ : // eslint-disable-next-line no-nested-ternary
1016
+ typeof (options.server || {}).type === "string"
1017
+ ? /** @type {ServerConfiguration} */ (options.server).type || "http"
1018
+ : // eslint-disable-next-line no-nested-ternary
1019
+ isSPDY
1020
+ ? "spdy"
1021
+ : isHTTPs
1022
+ ? "https"
1023
+ : "http",
1024
+ options: {
1025
+ .../** @type {ServerOptions} */ (options.https),
1026
+ .../** @type {ServerConfiguration} */ (options.server || {}).options,
1027
+ },
1028
+ };
1029
+
1030
+ if (
1031
+ options.server.type === "spdy" &&
1032
+ typeof (/** @type {ServerOptions} */ (options.server.options).spdy) ===
1033
+ "undefined"
1034
+ ) {
1035
+ /** @type {ServerOptions} */
1036
+ (options.server.options).spdy = {
1037
+ protocols: ["h2", "http/1.1"],
515
1038
  };
516
1039
  }
517
1040
 
518
- // https option
519
- if (options.https) {
520
- // TODO remove the `cacert` option in favor `ca` in the next major release
521
- for (const property of ["cacert", "ca", "cert", "crl", "key", "pfx"]) {
522
- if (typeof options.https[property] === "undefined") {
1041
+ if (options.server.type === "https" || options.server.type === "spdy") {
1042
+ if (
1043
+ typeof (
1044
+ /** @type {ServerOptions} */ (options.server.options).requestCert
1045
+ ) === "undefined"
1046
+ ) {
1047
+ /** @type {ServerOptions} */
1048
+ (options.server.options).requestCert = false;
1049
+ }
1050
+
1051
+ const httpsProperties =
1052
+ /** @type {Array<keyof ServerOptions>} */
1053
+ (["cacert", "ca", "cert", "crl", "key", "pfx"]);
1054
+
1055
+ for (const property of httpsProperties) {
1056
+ if (
1057
+ typeof (
1058
+ /** @type {ServerOptions} */ (options.server.options)[property]
1059
+ ) === "undefined"
1060
+ ) {
523
1061
  // eslint-disable-next-line no-continue
524
1062
  continue;
525
1063
  }
526
1064
 
527
- const value = options.https[property];
1065
+ // @ts-ignore
1066
+ if (property === "cacert") {
1067
+ // TODO remove the `cacert` option in favor `ca` in the next major release
1068
+ util.deprecate(
1069
+ () => {},
1070
+ "The 'cacert' option is deprecated. Please use the 'ca' option.",
1071
+ "DEP_WEBPACK_DEV_SERVER_CACERT"
1072
+ )();
1073
+ }
1074
+
1075
+ /** @type {any} */
1076
+ const value =
1077
+ /** @type {ServerOptions} */
1078
+ (options.server.options)[property];
1079
+ /**
1080
+ * @param {string | Buffer | undefined} item
1081
+ * @returns {string | Buffer | undefined}
1082
+ */
528
1083
  const readFile = (item) => {
529
1084
  if (
530
1085
  Buffer.isBuffer(item) ||
@@ -547,14 +1102,18 @@ class Server {
547
1102
  }
548
1103
  };
549
1104
 
550
- options.https[property] = Array.isArray(value)
1105
+ /** @type {any} */
1106
+ (options.server.options)[property] = Array.isArray(value)
551
1107
  ? value.map((item) => readFile(item))
552
1108
  : readFile(value);
553
1109
  }
554
1110
 
555
1111
  let fakeCert;
556
1112
 
557
- if (!options.https.key || !options.https.cert) {
1113
+ if (
1114
+ !(/** @type {ServerOptions} */ (options.server.options).key) ||
1115
+ /** @type {ServerOptions} */ (!options.server.options).cert
1116
+ ) {
558
1117
  const certificateDir = Server.findCacheDir();
559
1118
  const certificatePath = path.join(certificateDir, "server.pem");
560
1119
  let certificateExists;
@@ -569,15 +1128,14 @@ class Server {
569
1128
  if (certificateExists) {
570
1129
  const certificateTtl = 1000 * 60 * 60 * 24;
571
1130
  const certificateStat = await fs.promises.stat(certificatePath);
572
-
573
- const now = new Date();
1131
+ const now = Number(new Date());
574
1132
 
575
1133
  // cert is more than 30 days old, kill it with fire
576
- if ((now - certificateStat.ctime) / certificateTtl > 30) {
1134
+ if ((now - Number(certificateStat.ctime)) / certificateTtl > 30) {
577
1135
  const del = require("del");
578
1136
 
579
1137
  this.logger.info(
580
- "SSL Certificate is more than 30 days old. Removing..."
1138
+ "SSL certificate is more than 30 days old. Removing..."
581
1139
  );
582
1140
 
583
1141
  await del([certificatePath], { force: true });
@@ -587,8 +1145,9 @@ class Server {
587
1145
  }
588
1146
 
589
1147
  if (!certificateExists) {
590
- this.logger.info("Generating SSL Certificate...");
1148
+ this.logger.info("Generating SSL certificate...");
591
1149
 
1150
+ // @ts-ignore
592
1151
  const selfsigned = require("selfsigned");
593
1152
  const attributes = [{ name: "commonName", value: "localhost" }];
594
1153
  const pems = selfsigned.generate(attributes, {
@@ -669,20 +1228,37 @@ class Server {
669
1228
  this.logger.info(`SSL certificate: ${certificatePath}`);
670
1229
  }
671
1230
 
672
- if (options.https.cacert) {
673
- if (options.https.ca) {
1231
+ if (
1232
+ /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */ (
1233
+ options.server.options
1234
+ ).cacert
1235
+ ) {
1236
+ if (/** @type {ServerOptions} */ (options.server.options).ca) {
674
1237
  this.logger.warn(
675
- "Do not specify 'https.ca' and 'https.cacert' options together, the 'https.ca' option will be used."
1238
+ "Do not specify 'ca' and 'cacert' options together, the 'ca' option will be used."
676
1239
  );
677
1240
  } else {
678
- options.https.ca = options.https.cacert;
1241
+ /** @type {ServerOptions} */
1242
+ (options.server.options).ca =
1243
+ /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */
1244
+ (options.server.options).cacert;
679
1245
  }
680
1246
 
681
- delete options.https.cacert;
1247
+ delete (
1248
+ /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */ (
1249
+ options.server.options
1250
+ ).cacert
1251
+ );
682
1252
  }
683
1253
 
684
- options.https.key = options.https.key || fakeCert;
685
- options.https.cert = options.https.cert || fakeCert;
1254
+ /** @type {ServerOptions} */
1255
+ (options.server.options).key =
1256
+ /** @type {ServerOptions} */
1257
+ (options.server.options).key || fakeCert;
1258
+ /** @type {ServerOptions} */
1259
+ (options.server.options).cert =
1260
+ /** @type {ServerOptions} */
1261
+ (options.server.options).cert || fakeCert;
686
1262
  }
687
1263
 
688
1264
  if (typeof options.ipc === "boolean") {
@@ -701,6 +1277,11 @@ class Server {
701
1277
 
702
1278
  // https://github.com/webpack/webpack-dev-server/issues/1990
703
1279
  const defaultOpenOptions = { wait: false };
1280
+ /**
1281
+ * @param {any} target
1282
+ * @returns {NormalizedOpen[]}
1283
+ */
1284
+ // TODO: remove --open-app in favor of --open-app-name
704
1285
  const getOpenItemsFromObject = ({ target, ...rest }) => {
705
1286
  const normalizedOptions = { ...defaultOpenOptions, ...rest };
706
1287
 
@@ -722,14 +1303,25 @@ class Server {
722
1303
  };
723
1304
 
724
1305
  if (typeof options.open === "undefined") {
725
- options.open = [];
1306
+ /** @type {NormalizedOpen[]} */
1307
+ (options.open) = [];
726
1308
  } else if (typeof options.open === "boolean") {
727
- options.open = options.open
728
- ? [{ target: "<url>", options: defaultOpenOptions }]
1309
+ /** @type {NormalizedOpen[]} */
1310
+ (options.open) = options.open
1311
+ ? [
1312
+ {
1313
+ target: "<url>",
1314
+ options: /** @type {OpenOptions} */ (defaultOpenOptions),
1315
+ },
1316
+ ]
729
1317
  : [];
730
1318
  } else if (typeof options.open === "string") {
731
- options.open = [{ target: options.open, options: defaultOpenOptions }];
1319
+ /** @type {NormalizedOpen[]} */
1320
+ (options.open) = [{ target: options.open, options: defaultOpenOptions }];
732
1321
  } else if (Array.isArray(options.open)) {
1322
+ /**
1323
+ * @type {NormalizedOpen[]}
1324
+ */
733
1325
  const result = [];
734
1326
 
735
1327
  options.open.forEach((item) => {
@@ -742,9 +1334,29 @@ class Server {
742
1334
  result.push(...getOpenItemsFromObject(item));
743
1335
  });
744
1336
 
745
- options.open = result;
1337
+ /** @type {NormalizedOpen[]} */
1338
+ (options.open) = result;
746
1339
  } else {
747
- options.open = [...getOpenItemsFromObject(options.open)];
1340
+ /** @type {NormalizedOpen[]} */
1341
+ (options.open) = [...getOpenItemsFromObject(options.open)];
1342
+ }
1343
+
1344
+ if (options.onAfterSetupMiddleware) {
1345
+ // TODO: remove in the next major release
1346
+ util.deprecate(
1347
+ () => {},
1348
+ "'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.",
1349
+ `DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE`
1350
+ )();
1351
+ }
1352
+
1353
+ if (options.onBeforeSetupMiddleware) {
1354
+ // TODO: remove in the next major release
1355
+ util.deprecate(
1356
+ () => {},
1357
+ "'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.",
1358
+ `DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE`
1359
+ )();
748
1360
  }
749
1361
 
750
1362
  if (typeof options.port === "string" && options.port !== "auto") {
@@ -768,61 +1380,91 @@ class Server {
768
1380
  Object.prototype.hasOwnProperty.call(options.proxy, "target") ||
769
1381
  Object.prototype.hasOwnProperty.call(options.proxy, "router")
770
1382
  ) {
771
- options.proxy = [options.proxy];
1383
+ /** @type {ProxyArray} */
1384
+ (options.proxy) = [/** @type {ProxyConfigMap} */ (options.proxy)];
772
1385
  } else {
773
- options.proxy = Object.keys(options.proxy).map((context) => {
774
- let proxyOptions;
775
- // For backwards compatibility reasons.
776
- const correctedContext = context
777
- .replace(/^\*$/, "**")
778
- .replace(/\/\*$/, "");
779
-
780
- if (typeof options.proxy[context] === "string") {
781
- proxyOptions = {
782
- context: correctedContext,
783
- target: options.proxy[context],
784
- };
785
- } else {
786
- proxyOptions = { ...options.proxy[context] };
787
- proxyOptions.context = correctedContext;
1386
+ /** @type {ProxyArray} */
1387
+ (options.proxy) = Object.keys(options.proxy).map(
1388
+ /**
1389
+ * @param {string} context
1390
+ * @returns {HttpProxyMiddlewareOptions}
1391
+ */
1392
+ (context) => {
1393
+ let proxyOptions;
1394
+ // For backwards compatibility reasons.
1395
+ const correctedContext = context
1396
+ .replace(/^\*$/, "**")
1397
+ .replace(/\/\*$/, "");
1398
+
1399
+ if (
1400
+ typeof (
1401
+ /** @type {ProxyConfigMap} */ (options.proxy)[context]
1402
+ ) === "string"
1403
+ ) {
1404
+ proxyOptions = {
1405
+ context: correctedContext,
1406
+ target:
1407
+ /** @type {ProxyConfigMap} */
1408
+ (options.proxy)[context],
1409
+ };
1410
+ } else {
1411
+ proxyOptions = {
1412
+ // @ts-ignore
1413
+ .../** @type {ProxyConfigMap} */ (options.proxy)[context],
1414
+ };
1415
+ proxyOptions.context = correctedContext;
1416
+ }
1417
+
1418
+ return proxyOptions;
788
1419
  }
789
-
790
- return proxyOptions;
791
- });
1420
+ );
792
1421
  }
793
1422
  }
794
1423
 
795
- options.proxy = options.proxy.map((item) => {
796
- const getLogLevelForProxy = (level) => {
797
- if (level === "none") {
798
- return "silent";
799
- }
1424
+ /** @type {ProxyArray} */
1425
+ (options.proxy) =
1426
+ /** @type {ProxyArray} */
1427
+ (options.proxy).map(
1428
+ /**
1429
+ * @param {HttpProxyMiddlewareOptions} item
1430
+ * @returns {HttpProxyMiddlewareOptions}
1431
+ */
1432
+ (item) => {
1433
+ /**
1434
+ * @param {"info" | "warn" | "error" | "debug" | "silent" | undefined | "none" | "log" | "verbose"} level
1435
+ * @returns {"info" | "warn" | "error" | "debug" | "silent" | undefined}
1436
+ */
1437
+ const getLogLevelForProxy = (level) => {
1438
+ if (level === "none") {
1439
+ return "silent";
1440
+ }
1441
+
1442
+ if (level === "log") {
1443
+ return "info";
1444
+ }
1445
+
1446
+ if (level === "verbose") {
1447
+ return "debug";
1448
+ }
1449
+
1450
+ return level;
1451
+ };
1452
+
1453
+ if (typeof item.logLevel === "undefined") {
1454
+ item.logLevel = getLogLevelForProxy(
1455
+ compilerOptions.infrastructureLogging
1456
+ ? compilerOptions.infrastructureLogging.level
1457
+ : "info"
1458
+ );
1459
+ }
800
1460
 
801
- if (level === "log") {
802
- return "info";
803
- }
1461
+ if (typeof item.logProvider === "undefined") {
1462
+ item.logProvider = () => this.logger;
1463
+ }
804
1464
 
805
- if (level === "verbose") {
806
- return "debug";
1465
+ return item;
807
1466
  }
808
-
809
- return level;
810
- };
811
-
812
- if (typeof item.logLevel === "undefined") {
813
- item.logLevel = getLogLevelForProxy(
814
- compilerOptions.infrastructureLogging
815
- ? compilerOptions.infrastructureLogging.level
816
- : "info"
817
- );
818
- }
819
-
820
- if (typeof item.logProvider === "undefined") {
821
- item.logProvider = () => this.logger;
822
- }
823
-
824
- return item;
825
- });
1467
+ );
826
1468
  }
827
1469
 
828
1470
  if (typeof options.setupExitSignals === "undefined") {
@@ -830,50 +1472,27 @@ class Server {
830
1472
  }
831
1473
 
832
1474
  if (typeof options.static === "undefined") {
833
- options.static = [defaultOptionsForStatic];
1475
+ options.static = [getStaticItem()];
834
1476
  } else if (typeof options.static === "boolean") {
835
- options.static = options.static ? [defaultOptionsForStatic] : false;
1477
+ options.static = options.static ? [getStaticItem()] : false;
836
1478
  } else if (typeof options.static === "string") {
837
- options.static = [
838
- { ...defaultOptionsForStatic, directory: options.static },
839
- ];
1479
+ options.static = [getStaticItem(options.static)];
840
1480
  } else if (Array.isArray(options.static)) {
841
1481
  options.static = options.static.map((item) => {
842
1482
  if (typeof item === "string") {
843
- return { ...defaultOptionsForStatic, directory: item };
1483
+ return getStaticItem(item);
844
1484
  }
845
1485
 
846
- return { ...defaultOptionsForStatic, ...item };
1486
+ return getStaticItem(item);
847
1487
  });
848
1488
  } 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
- });
1489
+ options.static = [getStaticItem(options.static)];
873
1490
  }
874
1491
 
875
1492
  if (typeof options.watchFiles === "string") {
876
- options.watchFiles = [{ paths: options.watchFiles, options: {} }];
1493
+ options.watchFiles = [
1494
+ { paths: options.watchFiles, options: getWatchOptions() },
1495
+ ];
877
1496
  } else if (
878
1497
  typeof options.watchFiles === "object" &&
879
1498
  options.watchFiles !== null &&
@@ -882,16 +1501,19 @@ class Server {
882
1501
  options.watchFiles = [
883
1502
  {
884
1503
  paths: options.watchFiles.paths,
885
- options: options.watchFiles.options || {},
1504
+ options: getWatchOptions(options.watchFiles.options || {}),
886
1505
  },
887
1506
  ];
888
1507
  } else if (Array.isArray(options.watchFiles)) {
889
1508
  options.watchFiles = options.watchFiles.map((item) => {
890
1509
  if (typeof item === "string") {
891
- return { paths: item, options: {} };
1510
+ return { paths: item, options: getWatchOptions() };
892
1511
  }
893
1512
 
894
- return { paths: item.paths, options: item.options || {} };
1513
+ return {
1514
+ paths: item.paths,
1515
+ options: getWatchOptions(item.options || {}),
1516
+ };
895
1517
  });
896
1518
  } else {
897
1519
  options.watchFiles = [];
@@ -920,38 +1542,61 @@ class Server {
920
1542
  };
921
1543
  } else {
922
1544
  options.webSocketServer = {
923
- type: options.webSocketServer.type || defaultWebSocketServerType,
1545
+ type:
1546
+ /** @type {WebSocketServerConfiguration} */
1547
+ (options.webSocketServer).type || defaultWebSocketServerType,
924
1548
  options: {
925
1549
  ...defaultWebSocketServerOptions,
926
- ...options.webSocketServer.options,
1550
+ .../** @type {WebSocketServerConfiguration} */
1551
+ (options.webSocketServer).options,
927
1552
  },
928
1553
  };
929
1554
 
930
- if (typeof options.webSocketServer.options.port === "string") {
931
- options.webSocketServer.options.port = Number(
932
- options.webSocketServer.options.port
933
- );
1555
+ const webSocketServer =
1556
+ /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable<WebSocketServerConfiguration["options"]> }} */
1557
+ (options.webSocketServer);
1558
+
1559
+ if (typeof webSocketServer.options.port === "string") {
1560
+ webSocketServer.options.port = Number(webSocketServer.options.port);
934
1561
  }
935
1562
  }
936
1563
  }
937
1564
 
1565
+ /**
1566
+ * @private
1567
+ * @returns {string}
1568
+ */
938
1569
  getClientTransport() {
939
- let ClientImplementation;
1570
+ let clientImplementation;
940
1571
  let clientImplementationFound = true;
941
1572
 
942
1573
  const isKnownWebSocketServerImplementation =
943
1574
  this.options.webSocketServer &&
944
- typeof this.options.webSocketServer.type === "string" &&
1575
+ typeof (
1576
+ /** @type {WebSocketServerConfiguration} */
1577
+ (this.options.webSocketServer).type
1578
+ ) === "string" &&
1579
+ // @ts-ignore
945
1580
  (this.options.webSocketServer.type === "ws" ||
946
- this.options.webSocketServer.type === "sockjs");
1581
+ /** @type {WebSocketServerConfiguration} */
1582
+ (this.options.webSocketServer).type === "sockjs");
947
1583
 
948
1584
  let clientTransport;
949
1585
 
950
1586
  if (this.options.client) {
951
- if (typeof this.options.client.webSocketTransport !== "undefined") {
952
- clientTransport = this.options.client.webSocketTransport;
1587
+ if (
1588
+ typeof (
1589
+ /** @type {ClientConfiguration} */
1590
+ (this.options.client).webSocketTransport
1591
+ ) !== "undefined"
1592
+ ) {
1593
+ clientTransport =
1594
+ /** @type {ClientConfiguration} */
1595
+ (this.options.client).webSocketTransport;
953
1596
  } else if (isKnownWebSocketServerImplementation) {
954
- clientTransport = this.options.webSocketServer.type;
1597
+ clientTransport =
1598
+ /** @type {WebSocketServerConfiguration} */
1599
+ (this.options.webSocketServer).type;
955
1600
  } else {
956
1601
  clientTransport = "ws";
957
1602
  }
@@ -963,17 +1608,16 @@ class Server {
963
1608
  case "string":
964
1609
  // could be 'sockjs', 'ws', or a path that should be required
965
1610
  if (clientTransport === "sockjs") {
966
- ClientImplementation = require.resolve(
1611
+ clientImplementation = require.resolve(
967
1612
  "../client/clients/SockJSClient"
968
1613
  );
969
1614
  } else if (clientTransport === "ws") {
970
- ClientImplementation = require.resolve(
1615
+ clientImplementation = require.resolve(
971
1616
  "../client/clients/WebSocketClient"
972
1617
  );
973
1618
  } else {
974
1619
  try {
975
- // eslint-disable-next-line import/no-dynamic-require
976
- ClientImplementation = require.resolve(clientTransport);
1620
+ clientImplementation = require.resolve(clientTransport);
977
1621
  } catch (e) {
978
1622
  clientImplementationFound = false;
979
1623
  }
@@ -993,31 +1637,52 @@ class Server {
993
1637
  );
994
1638
  }
995
1639
 
996
- return ClientImplementation;
1640
+ return /** @type {string} */ (clientImplementation);
997
1641
  }
998
1642
 
1643
+ /**
1644
+ * @private
1645
+ * @returns {string}
1646
+ */
999
1647
  getServerTransport() {
1000
1648
  let implementation;
1001
1649
  let implementationFound = true;
1002
1650
 
1003
- switch (typeof this.options.webSocketServer.type) {
1651
+ switch (
1652
+ typeof (
1653
+ /** @type {WebSocketServerConfiguration} */
1654
+ (this.options.webSocketServer).type
1655
+ )
1656
+ ) {
1004
1657
  case "string":
1005
1658
  // Could be 'sockjs', in the future 'ws', or a path that should be required
1006
- if (this.options.webSocketServer.type === "sockjs") {
1659
+ if (
1660
+ /** @type {WebSocketServerConfiguration} */ (
1661
+ this.options.webSocketServer
1662
+ ).type === "sockjs"
1663
+ ) {
1007
1664
  implementation = require("./servers/SockJSServer");
1008
- } else if (this.options.webSocketServer.type === "ws") {
1665
+ } else if (
1666
+ /** @type {WebSocketServerConfiguration} */ (
1667
+ this.options.webSocketServer
1668
+ ).type === "ws"
1669
+ ) {
1009
1670
  implementation = require("./servers/WebsocketServer");
1010
1671
  } else {
1011
1672
  try {
1012
1673
  // eslint-disable-next-line import/no-dynamic-require
1013
- implementation = require(this.options.webSocketServer.type);
1674
+ implementation = require(/** @type {WebSocketServerConfiguration} */ (
1675
+ this.options.webSocketServer
1676
+ ).type);
1014
1677
  } catch (error) {
1015
1678
  implementationFound = false;
1016
1679
  }
1017
1680
  }
1018
1681
  break;
1019
1682
  case "function":
1020
- implementation = this.options.webSocketServer.type;
1683
+ implementation = /** @type {WebSocketServerConfiguration} */ (
1684
+ this.options.webSocketServer
1685
+ ).type;
1021
1686
  break;
1022
1687
  default:
1023
1688
  implementationFound = false;
@@ -1034,39 +1699,62 @@ class Server {
1034
1699
  return implementation;
1035
1700
  }
1036
1701
 
1702
+ /**
1703
+ * @private
1704
+ * @returns {void}
1705
+ */
1037
1706
  setupProgressPlugin() {
1038
- const { ProgressPlugin } = this.compiler.webpack || require("webpack");
1039
-
1040
- new ProgressPlugin((percent, msg, addInfo, pluginName) => {
1041
- percent = Math.floor(percent * 100);
1707
+ const { ProgressPlugin } =
1708
+ /** @type {MultiCompiler}*/
1709
+ (this.compiler).compilers
1710
+ ? /** @type {MultiCompiler}*/ (this.compiler).compilers[0].webpack
1711
+ : /** @type {Compiler}*/ (this.compiler).webpack ||
1712
+ // TODO remove me after drop webpack v4
1713
+ require("webpack");
1714
+
1715
+ new ProgressPlugin(
1716
+ /**
1717
+ * @param {number} percent
1718
+ * @param {string} msg
1719
+ * @param {string} addInfo
1720
+ * @param {string} pluginName
1721
+ */
1722
+ (percent, msg, addInfo, pluginName) => {
1723
+ percent = Math.floor(percent * 100);
1042
1724
 
1043
- if (percent === 100) {
1044
- msg = "Compilation completed";
1045
- }
1725
+ if (percent === 100) {
1726
+ msg = "Compilation completed";
1727
+ }
1046
1728
 
1047
- if (addInfo) {
1048
- msg = `${msg} (${addInfo})`;
1049
- }
1729
+ if (addInfo) {
1730
+ msg = `${msg} (${addInfo})`;
1731
+ }
1050
1732
 
1051
- if (this.webSocketServer) {
1052
- this.sendMessage(this.webSocketServer.clients, "progress-update", {
1053
- percent,
1054
- msg,
1055
- pluginName,
1056
- });
1057
- }
1733
+ if (this.webSocketServer) {
1734
+ this.sendMessage(this.webSocketServer.clients, "progress-update", {
1735
+ percent,
1736
+ msg,
1737
+ pluginName,
1738
+ });
1739
+ }
1058
1740
 
1059
- if (this.server) {
1060
- this.server.emit("progress-update", { percent, msg, pluginName });
1741
+ if (this.server) {
1742
+ this.server.emit("progress-update", { percent, msg, pluginName });
1743
+ }
1061
1744
  }
1062
- }).apply(this.compiler);
1745
+ ).apply(this.compiler);
1063
1746
  }
1064
1747
 
1748
+ /**
1749
+ * @private
1750
+ * @returns {Promise<void>}
1751
+ */
1065
1752
  async initialize() {
1066
1753
  if (this.options.webSocketServer) {
1067
- const compilers = this.compiler.compilers || [this.compiler];
1754
+ const compilers =
1755
+ /** @type {MultiCompiler} */
1756
+ (this.compiler).compilers || [this.compiler];
1068
1757
 
1069
- // eslint-disable-next-line no-shadow
1070
1758
  compilers.forEach((compiler) => {
1071
1759
  this.addAdditionalEntries(compiler);
1072
1760
 
@@ -1097,7 +1785,10 @@ class Server {
1097
1785
  }
1098
1786
  });
1099
1787
 
1100
- if (this.options.client && this.options.client.progress) {
1788
+ if (
1789
+ this.options.client &&
1790
+ /** @type {ClientConfiguration} */ (this.options.client).progress
1791
+ ) {
1101
1792
  this.setupProgressPlugin();
1102
1793
  }
1103
1794
  }
@@ -1109,7 +1800,8 @@ class Server {
1109
1800
  // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
1110
1801
  this.setupBuiltInRoutes();
1111
1802
  this.setupWatchFiles();
1112
- this.setupFeatures();
1803
+ this.setupWatchStaticFiles();
1804
+ this.setupMiddlewares();
1113
1805
  this.createServer();
1114
1806
 
1115
1807
  if (this.options.setupExitSignals) {
@@ -1117,15 +1809,10 @@ class Server {
1117
1809
 
1118
1810
  let needForceShutdown = false;
1119
1811
 
1120
- const exitProcess = () => {
1121
- // eslint-disable-next-line no-process-exit
1122
- process.exit();
1123
- };
1124
-
1125
1812
  signals.forEach((signal) => {
1126
- process.on(signal, () => {
1813
+ const listener = () => {
1127
1814
  if (needForceShutdown) {
1128
- exitProcess();
1815
+ process.exit();
1129
1816
  }
1130
1817
 
1131
1818
  this.logger.info(
@@ -1136,65 +1823,125 @@ class Server {
1136
1823
 
1137
1824
  this.stopCallback(() => {
1138
1825
  if (typeof this.compiler.close === "function") {
1139
- this.compiler.close(exitProcess);
1826
+ this.compiler.close(() => {
1827
+ process.exit();
1828
+ });
1140
1829
  } else {
1141
- exitProcess();
1830
+ process.exit();
1142
1831
  }
1143
1832
  });
1144
- });
1833
+ };
1834
+
1835
+ this.listeners.push({ name: signal, listener });
1836
+
1837
+ process.on(signal, listener);
1145
1838
  });
1146
1839
  }
1147
1840
 
1148
1841
  // Proxy WebSocket without the initial http request
1149
1842
  // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
1150
- // eslint-disable-next-line func-names
1151
- this.webSocketProxies.forEach(function (webSocketProxy) {
1152
- this.server.on("upgrade", webSocketProxy.upgrade);
1843
+ /** @type {RequestHandler[]} */
1844
+ (this.webSocketProxies).forEach((webSocketProxy) => {
1845
+ /** @type {import("http").Server} */
1846
+ (this.server).on(
1847
+ "upgrade",
1848
+ /** @type {RequestHandler & { upgrade: NonNullable<RequestHandler["upgrade"]> }} */
1849
+ (webSocketProxy).upgrade
1850
+ );
1153
1851
  }, this);
1154
1852
  }
1155
1853
 
1854
+ /**
1855
+ * @private
1856
+ * @returns {void}
1857
+ */
1156
1858
  setupApp() {
1157
- // Init express server
1859
+ /** @type {import("express").Application | undefined}*/
1158
1860
  // eslint-disable-next-line new-cap
1159
- this.app = new express();
1861
+ this.app = new /** @type {any} */ (express)();
1160
1862
  }
1161
1863
 
1864
+ /**
1865
+ * @private
1866
+ * @param {Stats | MultiStats} statsObj
1867
+ * @returns {StatsCompilation}
1868
+ */
1162
1869
  getStats(statsObj) {
1163
1870
  const stats = Server.DEFAULT_STATS;
1164
1871
  const compilerOptions = this.getCompilerOptions();
1165
1872
 
1873
+ // @ts-ignore
1166
1874
  if (compilerOptions.stats && compilerOptions.stats.warningsFilter) {
1875
+ // @ts-ignore
1167
1876
  stats.warningsFilter = compilerOptions.stats.warningsFilter;
1168
1877
  }
1169
1878
 
1170
1879
  return statsObj.toJson(stats);
1171
1880
  }
1172
1881
 
1882
+ /**
1883
+ * @private
1884
+ * @returns {void}
1885
+ */
1173
1886
  setupHooks() {
1174
1887
  this.compiler.hooks.invalid.tap("webpack-dev-server", () => {
1175
1888
  if (this.webSocketServer) {
1176
1889
  this.sendMessage(this.webSocketServer.clients, "invalid");
1177
1890
  }
1178
1891
  });
1179
- this.compiler.hooks.done.tap("webpack-dev-server", (stats) => {
1180
- if (this.webSocketServer) {
1181
- this.sendStats(this.webSocketServer.clients, this.getStats(stats));
1182
- }
1892
+ this.compiler.hooks.done.tap(
1893
+ "webpack-dev-server",
1894
+ /**
1895
+ * @param {Stats | MultiStats} stats
1896
+ */
1897
+ (stats) => {
1898
+ if (this.webSocketServer) {
1899
+ this.sendStats(this.webSocketServer.clients, this.getStats(stats));
1900
+ }
1183
1901
 
1184
- this.stats = stats;
1185
- });
1902
+ /**
1903
+ * @private
1904
+ * @type {Stats | MultiStats}
1905
+ */
1906
+ this.stats = stats;
1907
+ }
1908
+ );
1186
1909
  }
1187
1910
 
1911
+ /**
1912
+ * @private
1913
+ * @returns {void}
1914
+ */
1188
1915
  setupHostHeaderCheck() {
1189
- this.app.all("*", (req, res, next) => {
1190
- if (this.checkHeader(req.headers, "host")) {
1191
- return next();
1192
- }
1916
+ /** @type {import("express").Application} */
1917
+ (this.app).all(
1918
+ "*",
1919
+ /**
1920
+ * @param {Request} req
1921
+ * @param {Response} res
1922
+ * @param {NextFunction} next
1923
+ * @returns {void}
1924
+ */
1925
+ (req, res, next) => {
1926
+ if (
1927
+ this.checkHeader(
1928
+ /** @type {{ [key: string]: string | undefined }} */
1929
+ (req.headers),
1930
+ "host"
1931
+ )
1932
+ ) {
1933
+ return next();
1934
+ }
1193
1935
 
1194
- res.send("Invalid Host header");
1195
- });
1936
+ res.send("Invalid Host header");
1937
+ }
1938
+ );
1196
1939
  }
1197
1940
 
1941
+ /**
1942
+ * @private
1943
+ * @returns {void}
1944
+ */
1198
1945
  setupDevMiddleware() {
1199
1946
  const webpackDevMiddleware = require("webpack-dev-middleware");
1200
1947
 
@@ -1205,445 +1952,616 @@ class Server {
1205
1952
  );
1206
1953
  }
1207
1954
 
1955
+ /**
1956
+ * @private
1957
+ * @returns {void}
1958
+ */
1208
1959
  setupBuiltInRoutes() {
1209
1960
  const { app, middleware } = this;
1210
1961
 
1211
- app.get("/__webpack_dev_server__/sockjs.bundle.js", (req, res) => {
1212
- res.setHeader("Content-Type", "application/javascript");
1213
-
1214
- const { createReadStream } = fs;
1215
- const clientPath = path.join(__dirname, "..", "client");
1216
-
1217
- createReadStream(
1218
- path.join(clientPath, "modules/sockjs-client/index.js")
1219
- ).pipe(res);
1220
- });
1221
-
1222
- app.get("/webpack-dev-server/invalidate", (_req, res) => {
1223
- this.invalidate();
1224
-
1225
- res.end();
1226
- });
1962
+ /** @type {import("express").Application} */
1963
+ (app).get(
1964
+ "/__webpack_dev_server__/sockjs.bundle.js",
1965
+ /**
1966
+ * @param {Request} req
1967
+ * @param {Response} res
1968
+ * @returns {void}
1969
+ */
1970
+ (req, res) => {
1971
+ res.setHeader("Content-Type", "application/javascript");
1227
1972
 
1228
- app.get("/webpack-dev-server", (req, res) => {
1229
- middleware.waitUntilValid((stats) => {
1230
- res.setHeader("Content-Type", "text/html");
1231
- res.write(
1232
- '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
1233
- );
1973
+ const { createReadStream } = fs;
1974
+ const clientPath = path.join(__dirname, "..", "client");
1234
1975
 
1235
- const statsForPrint =
1236
- typeof stats.stats !== "undefined"
1237
- ? stats.toJson().children
1238
- : [stats.toJson()];
1976
+ createReadStream(
1977
+ path.join(clientPath, "modules/sockjs-client/index.js")
1978
+ ).pipe(res);
1979
+ }
1980
+ );
1239
1981
 
1240
- res.write(`<h1>Assets Report:</h1>`);
1982
+ /** @type {import("express").Application} */
1983
+ (app).get(
1984
+ "/webpack-dev-server/invalidate",
1985
+ /**
1986
+ * @param {Request} _req
1987
+ * @param {Response} res
1988
+ * @returns {void}
1989
+ */
1990
+ (_req, res) => {
1991
+ this.invalidate();
1241
1992
 
1242
- statsForPrint.forEach((item, index) => {
1243
- res.write("<div>");
1993
+ res.end();
1994
+ }
1995
+ );
1244
1996
 
1245
- const name =
1246
- item.name || (stats.stats ? `unnamed[${index}]` : "unnamed");
1997
+ /** @type {import("express").Application} */
1998
+ (app).get(
1999
+ "/webpack-dev-server",
2000
+ /**
2001
+ * @param {Request} req
2002
+ * @param {Response} res
2003
+ * @returns {void}
2004
+ */
2005
+ (req, res) => {
2006
+ /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
2007
+ (middleware).waitUntilValid((stats) => {
2008
+ res.setHeader("Content-Type", "text/html");
2009
+ res.write(
2010
+ '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
2011
+ );
1247
2012
 
1248
- res.write(`<h2>Compilation: ${name}</h2>`);
1249
- res.write("<ul>");
2013
+ const statsForPrint =
2014
+ typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined"
2015
+ ? /** @type {MultiStats} */ (stats).toJson().children
2016
+ : [/** @type {Stats} */ (stats).toJson()];
1250
2017
 
1251
- const publicPath = item.publicPath === "auto" ? "" : item.publicPath;
2018
+ res.write(`<h1>Assets Report:</h1>`);
1252
2019
 
1253
- for (const asset of item.assets) {
1254
- const assetName = asset.name;
1255
- const assetURL = `${publicPath}${assetName}`;
2020
+ /**
2021
+ * @type {StatsCompilation[]}
2022
+ */
2023
+ (statsForPrint).forEach((item, index) => {
2024
+ res.write("<div>");
1256
2025
 
1257
- res.write(
1258
- `<li>
2026
+ const name =
2027
+ // eslint-disable-next-line no-nested-ternary
2028
+ typeof item.name !== "undefined"
2029
+ ? item.name
2030
+ : /** @type {MultiStats} */ (stats).stats
2031
+ ? `unnamed[${index}]`
2032
+ : "unnamed";
2033
+
2034
+ res.write(`<h2>Compilation: ${name}</h2>`);
2035
+ res.write("<ul>");
2036
+
2037
+ const publicPath =
2038
+ item.publicPath === "auto" ? "" : item.publicPath;
2039
+
2040
+ for (const asset of /** @type {NonNullable<StatsCompilation["assets"]>} */ (
2041
+ item.assets
2042
+ )) {
2043
+ const assetName = asset.name;
2044
+ const assetURL = `${publicPath}${assetName}`;
2045
+
2046
+ res.write(
2047
+ `<li>
1259
2048
  <strong><a href="${assetURL}" target="_blank">${assetName}</a></strong>
1260
2049
  </li>`
1261
- );
1262
- }
2050
+ );
2051
+ }
2052
+
2053
+ res.write("</ul>");
2054
+ res.write("</div>");
2055
+ });
1263
2056
 
1264
- res.write("</ul>");
1265
- res.write("</div>");
2057
+ res.end("</body></html>");
1266
2058
  });
2059
+ }
2060
+ );
2061
+ }
1267
2062
 
1268
- res.end("</body></html>");
2063
+ /**
2064
+ * @private
2065
+ * @returns {void}
2066
+ */
2067
+ setupWatchStaticFiles() {
2068
+ if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
2069
+ /** @type {NormalizedStatic[]} */
2070
+ (this.options.static).forEach((staticOption) => {
2071
+ if (staticOption.watch) {
2072
+ this.watchFiles(staticOption.directory, staticOption.watch);
2073
+ }
1269
2074
  });
1270
- });
2075
+ }
1271
2076
  }
1272
2077
 
1273
- setupCompressFeature() {
1274
- const compress = require("compression");
2078
+ /**
2079
+ * @private
2080
+ * @returns {void}
2081
+ */
2082
+ setupWatchFiles() {
2083
+ const { watchFiles } = this.options;
1275
2084
 
1276
- this.app.use(compress());
2085
+ if (/** @type {WatchFiles[]} */ (watchFiles).length > 0) {
2086
+ /** @type {WatchFiles[]} */
2087
+ (watchFiles).forEach((item) => {
2088
+ this.watchFiles(item.paths, item.options);
2089
+ });
2090
+ }
1277
2091
  }
1278
2092
 
1279
- setupProxyFeature() {
1280
- const { createProxyMiddleware } = require("http-proxy-middleware");
1281
-
1282
- const getProxyMiddleware = (proxyConfig) => {
1283
- // It is possible to use the `bypass` method without a `target` or `router`.
1284
- // However, the proxy middleware has no use in this case, and will fail to instantiate.
1285
- if (proxyConfig.target) {
1286
- const context = proxyConfig.context || proxyConfig.path;
1287
-
1288
- return createProxyMiddleware(context, proxyConfig);
1289
- }
1290
-
1291
- if (proxyConfig.router) {
1292
- return createProxyMiddleware(proxyConfig);
1293
- }
1294
- };
2093
+ /**
2094
+ * @private
2095
+ * @returns {void}
2096
+ */
2097
+ setupMiddlewares() {
1295
2098
  /**
1296
- * Assume a proxy configuration specified as:
1297
- * proxy: [
1298
- * {
1299
- * context: "value",
1300
- * ...options,
1301
- * },
1302
- * // or:
1303
- * function() {
1304
- * return {
1305
- * context: "context",
1306
- * ...options,
1307
- * };
1308
- * }
1309
- * ]
2099
+ * @type {Array<Middleware>}
1310
2100
  */
1311
- this.options.proxy.forEach((proxyConfigOrCallback) => {
1312
- let proxyMiddleware;
1313
-
1314
- let proxyConfig =
1315
- typeof proxyConfigOrCallback === "function"
1316
- ? proxyConfigOrCallback()
1317
- : proxyConfigOrCallback;
1318
-
1319
- proxyMiddleware = getProxyMiddleware(proxyConfig);
2101
+ let middlewares = [];
1320
2102
 
1321
- if (proxyConfig.ws) {
1322
- this.webSocketProxies.push(proxyMiddleware);
1323
- }
2103
+ // compress is placed last and uses unshift so that it will be the first middleware used
2104
+ if (this.options.compress) {
2105
+ const compression = require("compression");
1324
2106
 
1325
- const handle = async (req, res, next) => {
1326
- if (typeof proxyConfigOrCallback === "function") {
1327
- const newProxyConfig = proxyConfigOrCallback(req, res, next);
2107
+ middlewares.push({ name: "compression", middleware: compression() });
2108
+ }
1328
2109
 
1329
- if (newProxyConfig !== proxyConfig) {
1330
- proxyConfig = newProxyConfig;
1331
- proxyMiddleware = getProxyMiddleware(proxyConfig);
1332
- }
1333
- }
2110
+ if (typeof this.options.onBeforeSetupMiddleware === "function") {
2111
+ this.options.onBeforeSetupMiddleware(this);
2112
+ }
1334
2113
 
1335
- // - Check if we have a bypass function defined
1336
- // - In case the bypass function is defined we'll retrieve the
1337
- // bypassUrl from it otherwise bypassUrl would be null
1338
- // TODO remove in the next major in favor `context` and `router` options
1339
- const isByPassFuncDefined = typeof proxyConfig.bypass === "function";
1340
- const bypassUrl = isByPassFuncDefined
1341
- ? await proxyConfig.bypass(req, res, proxyConfig)
1342
- : null;
1343
-
1344
- if (typeof bypassUrl === "boolean") {
1345
- // skip the proxy
1346
- req.url = null;
1347
- next();
1348
- } else if (typeof bypassUrl === "string") {
1349
- // byPass to that url
1350
- req.url = bypassUrl;
1351
- next();
1352
- } else if (proxyMiddleware) {
1353
- return proxyMiddleware(req, res, next);
1354
- } else {
1355
- next();
1356
- }
1357
- };
2114
+ if (typeof this.options.headers !== "undefined") {
2115
+ middlewares.push({
2116
+ name: "set-headers",
2117
+ path: "*",
2118
+ middleware: this.setHeaders.bind(this),
2119
+ });
2120
+ }
1358
2121
 
1359
- this.app.use(handle);
1360
- // Also forward error requests to the proxy so it can handle them.
1361
- this.app.use((error, req, res, next) => handle(req, res, next));
2122
+ middlewares.push({
2123
+ name: "webpack-dev-middleware",
2124
+ middleware:
2125
+ /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/
2126
+ (this.middleware),
1362
2127
  });
1363
- }
1364
2128
 
1365
- setupHistoryApiFallbackFeature() {
1366
- const { historyApiFallback } = this.options;
2129
+ if (this.options.proxy) {
2130
+ const { createProxyMiddleware } = require("http-proxy-middleware");
1367
2131
 
1368
- if (
1369
- typeof historyApiFallback.logger === "undefined" &&
1370
- !historyApiFallback.verbose
1371
- ) {
1372
- historyApiFallback.logger = this.logger.log.bind(
1373
- this.logger,
1374
- "[connect-history-api-fallback]"
1375
- );
1376
- }
2132
+ /**
2133
+ * @param {ProxyConfigArray} proxyConfig
2134
+ * @returns {RequestHandler | undefined}
2135
+ */
2136
+ const getProxyMiddleware = (proxyConfig) => {
2137
+ // It is possible to use the `bypass` method without a `target` or `router`.
2138
+ // However, the proxy middleware has no use in this case, and will fail to instantiate.
2139
+ if (proxyConfig.target) {
2140
+ const context = proxyConfig.context || proxyConfig.path;
2141
+
2142
+ return createProxyMiddleware(
2143
+ /** @type {string} */ (context),
2144
+ proxyConfig
2145
+ );
2146
+ }
1377
2147
 
1378
- // Fall back to /index.html if nothing else matches.
1379
- this.app.use(require("connect-history-api-fallback")(historyApiFallback));
1380
- }
2148
+ if (proxyConfig.router) {
2149
+ return createProxyMiddleware(proxyConfig);
2150
+ }
2151
+ };
1381
2152
 
1382
- setupStaticFeature() {
1383
- this.options.static.forEach((staticOption) => {
1384
- staticOption.publicPath.forEach((publicPath) => {
1385
- this.app.use(
1386
- publicPath,
1387
- express.static(staticOption.directory, staticOption.staticOptions)
1388
- );
1389
- });
1390
- });
1391
- }
2153
+ /**
2154
+ * Assume a proxy configuration specified as:
2155
+ * proxy: [
2156
+ * {
2157
+ * context: "value",
2158
+ * ...options,
2159
+ * },
2160
+ * // or:
2161
+ * function() {
2162
+ * return {
2163
+ * context: "context",
2164
+ * ...options,
2165
+ * };
2166
+ * }
2167
+ * ]
2168
+ */
2169
+ /** @type {ProxyArray} */
2170
+ (this.options.proxy).forEach(
2171
+ /**
2172
+ * @param {any} proxyConfigOrCallback
2173
+ */
2174
+ (proxyConfigOrCallback) => {
2175
+ /**
2176
+ * @type {RequestHandler}
2177
+ */
2178
+ let proxyMiddleware;
2179
+
2180
+ let proxyConfig =
2181
+ typeof proxyConfigOrCallback === "function"
2182
+ ? proxyConfigOrCallback()
2183
+ : proxyConfigOrCallback;
2184
+
2185
+ proxyMiddleware =
2186
+ /** @type {RequestHandler} */
2187
+ (getProxyMiddleware(proxyConfig));
2188
+
2189
+ if (proxyConfig.ws) {
2190
+ this.webSocketProxies.push(proxyMiddleware);
2191
+ }
1392
2192
 
1393
- setupStaticServeIndexFeature() {
1394
- const serveIndex = require("serve-index");
2193
+ /**
2194
+ * @param {Request} req
2195
+ * @param {Response} res
2196
+ * @param {NextFunction} next
2197
+ * @returns {Promise<void>}
2198
+ */
2199
+ const handler = async (req, res, next) => {
2200
+ if (typeof proxyConfigOrCallback === "function") {
2201
+ const newProxyConfig = proxyConfigOrCallback(req, res, next);
2202
+
2203
+ if (newProxyConfig !== proxyConfig) {
2204
+ proxyConfig = newProxyConfig;
2205
+ proxyMiddleware =
2206
+ /** @type {RequestHandler} */
2207
+ (getProxyMiddleware(proxyConfig));
2208
+ }
2209
+ }
1395
2210
 
1396
- this.options.static.forEach((staticOption) => {
1397
- staticOption.publicPath.forEach((publicPath) => {
1398
- if (staticOption.serveIndex) {
1399
- this.app.use(publicPath, (req, res, next) => {
1400
- // serve-index doesn't fallthrough non-get/head request to next middleware
1401
- if (req.method !== "GET" && req.method !== "HEAD") {
1402
- return next();
2211
+ // - Check if we have a bypass function defined
2212
+ // - In case the bypass function is defined we'll retrieve the
2213
+ // bypassUrl from it otherwise bypassUrl would be null
2214
+ // TODO remove in the next major in favor `context` and `router` options
2215
+ const isByPassFuncDefined =
2216
+ typeof proxyConfig.bypass === "function";
2217
+ const bypassUrl = isByPassFuncDefined
2218
+ ? await proxyConfig.bypass(req, res, proxyConfig)
2219
+ : null;
2220
+
2221
+ if (typeof bypassUrl === "boolean") {
2222
+ // skip the proxy
2223
+ // @ts-ignore
2224
+ req.url = null;
2225
+ next();
2226
+ } else if (typeof bypassUrl === "string") {
2227
+ // byPass to that url
2228
+ req.url = bypassUrl;
2229
+ next();
2230
+ } else if (proxyMiddleware) {
2231
+ return proxyMiddleware(req, res, next);
2232
+ } else {
2233
+ next();
1403
2234
  }
2235
+ };
1404
2236
 
1405
- serveIndex(staticOption.directory, staticOption.serveIndex)(
1406
- req,
1407
- res,
1408
- next
1409
- );
2237
+ middlewares.push({
2238
+ name: "http-proxy-middleware",
2239
+ middleware: handler,
2240
+ });
2241
+ // Also forward error requests to the proxy so it can handle them.
2242
+ middlewares.push({
2243
+ name: "http-proxy-middleware-error-handler",
2244
+ middleware:
2245
+ /**
2246
+ * @param {Error} error
2247
+ * @param {Request} req
2248
+ * @param {Response} res
2249
+ * @param {NextFunction} next
2250
+ * @returns {any}
2251
+ */
2252
+ (error, req, res, next) => handler(req, res, next),
1410
2253
  });
1411
2254
  }
1412
- });
1413
- });
1414
- }
1415
-
1416
- setupStaticWatchFeature() {
1417
- this.options.static.forEach((staticOption) => {
1418
- if (staticOption.watch) {
1419
- this.watchFiles(staticOption.directory, staticOption.watch);
1420
- }
1421
- });
1422
- }
1423
-
1424
- setupOnBeforeSetupMiddlewareFeature() {
1425
- this.options.onBeforeSetupMiddleware(this);
1426
- }
1427
-
1428
- setupWatchFiles() {
1429
- const { watchFiles } = this.options;
2255
+ );
1430
2256
 
1431
- if (watchFiles.length > 0) {
1432
- watchFiles.forEach((item) => {
1433
- this.watchFiles(item.paths, item.options);
2257
+ middlewares.push({
2258
+ name: "webpack-dev-middleware",
2259
+ middleware:
2260
+ /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/
2261
+ (this.middleware),
1434
2262
  });
1435
2263
  }
1436
- }
1437
-
1438
- setupMiddleware() {
1439
- this.app.use(this.middleware);
1440
- }
1441
-
1442
- setupOnAfterSetupMiddlewareFeature() {
1443
- this.options.onAfterSetupMiddleware(this);
1444
- }
1445
2264
 
1446
- setupHeadersFeature() {
1447
- this.app.all("*", this.setHeaders.bind(this));
1448
- }
1449
-
1450
- setupMagicHtmlFeature() {
1451
- this.app.get("*", this.serveMagicHtml.bind(this));
1452
- }
1453
-
1454
- setupFeatures() {
1455
- const features = {
1456
- compress: () => {
1457
- if (this.options.compress) {
1458
- this.setupCompressFeature();
1459
- }
1460
- },
1461
- proxy: () => {
1462
- if (this.options.proxy) {
1463
- this.setupProxyFeature();
1464
- }
1465
- },
1466
- historyApiFallback: () => {
1467
- if (this.options.historyApiFallback) {
1468
- this.setupHistoryApiFallbackFeature();
1469
- }
1470
- },
1471
- static: () => {
1472
- this.setupStaticFeature();
1473
- },
1474
- staticServeIndex: () => {
1475
- this.setupStaticServeIndexFeature();
1476
- },
1477
- staticWatch: () => {
1478
- this.setupStaticWatchFeature();
1479
- },
1480
- onBeforeSetupMiddleware: () => {
1481
- if (typeof this.options.onBeforeSetupMiddleware === "function") {
1482
- this.setupOnBeforeSetupMiddlewareFeature();
1483
- }
1484
- },
1485
- onAfterSetupMiddleware: () => {
1486
- if (typeof this.options.onAfterSetupMiddleware === "function") {
1487
- this.setupOnAfterSetupMiddlewareFeature();
1488
- }
1489
- },
1490
- middleware: () => {
1491
- // include our middleware to ensure
1492
- // it is able to handle '/index.html' request after redirect
1493
- this.setupMiddleware();
1494
- },
1495
- headers: () => {
1496
- this.setupHeadersFeature();
1497
- },
1498
- magicHtml: () => {
1499
- this.setupMagicHtmlFeature();
1500
- },
1501
- };
1502
-
1503
- const runnableFeatures = [];
1504
-
1505
- // compress is placed last and uses unshift so that it will be the first middleware used
1506
- if (this.options.compress) {
1507
- runnableFeatures.push("compress");
1508
- }
1509
-
1510
- if (this.options.onBeforeSetupMiddleware) {
1511
- runnableFeatures.push("onBeforeSetupMiddleware");
2265
+ if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
2266
+ /** @type {NormalizedStatic[]} */
2267
+ (this.options.static).forEach((staticOption) => {
2268
+ staticOption.publicPath.forEach((publicPath) => {
2269
+ middlewares.push({
2270
+ name: "express-static",
2271
+ path: publicPath,
2272
+ middleware: express.static(
2273
+ staticOption.directory,
2274
+ staticOption.staticOptions
2275
+ ),
2276
+ });
2277
+ });
2278
+ });
1512
2279
  }
1513
2280
 
1514
- runnableFeatures.push("headers", "middleware");
2281
+ if (this.options.historyApiFallback) {
2282
+ const connectHistoryApiFallback = require("connect-history-api-fallback");
2283
+ const { historyApiFallback } = this.options;
1515
2284
 
1516
- if (this.options.proxy) {
1517
- runnableFeatures.push("proxy", "middleware");
1518
- }
2285
+ if (
2286
+ typeof (
2287
+ /** @type {ConnectHistoryApiFallbackOptions} */
2288
+ (historyApiFallback).logger
2289
+ ) === "undefined" &&
2290
+ !(
2291
+ /** @type {ConnectHistoryApiFallbackOptions} */
2292
+ (historyApiFallback).verbose
2293
+ )
2294
+ ) {
2295
+ // @ts-ignore
2296
+ historyApiFallback.logger = this.logger.log.bind(
2297
+ this.logger,
2298
+ "[connect-history-api-fallback]"
2299
+ );
2300
+ }
1519
2301
 
1520
- if (this.options.static) {
1521
- runnableFeatures.push("static");
1522
- }
2302
+ // Fall back to /index.html if nothing else matches.
2303
+ middlewares.push({
2304
+ name: "connect-history-api-fallback",
2305
+ middleware: connectHistoryApiFallback(
2306
+ /** @type {ConnectHistoryApiFallbackOptions} */
2307
+ (historyApiFallback)
2308
+ ),
2309
+ });
1523
2310
 
1524
- if (this.options.historyApiFallback) {
1525
- runnableFeatures.push("historyApiFallback", "middleware");
2311
+ // include our middleware to ensure
2312
+ // it is able to handle '/index.html' request after redirect
2313
+ middlewares.push({
2314
+ name: "webpack-dev-middleware",
2315
+ middleware:
2316
+ /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/
2317
+ (this.middleware),
2318
+ });
1526
2319
 
1527
- if (this.options.static) {
1528
- runnableFeatures.push("static");
2320
+ if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
2321
+ /** @type {NormalizedStatic[]} */
2322
+ (this.options.static).forEach((staticOption) => {
2323
+ staticOption.publicPath.forEach((publicPath) => {
2324
+ middlewares.push({
2325
+ name: "express-static",
2326
+ path: publicPath,
2327
+ middleware: express.static(
2328
+ staticOption.directory,
2329
+ staticOption.staticOptions
2330
+ ),
2331
+ });
2332
+ });
2333
+ });
1529
2334
  }
1530
2335
  }
1531
2336
 
1532
- if (this.options.static) {
1533
- runnableFeatures.push("staticServeIndex", "staticWatch");
2337
+ if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
2338
+ const serveIndex = require("serve-index");
2339
+
2340
+ /** @type {NormalizedStatic[]} */
2341
+ (this.options.static).forEach((staticOption) => {
2342
+ staticOption.publicPath.forEach((publicPath) => {
2343
+ if (staticOption.serveIndex) {
2344
+ middlewares.push({
2345
+ name: "serve-index",
2346
+ path: publicPath,
2347
+ /**
2348
+ * @param {Request} req
2349
+ * @param {Response} res
2350
+ * @param {NextFunction} next
2351
+ * @returns {void}
2352
+ */
2353
+ middleware: (req, res, next) => {
2354
+ // serve-index doesn't fallthrough non-get/head request to next middleware
2355
+ if (req.method !== "GET" && req.method !== "HEAD") {
2356
+ return next();
2357
+ }
2358
+
2359
+ serveIndex(
2360
+ staticOption.directory,
2361
+ /** @type {ServeIndexOptions} */
2362
+ (staticOption.serveIndex)
2363
+ )(req, res, next);
2364
+ },
2365
+ });
2366
+ }
2367
+ });
2368
+ });
1534
2369
  }
1535
2370
 
1536
2371
  if (this.options.magicHtml) {
1537
- runnableFeatures.push("magicHtml");
2372
+ middlewares.push({
2373
+ name: "serve-magic-html",
2374
+ middleware: this.serveMagicHtml.bind(this),
2375
+ });
2376
+ }
2377
+
2378
+ if (typeof this.options.onAfterSetupMiddleware === "function") {
2379
+ this.options.onAfterSetupMiddleware(this);
1538
2380
  }
1539
2381
 
1540
- if (this.options.onAfterSetupMiddleware) {
1541
- runnableFeatures.push("onAfterSetupMiddleware");
2382
+ if (typeof this.options.setupMiddlewares === "function") {
2383
+ middlewares = this.options.setupMiddlewares(middlewares, this);
1542
2384
  }
1543
2385
 
1544
- runnableFeatures.forEach((feature) => {
1545
- features[feature]();
2386
+ middlewares.forEach((middleware) => {
2387
+ if (typeof middleware === "function") {
2388
+ /** @type {import("express").Application} */
2389
+ (this.app).use(middleware);
2390
+ } else if (typeof middleware.path !== "undefined") {
2391
+ /** @type {import("express").Application} */
2392
+ (this.app).use(middleware.path, middleware.middleware);
2393
+ } else {
2394
+ /** @type {import("express").Application} */
2395
+ (this.app).use(middleware.middleware);
2396
+ }
1546
2397
  });
1547
2398
  }
1548
2399
 
2400
+ /**
2401
+ * @private
2402
+ * @returns {void}
2403
+ */
1549
2404
  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");
2405
+ const { type, options } = /** @type {ServerConfiguration} */ (
2406
+ this.options.server
2407
+ );
1569
2408
 
1570
- this.server = http.createServer(this.app);
1571
- }
2409
+ /** @type {import("http").Server | undefined | null} */
2410
+ // eslint-disable-next-line import/no-dynamic-require
2411
+ this.server = require(/** @type {string} */ (type)).createServer(
2412
+ options,
2413
+ this.app
2414
+ );
1572
2415
 
1573
- this.server.on("connection", (socket) => {
1574
- // Add socket to list
1575
- this.sockets.push(socket);
2416
+ /** @type {import("http").Server} */
2417
+ (this.server).on(
2418
+ "connection",
2419
+ /**
2420
+ * @param {Socket} socket
2421
+ */
2422
+ (socket) => {
2423
+ // Add socket to list
2424
+ this.sockets.push(socket);
1576
2425
 
1577
- socket.once("close", () => {
1578
- // Remove socket from list
1579
- this.sockets.splice(this.sockets.indexOf(socket), 1);
1580
- });
1581
- });
2426
+ socket.once("close", () => {
2427
+ // Remove socket from list
2428
+ this.sockets.splice(this.sockets.indexOf(socket), 1);
2429
+ });
2430
+ }
2431
+ );
1582
2432
 
1583
- this.server.on("error", (error) => {
1584
- throw error;
1585
- });
2433
+ /** @type {import("http").Server} */
2434
+ (this.server).on(
2435
+ "error",
2436
+ /**
2437
+ * @param {Error} error
2438
+ */
2439
+ (error) => {
2440
+ throw error;
2441
+ }
2442
+ );
1586
2443
  }
1587
2444
 
2445
+ /**
2446
+ * @private
2447
+ * @returns {void}
2448
+ */
2449
+ // TODO: remove `--web-socket-server` in favor of `--web-socket-server-type`
1588
2450
  createWebSocketServer() {
1589
- this.webSocketServer = new (this.getServerTransport())(this);
1590
- this.webSocketServer.implementation.on("connection", (client, request) => {
1591
- const headers =
1592
- // eslint-disable-next-line no-nested-ternary
1593
- typeof request !== "undefined"
1594
- ? request.headers
1595
- : typeof client.headers !== "undefined"
1596
- ? client.headers
1597
- : // eslint-disable-next-line no-undefined
1598
- undefined;
1599
-
1600
- if (!headers) {
1601
- this.logger.warn(
1602
- 'webSocketServer implementation must pass headers for the "connection" event'
1603
- );
1604
- }
2451
+ /** @type {WebSocketServerImplementation | undefined | null} */
2452
+ this.webSocketServer = new /** @type {any} */ (this.getServerTransport())(
2453
+ this
2454
+ );
2455
+ /** @type {WebSocketServerImplementation} */
2456
+ (this.webSocketServer).implementation.on(
2457
+ "connection",
2458
+ /**
2459
+ * @param {ClientConnection} client
2460
+ * @param {IncomingMessage} request
2461
+ */
2462
+ (client, request) => {
2463
+ /** @type {{ [key: string]: string | undefined } | undefined} */
2464
+ const headers =
2465
+ // eslint-disable-next-line no-nested-ternary
2466
+ typeof request !== "undefined"
2467
+ ? /** @type {{ [key: string]: string | undefined }} */
2468
+ (request.headers)
2469
+ : typeof (
2470
+ /** @type {import("sockjs").Connection} */ (client).headers
2471
+ ) !== "undefined"
2472
+ ? /** @type {import("sockjs").Connection} */ (client).headers
2473
+ : // eslint-disable-next-line no-undefined
2474
+ undefined;
2475
+
2476
+ if (!headers) {
2477
+ this.logger.warn(
2478
+ 'webSocketServer implementation must pass headers for the "connection" event'
2479
+ );
2480
+ }
1605
2481
 
1606
- if (
1607
- !headers ||
1608
- !this.checkHeader(headers, "host") ||
1609
- !this.checkHeader(headers, "origin")
1610
- ) {
1611
- this.sendMessage([client], "error", "Invalid Host/Origin header");
2482
+ if (
2483
+ !headers ||
2484
+ !this.checkHeader(headers, "host") ||
2485
+ !this.checkHeader(headers, "origin")
2486
+ ) {
2487
+ this.sendMessage([client], "error", "Invalid Host/Origin header");
1612
2488
 
1613
- client.terminate();
2489
+ // With https enabled, the sendMessage above is encrypted asynchronously so not yet sent
2490
+ // Terminate would prevent it sending, so use close to allow it to be sent
2491
+ client.close();
1614
2492
 
1615
- return;
1616
- }
2493
+ return;
2494
+ }
1617
2495
 
1618
- if (this.options.hot === true || this.options.hot === "only") {
1619
- this.sendMessage([client], "hot");
1620
- }
2496
+ if (this.options.hot === true || this.options.hot === "only") {
2497
+ this.sendMessage([client], "hot");
2498
+ }
1621
2499
 
1622
- if (this.options.liveReload) {
1623
- this.sendMessage([client], "liveReload");
1624
- }
2500
+ if (this.options.liveReload) {
2501
+ this.sendMessage([client], "liveReload");
2502
+ }
1625
2503
 
1626
- if (this.options.client && this.options.client.progress) {
1627
- this.sendMessage([client], "progress", this.options.client.progress);
1628
- }
2504
+ if (
2505
+ this.options.client &&
2506
+ /** @type {ClientConfiguration} */
2507
+ (this.options.client).progress
2508
+ ) {
2509
+ this.sendMessage(
2510
+ [client],
2511
+ "progress",
2512
+ /** @type {ClientConfiguration} */
2513
+ (this.options.client).progress
2514
+ );
2515
+ }
1629
2516
 
1630
- if (this.options.client && this.options.client.overlay) {
1631
- this.sendMessage([client], "overlay", this.options.client.overlay);
1632
- }
2517
+ if (
2518
+ this.options.client &&
2519
+ /** @type {ClientConfiguration} */ (this.options.client).reconnect
2520
+ ) {
2521
+ this.sendMessage(
2522
+ [client],
2523
+ "reconnect",
2524
+ /** @type {ClientConfiguration} */
2525
+ (this.options.client).reconnect
2526
+ );
2527
+ }
1633
2528
 
1634
- if (!this.stats) {
1635
- return;
1636
- }
2529
+ if (
2530
+ this.options.client &&
2531
+ /** @type {ClientConfiguration} */
2532
+ (this.options.client).overlay
2533
+ ) {
2534
+ this.sendMessage(
2535
+ [client],
2536
+ "overlay",
2537
+ /** @type {ClientConfiguration} */
2538
+ (this.options.client).overlay
2539
+ );
2540
+ }
1637
2541
 
1638
- this.sendStats([client], this.getStats(this.stats), true);
1639
- });
2542
+ if (!this.stats) {
2543
+ return;
2544
+ }
2545
+
2546
+ this.sendStats([client], this.getStats(this.stats), true);
2547
+ }
2548
+ );
1640
2549
  }
1641
2550
 
2551
+ /**
2552
+ * @private
2553
+ * @param {string} defaultOpenTarget
2554
+ * @returns {void}
2555
+ */
1642
2556
  openBrowser(defaultOpenTarget) {
1643
2557
  const open = require("open");
1644
2558
 
1645
2559
  Promise.all(
1646
- this.options.open.map((item) => {
2560
+ /** @type {NormalizedOpen[]} */
2561
+ (this.options.open).map((item) => {
2562
+ /**
2563
+ * @type {string}
2564
+ */
1647
2565
  let openTarget;
1648
2566
 
1649
2567
  if (item.target === "<url>") {
@@ -1657,13 +2575,17 @@ class Server {
1657
2575
  return open(openTarget, item.options).catch(() => {
1658
2576
  this.logger.warn(
1659
2577
  `Unable to open "${openTarget}" page${
1660
- // eslint-disable-next-line no-nested-ternary
1661
2578
  item.options.app
1662
- ? ` in "${item.options.app.name}" app${
1663
- item.options.app.arguments
1664
- ? ` with "${item.options.app.arguments.join(
1665
- " "
1666
- )}" arguments`
2579
+ ? ` in "${
2580
+ /** @type {import("open").App} */
2581
+ (item.options.app).name
2582
+ }" app${
2583
+ /** @type {import("open").App} */
2584
+ (item.options.app).arguments
2585
+ ? ` with "${
2586
+ /** @type {import("open").App} */
2587
+ (item.options.app).arguments.join(" ")
2588
+ }" arguments`
1667
2589
  : ""
1668
2590
  }`
1669
2591
  : ""
@@ -1674,35 +2596,68 @@ class Server {
1674
2596
  );
1675
2597
  }
1676
2598
 
2599
+ /**
2600
+ * @private
2601
+ * @returns {void}
2602
+ */
1677
2603
  runBonjour() {
1678
- const bonjour = require("bonjour")();
1679
-
1680
- bonjour.publish({
2604
+ /**
2605
+ * @private
2606
+ * @type {import("bonjour").Bonjour | undefined}
2607
+ */
2608
+ this.bonjour = require("bonjour")();
2609
+ this.bonjour.publish({
1681
2610
  name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
1682
- port: this.options.port,
1683
- type: this.options.https ? "https" : "http",
2611
+ port: /** @type {number} */ (this.options.port),
2612
+ type:
2613
+ /** @type {ServerConfiguration} */
2614
+ (this.options.server).type === "http" ? "http" : "https",
1684
2615
  subtypes: ["webpack"],
1685
- ...this.options.bonjour,
2616
+ .../** @type {BonjourOptions} */ (this.options.bonjour),
1686
2617
  });
2618
+ }
1687
2619
 
1688
- process.on("exit", () => {
1689
- bonjour.unpublishAll(() => {
1690
- bonjour.destroy();
1691
- });
2620
+ /**
2621
+ * @private
2622
+ * @returns {void}
2623
+ */
2624
+ stopBonjour(callback = () => {}) {
2625
+ /** @type {Bonjour} */
2626
+ (this.bonjour).unpublishAll(() => {
2627
+ /** @type {Bonjour} */
2628
+ (this.bonjour).destroy();
2629
+
2630
+ if (callback) {
2631
+ callback();
2632
+ }
1692
2633
  });
1693
2634
  }
1694
2635
 
2636
+ /**
2637
+ * @private
2638
+ * @returns {void}
2639
+ */
1695
2640
  logStatus() {
1696
2641
  const { isColorSupported, cyan, red } = require("colorette");
1697
2642
 
2643
+ /**
2644
+ * @param {Compiler["options"]} compilerOptions
2645
+ * @returns {boolean}
2646
+ */
1698
2647
  const getColorsOption = (compilerOptions) => {
2648
+ /**
2649
+ * @type {boolean}
2650
+ */
1699
2651
  let colorsEnabled;
1700
2652
 
1701
2653
  if (
1702
2654
  compilerOptions.stats &&
1703
- typeof compilerOptions.stats.colors !== "undefined"
2655
+ typeof (/** @type {StatsOptions} */ (compilerOptions.stats).colors) !==
2656
+ "undefined"
1704
2657
  ) {
1705
- colorsEnabled = compilerOptions.stats;
2658
+ colorsEnabled =
2659
+ /** @type {boolean} */
2660
+ (/** @type {StatsOptions} */ (compilerOptions.stats).colors);
1706
2661
  } else {
1707
2662
  colorsEnabled = isColorSupported;
1708
2663
  }
@@ -1711,6 +2666,11 @@ class Server {
1711
2666
  };
1712
2667
 
1713
2668
  const colors = {
2669
+ /**
2670
+ * @param {boolean} useColor
2671
+ * @param {string} msg
2672
+ * @returns {string}
2673
+ */
1714
2674
  info(useColor, msg) {
1715
2675
  if (useColor) {
1716
2676
  return cyan(msg);
@@ -1718,6 +2678,11 @@ class Server {
1718
2678
 
1719
2679
  return msg;
1720
2680
  },
2681
+ /**
2682
+ * @param {boolean} useColor
2683
+ * @param {string} msg
2684
+ * @returns {string}
2685
+ */
1721
2686
  error(useColor, msg) {
1722
2687
  if (useColor) {
1723
2688
  return red(msg);
@@ -1729,10 +2694,26 @@ class Server {
1729
2694
  const useColor = getColorsOption(this.getCompilerOptions());
1730
2695
 
1731
2696
  if (this.options.ipc) {
1732
- this.logger.info(`Project is running at: "${this.server.address()}"`);
2697
+ this.logger.info(
2698
+ `Project is running at: "${
2699
+ /** @type {import("http").Server} */
2700
+ (this.server).address()
2701
+ }"`
2702
+ );
1733
2703
  } else {
1734
- const protocol = this.options.https ? "https" : "http";
1735
- const { address, port } = this.server.address();
2704
+ const protocol =
2705
+ /** @type {ServerConfiguration} */
2706
+ (this.options.server).type === "http" ? "http" : "https";
2707
+ const { address, port } =
2708
+ /** @type {import("net").AddressInfo} */
2709
+ (
2710
+ /** @type {import("http").Server} */
2711
+ (this.server).address()
2712
+ );
2713
+ /**
2714
+ * @param {string} newHostname
2715
+ * @returns {string}
2716
+ */
1736
2717
  const prettyPrintURL = (newHostname) =>
1737
2718
  url.format({ protocol, hostname: newHostname, port, pathname: "/" });
1738
2719
 
@@ -1766,13 +2747,13 @@ class Server {
1766
2747
  if (parsedIP.range() === "unspecified") {
1767
2748
  localhost = prettyPrintURL("localhost");
1768
2749
 
1769
- const networkIPv4 = internalIp.v4.sync();
2750
+ const networkIPv4 = Server.internalIPSync("v4");
1770
2751
 
1771
2752
  if (networkIPv4) {
1772
2753
  networkUrlIPv4 = prettyPrintURL(networkIPv4);
1773
2754
  }
1774
2755
 
1775
- const networkIPv6 = internalIp.v6.sync();
2756
+ const networkIPv6 = Server.internalIPSync("v6");
1776
2757
 
1777
2758
  if (networkIPv6) {
1778
2759
  networkUrlIPv6 = prettyPrintURL(networkIPv6);
@@ -1785,8 +2766,13 @@ class Server {
1785
2766
  }
1786
2767
  } else {
1787
2768
  networkUrlIPv4 =
1788
- parsedIP.kind() === "ipv6" && parsedIP.isIPv4MappedAddress()
1789
- ? prettyPrintURL(parsedIP.toIPv4Address().toString())
2769
+ parsedIP.kind() === "ipv6" &&
2770
+ /** @type {IPv6} */
2771
+ (parsedIP).isIPv4MappedAddress()
2772
+ ? prettyPrintURL(
2773
+ /** @type {IPv6} */
2774
+ (parsedIP).toIPv4Address().toString()
2775
+ )
1790
2776
  : prettyPrintURL(address);
1791
2777
 
1792
2778
  if (parsedIP.kind() === "ipv6") {
@@ -1801,10 +2787,19 @@ class Server {
1801
2787
  }
1802
2788
 
1803
2789
  if (localhost || loopbackIPv4 || loopbackIPv6) {
1804
- const loopbacks = []
1805
- .concat(localhost ? [colors.info(useColor, localhost)] : [])
1806
- .concat(loopbackIPv4 ? [colors.info(useColor, loopbackIPv4)] : [])
1807
- .concat(loopbackIPv6 ? [colors.info(useColor, loopbackIPv6)] : []);
2790
+ const loopbacks = [];
2791
+
2792
+ if (localhost) {
2793
+ loopbacks.push([colors.info(useColor, localhost)]);
2794
+ }
2795
+
2796
+ if (loopbackIPv4) {
2797
+ loopbacks.push([colors.info(useColor, loopbackIPv4)]);
2798
+ }
2799
+
2800
+ if (loopbackIPv6) {
2801
+ loopbacks.push([colors.info(useColor, loopbackIPv6)]);
2802
+ }
1808
2803
 
1809
2804
  this.logger.info(`Loopback: ${loopbacks.join(", ")}`);
1810
2805
  }
@@ -1821,18 +2816,19 @@ class Server {
1821
2816
  );
1822
2817
  }
1823
2818
 
1824
- if (this.options.open.length > 0) {
2819
+ if (/** @type {NormalizedOpen[]} */ (this.options.open).length > 0) {
1825
2820
  const openTarget = prettyPrintURL(this.options.host || "localhost");
1826
2821
 
1827
2822
  this.openBrowser(openTarget);
1828
2823
  }
1829
2824
  }
1830
2825
 
1831
- if (this.options.static && this.options.static.length > 0) {
2826
+ if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
1832
2827
  this.logger.info(
1833
2828
  `Content not from webpack is served from '${colors.info(
1834
2829
  useColor,
1835
- this.options.static
2830
+ /** @type {NormalizedStatic[]} */
2831
+ (this.options.static)
1836
2832
  .map((staticOption) => staticOption.directory)
1837
2833
  .join(", ")
1838
2834
  )}' directory`
@@ -1843,14 +2839,21 @@ class Server {
1843
2839
  this.logger.info(
1844
2840
  `404s will fallback to '${colors.info(
1845
2841
  useColor,
1846
- this.options.historyApiFallback.index || "/index.html"
2842
+ /** @type {ConnectHistoryApiFallbackOptions} */ (
2843
+ this.options.historyApiFallback
2844
+ ).index || "/index.html"
1847
2845
  )}'`
1848
2846
  );
1849
2847
  }
1850
2848
 
1851
2849
  if (this.options.bonjour) {
1852
2850
  const bonjourProtocol =
1853
- this.options.bonjour.type || this.options.https ? "https" : "http";
2851
+ /** @type {BonjourOptions} */
2852
+ (this.options.bonjour).type ||
2853
+ /** @type {ServerConfiguration} */
2854
+ (this.options.server).type === "http"
2855
+ ? "http"
2856
+ : "https";
1854
2857
 
1855
2858
  this.logger.info(
1856
2859
  `Broadcasting "${bonjourProtocol}" with subtype of "webpack" via ZeroConf DNS (Bonjour)`
@@ -1858,32 +2861,59 @@ class Server {
1858
2861
  }
1859
2862
  }
1860
2863
 
2864
+ /**
2865
+ * @private
2866
+ * @param {Request} req
2867
+ * @param {Response} res
2868
+ * @param {NextFunction} next
2869
+ */
1861
2870
  setHeaders(req, res, next) {
1862
2871
  let { headers } = this.options;
1863
2872
 
1864
2873
  if (headers) {
1865
2874
  if (typeof headers === "function") {
1866
- headers = headers(req, res, this.middleware.context);
2875
+ headers = headers(
2876
+ req,
2877
+ res,
2878
+ /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
2879
+ (this.middleware).context
2880
+ );
1867
2881
  }
1868
2882
 
2883
+ /**
2884
+ * @type {{key: string, value: string}[]}
2885
+ */
1869
2886
  const allHeaders = [];
1870
2887
 
1871
2888
  if (!Array.isArray(headers)) {
1872
2889
  // eslint-disable-next-line guard-for-in
1873
2890
  for (const name in headers) {
2891
+ // @ts-ignore
1874
2892
  allHeaders.push({ key: name, value: headers[name] });
1875
2893
  }
2894
+
1876
2895
  headers = allHeaders;
1877
2896
  }
1878
2897
 
1879
- headers.forEach((header) => {
1880
- res.setHeader(header.key, header.value);
1881
- });
2898
+ headers.forEach(
2899
+ /**
2900
+ * @param {{key: string, value: any}} header
2901
+ */
2902
+ (header) => {
2903
+ res.setHeader(header.key, header.value);
2904
+ }
2905
+ );
1882
2906
  }
1883
2907
 
1884
2908
  next();
1885
2909
  }
1886
2910
 
2911
+ /**
2912
+ * @private
2913
+ * @param {{ [key: string]: string | undefined }} headers
2914
+ * @param {string} headerToCheck
2915
+ * @returns {boolean}
2916
+ */
1887
2917
  checkHeader(headers, headerToCheck) {
1888
2918
  // allow user to opt out of this security check, at their own risk
1889
2919
  // by explicitly enabling allowedHosts
@@ -1920,8 +2950,8 @@ class Server {
1920
2950
  // always allow localhost host, for convenience (hostname === 'localhost')
1921
2951
  // allow hostname of listening address (hostname === this.options.host)
1922
2952
  const isValidHostname =
1923
- ipaddr.IPv4.isValid(hostname) ||
1924
- ipaddr.IPv6.isValid(hostname) ||
2953
+ (hostname !== null && ipaddr.IPv4.isValid(hostname)) ||
2954
+ (hostname !== null && ipaddr.IPv6.isValid(hostname)) ||
1925
2955
  hostname === "localhost" ||
1926
2956
  hostname === this.options.host;
1927
2957
 
@@ -1948,7 +2978,7 @@ class Server {
1948
2978
  // "*.example.com" (hostname.endsWith(allowedHost))
1949
2979
  if (
1950
2980
  hostname === allowedHost.substring(1) ||
1951
- hostname.endsWith(allowedHost)
2981
+ /** @type {string} */ (hostname).endsWith(allowedHost)
1952
2982
  ) {
1953
2983
  return true;
1954
2984
  }
@@ -1959,41 +2989,73 @@ class Server {
1959
2989
  // Also allow if `client.webSocketURL.hostname` provided
1960
2990
  if (
1961
2991
  this.options.client &&
1962
- typeof this.options.client.webSocketURL !== "undefined"
2992
+ typeof (
2993
+ /** @type {ClientConfiguration} */ (this.options.client).webSocketURL
2994
+ ) !== "undefined"
1963
2995
  ) {
1964
- return this.options.client.webSocketURL.hostname === hostname;
2996
+ return (
2997
+ /** @type {WebSocketURL} */
2998
+ (/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
2999
+ .hostname === hostname
3000
+ );
1965
3001
  }
1966
3002
 
1967
3003
  // disallow
1968
3004
  return false;
1969
3005
  }
1970
3006
 
3007
+ /**
3008
+ * @param {ClientConnection[]} clients
3009
+ * @param {string} type
3010
+ * @param {any} [data]
3011
+ * @param {any} [params]
3012
+ */
1971
3013
  // eslint-disable-next-line class-methods-use-this
1972
- sendMessage(clients, type, data) {
3014
+ sendMessage(clients, type, data, params) {
1973
3015
  for (const client of clients) {
1974
3016
  // `sockjs` uses `1` to indicate client is ready to accept data
1975
3017
  // `ws` uses `WebSocket.OPEN`, but it is mean `1` too
1976
3018
  if (client.readyState === 1) {
1977
- client.send(JSON.stringify({ type, data }));
3019
+ client.send(JSON.stringify({ type, data, params }));
1978
3020
  }
1979
3021
  }
1980
3022
  }
1981
3023
 
3024
+ /**
3025
+ * @private
3026
+ * @param {Request} req
3027
+ * @param {Response} res
3028
+ * @param {NextFunction} next
3029
+ * @returns {void}
3030
+ */
1982
3031
  serveMagicHtml(req, res, next) {
1983
- this.middleware.waitUntilValid(() => {
3032
+ if (req.method !== "GET" && req.method !== "HEAD") {
3033
+ return next();
3034
+ }
3035
+
3036
+ /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
3037
+ (this.middleware).waitUntilValid(() => {
1984
3038
  const _path = req.path;
1985
3039
 
1986
3040
  try {
1987
- const filename = this.middleware.getFilenameFromUrl(`${_path}.js`);
1988
- const isFile = this.middleware.context.outputFileSystem
1989
- .statSync(filename)
1990
- .isFile();
3041
+ const filename =
3042
+ /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
3043
+ (this.middleware).getFilenameFromUrl(`${_path}.js`);
3044
+ const isFile =
3045
+ /** @type {Compiler["outputFileSystem"] & { statSync: import("fs").StatSyncFn }}*/
3046
+ (
3047
+ /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
3048
+ (this.middleware).context.outputFileSystem
3049
+ )
3050
+ .statSync(/** @type {import("fs").PathLike} */ (filename))
3051
+ .isFile();
1991
3052
 
1992
3053
  if (!isFile) {
1993
3054
  return next();
1994
3055
  }
1995
3056
 
1996
3057
  // Serve a page that executes the javascript
3058
+ // @ts-ignore
1997
3059
  const queries = req._parsedUrl.search || "";
1998
3060
  const responsePage = `<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="${_path}.js${queries}"></script></body></html>`;
1999
3061
 
@@ -2005,6 +3067,12 @@ class Server {
2005
3067
  }
2006
3068
 
2007
3069
  // Send stats to a socket or multiple sockets
3070
+ /**
3071
+ * @private
3072
+ * @param {ClientConnection[]} clients
3073
+ * @param {StatsCompilation} stats
3074
+ * @param {boolean} [force]
3075
+ */
2008
3076
  sendStats(clients, stats, force) {
2009
3077
  const shouldEmit =
2010
3078
  !force &&
@@ -2022,12 +3090,33 @@ class Server {
2022
3090
  this.currentHash = stats.hash;
2023
3091
  this.sendMessage(clients, "hash", stats.hash);
2024
3092
 
2025
- if (stats.errors.length > 0 || stats.warnings.length > 0) {
2026
- if (stats.warnings.length > 0) {
2027
- this.sendMessage(clients, "warnings", stats.warnings);
3093
+ if (
3094
+ /** @type {NonNullable<StatsCompilation["errors"]>} */
3095
+ (stats.errors).length > 0 ||
3096
+ /** @type {NonNullable<StatsCompilation["warnings"]>} */
3097
+ (stats.warnings).length > 0
3098
+ ) {
3099
+ const hasErrors =
3100
+ /** @type {NonNullable<StatsCompilation["errors"]>} */
3101
+ (stats.errors).length > 0;
3102
+
3103
+ if (
3104
+ /** @type {NonNullable<StatsCompilation["warnings"]>} */
3105
+ (stats.warnings).length > 0
3106
+ ) {
3107
+ let params;
3108
+
3109
+ if (hasErrors) {
3110
+ params = { preventReloading: true };
3111
+ }
3112
+
3113
+ this.sendMessage(clients, "warnings", stats.warnings, params);
2028
3114
  }
2029
3115
 
2030
- if (stats.errors.length > 0) {
3116
+ if (
3117
+ /** @type {NonNullable<StatsCompilation["errors"]>} */ (stats.errors)
3118
+ .length > 0
3119
+ ) {
2031
3120
  this.sendMessage(clients, "errors", stats.errors);
2032
3121
  }
2033
3122
  } else {
@@ -2035,38 +3124,13 @@ class Server {
2035
3124
  }
2036
3125
  }
2037
3126
 
3127
+ /**
3128
+ * @param {string | string[]} watchPath
3129
+ * @param {WatchOptions} [watchOptions]
3130
+ */
2038
3131
  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
3132
  const chokidar = require("chokidar");
2069
- const watcher = chokidar.watch(watchPath, finalWatchOptions);
3133
+ const watcher = chokidar.watch(watchPath, watchOptions);
2070
3134
 
2071
3135
  // disabling refreshing on changing the content
2072
3136
  if (this.options.liveReload) {
@@ -2084,44 +3148,65 @@ class Server {
2084
3148
  this.staticWatchers.push(watcher);
2085
3149
  }
2086
3150
 
2087
- invalidate(callback) {
3151
+ /**
3152
+ * @param {import("webpack-dev-middleware").Callback} [callback]
3153
+ */
3154
+ invalidate(callback = () => {}) {
2088
3155
  if (this.middleware) {
2089
3156
  this.middleware.invalidate(callback);
2090
3157
  }
2091
3158
  }
2092
3159
 
3160
+ /**
3161
+ * @returns {Promise<void>}
3162
+ */
2093
3163
  async start() {
2094
3164
  await this.normalizeOptions();
2095
3165
 
2096
3166
  if (this.options.ipc) {
2097
- await new Promise((resolve, reject) => {
2098
- const net = require("net");
2099
- const socket = new net.Socket();
2100
-
2101
- socket.on("error", (error) => {
2102
- if (error.code === "ECONNREFUSED") {
2103
- // No other server listening on this socket so it can be safely removed
2104
- fs.unlinkSync(this.options.ipc);
2105
-
2106
- resolve();
3167
+ await /** @type {Promise<void>} */ (
3168
+ new Promise((resolve, reject) => {
3169
+ const net = require("net");
3170
+ const socket = new net.Socket();
3171
+
3172
+ socket.on(
3173
+ "error",
3174
+ /**
3175
+ * @param {Error & { code?: string }} error
3176
+ */
3177
+ (error) => {
3178
+ if (error.code === "ECONNREFUSED") {
3179
+ // No other server listening on this socket so it can be safely removed
3180
+ fs.unlinkSync(/** @type {string} */ (this.options.ipc));
3181
+
3182
+ resolve();
3183
+
3184
+ return;
3185
+ } else if (error.code === "ENOENT") {
3186
+ resolve();
3187
+
3188
+ return;
3189
+ }
2107
3190
 
2108
- return;
2109
- } else if (error.code === "ENOENT") {
2110
- resolve();
2111
-
2112
- return;
2113
- }
2114
-
2115
- reject(error);
2116
- });
3191
+ reject(error);
3192
+ }
3193
+ );
2117
3194
 
2118
- socket.connect({ path: this.options.ipc }, () => {
2119
- throw new Error(`IPC "${this.options.ipc}" is already used`);
2120
- });
2121
- });
3195
+ socket.connect(
3196
+ { path: /** @type {string} */ (this.options.ipc) },
3197
+ () => {
3198
+ throw new Error(`IPC "${this.options.ipc}" is already used`);
3199
+ }
3200
+ );
3201
+ })
3202
+ );
2122
3203
  } else {
2123
- this.options.host = await Server.getHostname(this.options.host);
2124
- this.options.port = await Server.getFreePort(this.options.port);
3204
+ this.options.host = await Server.getHostname(
3205
+ /** @type {Host} */ (this.options.host)
3206
+ );
3207
+ this.options.port = await Server.getFreePort(
3208
+ /** @type {Port} */ (this.options.port)
3209
+ );
2125
3210
  }
2126
3211
 
2127
3212
  await this.initialize();
@@ -2130,17 +3215,23 @@ class Server {
2130
3215
  ? { path: this.options.ipc }
2131
3216
  : { host: this.options.host, port: this.options.port };
2132
3217
 
2133
- await new Promise((resolve) => {
2134
- this.server.listen(listenOptions, () => {
2135
- resolve();
2136
- });
2137
- });
3218
+ await /** @type {Promise<void>} */ (
3219
+ new Promise((resolve) => {
3220
+ /** @type {import("http").Server} */
3221
+ (this.server).listen(listenOptions, () => {
3222
+ resolve();
3223
+ });
3224
+ })
3225
+ );
2138
3226
 
2139
3227
  if (this.options.ipc) {
2140
3228
  // chmod 666 (rw rw rw)
2141
3229
  const READ_WRITE = 438;
2142
3230
 
2143
- await fs.promises.chmod(this.options.ipc, READ_WRITE);
3231
+ await fs.promises.chmod(
3232
+ /** @type {string} */ (this.options.ipc),
3233
+ READ_WRITE
3234
+ );
2144
3235
  }
2145
3236
 
2146
3237
  if (this.options.webSocketServer) {
@@ -2158,11 +3249,29 @@ class Server {
2158
3249
  }
2159
3250
  }
2160
3251
 
2161
- startCallback(callback) {
2162
- this.start().then(() => callback(null), callback);
3252
+ /**
3253
+ * @param {(err?: Error) => void} [callback]
3254
+ */
3255
+ startCallback(callback = () => {}) {
3256
+ this.start()
3257
+ .then(() => callback(), callback)
3258
+ .catch(callback);
2163
3259
  }
2164
3260
 
3261
+ /**
3262
+ * @returns {Promise<void>}
3263
+ */
2165
3264
  async stop() {
3265
+ if (this.bonjour) {
3266
+ await /** @type {Promise<void>} */ (
3267
+ new Promise((resolve) => {
3268
+ this.stopBonjour(() => {
3269
+ resolve();
3270
+ });
3271
+ })
3272
+ );
3273
+ }
3274
+
2166
3275
  this.webSocketProxies = [];
2167
3276
 
2168
3277
  await Promise.all(this.staticWatchers.map((watcher) => watcher.close()));
@@ -2170,68 +3279,95 @@ class Server {
2170
3279
  this.staticWatchers = [];
2171
3280
 
2172
3281
  if (this.webSocketServer) {
2173
- await new Promise((resolve) => {
2174
- this.webSocketServer.implementation.close(() => {
2175
- this.webSocketServer = null;
3282
+ await /** @type {Promise<void>} */ (
3283
+ new Promise((resolve) => {
3284
+ /** @type {WebSocketServerImplementation} */
3285
+ (this.webSocketServer).implementation.close(() => {
3286
+ this.webSocketServer = null;
2176
3287
 
2177
- resolve();
2178
- });
3288
+ resolve();
3289
+ });
2179
3290
 
2180
- for (const client of this.webSocketServer.clients) {
2181
- client.terminate();
2182
- }
3291
+ for (const client of /** @type {WebSocketServerImplementation} */ (
3292
+ this.webSocketServer
3293
+ ).clients) {
3294
+ client.terminate();
3295
+ }
2183
3296
 
2184
- this.webSocketServer.clients = [];
2185
- });
3297
+ /** @type {WebSocketServerImplementation} */
3298
+ (this.webSocketServer).clients = [];
3299
+ })
3300
+ );
2186
3301
  }
2187
3302
 
2188
3303
  if (this.server) {
2189
- await new Promise((resolve) => {
2190
- this.server.close(() => {
2191
- this.server = null;
3304
+ await /** @type {Promise<void>} */ (
3305
+ new Promise((resolve) => {
3306
+ /** @type {import("http").Server} */
3307
+ (this.server).close(() => {
3308
+ this.server = null;
2192
3309
 
2193
- resolve();
2194
- });
3310
+ resolve();
3311
+ });
2195
3312
 
2196
- for (const socket of this.sockets) {
2197
- socket.destroy();
2198
- }
3313
+ for (const socket of this.sockets) {
3314
+ socket.destroy();
3315
+ }
2199
3316
 
2200
- this.sockets = [];
2201
- });
3317
+ this.sockets = [];
3318
+ })
3319
+ );
2202
3320
 
2203
3321
  if (this.middleware) {
2204
- await new Promise((resolve, reject) => {
2205
- this.middleware.close((error) => {
2206
- if (error) {
2207
- reject(error);
2208
-
2209
- return;
2210
- }
2211
-
2212
- resolve();
2213
- });
2214
- });
3322
+ await /** @type {Promise<void>} */ (
3323
+ new Promise((resolve, reject) => {
3324
+ /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
3325
+ (this.middleware).close((error) => {
3326
+ if (error) {
3327
+ reject(error);
3328
+
3329
+ return;
3330
+ }
3331
+
3332
+ resolve();
3333
+ });
3334
+ })
3335
+ );
2215
3336
 
2216
3337
  this.middleware = null;
2217
3338
  }
2218
3339
  }
3340
+
3341
+ // We add listeners to signals when creating a new Server instance
3342
+ // So ensure they are removed to prevent EventEmitter memory leak warnings
3343
+ for (const item of this.listeners) {
3344
+ process.removeListener(item.name, item.listener);
3345
+ }
2219
3346
  }
2220
3347
 
2221
- stopCallback(callback) {
2222
- this.stop().then(() => callback(null), callback);
3348
+ /**
3349
+ * @param {(err?: Error) => void} [callback]
3350
+ */
3351
+ stopCallback(callback = () => {}) {
3352
+ this.stop()
3353
+ .then(() => callback(), callback)
3354
+ .catch(callback);
2223
3355
  }
2224
3356
 
2225
3357
  // TODO remove in the next major release
3358
+ /**
3359
+ * @param {Port} port
3360
+ * @param {Host} hostname
3361
+ * @param {(err?: Error) => void} fn
3362
+ * @returns {void}
3363
+ */
2226
3364
  listen(port, hostname, fn) {
2227
3365
  util.deprecate(
2228
3366
  () => {},
2229
- "'listen' is deprecated. Please use async 'start' or 'startCallback' methods.",
3367
+ "'listen' is deprecated. Please use the async 'start' or 'startCallback' method.",
2230
3368
  "DEP_WEBPACK_DEV_SERVER_LISTEN"
2231
3369
  )();
2232
3370
 
2233
- this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
2234
-
2235
3371
  if (typeof port === "function") {
2236
3372
  fn = port;
2237
3373
  }
@@ -2268,7 +3404,7 @@ class Server {
2268
3404
  this.options.host = hostname;
2269
3405
  }
2270
3406
 
2271
- return this.start()
3407
+ this.start()
2272
3408
  .then(() => {
2273
3409
  if (fn) {
2274
3410
  fn.call(this.server);
@@ -2282,18 +3418,22 @@ class Server {
2282
3418
  });
2283
3419
  }
2284
3420
 
3421
+ /**
3422
+ * @param {(err?: Error) => void} [callback]
3423
+ * @returns {void}
3424
+ */
2285
3425
  // TODO remove in the next major release
2286
3426
  close(callback) {
2287
3427
  util.deprecate(
2288
3428
  () => {},
2289
- "'close' is deprecated. Please use async 'stop' or 'stopCallback' methods.",
3429
+ "'close' is deprecated. Please use the async 'stop' or 'stopCallback' method.",
2290
3430
  "DEP_WEBPACK_DEV_SERVER_CLOSE"
2291
3431
  )();
2292
3432
 
2293
- return this.stop()
3433
+ this.stop()
2294
3434
  .then(() => {
2295
3435
  if (callback) {
2296
- callback(null);
3436
+ callback();
2297
3437
  }
2298
3438
  })
2299
3439
  .catch((error) => {
@@ -2304,48 +3444,4 @@ class Server {
2304
3444
  }
2305
3445
  }
2306
3446
 
2307
- const mergeExports = (obj, exports) => {
2308
- const descriptors = Object.getOwnPropertyDescriptors(exports);
2309
-
2310
- for (const name of Object.keys(descriptors)) {
2311
- const descriptor = descriptors[name];
2312
-
2313
- if (descriptor.get) {
2314
- const fn = descriptor.get;
2315
-
2316
- Object.defineProperty(obj, name, {
2317
- configurable: false,
2318
- enumerable: true,
2319
- get: fn,
2320
- });
2321
- } else if (typeof descriptor.value === "object") {
2322
- Object.defineProperty(obj, name, {
2323
- configurable: false,
2324
- enumerable: true,
2325
- writable: false,
2326
- value: mergeExports({}, descriptor.value),
2327
- });
2328
- } else {
2329
- throw new Error(
2330
- "Exposed values must be either a getter or an nested object"
2331
- );
2332
- }
2333
- }
2334
-
2335
- return Object.freeze(obj);
2336
- };
2337
-
2338
- module.exports = mergeExports(Server, {
2339
- get schema() {
2340
- return schema;
2341
- },
2342
- // TODO compatibility with webpack v4, remove it after drop
2343
- cli: {
2344
- get getArguments() {
2345
- return () => require("../bin/cli-flags");
2346
- },
2347
- get processArguments() {
2348
- return require("../bin/process-arguments");
2349
- },
2350
- },
2351
- });
3447
+ module.exports = Server;