webpack-dev-server 4.4.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/Server.js CHANGED
@@ -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.server.type === "http" ? "ws:" : "wss:";
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,47 +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);
259
598
  }
260
599
 
261
- if (typeof this.options.client.reconnect !== "undefined") {
262
- searchParams.set("reconnect", this.options.client.reconnect);
600
+ if (typeof client.reconnect !== "undefined") {
601
+ searchParams.set(
602
+ "reconnect",
603
+ typeof client.reconnect === "number"
604
+ ? String(client.reconnect)
605
+ : "10"
606
+ );
263
607
  }
264
608
 
265
- webSocketURL = searchParams.toString();
609
+ webSocketURLStr = searchParams.toString();
266
610
  }
267
611
 
268
612
  additionalEntries.push(
269
- `${require.resolve("../client/index.js")}?${webSocketURL}`
613
+ `${require.resolve("../client/index.js")}?${webSocketURLStr}`
270
614
  );
271
615
  }
272
616
 
273
- if (this.options.hot) {
274
- let hotEntry;
275
-
276
- if (this.options.hot === "only") {
277
- hotEntry = require.resolve("webpack/hot/only-dev-server");
278
- } else if (this.options.hot) {
279
- hotEntry = require.resolve("webpack/hot/dev-server");
280
- }
281
-
282
- 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"));
283
621
  }
284
622
 
285
623
  const webpack = compiler.webpack || require("webpack");
@@ -297,9 +635,9 @@ class Server {
297
635
  else {
298
636
  /**
299
637
  * prependEntry Method for webpack 4
300
- * @param {Entry} originalEntry
301
- * @param {Entry} newAdditionalEntries
302
- * @returns {Entry}
638
+ * @param {any} originalEntry
639
+ * @param {any} newAdditionalEntries
640
+ * @returns {any}
303
641
  */
304
642
  const prependEntry = (originalEntry, newAdditionalEntries) => {
305
643
  if (typeof originalEntry === "function") {
@@ -328,7 +666,7 @@ class Server {
328
666
 
329
667
  // in this case, entry is a string or an array.
330
668
  // make sure that we do not add duplicates.
331
- /** @type {Entry} */
669
+ /** @type {any} */
332
670
  const entriesClone = additionalEntries.slice(0);
333
671
 
334
672
  [].concat(originalEntry).forEach((newEntry) => {
@@ -345,73 +683,201 @@ class Server {
345
683
  additionalEntries
346
684
  );
347
685
  compiler.hooks.entryOption.call(
348
- compiler.options.context,
686
+ /** @type {string} */ (compiler.options.context),
349
687
  compiler.options.entry
350
688
  );
351
689
  }
352
690
  }
353
691
 
692
+ /**
693
+ * @private
694
+ * @returns {Compiler["options"]}
695
+ */
354
696
  getCompilerOptions() {
355
- if (typeof this.compiler.compilers !== "undefined") {
356
- if (this.compiler.compilers.length === 1) {
357
- 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
+ );
358
706
  }
359
707
 
360
708
  // Configuration with the `devServer` options
361
- const compilerWithDevServer = this.compiler.compilers.find(
362
- (config) => config.options.devServer
363
- );
709
+ const compilerWithDevServer =
710
+ /** @type {MultiCompiler} */
711
+ (this.compiler).compilers.find((config) => config.options.devServer);
364
712
 
365
713
  if (compilerWithDevServer) {
366
714
  return compilerWithDevServer.options;
367
715
  }
368
716
 
369
717
  // Configuration with `web` preset
370
- const compilerWithWebPreset = this.compiler.compilers.find(
371
- (config) =>
372
- (config.options.externalsPresets &&
373
- config.options.externalsPresets.web) ||
374
- [
375
- "web",
376
- "webworker",
377
- "electron-preload",
378
- "electron-renderer",
379
- "node-webkit",
380
- // eslint-disable-next-line no-undefined
381
- undefined,
382
- null,
383
- ].includes(config.options.target)
384
- );
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
+ );
385
735
 
386
736
  if (compilerWithWebPreset) {
387
737
  return compilerWithWebPreset.options;
388
738
  }
389
739
 
390
740
  // Fallback
391
- return this.compiler.compilers[0].options;
741
+ return /** @type {MultiCompiler} */ (this.compiler).compilers[0].options;
392
742
  }
393
743
 
394
- return this.compiler.options;
744
+ return /** @type {Compiler} */ (this.compiler).options;
395
745
  }
396
746
 
397
- // eslint-disable-next-line class-methods-use-this
747
+ /**
748
+ * @private
749
+ * @returns {Promise<void>}
750
+ */
398
751
  async normalizeOptions() {
399
752
  const { options } = this;
400
-
401
- if (!this.logger) {
402
- this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
403
- }
404
-
405
753
  const compilerOptions = this.getCompilerOptions();
406
754
  // TODO remove `{}` after drop webpack v4 support
407
- const watchOptions = compilerOptions.watchOptions || {};
408
- const defaultOptionsForStatic = {
409
- directory: path.join(process.cwd(), "public"),
410
- staticOptions: {},
411
- publicPath: ["/"],
412
- serveIndex: { icons: true },
413
- // Respect options from compiler watchOptions
414
- 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;
415
881
  };
416
882
 
417
883
  if (typeof options.allowedHosts === "undefined") {
@@ -523,6 +989,24 @@ class Server {
523
989
  const isHTTPs = Boolean(options.https);
524
990
  const isSPDY = Boolean(options.http2);
525
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
+
526
1010
  options.server = {
527
1011
  type:
528
1012
  // eslint-disable-next-line no-nested-ternary
@@ -530,7 +1014,7 @@ class Server {
530
1014
  ? options.server
531
1015
  : // eslint-disable-next-line no-nested-ternary
532
1016
  typeof (options.server || {}).type === "string"
533
- ? options.server.type
1017
+ ? /** @type {ServerConfiguration} */ (options.server).type || "http"
534
1018
  : // eslint-disable-next-line no-nested-ternary
535
1019
  isSPDY
536
1020
  ? "spdy"
@@ -538,33 +1022,64 @@ class Server {
538
1022
  ? "https"
539
1023
  : "http",
540
1024
  options: {
541
- ...options.https,
542
- ...(options.server || {}).options,
1025
+ .../** @type {ServerOptions} */ (options.https),
1026
+ .../** @type {ServerConfiguration} */ (options.server || {}).options,
543
1027
  },
544
1028
  };
545
1029
 
546
1030
  if (
547
1031
  options.server.type === "spdy" &&
548
- typeof options.server.options.spdy === "undefined"
1032
+ typeof (/** @type {ServerOptions} */ (options.server.options).spdy) ===
1033
+ "undefined"
549
1034
  ) {
550
- options.server.options.spdy = {
1035
+ /** @type {ServerOptions} */
1036
+ (options.server.options).spdy = {
551
1037
  protocols: ["h2", "http/1.1"],
552
1038
  };
553
1039
  }
554
1040
 
555
1041
  if (options.server.type === "https" || options.server.type === "spdy") {
556
- if (typeof options.server.options.requestCert === "undefined") {
557
- options.server.options.requestCert = false;
1042
+ if (
1043
+ typeof (
1044
+ /** @type {ServerOptions} */ (options.server.options).requestCert
1045
+ ) === "undefined"
1046
+ ) {
1047
+ /** @type {ServerOptions} */
1048
+ (options.server.options).requestCert = false;
558
1049
  }
559
1050
 
560
- // TODO remove the `cacert` option in favor `ca` in the next major release
561
- for (const property of ["cacert", "ca", "cert", "crl", "key", "pfx"]) {
562
- if (typeof options.server.options[property] === "undefined") {
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
+ ) {
563
1061
  // eslint-disable-next-line no-continue
564
1062
  continue;
565
1063
  }
566
1064
 
567
- const value = options.server.options[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
+ */
568
1083
  const readFile = (item) => {
569
1084
  if (
570
1085
  Buffer.isBuffer(item) ||
@@ -587,14 +1102,18 @@ class Server {
587
1102
  }
588
1103
  };
589
1104
 
590
- options.server.options[property] = Array.isArray(value)
1105
+ /** @type {any} */
1106
+ (options.server.options)[property] = Array.isArray(value)
591
1107
  ? value.map((item) => readFile(item))
592
1108
  : readFile(value);
593
1109
  }
594
1110
 
595
1111
  let fakeCert;
596
1112
 
597
- if (!options.server.options.key || !options.server.options.cert) {
1113
+ if (
1114
+ !(/** @type {ServerOptions} */ (options.server.options).key) ||
1115
+ /** @type {ServerOptions} */ (!options.server.options).cert
1116
+ ) {
598
1117
  const certificateDir = Server.findCacheDir();
599
1118
  const certificatePath = path.join(certificateDir, "server.pem");
600
1119
  let certificateExists;
@@ -609,11 +1128,10 @@ class Server {
609
1128
  if (certificateExists) {
610
1129
  const certificateTtl = 1000 * 60 * 60 * 24;
611
1130
  const certificateStat = await fs.promises.stat(certificatePath);
612
-
613
- const now = new Date();
1131
+ const now = Number(new Date());
614
1132
 
615
1133
  // cert is more than 30 days old, kill it with fire
616
- if ((now - certificateStat.ctime) / certificateTtl > 30) {
1134
+ if ((now - Number(certificateStat.ctime)) / certificateTtl > 30) {
617
1135
  const del = require("del");
618
1136
 
619
1137
  this.logger.info(
@@ -629,6 +1147,7 @@ class Server {
629
1147
  if (!certificateExists) {
630
1148
  this.logger.info("Generating SSL certificate...");
631
1149
 
1150
+ // @ts-ignore
632
1151
  const selfsigned = require("selfsigned");
633
1152
  const attributes = [{ name: "commonName", value: "localhost" }];
634
1153
  const pems = selfsigned.generate(attributes, {
@@ -709,20 +1228,37 @@ class Server {
709
1228
  this.logger.info(`SSL certificate: ${certificatePath}`);
710
1229
  }
711
1230
 
712
- if (options.server.options.cacert) {
713
- if (options.server.options.ca) {
1231
+ if (
1232
+ /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */ (
1233
+ options.server.options
1234
+ ).cacert
1235
+ ) {
1236
+ if (/** @type {ServerOptions} */ (options.server.options).ca) {
714
1237
  this.logger.warn(
715
1238
  "Do not specify 'ca' and 'cacert' options together, the 'ca' option will be used."
716
1239
  );
717
1240
  } else {
718
- options.server.options.ca = options.server.options.cacert;
1241
+ /** @type {ServerOptions} */
1242
+ (options.server.options).ca =
1243
+ /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */
1244
+ (options.server.options).cacert;
719
1245
  }
720
1246
 
721
- delete options.server.options.cacert;
1247
+ delete (
1248
+ /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */ (
1249
+ options.server.options
1250
+ ).cacert
1251
+ );
722
1252
  }
723
1253
 
724
- options.server.options.key = options.server.options.key || fakeCert;
725
- options.server.options.cert = options.server.options.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;
726
1262
  }
727
1263
 
728
1264
  if (typeof options.ipc === "boolean") {
@@ -741,6 +1277,11 @@ class Server {
741
1277
 
742
1278
  // https://github.com/webpack/webpack-dev-server/issues/1990
743
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
744
1285
  const getOpenItemsFromObject = ({ target, ...rest }) => {
745
1286
  const normalizedOptions = { ...defaultOpenOptions, ...rest };
746
1287
 
@@ -762,14 +1303,25 @@ class Server {
762
1303
  };
763
1304
 
764
1305
  if (typeof options.open === "undefined") {
765
- options.open = [];
1306
+ /** @type {NormalizedOpen[]} */
1307
+ (options.open) = [];
766
1308
  } else if (typeof options.open === "boolean") {
767
- options.open = options.open
768
- ? [{ target: "<url>", options: defaultOpenOptions }]
1309
+ /** @type {NormalizedOpen[]} */
1310
+ (options.open) = options.open
1311
+ ? [
1312
+ {
1313
+ target: "<url>",
1314
+ options: /** @type {OpenOptions} */ (defaultOpenOptions),
1315
+ },
1316
+ ]
769
1317
  : [];
770
1318
  } else if (typeof options.open === "string") {
771
- options.open = [{ target: options.open, options: defaultOpenOptions }];
1319
+ /** @type {NormalizedOpen[]} */
1320
+ (options.open) = [{ target: options.open, options: defaultOpenOptions }];
772
1321
  } else if (Array.isArray(options.open)) {
1322
+ /**
1323
+ * @type {NormalizedOpen[]}
1324
+ */
773
1325
  const result = [];
774
1326
 
775
1327
  options.open.forEach((item) => {
@@ -782,9 +1334,29 @@ class Server {
782
1334
  result.push(...getOpenItemsFromObject(item));
783
1335
  });
784
1336
 
785
- options.open = result;
1337
+ /** @type {NormalizedOpen[]} */
1338
+ (options.open) = result;
786
1339
  } else {
787
- 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
+ )();
788
1360
  }
789
1361
 
790
1362
  if (typeof options.port === "string" && options.port !== "auto") {
@@ -808,61 +1380,91 @@ class Server {
808
1380
  Object.prototype.hasOwnProperty.call(options.proxy, "target") ||
809
1381
  Object.prototype.hasOwnProperty.call(options.proxy, "router")
810
1382
  ) {
811
- options.proxy = [options.proxy];
1383
+ /** @type {ProxyArray} */
1384
+ (options.proxy) = [/** @type {ProxyConfigMap} */ (options.proxy)];
812
1385
  } else {
813
- options.proxy = Object.keys(options.proxy).map((context) => {
814
- let proxyOptions;
815
- // For backwards compatibility reasons.
816
- const correctedContext = context
817
- .replace(/^\*$/, "**")
818
- .replace(/\/\*$/, "");
819
-
820
- if (typeof options.proxy[context] === "string") {
821
- proxyOptions = {
822
- context: correctedContext,
823
- target: options.proxy[context],
824
- };
825
- } else {
826
- proxyOptions = { ...options.proxy[context] };
827
- 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;
828
1419
  }
829
-
830
- return proxyOptions;
831
- });
1420
+ );
832
1421
  }
833
1422
  }
834
1423
 
835
- options.proxy = options.proxy.map((item) => {
836
- const getLogLevelForProxy = (level) => {
837
- if (level === "none") {
838
- return "silent";
839
- }
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
+ }
840
1460
 
841
- if (level === "log") {
842
- return "info";
843
- }
1461
+ if (typeof item.logProvider === "undefined") {
1462
+ item.logProvider = () => this.logger;
1463
+ }
844
1464
 
845
- if (level === "verbose") {
846
- return "debug";
1465
+ return item;
847
1466
  }
848
-
849
- return level;
850
- };
851
-
852
- if (typeof item.logLevel === "undefined") {
853
- item.logLevel = getLogLevelForProxy(
854
- compilerOptions.infrastructureLogging
855
- ? compilerOptions.infrastructureLogging.level
856
- : "info"
857
- );
858
- }
859
-
860
- if (typeof item.logProvider === "undefined") {
861
- item.logProvider = () => this.logger;
862
- }
863
-
864
- return item;
865
- });
1467
+ );
866
1468
  }
867
1469
 
868
1470
  if (typeof options.setupExitSignals === "undefined") {
@@ -870,50 +1472,27 @@ class Server {
870
1472
  }
871
1473
 
872
1474
  if (typeof options.static === "undefined") {
873
- options.static = [defaultOptionsForStatic];
1475
+ options.static = [getStaticItem()];
874
1476
  } else if (typeof options.static === "boolean") {
875
- options.static = options.static ? [defaultOptionsForStatic] : false;
1477
+ options.static = options.static ? [getStaticItem()] : false;
876
1478
  } else if (typeof options.static === "string") {
877
- options.static = [
878
- { ...defaultOptionsForStatic, directory: options.static },
879
- ];
1479
+ options.static = [getStaticItem(options.static)];
880
1480
  } else if (Array.isArray(options.static)) {
881
1481
  options.static = options.static.map((item) => {
882
1482
  if (typeof item === "string") {
883
- return { ...defaultOptionsForStatic, directory: item };
1483
+ return getStaticItem(item);
884
1484
  }
885
1485
 
886
- return { ...defaultOptionsForStatic, ...item };
1486
+ return getStaticItem(item);
887
1487
  });
888
1488
  } else {
889
- options.static = [{ ...defaultOptionsForStatic, ...options.static }];
890
- }
891
-
892
- if (options.static) {
893
- options.static.forEach((staticOption) => {
894
- if (Server.isAbsoluteURL(staticOption.directory)) {
895
- throw new Error("Using a URL as static.directory is not supported");
896
- }
897
-
898
- // ensure that publicPath is an array
899
- if (typeof staticOption.publicPath === "string") {
900
- staticOption.publicPath = [staticOption.publicPath];
901
- }
902
-
903
- // ensure that watch is an object if true
904
- if (staticOption.watch === true) {
905
- staticOption.watch = defaultOptionsForStatic.watch;
906
- }
907
-
908
- // ensure that serveIndex is an object if true
909
- if (staticOption.serveIndex === true) {
910
- staticOption.serveIndex = defaultOptionsForStatic.serveIndex;
911
- }
912
- });
1489
+ options.static = [getStaticItem(options.static)];
913
1490
  }
914
1491
 
915
1492
  if (typeof options.watchFiles === "string") {
916
- options.watchFiles = [{ paths: options.watchFiles, options: {} }];
1493
+ options.watchFiles = [
1494
+ { paths: options.watchFiles, options: getWatchOptions() },
1495
+ ];
917
1496
  } else if (
918
1497
  typeof options.watchFiles === "object" &&
919
1498
  options.watchFiles !== null &&
@@ -922,16 +1501,19 @@ class Server {
922
1501
  options.watchFiles = [
923
1502
  {
924
1503
  paths: options.watchFiles.paths,
925
- options: options.watchFiles.options || {},
1504
+ options: getWatchOptions(options.watchFiles.options || {}),
926
1505
  },
927
1506
  ];
928
1507
  } else if (Array.isArray(options.watchFiles)) {
929
1508
  options.watchFiles = options.watchFiles.map((item) => {
930
1509
  if (typeof item === "string") {
931
- return { paths: item, options: {} };
1510
+ return { paths: item, options: getWatchOptions() };
932
1511
  }
933
1512
 
934
- return { paths: item.paths, options: item.options || {} };
1513
+ return {
1514
+ paths: item.paths,
1515
+ options: getWatchOptions(item.options || {}),
1516
+ };
935
1517
  });
936
1518
  } else {
937
1519
  options.watchFiles = [];
@@ -960,38 +1542,61 @@ class Server {
960
1542
  };
961
1543
  } else {
962
1544
  options.webSocketServer = {
963
- type: options.webSocketServer.type || defaultWebSocketServerType,
1545
+ type:
1546
+ /** @type {WebSocketServerConfiguration} */
1547
+ (options.webSocketServer).type || defaultWebSocketServerType,
964
1548
  options: {
965
1549
  ...defaultWebSocketServerOptions,
966
- ...options.webSocketServer.options,
1550
+ .../** @type {WebSocketServerConfiguration} */
1551
+ (options.webSocketServer).options,
967
1552
  },
968
1553
  };
969
1554
 
970
- if (typeof options.webSocketServer.options.port === "string") {
971
- options.webSocketServer.options.port = Number(
972
- options.webSocketServer.options.port
973
- );
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);
974
1561
  }
975
1562
  }
976
1563
  }
977
1564
 
1565
+ /**
1566
+ * @private
1567
+ * @returns {string}
1568
+ */
978
1569
  getClientTransport() {
979
- let ClientImplementation;
1570
+ let clientImplementation;
980
1571
  let clientImplementationFound = true;
981
1572
 
982
1573
  const isKnownWebSocketServerImplementation =
983
1574
  this.options.webSocketServer &&
984
- typeof this.options.webSocketServer.type === "string" &&
1575
+ typeof (
1576
+ /** @type {WebSocketServerConfiguration} */
1577
+ (this.options.webSocketServer).type
1578
+ ) === "string" &&
1579
+ // @ts-ignore
985
1580
  (this.options.webSocketServer.type === "ws" ||
986
- this.options.webSocketServer.type === "sockjs");
1581
+ /** @type {WebSocketServerConfiguration} */
1582
+ (this.options.webSocketServer).type === "sockjs");
987
1583
 
988
1584
  let clientTransport;
989
1585
 
990
1586
  if (this.options.client) {
991
- if (typeof this.options.client.webSocketTransport !== "undefined") {
992
- 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;
993
1596
  } else if (isKnownWebSocketServerImplementation) {
994
- clientTransport = this.options.webSocketServer.type;
1597
+ clientTransport =
1598
+ /** @type {WebSocketServerConfiguration} */
1599
+ (this.options.webSocketServer).type;
995
1600
  } else {
996
1601
  clientTransport = "ws";
997
1602
  }
@@ -1003,17 +1608,16 @@ class Server {
1003
1608
  case "string":
1004
1609
  // could be 'sockjs', 'ws', or a path that should be required
1005
1610
  if (clientTransport === "sockjs") {
1006
- ClientImplementation = require.resolve(
1611
+ clientImplementation = require.resolve(
1007
1612
  "../client/clients/SockJSClient"
1008
1613
  );
1009
1614
  } else if (clientTransport === "ws") {
1010
- ClientImplementation = require.resolve(
1615
+ clientImplementation = require.resolve(
1011
1616
  "../client/clients/WebSocketClient"
1012
1617
  );
1013
1618
  } else {
1014
1619
  try {
1015
- // eslint-disable-next-line import/no-dynamic-require
1016
- ClientImplementation = require.resolve(clientTransport);
1620
+ clientImplementation = require.resolve(clientTransport);
1017
1621
  } catch (e) {
1018
1622
  clientImplementationFound = false;
1019
1623
  }
@@ -1033,31 +1637,52 @@ class Server {
1033
1637
  );
1034
1638
  }
1035
1639
 
1036
- return ClientImplementation;
1640
+ return /** @type {string} */ (clientImplementation);
1037
1641
  }
1038
1642
 
1643
+ /**
1644
+ * @private
1645
+ * @returns {string}
1646
+ */
1039
1647
  getServerTransport() {
1040
1648
  let implementation;
1041
1649
  let implementationFound = true;
1042
1650
 
1043
- switch (typeof this.options.webSocketServer.type) {
1651
+ switch (
1652
+ typeof (
1653
+ /** @type {WebSocketServerConfiguration} */
1654
+ (this.options.webSocketServer).type
1655
+ )
1656
+ ) {
1044
1657
  case "string":
1045
1658
  // Could be 'sockjs', in the future 'ws', or a path that should be required
1046
- if (this.options.webSocketServer.type === "sockjs") {
1659
+ if (
1660
+ /** @type {WebSocketServerConfiguration} */ (
1661
+ this.options.webSocketServer
1662
+ ).type === "sockjs"
1663
+ ) {
1047
1664
  implementation = require("./servers/SockJSServer");
1048
- } else if (this.options.webSocketServer.type === "ws") {
1665
+ } else if (
1666
+ /** @type {WebSocketServerConfiguration} */ (
1667
+ this.options.webSocketServer
1668
+ ).type === "ws"
1669
+ ) {
1049
1670
  implementation = require("./servers/WebsocketServer");
1050
1671
  } else {
1051
1672
  try {
1052
1673
  // eslint-disable-next-line import/no-dynamic-require
1053
- implementation = require(this.options.webSocketServer.type);
1674
+ implementation = require(/** @type {WebSocketServerConfiguration} */ (
1675
+ this.options.webSocketServer
1676
+ ).type);
1054
1677
  } catch (error) {
1055
1678
  implementationFound = false;
1056
1679
  }
1057
1680
  }
1058
1681
  break;
1059
1682
  case "function":
1060
- implementation = this.options.webSocketServer.type;
1683
+ implementation = /** @type {WebSocketServerConfiguration} */ (
1684
+ this.options.webSocketServer
1685
+ ).type;
1061
1686
  break;
1062
1687
  default:
1063
1688
  implementationFound = false;
@@ -1074,39 +1699,62 @@ class Server {
1074
1699
  return implementation;
1075
1700
  }
1076
1701
 
1702
+ /**
1703
+ * @private
1704
+ * @returns {void}
1705
+ */
1077
1706
  setupProgressPlugin() {
1078
- const { ProgressPlugin } = this.compiler.webpack || require("webpack");
1079
-
1080
- new ProgressPlugin((percent, msg, addInfo, pluginName) => {
1081
- 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);
1082
1724
 
1083
- if (percent === 100) {
1084
- msg = "Compilation completed";
1085
- }
1725
+ if (percent === 100) {
1726
+ msg = "Compilation completed";
1727
+ }
1086
1728
 
1087
- if (addInfo) {
1088
- msg = `${msg} (${addInfo})`;
1089
- }
1729
+ if (addInfo) {
1730
+ msg = `${msg} (${addInfo})`;
1731
+ }
1090
1732
 
1091
- if (this.webSocketServer) {
1092
- this.sendMessage(this.webSocketServer.clients, "progress-update", {
1093
- percent,
1094
- msg,
1095
- pluginName,
1096
- });
1097
- }
1733
+ if (this.webSocketServer) {
1734
+ this.sendMessage(this.webSocketServer.clients, "progress-update", {
1735
+ percent,
1736
+ msg,
1737
+ pluginName,
1738
+ });
1739
+ }
1098
1740
 
1099
- if (this.server) {
1100
- this.server.emit("progress-update", { percent, msg, pluginName });
1741
+ if (this.server) {
1742
+ this.server.emit("progress-update", { percent, msg, pluginName });
1743
+ }
1101
1744
  }
1102
- }).apply(this.compiler);
1745
+ ).apply(this.compiler);
1103
1746
  }
1104
1747
 
1748
+ /**
1749
+ * @private
1750
+ * @returns {Promise<void>}
1751
+ */
1105
1752
  async initialize() {
1106
1753
  if (this.options.webSocketServer) {
1107
- const compilers = this.compiler.compilers || [this.compiler];
1754
+ const compilers =
1755
+ /** @type {MultiCompiler} */
1756
+ (this.compiler).compilers || [this.compiler];
1108
1757
 
1109
- // eslint-disable-next-line no-shadow
1110
1758
  compilers.forEach((compiler) => {
1111
1759
  this.addAdditionalEntries(compiler);
1112
1760
 
@@ -1137,7 +1785,10 @@ class Server {
1137
1785
  }
1138
1786
  });
1139
1787
 
1140
- if (this.options.client && this.options.client.progress) {
1788
+ if (
1789
+ this.options.client &&
1790
+ /** @type {ClientConfiguration} */ (this.options.client).progress
1791
+ ) {
1141
1792
  this.setupProgressPlugin();
1142
1793
  }
1143
1794
  }
@@ -1149,7 +1800,8 @@ class Server {
1149
1800
  // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
1150
1801
  this.setupBuiltInRoutes();
1151
1802
  this.setupWatchFiles();
1152
- this.setupFeatures();
1803
+ this.setupWatchStaticFiles();
1804
+ this.setupMiddlewares();
1153
1805
  this.createServer();
1154
1806
 
1155
1807
  if (this.options.setupExitSignals) {
@@ -1157,15 +1809,10 @@ class Server {
1157
1809
 
1158
1810
  let needForceShutdown = false;
1159
1811
 
1160
- const exitProcess = () => {
1161
- // eslint-disable-next-line no-process-exit
1162
- process.exit();
1163
- };
1164
-
1165
1812
  signals.forEach((signal) => {
1166
- process.on(signal, () => {
1813
+ const listener = () => {
1167
1814
  if (needForceShutdown) {
1168
- exitProcess();
1815
+ process.exit();
1169
1816
  }
1170
1817
 
1171
1818
  this.logger.info(
@@ -1176,65 +1823,125 @@ class Server {
1176
1823
 
1177
1824
  this.stopCallback(() => {
1178
1825
  if (typeof this.compiler.close === "function") {
1179
- this.compiler.close(exitProcess);
1826
+ this.compiler.close(() => {
1827
+ process.exit();
1828
+ });
1180
1829
  } else {
1181
- exitProcess();
1830
+ process.exit();
1182
1831
  }
1183
1832
  });
1184
- });
1833
+ };
1834
+
1835
+ this.listeners.push({ name: signal, listener });
1836
+
1837
+ process.on(signal, listener);
1185
1838
  });
1186
1839
  }
1187
1840
 
1188
1841
  // Proxy WebSocket without the initial http request
1189
1842
  // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
1190
- // eslint-disable-next-line func-names
1191
- this.webSocketProxies.forEach(function (webSocketProxy) {
1192
- 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
+ );
1193
1851
  }, this);
1194
1852
  }
1195
1853
 
1854
+ /**
1855
+ * @private
1856
+ * @returns {void}
1857
+ */
1196
1858
  setupApp() {
1197
- // Init express server
1859
+ /** @type {import("express").Application | undefined}*/
1198
1860
  // eslint-disable-next-line new-cap
1199
- this.app = new express();
1861
+ this.app = new /** @type {any} */ (express)();
1200
1862
  }
1201
1863
 
1864
+ /**
1865
+ * @private
1866
+ * @param {Stats | MultiStats} statsObj
1867
+ * @returns {StatsCompilation}
1868
+ */
1202
1869
  getStats(statsObj) {
1203
1870
  const stats = Server.DEFAULT_STATS;
1204
1871
  const compilerOptions = this.getCompilerOptions();
1205
1872
 
1873
+ // @ts-ignore
1206
1874
  if (compilerOptions.stats && compilerOptions.stats.warningsFilter) {
1875
+ // @ts-ignore
1207
1876
  stats.warningsFilter = compilerOptions.stats.warningsFilter;
1208
1877
  }
1209
1878
 
1210
1879
  return statsObj.toJson(stats);
1211
1880
  }
1212
1881
 
1882
+ /**
1883
+ * @private
1884
+ * @returns {void}
1885
+ */
1213
1886
  setupHooks() {
1214
1887
  this.compiler.hooks.invalid.tap("webpack-dev-server", () => {
1215
1888
  if (this.webSocketServer) {
1216
1889
  this.sendMessage(this.webSocketServer.clients, "invalid");
1217
1890
  }
1218
1891
  });
1219
- this.compiler.hooks.done.tap("webpack-dev-server", (stats) => {
1220
- if (this.webSocketServer) {
1221
- this.sendStats(this.webSocketServer.clients, this.getStats(stats));
1222
- }
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
+ }
1223
1901
 
1224
- this.stats = stats;
1225
- });
1902
+ /**
1903
+ * @private
1904
+ * @type {Stats | MultiStats}
1905
+ */
1906
+ this.stats = stats;
1907
+ }
1908
+ );
1226
1909
  }
1227
1910
 
1911
+ /**
1912
+ * @private
1913
+ * @returns {void}
1914
+ */
1228
1915
  setupHostHeaderCheck() {
1229
- this.app.all("*", (req, res, next) => {
1230
- if (this.checkHeader(req.headers, "host")) {
1231
- return next();
1232
- }
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
+ }
1233
1935
 
1234
- res.send("Invalid Host header");
1235
- });
1936
+ res.send("Invalid Host header");
1937
+ }
1938
+ );
1236
1939
  }
1237
1940
 
1941
+ /**
1942
+ * @private
1943
+ * @returns {void}
1944
+ */
1238
1945
  setupDevMiddleware() {
1239
1946
  const webpackDevMiddleware = require("webpack-dev-middleware");
1240
1947
 
@@ -1245,432 +1952,616 @@ class Server {
1245
1952
  );
1246
1953
  }
1247
1954
 
1955
+ /**
1956
+ * @private
1957
+ * @returns {void}
1958
+ */
1248
1959
  setupBuiltInRoutes() {
1249
1960
  const { app, middleware } = this;
1250
1961
 
1251
- app.get("/__webpack_dev_server__/sockjs.bundle.js", (req, res) => {
1252
- res.setHeader("Content-Type", "application/javascript");
1253
-
1254
- const { createReadStream } = fs;
1255
- const clientPath = path.join(__dirname, "..", "client");
1256
-
1257
- createReadStream(
1258
- path.join(clientPath, "modules/sockjs-client/index.js")
1259
- ).pipe(res);
1260
- });
1261
-
1262
- app.get("/webpack-dev-server/invalidate", (_req, res) => {
1263
- this.invalidate();
1264
-
1265
- res.end();
1266
- });
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");
1267
1972
 
1268
- app.get("/webpack-dev-server", (req, res) => {
1269
- middleware.waitUntilValid((stats) => {
1270
- res.setHeader("Content-Type", "text/html");
1271
- res.write(
1272
- '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
1273
- );
1973
+ const { createReadStream } = fs;
1974
+ const clientPath = path.join(__dirname, "..", "client");
1274
1975
 
1275
- const statsForPrint =
1276
- typeof stats.stats !== "undefined"
1277
- ? stats.toJson().children
1278
- : [stats.toJson()];
1976
+ createReadStream(
1977
+ path.join(clientPath, "modules/sockjs-client/index.js")
1978
+ ).pipe(res);
1979
+ }
1980
+ );
1279
1981
 
1280
- 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();
1281
1992
 
1282
- statsForPrint.forEach((item, index) => {
1283
- res.write("<div>");
1993
+ res.end();
1994
+ }
1995
+ );
1284
1996
 
1285
- const name =
1286
- 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
+ );
1287
2012
 
1288
- res.write(`<h2>Compilation: ${name}</h2>`);
1289
- res.write("<ul>");
2013
+ const statsForPrint =
2014
+ typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined"
2015
+ ? /** @type {MultiStats} */ (stats).toJson().children
2016
+ : [/** @type {Stats} */ (stats).toJson()];
1290
2017
 
1291
- const publicPath = item.publicPath === "auto" ? "" : item.publicPath;
2018
+ res.write(`<h1>Assets Report:</h1>`);
1292
2019
 
1293
- for (const asset of item.assets) {
1294
- const assetName = asset.name;
1295
- const assetURL = `${publicPath}${assetName}`;
2020
+ /**
2021
+ * @type {StatsCompilation[]}
2022
+ */
2023
+ (statsForPrint).forEach((item, index) => {
2024
+ res.write("<div>");
1296
2025
 
1297
- res.write(
1298
- `<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>
1299
2048
  <strong><a href="${assetURL}" target="_blank">${assetName}</a></strong>
1300
2049
  </li>`
1301
- );
1302
- }
2050
+ );
2051
+ }
2052
+
2053
+ res.write("</ul>");
2054
+ res.write("</div>");
2055
+ });
1303
2056
 
1304
- res.write("</ul>");
1305
- res.write("</div>");
2057
+ res.end("</body></html>");
1306
2058
  });
2059
+ }
2060
+ );
2061
+ }
1307
2062
 
1308
- 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
+ }
1309
2074
  });
1310
- });
2075
+ }
1311
2076
  }
1312
2077
 
1313
- setupCompressFeature() {
1314
- const compress = require("compression");
2078
+ /**
2079
+ * @private
2080
+ * @returns {void}
2081
+ */
2082
+ setupWatchFiles() {
2083
+ const { watchFiles } = this.options;
1315
2084
 
1316
- this.app.use(compress());
1317
- }
1318
-
1319
- setupProxyFeature() {
1320
- const { createProxyMiddleware } = require("http-proxy-middleware");
1321
-
1322
- const getProxyMiddleware = (proxyConfig) => {
1323
- // It is possible to use the `bypass` method without a `target` or `router`.
1324
- // However, the proxy middleware has no use in this case, and will fail to instantiate.
1325
- if (proxyConfig.target) {
1326
- const context = proxyConfig.context || proxyConfig.path;
1327
-
1328
- return createProxyMiddleware(context, proxyConfig);
1329
- }
2085
+ if (/** @type {WatchFiles[]} */ (watchFiles).length > 0) {
2086
+ /** @type {WatchFiles[]} */
2087
+ (watchFiles).forEach((item) => {
2088
+ this.watchFiles(item.paths, item.options);
2089
+ });
2090
+ }
2091
+ }
1330
2092
 
1331
- if (proxyConfig.router) {
1332
- return createProxyMiddleware(proxyConfig);
1333
- }
1334
- };
2093
+ /**
2094
+ * @private
2095
+ * @returns {void}
2096
+ */
2097
+ setupMiddlewares() {
1335
2098
  /**
1336
- * Assume a proxy configuration specified as:
1337
- * proxy: [
1338
- * {
1339
- * context: "value",
1340
- * ...options,
1341
- * },
1342
- * // or:
1343
- * function() {
1344
- * return {
1345
- * context: "context",
1346
- * ...options,
1347
- * };
1348
- * }
1349
- * ]
2099
+ * @type {Array<Middleware>}
1350
2100
  */
1351
- this.options.proxy.forEach((proxyConfigOrCallback) => {
1352
- let proxyMiddleware;
1353
-
1354
- let proxyConfig =
1355
- typeof proxyConfigOrCallback === "function"
1356
- ? proxyConfigOrCallback()
1357
- : proxyConfigOrCallback;
1358
-
1359
- proxyMiddleware = getProxyMiddleware(proxyConfig);
2101
+ let middlewares = [];
1360
2102
 
1361
- if (proxyConfig.ws) {
1362
- this.webSocketProxies.push(proxyMiddleware);
1363
- }
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");
1364
2106
 
1365
- const handle = async (req, res, next) => {
1366
- if (typeof proxyConfigOrCallback === "function") {
1367
- const newProxyConfig = proxyConfigOrCallback(req, res, next);
2107
+ middlewares.push({ name: "compression", middleware: compression() });
2108
+ }
1368
2109
 
1369
- if (newProxyConfig !== proxyConfig) {
1370
- proxyConfig = newProxyConfig;
1371
- proxyMiddleware = getProxyMiddleware(proxyConfig);
1372
- }
1373
- }
2110
+ if (typeof this.options.onBeforeSetupMiddleware === "function") {
2111
+ this.options.onBeforeSetupMiddleware(this);
2112
+ }
1374
2113
 
1375
- // - Check if we have a bypass function defined
1376
- // - In case the bypass function is defined we'll retrieve the
1377
- // bypassUrl from it otherwise bypassUrl would be null
1378
- // TODO remove in the next major in favor `context` and `router` options
1379
- const isByPassFuncDefined = typeof proxyConfig.bypass === "function";
1380
- const bypassUrl = isByPassFuncDefined
1381
- ? await proxyConfig.bypass(req, res, proxyConfig)
1382
- : null;
1383
-
1384
- if (typeof bypassUrl === "boolean") {
1385
- // skip the proxy
1386
- req.url = null;
1387
- next();
1388
- } else if (typeof bypassUrl === "string") {
1389
- // byPass to that url
1390
- req.url = bypassUrl;
1391
- next();
1392
- } else if (proxyMiddleware) {
1393
- return proxyMiddleware(req, res, next);
1394
- } else {
1395
- next();
1396
- }
1397
- };
2114
+ if (typeof this.options.headers !== "undefined") {
2115
+ middlewares.push({
2116
+ name: "set-headers",
2117
+ path: "*",
2118
+ middleware: this.setHeaders.bind(this),
2119
+ });
2120
+ }
1398
2121
 
1399
- this.app.use(handle);
1400
- // Also forward error requests to the proxy so it can handle them.
1401
- 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),
1402
2127
  });
1403
- }
1404
2128
 
1405
- setupHistoryApiFallbackFeature() {
1406
- const { historyApiFallback } = this.options;
2129
+ if (this.options.proxy) {
2130
+ const { createProxyMiddleware } = require("http-proxy-middleware");
1407
2131
 
1408
- if (
1409
- typeof historyApiFallback.logger === "undefined" &&
1410
- !historyApiFallback.verbose
1411
- ) {
1412
- historyApiFallback.logger = this.logger.log.bind(
1413
- this.logger,
1414
- "[connect-history-api-fallback]"
1415
- );
1416
- }
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
+ }
1417
2147
 
1418
- // Fall back to /index.html if nothing else matches.
1419
- this.app.use(require("connect-history-api-fallback")(historyApiFallback));
1420
- }
2148
+ if (proxyConfig.router) {
2149
+ return createProxyMiddleware(proxyConfig);
2150
+ }
2151
+ };
1421
2152
 
1422
- setupStaticFeature() {
1423
- this.options.static.forEach((staticOption) => {
1424
- staticOption.publicPath.forEach((publicPath) => {
1425
- this.app.use(
1426
- publicPath,
1427
- express.static(staticOption.directory, staticOption.staticOptions)
1428
- );
1429
- });
1430
- });
1431
- }
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
+ }
1432
2192
 
1433
- setupStaticServeIndexFeature() {
1434
- 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
+ }
1435
2210
 
1436
- this.options.static.forEach((staticOption) => {
1437
- staticOption.publicPath.forEach((publicPath) => {
1438
- if (staticOption.serveIndex) {
1439
- this.app.use(publicPath, (req, res, next) => {
1440
- // serve-index doesn't fallthrough non-get/head request to next middleware
1441
- if (req.method !== "GET" && req.method !== "HEAD") {
1442
- 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();
1443
2234
  }
2235
+ };
1444
2236
 
1445
- serveIndex(staticOption.directory, staticOption.serveIndex)(
1446
- req,
1447
- res,
1448
- next
1449
- );
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),
1450
2253
  });
1451
2254
  }
1452
- });
1453
- });
1454
- }
1455
-
1456
- setupStaticWatchFeature() {
1457
- this.options.static.forEach((staticOption) => {
1458
- if (staticOption.watch) {
1459
- this.watchFiles(staticOption.directory, staticOption.watch);
1460
- }
1461
- });
1462
- }
1463
-
1464
- setupOnBeforeSetupMiddlewareFeature() {
1465
- this.options.onBeforeSetupMiddleware(this);
1466
- }
1467
-
1468
- setupWatchFiles() {
1469
- const { watchFiles } = this.options;
2255
+ );
1470
2256
 
1471
- if (watchFiles.length > 0) {
1472
- watchFiles.forEach((item) => {
1473
- 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),
1474
2262
  });
1475
2263
  }
1476
- }
1477
2264
 
1478
- setupMiddleware() {
1479
- this.app.use(this.middleware);
1480
- }
1481
-
1482
- setupOnAfterSetupMiddlewareFeature() {
1483
- this.options.onAfterSetupMiddleware(this);
1484
- }
1485
-
1486
- setupHeadersFeature() {
1487
- this.app.all("*", this.setHeaders.bind(this));
1488
- }
1489
-
1490
- setupMagicHtmlFeature() {
1491
- this.app.get("*", this.serveMagicHtml.bind(this));
1492
- }
1493
-
1494
- setupFeatures() {
1495
- const features = {
1496
- compress: () => {
1497
- if (this.options.compress) {
1498
- this.setupCompressFeature();
1499
- }
1500
- },
1501
- proxy: () => {
1502
- if (this.options.proxy) {
1503
- this.setupProxyFeature();
1504
- }
1505
- },
1506
- historyApiFallback: () => {
1507
- if (this.options.historyApiFallback) {
1508
- this.setupHistoryApiFallbackFeature();
1509
- }
1510
- },
1511
- static: () => {
1512
- this.setupStaticFeature();
1513
- },
1514
- staticServeIndex: () => {
1515
- this.setupStaticServeIndexFeature();
1516
- },
1517
- staticWatch: () => {
1518
- this.setupStaticWatchFeature();
1519
- },
1520
- onBeforeSetupMiddleware: () => {
1521
- if (typeof this.options.onBeforeSetupMiddleware === "function") {
1522
- this.setupOnBeforeSetupMiddlewareFeature();
1523
- }
1524
- },
1525
- onAfterSetupMiddleware: () => {
1526
- if (typeof this.options.onAfterSetupMiddleware === "function") {
1527
- this.setupOnAfterSetupMiddlewareFeature();
1528
- }
1529
- },
1530
- middleware: () => {
1531
- // include our middleware to ensure
1532
- // it is able to handle '/index.html' request after redirect
1533
- this.setupMiddleware();
1534
- },
1535
- headers: () => {
1536
- this.setupHeadersFeature();
1537
- },
1538
- magicHtml: () => {
1539
- this.setupMagicHtmlFeature();
1540
- },
1541
- };
1542
-
1543
- const runnableFeatures = [];
1544
-
1545
- // compress is placed last and uses unshift so that it will be the first middleware used
1546
- if (this.options.compress) {
1547
- runnableFeatures.push("compress");
1548
- }
1549
-
1550
- if (this.options.onBeforeSetupMiddleware) {
1551
- 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
+ });
1552
2279
  }
1553
2280
 
1554
- runnableFeatures.push("headers", "middleware");
2281
+ if (this.options.historyApiFallback) {
2282
+ const connectHistoryApiFallback = require("connect-history-api-fallback");
2283
+ const { historyApiFallback } = this.options;
1555
2284
 
1556
- if (this.options.proxy) {
1557
- runnableFeatures.push("proxy", "middleware");
1558
- }
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
+ }
1559
2301
 
1560
- if (this.options.static) {
1561
- runnableFeatures.push("static");
1562
- }
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
+ });
1563
2310
 
1564
- if (this.options.historyApiFallback) {
1565
- 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
+ });
1566
2319
 
1567
- if (this.options.static) {
1568
- 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
+ });
1569
2334
  }
1570
2335
  }
1571
2336
 
1572
- if (this.options.static) {
1573
- 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
+ });
1574
2369
  }
1575
2370
 
1576
2371
  if (this.options.magicHtml) {
1577
- runnableFeatures.push("magicHtml");
2372
+ middlewares.push({
2373
+ name: "serve-magic-html",
2374
+ middleware: this.serveMagicHtml.bind(this),
2375
+ });
1578
2376
  }
1579
2377
 
1580
- if (this.options.onAfterSetupMiddleware) {
1581
- runnableFeatures.push("onAfterSetupMiddleware");
2378
+ if (typeof this.options.onAfterSetupMiddleware === "function") {
2379
+ this.options.onAfterSetupMiddleware(this);
1582
2380
  }
1583
2381
 
1584
- runnableFeatures.forEach((feature) => {
1585
- features[feature]();
2382
+ if (typeof this.options.setupMiddlewares === "function") {
2383
+ middlewares = this.options.setupMiddlewares(middlewares, this);
2384
+ }
2385
+
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
+ }
1586
2397
  });
1587
2398
  }
1588
2399
 
2400
+ /**
2401
+ * @private
2402
+ * @returns {void}
2403
+ */
1589
2404
  createServer() {
2405
+ const { type, options } = /** @type {ServerConfiguration} */ (
2406
+ this.options.server
2407
+ );
2408
+
2409
+ /** @type {import("http").Server | undefined | null} */
1590
2410
  // eslint-disable-next-line import/no-dynamic-require
1591
- this.server = require(this.options.server.type).createServer(
1592
- this.options.server.options,
2411
+ this.server = require(/** @type {string} */ (type)).createServer(
2412
+ options,
1593
2413
  this.app
1594
2414
  );
1595
2415
 
1596
- this.server.on("connection", (socket) => {
1597
- // Add socket to list
1598
- 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);
1599
2425
 
1600
- socket.once("close", () => {
1601
- // Remove socket from list
1602
- this.sockets.splice(this.sockets.indexOf(socket), 1);
1603
- });
1604
- });
2426
+ socket.once("close", () => {
2427
+ // Remove socket from list
2428
+ this.sockets.splice(this.sockets.indexOf(socket), 1);
2429
+ });
2430
+ }
2431
+ );
1605
2432
 
1606
- this.server.on("error", (error) => {
1607
- throw error;
1608
- });
2433
+ /** @type {import("http").Server} */
2434
+ (this.server).on(
2435
+ "error",
2436
+ /**
2437
+ * @param {Error} error
2438
+ */
2439
+ (error) => {
2440
+ throw error;
2441
+ }
2442
+ );
1609
2443
  }
1610
2444
 
2445
+ /**
2446
+ * @private
2447
+ * @returns {void}
2448
+ */
2449
+ // TODO: remove `--web-socket-server` in favor of `--web-socket-server-type`
1611
2450
  createWebSocketServer() {
1612
- this.webSocketServer = new (this.getServerTransport())(this);
1613
- this.webSocketServer.implementation.on("connection", (client, request) => {
1614
- const headers =
1615
- // eslint-disable-next-line no-nested-ternary
1616
- typeof request !== "undefined"
1617
- ? request.headers
1618
- : typeof client.headers !== "undefined"
1619
- ? client.headers
1620
- : // eslint-disable-next-line no-undefined
1621
- undefined;
1622
-
1623
- if (!headers) {
1624
- this.logger.warn(
1625
- 'webSocketServer implementation must pass headers for the "connection" event'
1626
- );
1627
- }
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
+ }
1628
2481
 
1629
- if (
1630
- !headers ||
1631
- !this.checkHeader(headers, "host") ||
1632
- !this.checkHeader(headers, "origin")
1633
- ) {
1634
- 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");
1635
2488
 
1636
- 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();
1637
2492
 
1638
- return;
1639
- }
2493
+ return;
2494
+ }
1640
2495
 
1641
- if (this.options.hot === true || this.options.hot === "only") {
1642
- this.sendMessage([client], "hot");
1643
- }
2496
+ if (this.options.hot === true || this.options.hot === "only") {
2497
+ this.sendMessage([client], "hot");
2498
+ }
1644
2499
 
1645
- if (this.options.liveReload) {
1646
- this.sendMessage([client], "liveReload");
1647
- }
2500
+ if (this.options.liveReload) {
2501
+ this.sendMessage([client], "liveReload");
2502
+ }
1648
2503
 
1649
- if (this.options.client && this.options.client.progress) {
1650
- this.sendMessage([client], "progress", this.options.client.progress);
1651
- }
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
+ }
1652
2516
 
1653
- if (this.options.client && this.options.client.reconnect) {
1654
- this.sendMessage([client], "reconnect", this.options.client.reconnect);
1655
- }
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
+ }
1656
2528
 
1657
- if (this.options.client && this.options.client.overlay) {
1658
- this.sendMessage([client], "overlay", this.options.client.overlay);
1659
- }
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
+ }
1660
2541
 
1661
- if (!this.stats) {
1662
- return;
1663
- }
2542
+ if (!this.stats) {
2543
+ return;
2544
+ }
1664
2545
 
1665
- this.sendStats([client], this.getStats(this.stats), true);
1666
- });
2546
+ this.sendStats([client], this.getStats(this.stats), true);
2547
+ }
2548
+ );
1667
2549
  }
1668
2550
 
2551
+ /**
2552
+ * @private
2553
+ * @param {string} defaultOpenTarget
2554
+ * @returns {void}
2555
+ */
1669
2556
  openBrowser(defaultOpenTarget) {
1670
2557
  const open = require("open");
1671
2558
 
1672
2559
  Promise.all(
1673
- this.options.open.map((item) => {
2560
+ /** @type {NormalizedOpen[]} */
2561
+ (this.options.open).map((item) => {
2562
+ /**
2563
+ * @type {string}
2564
+ */
1674
2565
  let openTarget;
1675
2566
 
1676
2567
  if (item.target === "<url>") {
@@ -1684,13 +2575,17 @@ class Server {
1684
2575
  return open(openTarget, item.options).catch(() => {
1685
2576
  this.logger.warn(
1686
2577
  `Unable to open "${openTarget}" page${
1687
- // eslint-disable-next-line no-nested-ternary
1688
2578
  item.options.app
1689
- ? ` in "${item.options.app.name}" app${
1690
- item.options.app.arguments
1691
- ? ` with "${item.options.app.arguments.join(
1692
- " "
1693
- )}" 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`
1694
2589
  : ""
1695
2590
  }`
1696
2591
  : ""
@@ -1701,35 +2596,68 @@ class Server {
1701
2596
  );
1702
2597
  }
1703
2598
 
2599
+ /**
2600
+ * @private
2601
+ * @returns {void}
2602
+ */
1704
2603
  runBonjour() {
1705
- const bonjour = require("bonjour")();
1706
-
1707
- bonjour.publish({
2604
+ /**
2605
+ * @private
2606
+ * @type {import("bonjour").Bonjour | undefined}
2607
+ */
2608
+ this.bonjour = require("bonjour")();
2609
+ this.bonjour.publish({
1708
2610
  name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
1709
- port: this.options.port,
1710
- type: this.options.server.type === "http" ? "http" : "https",
2611
+ port: /** @type {number} */ (this.options.port),
2612
+ type:
2613
+ /** @type {ServerConfiguration} */
2614
+ (this.options.server).type === "http" ? "http" : "https",
1711
2615
  subtypes: ["webpack"],
1712
- ...this.options.bonjour,
2616
+ .../** @type {BonjourOptions} */ (this.options.bonjour),
1713
2617
  });
2618
+ }
1714
2619
 
1715
- process.on("exit", () => {
1716
- bonjour.unpublishAll(() => {
1717
- bonjour.destroy();
1718
- });
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
+ }
1719
2633
  });
1720
2634
  }
1721
2635
 
2636
+ /**
2637
+ * @private
2638
+ * @returns {void}
2639
+ */
1722
2640
  logStatus() {
1723
2641
  const { isColorSupported, cyan, red } = require("colorette");
1724
2642
 
2643
+ /**
2644
+ * @param {Compiler["options"]} compilerOptions
2645
+ * @returns {boolean}
2646
+ */
1725
2647
  const getColorsOption = (compilerOptions) => {
2648
+ /**
2649
+ * @type {boolean}
2650
+ */
1726
2651
  let colorsEnabled;
1727
2652
 
1728
2653
  if (
1729
2654
  compilerOptions.stats &&
1730
- typeof compilerOptions.stats.colors !== "undefined"
2655
+ typeof (/** @type {StatsOptions} */ (compilerOptions.stats).colors) !==
2656
+ "undefined"
1731
2657
  ) {
1732
- colorsEnabled = compilerOptions.stats;
2658
+ colorsEnabled =
2659
+ /** @type {boolean} */
2660
+ (/** @type {StatsOptions} */ (compilerOptions.stats).colors);
1733
2661
  } else {
1734
2662
  colorsEnabled = isColorSupported;
1735
2663
  }
@@ -1738,6 +2666,11 @@ class Server {
1738
2666
  };
1739
2667
 
1740
2668
  const colors = {
2669
+ /**
2670
+ * @param {boolean} useColor
2671
+ * @param {string} msg
2672
+ * @returns {string}
2673
+ */
1741
2674
  info(useColor, msg) {
1742
2675
  if (useColor) {
1743
2676
  return cyan(msg);
@@ -1745,6 +2678,11 @@ class Server {
1745
2678
 
1746
2679
  return msg;
1747
2680
  },
2681
+ /**
2682
+ * @param {boolean} useColor
2683
+ * @param {string} msg
2684
+ * @returns {string}
2685
+ */
1748
2686
  error(useColor, msg) {
1749
2687
  if (useColor) {
1750
2688
  return red(msg);
@@ -1756,10 +2694,26 @@ class Server {
1756
2694
  const useColor = getColorsOption(this.getCompilerOptions());
1757
2695
 
1758
2696
  if (this.options.ipc) {
1759
- 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
+ );
1760
2703
  } else {
1761
- const protocol = this.options.server.type === "http" ? "http" : "https";
1762
- 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
+ */
1763
2717
  const prettyPrintURL = (newHostname) =>
1764
2718
  url.format({ protocol, hostname: newHostname, port, pathname: "/" });
1765
2719
 
@@ -1793,13 +2747,13 @@ class Server {
1793
2747
  if (parsedIP.range() === "unspecified") {
1794
2748
  localhost = prettyPrintURL("localhost");
1795
2749
 
1796
- const networkIPv4 = internalIp.v4.sync();
2750
+ const networkIPv4 = Server.internalIPSync("v4");
1797
2751
 
1798
2752
  if (networkIPv4) {
1799
2753
  networkUrlIPv4 = prettyPrintURL(networkIPv4);
1800
2754
  }
1801
2755
 
1802
- const networkIPv6 = internalIp.v6.sync();
2756
+ const networkIPv6 = Server.internalIPSync("v6");
1803
2757
 
1804
2758
  if (networkIPv6) {
1805
2759
  networkUrlIPv6 = prettyPrintURL(networkIPv6);
@@ -1812,8 +2766,13 @@ class Server {
1812
2766
  }
1813
2767
  } else {
1814
2768
  networkUrlIPv4 =
1815
- parsedIP.kind() === "ipv6" && parsedIP.isIPv4MappedAddress()
1816
- ? prettyPrintURL(parsedIP.toIPv4Address().toString())
2769
+ parsedIP.kind() === "ipv6" &&
2770
+ /** @type {IPv6} */
2771
+ (parsedIP).isIPv4MappedAddress()
2772
+ ? prettyPrintURL(
2773
+ /** @type {IPv6} */
2774
+ (parsedIP).toIPv4Address().toString()
2775
+ )
1817
2776
  : prettyPrintURL(address);
1818
2777
 
1819
2778
  if (parsedIP.kind() === "ipv6") {
@@ -1828,10 +2787,19 @@ class Server {
1828
2787
  }
1829
2788
 
1830
2789
  if (localhost || loopbackIPv4 || loopbackIPv6) {
1831
- const loopbacks = []
1832
- .concat(localhost ? [colors.info(useColor, localhost)] : [])
1833
- .concat(loopbackIPv4 ? [colors.info(useColor, loopbackIPv4)] : [])
1834
- .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
+ }
1835
2803
 
1836
2804
  this.logger.info(`Loopback: ${loopbacks.join(", ")}`);
1837
2805
  }
@@ -1848,18 +2816,19 @@ class Server {
1848
2816
  );
1849
2817
  }
1850
2818
 
1851
- if (this.options.open.length > 0) {
2819
+ if (/** @type {NormalizedOpen[]} */ (this.options.open).length > 0) {
1852
2820
  const openTarget = prettyPrintURL(this.options.host || "localhost");
1853
2821
 
1854
2822
  this.openBrowser(openTarget);
1855
2823
  }
1856
2824
  }
1857
2825
 
1858
- if (this.options.static && this.options.static.length > 0) {
2826
+ if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
1859
2827
  this.logger.info(
1860
2828
  `Content not from webpack is served from '${colors.info(
1861
2829
  useColor,
1862
- this.options.static
2830
+ /** @type {NormalizedStatic[]} */
2831
+ (this.options.static)
1863
2832
  .map((staticOption) => staticOption.directory)
1864
2833
  .join(", ")
1865
2834
  )}' directory`
@@ -1870,14 +2839,19 @@ class Server {
1870
2839
  this.logger.info(
1871
2840
  `404s will fallback to '${colors.info(
1872
2841
  useColor,
1873
- this.options.historyApiFallback.index || "/index.html"
2842
+ /** @type {ConnectHistoryApiFallbackOptions} */ (
2843
+ this.options.historyApiFallback
2844
+ ).index || "/index.html"
1874
2845
  )}'`
1875
2846
  );
1876
2847
  }
1877
2848
 
1878
2849
  if (this.options.bonjour) {
1879
2850
  const bonjourProtocol =
1880
- this.options.bonjour.type || this.options.server.type === "http"
2851
+ /** @type {BonjourOptions} */
2852
+ (this.options.bonjour).type ||
2853
+ /** @type {ServerConfiguration} */
2854
+ (this.options.server).type === "http"
1881
2855
  ? "http"
1882
2856
  : "https";
1883
2857
 
@@ -1887,32 +2861,59 @@ class Server {
1887
2861
  }
1888
2862
  }
1889
2863
 
2864
+ /**
2865
+ * @private
2866
+ * @param {Request} req
2867
+ * @param {Response} res
2868
+ * @param {NextFunction} next
2869
+ */
1890
2870
  setHeaders(req, res, next) {
1891
2871
  let { headers } = this.options;
1892
2872
 
1893
2873
  if (headers) {
1894
2874
  if (typeof headers === "function") {
1895
- 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
+ );
1896
2881
  }
1897
2882
 
2883
+ /**
2884
+ * @type {{key: string, value: string}[]}
2885
+ */
1898
2886
  const allHeaders = [];
1899
2887
 
1900
2888
  if (!Array.isArray(headers)) {
1901
2889
  // eslint-disable-next-line guard-for-in
1902
2890
  for (const name in headers) {
2891
+ // @ts-ignore
1903
2892
  allHeaders.push({ key: name, value: headers[name] });
1904
2893
  }
2894
+
1905
2895
  headers = allHeaders;
1906
2896
  }
1907
2897
 
1908
- headers.forEach((header) => {
1909
- res.setHeader(header.key, header.value);
1910
- });
2898
+ headers.forEach(
2899
+ /**
2900
+ * @param {{key: string, value: any}} header
2901
+ */
2902
+ (header) => {
2903
+ res.setHeader(header.key, header.value);
2904
+ }
2905
+ );
1911
2906
  }
1912
2907
 
1913
2908
  next();
1914
2909
  }
1915
2910
 
2911
+ /**
2912
+ * @private
2913
+ * @param {{ [key: string]: string | undefined }} headers
2914
+ * @param {string} headerToCheck
2915
+ * @returns {boolean}
2916
+ */
1916
2917
  checkHeader(headers, headerToCheck) {
1917
2918
  // allow user to opt out of this security check, at their own risk
1918
2919
  // by explicitly enabling allowedHosts
@@ -1949,8 +2950,8 @@ class Server {
1949
2950
  // always allow localhost host, for convenience (hostname === 'localhost')
1950
2951
  // allow hostname of listening address (hostname === this.options.host)
1951
2952
  const isValidHostname =
1952
- ipaddr.IPv4.isValid(hostname) ||
1953
- ipaddr.IPv6.isValid(hostname) ||
2953
+ (hostname !== null && ipaddr.IPv4.isValid(hostname)) ||
2954
+ (hostname !== null && ipaddr.IPv6.isValid(hostname)) ||
1954
2955
  hostname === "localhost" ||
1955
2956
  hostname === this.options.host;
1956
2957
 
@@ -1977,7 +2978,7 @@ class Server {
1977
2978
  // "*.example.com" (hostname.endsWith(allowedHost))
1978
2979
  if (
1979
2980
  hostname === allowedHost.substring(1) ||
1980
- hostname.endsWith(allowedHost)
2981
+ /** @type {string} */ (hostname).endsWith(allowedHost)
1981
2982
  ) {
1982
2983
  return true;
1983
2984
  }
@@ -1988,41 +2989,73 @@ class Server {
1988
2989
  // Also allow if `client.webSocketURL.hostname` provided
1989
2990
  if (
1990
2991
  this.options.client &&
1991
- typeof this.options.client.webSocketURL !== "undefined"
2992
+ typeof (
2993
+ /** @type {ClientConfiguration} */ (this.options.client).webSocketURL
2994
+ ) !== "undefined"
1992
2995
  ) {
1993
- return this.options.client.webSocketURL.hostname === hostname;
2996
+ return (
2997
+ /** @type {WebSocketURL} */
2998
+ (/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
2999
+ .hostname === hostname
3000
+ );
1994
3001
  }
1995
3002
 
1996
3003
  // disallow
1997
3004
  return false;
1998
3005
  }
1999
3006
 
3007
+ /**
3008
+ * @param {ClientConnection[]} clients
3009
+ * @param {string} type
3010
+ * @param {any} [data]
3011
+ * @param {any} [params]
3012
+ */
2000
3013
  // eslint-disable-next-line class-methods-use-this
2001
- sendMessage(clients, type, data) {
3014
+ sendMessage(clients, type, data, params) {
2002
3015
  for (const client of clients) {
2003
3016
  // `sockjs` uses `1` to indicate client is ready to accept data
2004
3017
  // `ws` uses `WebSocket.OPEN`, but it is mean `1` too
2005
3018
  if (client.readyState === 1) {
2006
- client.send(JSON.stringify({ type, data }));
3019
+ client.send(JSON.stringify({ type, data, params }));
2007
3020
  }
2008
3021
  }
2009
3022
  }
2010
3023
 
3024
+ /**
3025
+ * @private
3026
+ * @param {Request} req
3027
+ * @param {Response} res
3028
+ * @param {NextFunction} next
3029
+ * @returns {void}
3030
+ */
2011
3031
  serveMagicHtml(req, res, next) {
2012
- 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(() => {
2013
3038
  const _path = req.path;
2014
3039
 
2015
3040
  try {
2016
- const filename = this.middleware.getFilenameFromUrl(`${_path}.js`);
2017
- const isFile = this.middleware.context.outputFileSystem
2018
- .statSync(filename)
2019
- .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();
2020
3052
 
2021
3053
  if (!isFile) {
2022
3054
  return next();
2023
3055
  }
2024
3056
 
2025
3057
  // Serve a page that executes the javascript
3058
+ // @ts-ignore
2026
3059
  const queries = req._parsedUrl.search || "";
2027
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>`;
2028
3061
 
@@ -2034,6 +3067,12 @@ class Server {
2034
3067
  }
2035
3068
 
2036
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
+ */
2037
3076
  sendStats(clients, stats, force) {
2038
3077
  const shouldEmit =
2039
3078
  !force &&
@@ -2051,12 +3090,33 @@ class Server {
2051
3090
  this.currentHash = stats.hash;
2052
3091
  this.sendMessage(clients, "hash", stats.hash);
2053
3092
 
2054
- if (stats.errors.length > 0 || stats.warnings.length > 0) {
2055
- if (stats.warnings.length > 0) {
2056
- 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);
2057
3114
  }
2058
3115
 
2059
- if (stats.errors.length > 0) {
3116
+ if (
3117
+ /** @type {NonNullable<StatsCompilation["errors"]>} */ (stats.errors)
3118
+ .length > 0
3119
+ ) {
2060
3120
  this.sendMessage(clients, "errors", stats.errors);
2061
3121
  }
2062
3122
  } else {
@@ -2064,38 +3124,13 @@ class Server {
2064
3124
  }
2065
3125
  }
2066
3126
 
3127
+ /**
3128
+ * @param {string | string[]} watchPath
3129
+ * @param {WatchOptions} [watchOptions]
3130
+ */
2067
3131
  watchFiles(watchPath, watchOptions) {
2068
- // duplicate the same massaging of options that watchpack performs
2069
- // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
2070
- // this isn't an elegant solution, but we'll improve it in the future
2071
- // eslint-disable-next-line no-undefined
2072
- const usePolling =
2073
- typeof watchOptions.usePolling !== "undefined"
2074
- ? watchOptions.usePolling
2075
- : Boolean(watchOptions.poll);
2076
- const interval =
2077
- // eslint-disable-next-line no-nested-ternary
2078
- typeof watchOptions.interval !== "undefined"
2079
- ? watchOptions.interval
2080
- : typeof watchOptions.poll === "number"
2081
- ? watchOptions.poll
2082
- : // eslint-disable-next-line no-undefined
2083
- undefined;
2084
-
2085
- const finalWatchOptions = {
2086
- ignoreInitial: true,
2087
- persistent: true,
2088
- followSymlinks: false,
2089
- atomic: false,
2090
- alwaysStat: true,
2091
- ignorePermissionErrors: true,
2092
- ignored: watchOptions.ignored,
2093
- usePolling,
2094
- interval,
2095
- };
2096
-
2097
3132
  const chokidar = require("chokidar");
2098
- const watcher = chokidar.watch(watchPath, finalWatchOptions);
3133
+ const watcher = chokidar.watch(watchPath, watchOptions);
2099
3134
 
2100
3135
  // disabling refreshing on changing the content
2101
3136
  if (this.options.liveReload) {
@@ -2113,44 +3148,65 @@ class Server {
2113
3148
  this.staticWatchers.push(watcher);
2114
3149
  }
2115
3150
 
2116
- invalidate(callback) {
3151
+ /**
3152
+ * @param {import("webpack-dev-middleware").Callback} [callback]
3153
+ */
3154
+ invalidate(callback = () => {}) {
2117
3155
  if (this.middleware) {
2118
3156
  this.middleware.invalidate(callback);
2119
3157
  }
2120
3158
  }
2121
3159
 
3160
+ /**
3161
+ * @returns {Promise<void>}
3162
+ */
2122
3163
  async start() {
2123
3164
  await this.normalizeOptions();
2124
3165
 
2125
3166
  if (this.options.ipc) {
2126
- await new Promise((resolve, reject) => {
2127
- const net = require("net");
2128
- const socket = new net.Socket();
2129
-
2130
- socket.on("error", (error) => {
2131
- if (error.code === "ECONNREFUSED") {
2132
- // No other server listening on this socket so it can be safely removed
2133
- fs.unlinkSync(this.options.ipc);
2134
-
2135
- resolve();
2136
-
2137
- return;
2138
- } else if (error.code === "ENOENT") {
2139
- resolve();
2140
-
2141
- return;
2142
- }
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
+ }
2143
3190
 
2144
- reject(error);
2145
- });
3191
+ reject(error);
3192
+ }
3193
+ );
2146
3194
 
2147
- socket.connect({ path: this.options.ipc }, () => {
2148
- throw new Error(`IPC "${this.options.ipc}" is already used`);
2149
- });
2150
- });
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
+ );
2151
3203
  } else {
2152
- this.options.host = await Server.getHostname(this.options.host);
2153
- 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
+ );
2154
3210
  }
2155
3211
 
2156
3212
  await this.initialize();
@@ -2159,17 +3215,23 @@ class Server {
2159
3215
  ? { path: this.options.ipc }
2160
3216
  : { host: this.options.host, port: this.options.port };
2161
3217
 
2162
- await new Promise((resolve) => {
2163
- this.server.listen(listenOptions, () => {
2164
- resolve();
2165
- });
2166
- });
3218
+ await /** @type {Promise<void>} */ (
3219
+ new Promise((resolve) => {
3220
+ /** @type {import("http").Server} */
3221
+ (this.server).listen(listenOptions, () => {
3222
+ resolve();
3223
+ });
3224
+ })
3225
+ );
2167
3226
 
2168
3227
  if (this.options.ipc) {
2169
3228
  // chmod 666 (rw rw rw)
2170
3229
  const READ_WRITE = 438;
2171
3230
 
2172
- await fs.promises.chmod(this.options.ipc, READ_WRITE);
3231
+ await fs.promises.chmod(
3232
+ /** @type {string} */ (this.options.ipc),
3233
+ READ_WRITE
3234
+ );
2173
3235
  }
2174
3236
 
2175
3237
  if (this.options.webSocketServer) {
@@ -2187,13 +3249,29 @@ class Server {
2187
3249
  }
2188
3250
  }
2189
3251
 
2190
- startCallback(callback) {
3252
+ /**
3253
+ * @param {(err?: Error) => void} [callback]
3254
+ */
3255
+ startCallback(callback = () => {}) {
2191
3256
  this.start()
2192
- .then(() => callback(null), callback)
3257
+ .then(() => callback(), callback)
2193
3258
  .catch(callback);
2194
3259
  }
2195
3260
 
3261
+ /**
3262
+ * @returns {Promise<void>}
3263
+ */
2196
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
+
2197
3275
  this.webSocketProxies = [];
2198
3276
 
2199
3277
  await Promise.all(this.staticWatchers.map((watcher) => watcher.close()));
@@ -2201,70 +3279,95 @@ class Server {
2201
3279
  this.staticWatchers = [];
2202
3280
 
2203
3281
  if (this.webSocketServer) {
2204
- await new Promise((resolve) => {
2205
- this.webSocketServer.implementation.close(() => {
2206
- this.webSocketServer = null;
3282
+ await /** @type {Promise<void>} */ (
3283
+ new Promise((resolve) => {
3284
+ /** @type {WebSocketServerImplementation} */
3285
+ (this.webSocketServer).implementation.close(() => {
3286
+ this.webSocketServer = null;
2207
3287
 
2208
- resolve();
2209
- });
3288
+ resolve();
3289
+ });
2210
3290
 
2211
- for (const client of this.webSocketServer.clients) {
2212
- client.terminate();
2213
- }
3291
+ for (const client of /** @type {WebSocketServerImplementation} */ (
3292
+ this.webSocketServer
3293
+ ).clients) {
3294
+ client.terminate();
3295
+ }
2214
3296
 
2215
- this.webSocketServer.clients = [];
2216
- });
3297
+ /** @type {WebSocketServerImplementation} */
3298
+ (this.webSocketServer).clients = [];
3299
+ })
3300
+ );
2217
3301
  }
2218
3302
 
2219
3303
  if (this.server) {
2220
- await new Promise((resolve) => {
2221
- this.server.close(() => {
2222
- 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;
2223
3309
 
2224
- resolve();
2225
- });
3310
+ resolve();
3311
+ });
2226
3312
 
2227
- for (const socket of this.sockets) {
2228
- socket.destroy();
2229
- }
3313
+ for (const socket of this.sockets) {
3314
+ socket.destroy();
3315
+ }
2230
3316
 
2231
- this.sockets = [];
2232
- });
3317
+ this.sockets = [];
3318
+ })
3319
+ );
2233
3320
 
2234
3321
  if (this.middleware) {
2235
- await new Promise((resolve, reject) => {
2236
- this.middleware.close((error) => {
2237
- if (error) {
2238
- reject(error);
2239
-
2240
- return;
2241
- }
2242
-
2243
- resolve();
2244
- });
2245
- });
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
+ );
2246
3336
 
2247
3337
  this.middleware = null;
2248
3338
  }
2249
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
+ }
2250
3346
  }
2251
3347
 
2252
- stopCallback(callback) {
3348
+ /**
3349
+ * @param {(err?: Error) => void} [callback]
3350
+ */
3351
+ stopCallback(callback = () => {}) {
2253
3352
  this.stop()
2254
- .then(() => callback(null), callback)
3353
+ .then(() => callback(), callback)
2255
3354
  .catch(callback);
2256
3355
  }
2257
3356
 
2258
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
+ */
2259
3364
  listen(port, hostname, fn) {
2260
3365
  util.deprecate(
2261
3366
  () => {},
2262
- "'listen' is deprecated. Please use async 'start' or 'startCallback' methods.",
3367
+ "'listen' is deprecated. Please use the async 'start' or 'startCallback' method.",
2263
3368
  "DEP_WEBPACK_DEV_SERVER_LISTEN"
2264
3369
  )();
2265
3370
 
2266
- this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
2267
-
2268
3371
  if (typeof port === "function") {
2269
3372
  fn = port;
2270
3373
  }
@@ -2301,7 +3404,7 @@ class Server {
2301
3404
  this.options.host = hostname;
2302
3405
  }
2303
3406
 
2304
- return this.start()
3407
+ this.start()
2305
3408
  .then(() => {
2306
3409
  if (fn) {
2307
3410
  fn.call(this.server);
@@ -2315,18 +3418,22 @@ class Server {
2315
3418
  });
2316
3419
  }
2317
3420
 
3421
+ /**
3422
+ * @param {(err?: Error) => void} [callback]
3423
+ * @returns {void}
3424
+ */
2318
3425
  // TODO remove in the next major release
2319
3426
  close(callback) {
2320
3427
  util.deprecate(
2321
3428
  () => {},
2322
- "'close' is deprecated. Please use async 'stop' or 'stopCallback' methods.",
3429
+ "'close' is deprecated. Please use the async 'stop' or 'stopCallback' method.",
2323
3430
  "DEP_WEBPACK_DEV_SERVER_CLOSE"
2324
3431
  )();
2325
3432
 
2326
- return this.stop()
3433
+ this.stop()
2327
3434
  .then(() => {
2328
3435
  if (callback) {
2329
- callback(null);
3436
+ callback();
2330
3437
  }
2331
3438
  })
2332
3439
  .catch((error) => {
@@ -2337,48 +3444,4 @@ class Server {
2337
3444
  }
2338
3445
  }
2339
3446
 
2340
- const mergeExports = (obj, exports) => {
2341
- const descriptors = Object.getOwnPropertyDescriptors(exports);
2342
-
2343
- for (const name of Object.keys(descriptors)) {
2344
- const descriptor = descriptors[name];
2345
-
2346
- if (descriptor.get) {
2347
- const fn = descriptor.get;
2348
-
2349
- Object.defineProperty(obj, name, {
2350
- configurable: false,
2351
- enumerable: true,
2352
- get: fn,
2353
- });
2354
- } else if (typeof descriptor.value === "object") {
2355
- Object.defineProperty(obj, name, {
2356
- configurable: false,
2357
- enumerable: true,
2358
- writable: false,
2359
- value: mergeExports({}, descriptor.value),
2360
- });
2361
- } else {
2362
- throw new Error(
2363
- "Exposed values must be either a getter or an nested object"
2364
- );
2365
- }
2366
- }
2367
-
2368
- return Object.freeze(obj);
2369
- };
2370
-
2371
- module.exports = mergeExports(Server, {
2372
- get schema() {
2373
- return schema;
2374
- },
2375
- // TODO compatibility with webpack v4, remove it after drop
2376
- cli: {
2377
- get getArguments() {
2378
- return () => require("../bin/cli-flags");
2379
- },
2380
- get processArguments() {
2381
- return require("../bin/process-arguments");
2382
- },
2383
- },
2384
- });
3447
+ module.exports = Server;