webpack-dev-server 4.0.0-beta.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +109 -58
  2. package/bin/cli-flags.js +827 -269
  3. package/bin/process-arguments.js +332 -0
  4. package/bin/webpack-dev-server.js +46 -30
  5. package/client/clients/SockJSClient.js +11 -44
  6. package/client/clients/WebSocketClient.js +43 -0
  7. package/client/index.js +90 -98
  8. package/client/modules/logger/index.js +90 -2736
  9. package/client/modules/sockjs-client/index.js +127 -41
  10. package/client/modules/strip-ansi/index.js +69 -37
  11. package/client/overlay.js +111 -95
  12. package/client/socket.js +11 -12
  13. package/client/utils/createSocketURL.js +70 -0
  14. package/client/utils/getCurrentScriptSource.js +10 -9
  15. package/client/utils/log.js +5 -10
  16. package/client/utils/parseURL.js +43 -0
  17. package/client/utils/reloadApp.js +49 -36
  18. package/client/utils/sendMessage.js +3 -5
  19. package/lib/Server.js +1456 -539
  20. package/lib/options.json +562 -314
  21. package/lib/servers/BaseServer.js +2 -1
  22. package/lib/servers/SockJSServer.js +32 -31
  23. package/lib/servers/WebsocketServer.js +42 -41
  24. package/lib/utils/DevServerPlugin.js +275 -128
  25. package/package.json +51 -52
  26. package/CHANGELOG.md +0 -569
  27. package/client/clients/BaseClient.js +0 -23
  28. package/client/clients/WebsocketClient.js +0 -76
  29. package/client/modules/logger/SyncBailHookFake.js +0 -10
  30. package/client/utils/createSocketUrl.js +0 -94
  31. package/client/webpack.config.js +0 -57
  32. package/lib/utils/colors.js +0 -22
  33. package/lib/utils/createCertificate.js +0 -69
  34. package/lib/utils/createDomain.js +0 -31
  35. package/lib/utils/defaultPort.js +0 -3
  36. package/lib/utils/defaultTo.js +0 -7
  37. package/lib/utils/findPort.js +0 -39
  38. package/lib/utils/getCertificate.js +0 -50
  39. package/lib/utils/getColorsOption.js +0 -15
  40. package/lib/utils/getCompilerConfigArray.js +0 -8
  41. package/lib/utils/getSocketClientPath.d.ts +0 -3
  42. package/lib/utils/getSocketClientPath.js +0 -38
  43. package/lib/utils/getSocketServerImplementation.js +0 -42
  44. package/lib/utils/getStatsOption.js +0 -16
  45. package/lib/utils/getVersions.js +0 -10
  46. package/lib/utils/normalizeOptions.js +0 -122
  47. package/lib/utils/routes.js +0 -70
  48. package/lib/utils/runBonjour.js +0 -21
  49. package/lib/utils/runOpen.js +0 -83
  50. package/lib/utils/setupExitSignals.js +0 -25
  51. package/lib/utils/tryParseInt.js +0 -13
  52. package/lib/utils/updateCompiler.js +0 -14
package/lib/Server.js CHANGED
@@ -1,108 +1,745 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const url = require('url');
6
- const http = require('http');
7
- const https = require('https');
8
- const ipaddr = require('ipaddr.js');
9
- const internalIp = require('internal-ip');
10
- const killable = require('killable');
11
- const chokidar = require('chokidar');
12
- const express = require('express');
13
- const { createProxyMiddleware } = require('http-proxy-middleware');
14
- const historyApiFallback = require('connect-history-api-fallback');
15
- const compress = require('compression');
16
- const serveIndex = require('serve-index');
17
- const webpack = require('webpack');
18
- const webpackDevMiddleware = require('webpack-dev-middleware');
19
- const getFilenameFromUrl = require('webpack-dev-middleware/dist/utils/getFilenameFromUrl')
20
- .default;
21
- const { validate } = require('schema-utils');
22
- const normalizeOptions = require('./utils/normalizeOptions');
23
- const updateCompiler = require('./utils/updateCompiler');
24
- const getCertificate = require('./utils/getCertificate');
25
- const colors = require('./utils/colors');
26
- const runOpen = require('./utils/runOpen');
27
- const runBonjour = require('./utils/runBonjour');
28
- const routes = require('./utils/routes');
29
- const getSocketServerImplementation = require('./utils/getSocketServerImplementation');
30
- const getCompilerConfigArray = require('./utils/getCompilerConfigArray');
31
- const getStatsOption = require('./utils/getStatsOption');
32
- const getColorsOption = require('./utils/getColorsOption');
33
- const setupExitSignals = require('./utils/setupExitSignals');
34
- const findPort = require('./utils/findPort');
35
- const schema = require('./options.json');
1
+ "use strict";
2
+
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const url = require("url");
6
+ const util = require("util");
7
+ const fs = require("graceful-fs");
8
+ const ipaddr = require("ipaddr.js");
9
+ const internalIp = require("internal-ip");
10
+ const express = require("express");
11
+ const { validate } = require("schema-utils");
12
+ const schema = require("./options.json");
36
13
 
37
14
  if (!process.env.WEBPACK_SERVE) {
38
15
  process.env.WEBPACK_SERVE = true;
39
16
  }
40
17
 
41
18
  class Server {
42
- constructor(compiler, options = {}) {
43
- validate(schema, options, 'webpack Dev Server');
19
+ constructor(options = {}, compiler) {
20
+ // TODO: remove this after plugin support is published
21
+ if (options.hooks) {
22
+ util.deprecate(
23
+ () => {},
24
+ "Using 'compiler' as the first argument is deprecated. Please use 'options' as the first argument and 'compiler' as the second argument.",
25
+ "DEP_WEBPACK_DEV_SERVER_CONSTRUCTOR"
26
+ )();
27
+
28
+ [options = {}, compiler] = [compiler, options];
29
+ }
30
+
31
+ validate(schema, options, "webpack Dev Server");
44
32
 
45
- this.compiler = compiler;
46
33
  this.options = options;
47
- this.logger = this.compiler.getInfrastructureLogger('webpack-dev-server');
48
- this.sockets = [];
49
34
  this.staticWatchers = [];
50
35
  // Keep track of websocket proxies for external websocket upgrade.
51
- this.websocketProxies = [];
52
- // this value of ws can be overwritten for tests
53
- this.wsHeartbeatInterval = 30000;
36
+ this.webSocketProxies = [];
37
+ this.sockets = [];
38
+ this.compiler = compiler;
39
+ }
40
+
41
+ static get DEFAULT_STATS() {
42
+ return {
43
+ all: false,
44
+ hash: true,
45
+ assets: true,
46
+ warnings: true,
47
+ errors: true,
48
+ errorDetails: false,
49
+ };
50
+ }
54
51
 
55
- normalizeOptions(this.compiler, this.options);
56
- updateCompiler(this.compiler, this.options);
52
+ // eslint-disable-next-line class-methods-use-this
53
+ static isAbsoluteURL(URL) {
54
+ // Don't match Windows paths `c:\`
55
+ if (/^[a-zA-Z]:\\/.test(URL)) {
56
+ return false;
57
+ }
57
58
 
58
- this.SocketServerImplementation = getSocketServerImplementation(
59
- this.options
60
- );
59
+ // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
60
+ // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
61
+ return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL);
62
+ }
63
+
64
+ static async getHostname(hostname) {
65
+ if (hostname === "local-ip") {
66
+ return (await internalIp.v4()) || (await internalIp.v6()) || "0.0.0.0";
67
+ } else if (hostname === "local-ipv4") {
68
+ return (await internalIp.v4()) || "0.0.0.0";
69
+ } else if (hostname === "local-ipv6") {
70
+ return (await internalIp.v6()) || "::";
71
+ }
72
+
73
+ return hostname;
74
+ }
75
+
76
+ static async getFreePort(port) {
77
+ if (port && port !== "auto") {
78
+ return port;
79
+ }
80
+
81
+ const pRetry = require("p-retry");
82
+ const portfinder = require("portfinder");
83
+
84
+ portfinder.basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT || 8080;
85
+
86
+ // Try to find unused port and listen on it for 3 times,
87
+ // if port is not specified in options.
88
+ const defaultPortRetry =
89
+ parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10) || 3;
90
+
91
+ return pRetry(() => portfinder.getPortPromise(), {
92
+ retries: defaultPortRetry,
93
+ });
94
+ }
95
+
96
+ static findCacheDir() {
97
+ const cwd = process.cwd();
98
+
99
+ let dir = cwd;
100
+
101
+ for (;;) {
102
+ try {
103
+ if (fs.statSync(path.join(dir, "package.json")).isFile()) break;
104
+ // eslint-disable-next-line no-empty
105
+ } catch (e) {}
106
+
107
+ const parent = path.dirname(dir);
108
+
109
+ if (dir === parent) {
110
+ // eslint-disable-next-line no-undefined
111
+ dir = undefined;
112
+ break;
113
+ }
114
+
115
+ dir = parent;
116
+ }
117
+
118
+ if (!dir) {
119
+ return path.resolve(cwd, ".cache/webpack-dev-server");
120
+ } else if (process.versions.pnp === "1") {
121
+ return path.resolve(dir, ".pnp/.cache/webpack-dev-server");
122
+ } else if (process.versions.pnp === "3") {
123
+ return path.resolve(dir, ".yarn/.cache/webpack-dev-server");
124
+ }
125
+
126
+ return path.resolve(dir, "node_modules/.cache/webpack-dev-server");
127
+ }
128
+
129
+ getCompilerOptions() {
130
+ if (typeof this.compiler.compilers !== "undefined") {
131
+ if (this.compiler.compilers.length === 1) {
132
+ return this.compiler.compilers[0].options;
133
+ }
134
+
135
+ // Configuration with the `devServer` options
136
+ const compilerWithDevServer = this.compiler.compilers.find(
137
+ (config) => config.options.devServer
138
+ );
139
+
140
+ if (compilerWithDevServer) {
141
+ return compilerWithDevServer.options;
142
+ }
143
+
144
+ // Configuration with `web` preset
145
+ const compilerWithWebPreset = this.compiler.compilers.find(
146
+ (config) =>
147
+ (config.options.externalsPresets &&
148
+ config.options.externalsPresets.web) ||
149
+ [
150
+ "web",
151
+ "webworker",
152
+ "electron-preload",
153
+ "electron-renderer",
154
+ "node-webkit",
155
+ // eslint-disable-next-line no-undefined
156
+ undefined,
157
+ null,
158
+ ].includes(config.options.target)
159
+ );
160
+
161
+ if (compilerWithWebPreset) {
162
+ return compilerWithWebPreset.options;
163
+ }
164
+
165
+ // Fallback
166
+ return this.compiler.compilers[0].options;
167
+ }
168
+
169
+ return this.compiler.options;
170
+ }
171
+
172
+ // eslint-disable-next-line class-methods-use-this
173
+ async normalizeOptions() {
174
+ const { options } = this;
175
+
176
+ if (!this.logger) {
177
+ this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
178
+ }
179
+
180
+ const compilerOptions = this.getCompilerOptions();
181
+ // TODO remove `{}` after drop webpack v4 support
182
+ const watchOptions = compilerOptions.watchOptions || {};
183
+ const defaultOptionsForStatic = {
184
+ directory: path.join(process.cwd(), "public"),
185
+ staticOptions: {},
186
+ publicPath: ["/"],
187
+ serveIndex: { icons: true },
188
+ // Respect options from compiler watchOptions
189
+ watch: watchOptions,
190
+ };
191
+
192
+ if (typeof options.allowedHosts === "undefined") {
193
+ // allowedHosts allows some default hosts picked from
194
+ // `options.host` or `webSocketURL.hostname` and `localhost`
195
+ options.allowedHosts = "auto";
196
+ }
197
+
198
+ if (
199
+ typeof options.allowedHosts === "string" &&
200
+ options.allowedHosts !== "auto" &&
201
+ options.allowedHosts !== "all"
202
+ ) {
203
+ // we store allowedHosts as array when supplied as string
204
+ options.allowedHosts = [options.allowedHosts];
205
+ }
206
+
207
+ if (typeof options.bonjour === "undefined") {
208
+ options.bonjour = false;
209
+ } else if (typeof options.bonjour === "boolean") {
210
+ options.bonjour = options.bonjour ? {} : false;
211
+ }
212
+
213
+ if (
214
+ typeof options.client === "undefined" ||
215
+ (typeof options.client === "object" && options.client !== null)
216
+ ) {
217
+ if (!options.client) {
218
+ options.client = {};
219
+ }
220
+
221
+ if (typeof options.client.webSocketURL === "undefined") {
222
+ options.client.webSocketURL = {};
223
+ } else if (typeof options.client.webSocketURL === "string") {
224
+ const parsedURL = new URL(options.client.webSocketURL);
225
+
226
+ options.client.webSocketURL = {
227
+ protocol: parsedURL.protocol,
228
+ hostname: parsedURL.hostname,
229
+ port: parsedURL.port.length > 0 ? Number(parsedURL.port) : "",
230
+ pathname: parsedURL.pathname,
231
+ username: parsedURL.username,
232
+ password: parsedURL.password,
233
+ };
234
+ } else if (typeof options.client.webSocketURL.port === "string") {
235
+ options.client.webSocketURL.port = Number(
236
+ options.client.webSocketURL.port
237
+ );
238
+ }
239
+
240
+ // Enable client overlay by default
241
+ if (typeof options.client.overlay === "undefined") {
242
+ options.client.overlay = true;
243
+ } else if (typeof options.client.overlay !== "boolean") {
244
+ options.client.overlay = {
245
+ errors: true,
246
+ warnings: true,
247
+ ...options.client.overlay,
248
+ };
249
+ }
250
+
251
+ // Respect infrastructureLogging.level
252
+ if (typeof options.client.logging === "undefined") {
253
+ options.client.logging = compilerOptions.infrastructureLogging
254
+ ? compilerOptions.infrastructureLogging.level
255
+ : "info";
256
+ }
257
+ }
258
+
259
+ if (typeof options.compress === "undefined") {
260
+ options.compress = true;
261
+ }
262
+
263
+ if (typeof options.devMiddleware === "undefined") {
264
+ options.devMiddleware = {};
265
+ }
266
+
267
+ // No need to normalize `headers`
268
+
269
+ if (typeof options.historyApiFallback === "undefined") {
270
+ options.historyApiFallback = false;
271
+ } else if (
272
+ typeof options.historyApiFallback === "boolean" &&
273
+ options.historyApiFallback
274
+ ) {
275
+ options.historyApiFallback = {};
276
+ }
277
+
278
+ // No need to normalize `host`
279
+
280
+ options.hot =
281
+ typeof options.hot === "boolean" || options.hot === "only"
282
+ ? options.hot
283
+ : true;
284
+
285
+ // if the user enables http2, we can safely enable https
286
+ if ((options.http2 && !options.https) || options.https === true) {
287
+ options.https = {
288
+ requestCert: false,
289
+ };
290
+ }
291
+
292
+ // https option
293
+ if (options.https) {
294
+ for (const property of ["cacert", "pfx", "key", "cert"]) {
295
+ const value = options.https[property];
296
+ const isBuffer = value instanceof Buffer;
297
+
298
+ if (value && !isBuffer) {
299
+ let stats = null;
300
+
301
+ try {
302
+ stats = fs.lstatSync(fs.realpathSync(value)).isFile();
303
+ } catch (error) {
304
+ // ignore error
305
+ }
306
+
307
+ // It is file
308
+ options.https[property] = stats
309
+ ? fs.readFileSync(path.resolve(value))
310
+ : value;
311
+ }
312
+ }
313
+
314
+ let fakeCert;
315
+
316
+ if (!options.https.key || !options.https.cert) {
317
+ const certificateDir = Server.findCacheDir();
318
+ const certificatePath = path.join(certificateDir, "server.pem");
319
+ let certificateExists = fs.existsSync(certificatePath);
320
+
321
+ if (certificateExists) {
322
+ const certificateTtl = 1000 * 60 * 60 * 24;
323
+ const certificateStat = fs.statSync(certificatePath);
324
+
325
+ const now = new Date();
326
+
327
+ // cert is more than 30 days old, kill it with fire
328
+ if ((now - certificateStat.ctime) / certificateTtl > 30) {
329
+ const del = require("del");
330
+
331
+ this.logger.info(
332
+ "SSL Certificate is more than 30 days old. Removing..."
333
+ );
334
+
335
+ del.sync([certificatePath], { force: true });
336
+
337
+ certificateExists = false;
338
+ }
339
+ }
340
+
341
+ if (!certificateExists) {
342
+ this.logger.info("Generating SSL Certificate...");
343
+
344
+ const selfsigned = require("selfsigned");
345
+ const attributes = [{ name: "commonName", value: "localhost" }];
346
+ const pems = selfsigned.generate(attributes, {
347
+ algorithm: "sha256",
348
+ days: 30,
349
+ keySize: 2048,
350
+ extensions: [
351
+ {
352
+ name: "basicConstraints",
353
+ cA: true,
354
+ },
355
+ {
356
+ name: "keyUsage",
357
+ keyCertSign: true,
358
+ digitalSignature: true,
359
+ nonRepudiation: true,
360
+ keyEncipherment: true,
361
+ dataEncipherment: true,
362
+ },
363
+ {
364
+ name: "extKeyUsage",
365
+ serverAuth: true,
366
+ clientAuth: true,
367
+ codeSigning: true,
368
+ timeStamping: true,
369
+ },
370
+ {
371
+ name: "subjectAltName",
372
+ altNames: [
373
+ {
374
+ // type 2 is DNS
375
+ type: 2,
376
+ value: "localhost",
377
+ },
378
+ {
379
+ type: 2,
380
+ value: "localhost.localdomain",
381
+ },
382
+ {
383
+ type: 2,
384
+ value: "lvh.me",
385
+ },
386
+ {
387
+ type: 2,
388
+ value: "*.lvh.me",
389
+ },
390
+ {
391
+ type: 2,
392
+ value: "[::1]",
393
+ },
394
+ {
395
+ // type 7 is IP
396
+ type: 7,
397
+ ip: "127.0.0.1",
398
+ },
399
+ {
400
+ type: 7,
401
+ ip: "fe80::1",
402
+ },
403
+ ],
404
+ },
405
+ ],
406
+ });
407
+
408
+ fs.mkdirSync(certificateDir, { recursive: true });
409
+ fs.writeFileSync(certificatePath, pems.private + pems.cert, {
410
+ encoding: "utf8",
411
+ });
412
+ }
413
+
414
+ fakeCert = fs.readFileSync(certificatePath);
415
+
416
+ this.logger.info(`SSL certificate: ${certificatePath}`);
417
+ }
418
+
419
+ options.https.key = options.https.key || fakeCert;
420
+ options.https.cert = options.https.cert || fakeCert;
421
+ }
422
+
423
+ if (typeof options.ipc === "boolean") {
424
+ const isWindows = process.platform === "win32";
425
+ const pipePrefix = isWindows ? "\\\\.\\pipe\\" : os.tmpdir();
426
+ const pipeName = "webpack-dev-server.sock";
427
+
428
+ options.ipc = path.join(pipePrefix, pipeName);
429
+ }
430
+
431
+ options.liveReload =
432
+ typeof options.liveReload !== "undefined" ? options.liveReload : true;
433
+
434
+ // https://github.com/webpack/webpack-dev-server/issues/1990
435
+ const defaultOpenOptions = { wait: false };
436
+ const getOpenItemsFromObject = ({ target, ...rest }) => {
437
+ const normalizedOptions = { ...defaultOpenOptions, ...rest };
438
+
439
+ if (typeof normalizedOptions.app === "string") {
440
+ normalizedOptions.app = {
441
+ name: normalizedOptions.app,
442
+ };
443
+ }
444
+
445
+ const normalizedTarget = typeof target === "undefined" ? "<url>" : target;
446
+
447
+ if (Array.isArray(normalizedTarget)) {
448
+ return normalizedTarget.map((singleTarget) => {
449
+ return { target: singleTarget, options: normalizedOptions };
450
+ });
451
+ }
452
+
453
+ return [{ target: normalizedTarget, options: normalizedOptions }];
454
+ };
455
+
456
+ if (typeof options.open === "undefined") {
457
+ options.open = [];
458
+ } else if (typeof options.open === "boolean") {
459
+ options.open = options.open
460
+ ? [{ target: "<url>", options: defaultOpenOptions }]
461
+ : [];
462
+ } else if (typeof options.open === "string") {
463
+ options.open = [{ target: options.open, options: defaultOpenOptions }];
464
+ } else if (Array.isArray(options.open)) {
465
+ const result = [];
466
+
467
+ options.open.forEach((item) => {
468
+ if (typeof item === "string") {
469
+ result.push({ target: item, options: defaultOpenOptions });
470
+
471
+ return;
472
+ }
473
+
474
+ result.push(...getOpenItemsFromObject(item));
475
+ });
61
476
 
62
- if (this.options.client.progress) {
477
+ options.open = result;
478
+ } else {
479
+ options.open = [...getOpenItemsFromObject(options.open)];
480
+ }
481
+
482
+ if (typeof options.port === "string" && options.port !== "auto") {
483
+ options.port = Number(options.port);
484
+ }
485
+
486
+ /**
487
+ * Assume a proxy configuration specified as:
488
+ * proxy: {
489
+ * 'context': { options }
490
+ * }
491
+ * OR
492
+ * proxy: {
493
+ * 'context': 'target'
494
+ * }
495
+ */
496
+ if (typeof options.proxy !== "undefined") {
497
+ // TODO remove in the next major release, only accept `Array`
498
+ if (!Array.isArray(options.proxy)) {
499
+ if (
500
+ Object.prototype.hasOwnProperty.call(options.proxy, "target") ||
501
+ Object.prototype.hasOwnProperty.call(options.proxy, "router")
502
+ ) {
503
+ options.proxy = [options.proxy];
504
+ } else {
505
+ options.proxy = Object.keys(options.proxy).map((context) => {
506
+ let proxyOptions;
507
+ // For backwards compatibility reasons.
508
+ const correctedContext = context
509
+ .replace(/^\*$/, "**")
510
+ .replace(/\/\*$/, "");
511
+
512
+ if (typeof options.proxy[context] === "string") {
513
+ proxyOptions = {
514
+ context: correctedContext,
515
+ target: options.proxy[context],
516
+ };
517
+ } else {
518
+ proxyOptions = { ...options.proxy[context] };
519
+ proxyOptions.context = correctedContext;
520
+ }
521
+
522
+ return proxyOptions;
523
+ });
524
+ }
525
+ }
526
+
527
+ options.proxy = options.proxy.map((item) => {
528
+ const getLogLevelForProxy = (level) => {
529
+ if (level === "none") {
530
+ return "silent";
531
+ }
532
+
533
+ if (level === "log") {
534
+ return "info";
535
+ }
536
+
537
+ if (level === "verbose") {
538
+ return "debug";
539
+ }
540
+
541
+ return level;
542
+ };
543
+
544
+ if (typeof item.logLevel === "undefined") {
545
+ item.logLevel = getLogLevelForProxy(
546
+ compilerOptions.infrastructureLogging
547
+ ? compilerOptions.infrastructureLogging.level
548
+ : "info"
549
+ );
550
+ }
551
+
552
+ if (typeof item.logProvider === "undefined") {
553
+ item.logProvider = () => this.logger;
554
+ }
555
+
556
+ return item;
557
+ });
558
+ }
559
+
560
+ if (typeof options.setupExitSignals === "undefined") {
561
+ options.setupExitSignals = true;
562
+ }
563
+
564
+ if (typeof options.static === "undefined") {
565
+ options.static = [defaultOptionsForStatic];
566
+ } else if (typeof options.static === "boolean") {
567
+ options.static = options.static ? [defaultOptionsForStatic] : false;
568
+ } else if (typeof options.static === "string") {
569
+ options.static = [
570
+ { ...defaultOptionsForStatic, directory: options.static },
571
+ ];
572
+ } else if (Array.isArray(options.static)) {
573
+ options.static = options.static.map((item) => {
574
+ if (typeof item === "string") {
575
+ return { ...defaultOptionsForStatic, directory: item };
576
+ }
577
+
578
+ return { ...defaultOptionsForStatic, ...item };
579
+ });
580
+ } else {
581
+ options.static = [{ ...defaultOptionsForStatic, ...options.static }];
582
+ }
583
+
584
+ if (options.static) {
585
+ options.static.forEach((staticOption) => {
586
+ if (Server.isAbsoluteURL(staticOption.directory)) {
587
+ throw new Error("Using a URL as static.directory is not supported");
588
+ }
589
+
590
+ // ensure that publicPath is an array
591
+ if (typeof staticOption.publicPath === "string") {
592
+ staticOption.publicPath = [staticOption.publicPath];
593
+ }
594
+
595
+ // ensure that watch is an object if true
596
+ if (staticOption.watch === true) {
597
+ staticOption.watch = defaultOptionsForStatic.watch;
598
+ }
599
+
600
+ // ensure that serveIndex is an object if true
601
+ if (staticOption.serveIndex === true) {
602
+ staticOption.serveIndex = defaultOptionsForStatic.serveIndex;
603
+ }
604
+ });
605
+ }
606
+
607
+ if (typeof options.watchFiles === "string") {
608
+ options.watchFiles = [{ paths: options.watchFiles, options: {} }];
609
+ } else if (
610
+ typeof options.watchFiles === "object" &&
611
+ options.watchFiles !== null &&
612
+ !Array.isArray(options.watchFiles)
613
+ ) {
614
+ options.watchFiles = [
615
+ {
616
+ paths: options.watchFiles.paths,
617
+ options: options.watchFiles.options || {},
618
+ },
619
+ ];
620
+ } else if (Array.isArray(options.watchFiles)) {
621
+ options.watchFiles = options.watchFiles.map((item) => {
622
+ if (typeof item === "string") {
623
+ return { paths: item, options: {} };
624
+ }
625
+
626
+ return { paths: item.paths, options: item.options || {} };
627
+ });
628
+ } else {
629
+ options.watchFiles = [];
630
+ }
631
+
632
+ const defaultWebSocketServerType = "ws";
633
+ const defaultWebSocketServerOptions = { path: "/ws" };
634
+
635
+ if (typeof options.webSocketServer === "undefined") {
636
+ options.webSocketServer = {
637
+ type: defaultWebSocketServerType,
638
+ options: defaultWebSocketServerOptions,
639
+ };
640
+ } else if (
641
+ typeof options.webSocketServer === "boolean" &&
642
+ !options.webSocketServer
643
+ ) {
644
+ options.webSocketServer = false;
645
+ } else if (
646
+ typeof options.webSocketServer === "string" ||
647
+ typeof options.webSocketServer === "function"
648
+ ) {
649
+ options.webSocketServer = {
650
+ type: options.webSocketServer,
651
+ options: defaultWebSocketServerOptions,
652
+ };
653
+ } else {
654
+ options.webSocketServer = {
655
+ type: options.webSocketServer.type || defaultWebSocketServerType,
656
+ options: {
657
+ ...defaultWebSocketServerOptions,
658
+ ...options.webSocketServer.options,
659
+ },
660
+ };
661
+
662
+ if (typeof options.webSocketServer.options.port === "string") {
663
+ options.webSocketServer.options.port = Number(
664
+ options.webSocketServer.options.port
665
+ );
666
+ }
667
+ }
668
+ }
669
+
670
+ async initialize() {
671
+ this.applyDevServerPlugin();
672
+
673
+ if (this.options.client && this.options.client.progress) {
63
674
  this.setupProgressPlugin();
64
675
  }
65
676
 
66
677
  this.setupHooks();
67
678
  this.setupApp();
68
- this.setupCheckHostRoute();
679
+ this.setupHostHeaderCheck();
69
680
  this.setupDevMiddleware();
70
-
71
681
  // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
72
- routes(this);
73
-
682
+ this.setupBuiltInRoutes();
74
683
  this.setupWatchFiles();
75
684
  this.setupFeatures();
76
- this.setupHttps();
77
685
  this.createServer();
78
686
 
79
- killable(this.server);
80
- setupExitSignals(this);
687
+ if (this.options.setupExitSignals) {
688
+ const signals = ["SIGINT", "SIGTERM"];
689
+
690
+ signals.forEach((signal) => {
691
+ process.on(signal, () => {
692
+ this.stopCallback(() => {
693
+ // eslint-disable-next-line no-process-exit
694
+ process.exit();
695
+ });
696
+ });
697
+ });
698
+ }
81
699
 
82
700
  // Proxy WebSocket without the initial http request
83
701
  // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
84
702
  // eslint-disable-next-line func-names
85
- this.websocketProxies.forEach(function (wsProxy) {
86
- this.server.on('upgrade', wsProxy.upgrade);
703
+ this.webSocketProxies.forEach(function (webSocketProxy) {
704
+ this.server.on("upgrade", webSocketProxy.upgrade);
87
705
  }, this);
88
706
  }
89
707
 
708
+ applyDevServerPlugin() {
709
+ const DevServerPlugin = require("./utils/DevServerPlugin");
710
+
711
+ const compilers = this.compiler.compilers || [this.compiler];
712
+
713
+ // eslint-disable-next-line no-shadow
714
+ compilers.forEach((compiler) => {
715
+ new DevServerPlugin(this.options).apply(compiler);
716
+ });
717
+ }
718
+
90
719
  setupProgressPlugin() {
91
- new webpack.ProgressPlugin((percent, msg, addInfo) => {
720
+ const { ProgressPlugin } = this.compiler.webpack || require("webpack");
721
+
722
+ new ProgressPlugin((percent, msg, addInfo, pluginName) => {
92
723
  percent = Math.floor(percent * 100);
93
724
 
94
725
  if (percent === 100) {
95
- msg = 'Compilation completed';
726
+ msg = "Compilation completed";
96
727
  }
97
728
 
98
729
  if (addInfo) {
99
730
  msg = `${msg} (${addInfo})`;
100
731
  }
101
732
 
102
- this.sockWrite(this.sockets, 'progress-update', { percent, msg });
733
+ if (this.webSocketServer) {
734
+ this.sendMessage(this.webSocketServer.clients, "progress-update", {
735
+ percent,
736
+ msg,
737
+ pluginName,
738
+ });
739
+ }
103
740
 
104
741
  if (this.server) {
105
- this.server.emit('progress-update', { percent, msg });
742
+ this.server.emit("progress-update", { percent, msg, pluginName });
106
743
  }
107
744
  }).apply(this.compiler);
108
745
  }
@@ -113,19 +750,29 @@ class Server {
113
750
  this.app = new express();
114
751
  }
115
752
 
116
- setupHooks() {
117
- // Listening for events
118
- const invalidPlugin = () => {
119
- this.sockWrite(this.sockets, 'invalid');
120
- };
753
+ getStats(statsObj) {
754
+ const stats = Server.DEFAULT_STATS;
755
+ const compilerOptions = this.getCompilerOptions();
121
756
 
757
+ if (compilerOptions.stats && compilerOptions.stats.warningsFilter) {
758
+ stats.warningsFilter = compilerOptions.stats.warningsFilter;
759
+ }
760
+
761
+ return statsObj.toJson(stats);
762
+ }
763
+
764
+ setupHooks() {
122
765
  const addHooks = (compiler) => {
123
- const { compile, invalid, done } = compiler.hooks;
766
+ compiler.hooks.invalid.tap("webpack-dev-server", () => {
767
+ if (this.webSocketServer) {
768
+ this.sendMessage(this.webSocketServer.clients, "invalid");
769
+ }
770
+ });
771
+ compiler.hooks.done.tap("webpack-dev-server", (stats) => {
772
+ if (this.webSocketServer) {
773
+ this.sendStats(this.webSocketServer.clients, this.getStats(stats));
774
+ }
124
775
 
125
- compile.tap('webpack-dev-server', invalidPlugin);
126
- invalid.tap('webpack-dev-server', invalidPlugin);
127
- done.tap('webpack-dev-server', (stats) => {
128
- this.sendStats(this.sockets, this.getStats(stats));
129
776
  this.stats = stats;
130
777
  });
131
778
  };
@@ -137,108 +784,123 @@ class Server {
137
784
  }
138
785
  }
139
786
 
140
- setupCheckHostRoute() {
141
- this.app.all('*', (req, res, next) => {
142
- if (this.checkHost(req.headers)) {
787
+ setupHostHeaderCheck() {
788
+ this.app.all("*", (req, res, next) => {
789
+ if (this.checkHeader(req.headers, "host")) {
143
790
  return next();
144
791
  }
145
792
 
146
- res.send('Invalid Host header');
793
+ res.send("Invalid Host header");
147
794
  });
148
795
  }
149
796
 
150
797
  setupDevMiddleware() {
798
+ const webpackDevMiddleware = require("webpack-dev-middleware");
799
+
151
800
  // middleware for serving webpack bundle
152
- this.middleware = webpackDevMiddleware(this.compiler, this.options.dev);
801
+ this.middleware = webpackDevMiddleware(
802
+ this.compiler,
803
+ this.options.devMiddleware
804
+ );
153
805
  }
154
806
 
155
- setupCompressFeature() {
156
- this.app.use(compress());
157
- }
807
+ setupBuiltInRoutes() {
808
+ const { app, middleware } = this;
158
809
 
159
- setupProxyFeature() {
160
- /**
161
- * Assume a proxy configuration specified as:
162
- * proxy: {
163
- * 'context': { options }
164
- * }
165
- * OR
166
- * proxy: {
167
- * 'context': 'target'
168
- * }
169
- */
170
- if (!Array.isArray(this.options.proxy)) {
171
- if (Object.prototype.hasOwnProperty.call(this.options.proxy, 'target')) {
172
- this.options.proxy = [this.options.proxy];
173
- } else {
174
- this.options.proxy = Object.keys(this.options.proxy).map((context) => {
175
- let proxyOptions;
176
- // For backwards compatibility reasons.
177
- const correctedContext = context
178
- .replace(/^\*$/, '**')
179
- .replace(/\/\*$/, '');
180
-
181
- if (typeof this.options.proxy[context] === 'string') {
182
- proxyOptions = {
183
- context: correctedContext,
184
- target: this.options.proxy[context],
185
- };
186
- } else {
187
- proxyOptions = Object.assign({}, this.options.proxy[context]);
188
- proxyOptions.context = correctedContext;
189
- }
810
+ app.get("/__webpack_dev_server__/sockjs.bundle.js", (req, res) => {
811
+ res.setHeader("Content-Type", "application/javascript");
190
812
 
191
- const getLogLevelForProxy = (level) => {
192
- if (level === 'none') {
193
- return 'silent';
194
- }
813
+ const { createReadStream } = require("graceful-fs");
814
+ const clientPath = path.join(__dirname, "..", "client");
195
815
 
196
- if (level === 'log') {
197
- return 'info';
198
- }
816
+ createReadStream(
817
+ path.join(clientPath, "modules/sockjs-client/index.js")
818
+ ).pipe(res);
819
+ });
199
820
 
200
- if (level === 'verbose') {
201
- return 'debug';
202
- }
821
+ app.get("/webpack-dev-server/invalidate", (_req, res) => {
822
+ this.invalidate();
823
+
824
+ res.end();
825
+ });
203
826
 
204
- return level;
205
- };
827
+ app.get("/webpack-dev-server", (req, res) => {
828
+ middleware.waitUntilValid((stats) => {
829
+ res.setHeader("Content-Type", "text/html");
830
+ res.write(
831
+ '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
832
+ );
206
833
 
207
- const configs = getCompilerConfigArray(this.compiler);
208
- const configWithDevServer =
209
- configs.find((config) => config.devServer) || configs[0];
834
+ const statsForPrint =
835
+ typeof stats.stats !== "undefined"
836
+ ? stats.toJson().children
837
+ : [stats.toJson()];
210
838
 
211
- proxyOptions.logLevel = getLogLevelForProxy(
212
- configWithDevServer.infrastructureLogging.level
213
- );
214
- proxyOptions.logProvider = () => this.logger;
839
+ res.write(`<h1>Assets Report:</h1>`);
215
840
 
216
- return proxyOptions;
841
+ statsForPrint.forEach((item, index) => {
842
+ res.write("<div>");
843
+
844
+ const name =
845
+ item.name || (stats.stats ? `unnamed[${index}]` : "unnamed");
846
+
847
+ res.write(`<h2>Compilation: ${name}</h2>`);
848
+ res.write("<ul>");
849
+
850
+ const publicPath = item.publicPath === "auto" ? "" : item.publicPath;
851
+
852
+ for (const asset of item.assets) {
853
+ const assetName = asset.name;
854
+ const assetURL = `${publicPath}${assetName}`;
855
+
856
+ res.write(
857
+ `<li>
858
+ <strong><a href="${assetURL}" target="_blank">${assetName}</a></strong>
859
+ </li>`
860
+ );
861
+ }
862
+
863
+ res.write("</ul>");
864
+ res.write("</div>");
217
865
  });
218
- }
219
- }
866
+
867
+ res.end("</body></html>");
868
+ });
869
+ });
870
+ }
871
+
872
+ setupCompressFeature() {
873
+ const compress = require("compression");
874
+
875
+ this.app.use(compress());
876
+ }
877
+
878
+ setupProxyFeature() {
879
+ const { createProxyMiddleware } = require("http-proxy-middleware");
220
880
 
221
881
  const getProxyMiddleware = (proxyConfig) => {
222
882
  const context = proxyConfig.context || proxyConfig.path;
223
883
 
224
884
  // It is possible to use the `bypass` method without a `target`.
225
885
  // However, the proxy middleware has no use in this case, and will fail to instantiate.
226
- if (proxyConfig.target) {
886
+ if (context) {
227
887
  return createProxyMiddleware(context, proxyConfig);
228
888
  }
889
+
890
+ return createProxyMiddleware(proxyConfig);
229
891
  };
230
892
  /**
231
893
  * Assume a proxy configuration specified as:
232
894
  * proxy: [
233
895
  * {
234
- * context: ...,
235
- * ...options...
896
+ * context: "value",
897
+ * ...options,
236
898
  * },
237
899
  * // or:
238
900
  * function() {
239
901
  * return {
240
- * context: ...,
241
- * ...options...
902
+ * context: "context",
903
+ * ...options,
242
904
  * };
243
905
  * }
244
906
  * ]
@@ -247,18 +909,20 @@ class Server {
247
909
  let proxyMiddleware;
248
910
 
249
911
  let proxyConfig =
250
- typeof proxyConfigOrCallback === 'function'
912
+ typeof proxyConfigOrCallback === "function"
251
913
  ? proxyConfigOrCallback()
252
914
  : proxyConfigOrCallback;
253
915
 
254
- proxyMiddleware = getProxyMiddleware(proxyConfig);
916
+ if (!proxyConfig.bypass) {
917
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
918
+ }
255
919
 
256
920
  if (proxyConfig.ws) {
257
- this.websocketProxies.push(proxyMiddleware);
921
+ this.webSocketProxies.push(proxyMiddleware);
258
922
  }
259
923
 
260
924
  const handle = async (req, res, next) => {
261
- if (typeof proxyConfigOrCallback === 'function') {
925
+ if (typeof proxyConfigOrCallback === "function") {
262
926
  const newProxyConfig = proxyConfigOrCallback(req, res, next);
263
927
 
264
928
  if (newProxyConfig !== proxyConfig) {
@@ -270,16 +934,17 @@ class Server {
270
934
  // - Check if we have a bypass function defined
271
935
  // - In case the bypass function is defined we'll retrieve the
272
936
  // bypassUrl from it otherwise bypassUrl would be null
273
- const isByPassFuncDefined = typeof proxyConfig.bypass === 'function';
937
+ // TODO remove in the next major in favor `context` and `router` options
938
+ const isByPassFuncDefined = typeof proxyConfig.bypass === "function";
274
939
  const bypassUrl = isByPassFuncDefined
275
940
  ? await proxyConfig.bypass(req, res, proxyConfig)
276
941
  : null;
277
942
 
278
- if (typeof bypassUrl === 'boolean') {
943
+ if (typeof bypassUrl === "boolean") {
279
944
  // skip the proxy
280
945
  req.url = null;
281
946
  next();
282
- } else if (typeof bypassUrl === 'string') {
947
+ } else if (typeof bypassUrl === "string") {
283
948
  // byPass to that url
284
949
  req.url = bypassUrl;
285
950
  next();
@@ -297,13 +962,20 @@ class Server {
297
962
  }
298
963
 
299
964
  setupHistoryApiFallbackFeature() {
300
- const fallback =
301
- typeof this.options.historyApiFallback === 'object'
302
- ? this.options.historyApiFallback
303
- : null;
965
+ const { historyApiFallback } = this.options;
966
+
967
+ if (
968
+ typeof historyApiFallback.logger === "undefined" &&
969
+ !historyApiFallback.verbose
970
+ ) {
971
+ historyApiFallback.logger = this.logger.log.bind(
972
+ this.logger,
973
+ "[connect-history-api-fallback]"
974
+ );
975
+ }
304
976
 
305
977
  // Fall back to /index.html if nothing else matches.
306
- this.app.use(historyApiFallback(fallback));
978
+ this.app.use(require("connect-history-api-fallback")(historyApiFallback));
307
979
  }
308
980
 
309
981
  setupStaticFeature() {
@@ -318,12 +990,14 @@ class Server {
318
990
  }
319
991
 
320
992
  setupStaticServeIndexFeature() {
993
+ const serveIndex = require("serve-index");
994
+
321
995
  this.options.static.forEach((staticOption) => {
322
996
  staticOption.publicPath.forEach((publicPath) => {
323
997
  if (staticOption.serveIndex) {
324
998
  this.app.use(publicPath, (req, res, next) => {
325
999
  // serve-index doesn't fallthrough non-get/head request to next middleware
326
- if (req.method !== 'GET' && req.method !== 'HEAD') {
1000
+ if (req.method !== "GET" && req.method !== "HEAD") {
327
1001
  return next();
328
1002
  }
329
1003
 
@@ -351,23 +1025,12 @@ class Server {
351
1025
  }
352
1026
 
353
1027
  setupWatchFiles() {
354
- if (this.options.watchFiles) {
355
- const { watchFiles } = this.options;
356
-
357
- if (typeof watchFiles === 'string') {
358
- this.watchFiles(watchFiles, {});
359
- } else if (Array.isArray(watchFiles)) {
360
- watchFiles.forEach((file) => {
361
- if (typeof file === 'string') {
362
- this.watchFiles(file, {});
363
- } else {
364
- this.watchFiles(file.paths, file.options || {});
365
- }
366
- });
367
- } else {
368
- // { paths: [...], options: {} }
369
- this.watchFiles(watchFiles.paths, watchFiles.options || {});
370
- }
1028
+ const { watchFiles } = this.options;
1029
+
1030
+ if (watchFiles.length > 0) {
1031
+ watchFiles.forEach((item) => {
1032
+ this.watchFiles(item.paths, item.options);
1033
+ });
371
1034
  }
372
1035
  }
373
1036
 
@@ -380,11 +1043,11 @@ class Server {
380
1043
  }
381
1044
 
382
1045
  setupHeadersFeature() {
383
- this.app.all('*', this.setContentHeaders.bind(this));
1046
+ this.app.all("*", this.setHeaders.bind(this));
384
1047
  }
385
1048
 
386
1049
  setupMagicHtmlFeature() {
387
- this.app.get('*', this.serveMagicHtml.bind(this));
1050
+ this.app.get("*", this.serveMagicHtml.bind(this));
388
1051
  }
389
1052
 
390
1053
  setupFeatures() {
@@ -414,12 +1077,12 @@ class Server {
414
1077
  this.setupStaticWatchFeature();
415
1078
  },
416
1079
  onBeforeSetupMiddleware: () => {
417
- if (typeof this.options.onBeforeSetupMiddleware === 'function') {
1080
+ if (typeof this.options.onBeforeSetupMiddleware === "function") {
418
1081
  this.setupOnBeforeSetupMiddlewareFeature();
419
1082
  }
420
1083
  },
421
1084
  onAfterSetupMiddleware: () => {
422
- if (typeof this.options.onAfterSetupMiddleware === 'function') {
1085
+ if (typeof this.options.onAfterSetupMiddleware === "function") {
423
1086
  this.setupOnAfterSetupMiddlewareFeature();
424
1087
  }
425
1088
  },
@@ -440,39 +1103,39 @@ class Server {
440
1103
 
441
1104
  // compress is placed last and uses unshift so that it will be the first middleware used
442
1105
  if (this.options.compress) {
443
- runnableFeatures.push('compress');
1106
+ runnableFeatures.push("compress");
444
1107
  }
445
1108
 
446
1109
  if (this.options.onBeforeSetupMiddleware) {
447
- runnableFeatures.push('onBeforeSetupMiddleware');
1110
+ runnableFeatures.push("onBeforeSetupMiddleware");
448
1111
  }
449
1112
 
450
- runnableFeatures.push('headers', 'middleware');
1113
+ runnableFeatures.push("headers", "middleware");
451
1114
 
452
1115
  if (this.options.proxy) {
453
- runnableFeatures.push('proxy', 'middleware');
1116
+ runnableFeatures.push("proxy", "middleware");
454
1117
  }
455
1118
 
456
1119
  if (this.options.static) {
457
- runnableFeatures.push('static');
1120
+ runnableFeatures.push("static");
458
1121
  }
459
1122
 
460
1123
  if (this.options.historyApiFallback) {
461
- runnableFeatures.push('historyApiFallback', 'middleware');
1124
+ runnableFeatures.push("historyApiFallback", "middleware");
462
1125
 
463
1126
  if (this.options.static) {
464
- runnableFeatures.push('static');
1127
+ runnableFeatures.push("static");
465
1128
  }
466
1129
  }
467
1130
 
468
1131
  if (this.options.static) {
469
- runnableFeatures.push('staticServeIndex', 'staticWatch');
1132
+ runnableFeatures.push("staticServeIndex", "staticWatch");
470
1133
  }
471
1134
 
472
- runnableFeatures.push('magicHtml');
1135
+ runnableFeatures.push("magicHtml");
473
1136
 
474
1137
  if (this.options.onAfterSetupMiddleware) {
475
- runnableFeatures.push('onAfterSetupMiddleware');
1138
+ runnableFeatures.push("onAfterSetupMiddleware");
476
1139
  }
477
1140
 
478
1141
  runnableFeatures.forEach((feature) => {
@@ -480,239 +1143,324 @@ class Server {
480
1143
  });
481
1144
  }
482
1145
 
483
- setupHttps() {
484
- // if the user enables http2, we can safely enable https
485
- if (
486
- (this.options.http2 && !this.options.https) ||
487
- this.options.https === true
488
- ) {
489
- this.options.https = {
490
- requestCert: false,
491
- };
492
- }
493
-
494
- if (this.options.https) {
495
- for (const property of ['ca', 'pfx', 'key', 'cert']) {
496
- const value = this.options.https[property];
497
- const isBuffer = value instanceof Buffer;
498
-
499
- if (value && !isBuffer) {
500
- let stats = null;
501
-
502
- try {
503
- stats = fs.lstatSync(fs.realpathSync(value)).isFile();
504
- } catch (error) {
505
- // ignore error
506
- }
507
-
508
- // It is file
509
- this.options.https[property] = stats
510
- ? fs.readFileSync(path.resolve(value))
511
- : value;
512
- }
513
- }
514
-
515
- let fakeCert;
516
-
517
- if (!this.options.https.key || !this.options.https.cert) {
518
- fakeCert = getCertificate(this.logger);
519
- }
520
-
521
- this.options.https.key = this.options.https.key || fakeCert;
522
- this.options.https.cert = this.options.https.cert || fakeCert;
523
- }
524
- }
525
-
526
1146
  createServer() {
527
1147
  if (this.options.https) {
528
1148
  if (this.options.http2) {
529
1149
  // TODO: we need to replace spdy with http2 which is an internal module
530
- this.server = require('spdy').createServer(
1150
+ this.server = require("spdy").createServer(
531
1151
  {
532
1152
  ...this.options.https,
533
1153
  spdy: {
534
- protocols: ['h2', 'http/1.1'],
1154
+ protocols: ["h2", "http/1.1"],
535
1155
  },
536
1156
  },
537
1157
  this.app
538
1158
  );
539
1159
  } else {
1160
+ const https = require("https");
1161
+
540
1162
  this.server = https.createServer(this.options.https, this.app);
541
1163
  }
542
1164
  } else {
1165
+ const http = require("http");
1166
+
543
1167
  this.server = http.createServer(this.app);
544
1168
  }
545
1169
 
546
- this.server.on('error', (error) => {
1170
+ this.server.on("connection", (socket) => {
1171
+ // Add socket to list
1172
+ this.sockets.push(socket);
1173
+
1174
+ socket.once("close", () => {
1175
+ // Remove socket from list
1176
+ this.sockets.splice(this.sockets.indexOf(socket), 1);
1177
+ });
1178
+ });
1179
+
1180
+ this.server.on("error", (error) => {
547
1181
  throw error;
548
1182
  });
549
1183
  }
550
1184
 
551
- createSocketServer() {
552
- this.socketServer = new this.SocketServerImplementation(this);
1185
+ getWebSocketServerImplementation() {
1186
+ let implementation;
1187
+ let implementationFound = true;
1188
+
1189
+ switch (typeof this.options.webSocketServer.type) {
1190
+ case "string":
1191
+ // Could be 'sockjs', in the future 'ws', or a path that should be required
1192
+ if (this.options.webSocketServer.type === "sockjs") {
1193
+ implementation = require("./servers/SockJSServer");
1194
+ } else if (this.options.webSocketServer.type === "ws") {
1195
+ implementation = require("./servers/WebsocketServer");
1196
+ } else {
1197
+ try {
1198
+ // eslint-disable-next-line import/no-dynamic-require
1199
+ implementation = require(this.options.webSocketServer.type);
1200
+ } catch (error) {
1201
+ implementationFound = false;
1202
+ }
1203
+ }
1204
+ break;
1205
+ case "function":
1206
+ implementation = this.options.webSocketServer.type;
1207
+ break;
1208
+ default:
1209
+ implementationFound = false;
1210
+ }
1211
+
1212
+ if (!implementationFound) {
1213
+ throw new Error(
1214
+ "webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. 'ws', 'sockjs'), a full path to " +
1215
+ "a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) " +
1216
+ "via require.resolve(...), or the class itself which extends BaseServer"
1217
+ );
1218
+ }
1219
+
1220
+ return implementation;
1221
+ }
1222
+
1223
+ createWebSocketServer() {
1224
+ this.webSocketServer = new (this.getWebSocketServerImplementation())(this);
1225
+ this.webSocketServer.implementation.on("connection", (client, request) => {
1226
+ const headers =
1227
+ // eslint-disable-next-line no-nested-ternary
1228
+ typeof request !== "undefined"
1229
+ ? request.headers
1230
+ : typeof client.headers !== "undefined"
1231
+ ? client.headers
1232
+ : // eslint-disable-next-line no-undefined
1233
+ undefined;
1234
+
1235
+ if (!headers) {
1236
+ this.logger.warn(
1237
+ 'webSocketServer implementation must pass headers for the "connection" event'
1238
+ );
1239
+ }
1240
+
1241
+ if (
1242
+ !headers ||
1243
+ !this.checkHeader(headers, "host") ||
1244
+ !this.checkHeader(headers, "origin")
1245
+ ) {
1246
+ this.sendMessage([client], "error", "Invalid Host/Origin header");
1247
+
1248
+ client.terminate();
1249
+
1250
+ return;
1251
+ }
1252
+
1253
+ if (this.options.hot === true || this.options.hot === "only") {
1254
+ this.sendMessage([client], "hot");
1255
+ }
1256
+
1257
+ if (this.options.liveReload) {
1258
+ this.sendMessage([client], "liveReload");
1259
+ }
1260
+
1261
+ if (this.options.client && this.options.client.progress) {
1262
+ this.sendMessage([client], "progress", this.options.client.progress);
1263
+ }
1264
+
1265
+ if (this.options.client && this.options.client.overlay) {
1266
+ this.sendMessage([client], "overlay", this.options.client.overlay);
1267
+ }
1268
+
1269
+ if (!this.stats) {
1270
+ return;
1271
+ }
1272
+
1273
+ this.sendStats([client], this.getStats(this.stats), true);
1274
+ });
1275
+ }
1276
+
1277
+ openBrowser(defaultOpenTarget) {
1278
+ const open = require("open");
1279
+
1280
+ Promise.all(
1281
+ this.options.open.map((item) => {
1282
+ let openTarget;
1283
+
1284
+ if (item.target === "<url>") {
1285
+ openTarget = defaultOpenTarget;
1286
+ } else {
1287
+ openTarget = Server.isAbsoluteURL(item.target)
1288
+ ? item.target
1289
+ : new URL(item.target, defaultOpenTarget).toString();
1290
+ }
1291
+
1292
+ return open(openTarget, item.options).catch(() => {
1293
+ this.logger.warn(
1294
+ `Unable to open "${openTarget}" page${
1295
+ // eslint-disable-next-line no-nested-ternary
1296
+ item.options.app
1297
+ ? ` in "${item.options.app.name}" app${
1298
+ item.options.app.arguments
1299
+ ? ` with "${item.options.app.arguments.join(
1300
+ " "
1301
+ )}" arguments`
1302
+ : ""
1303
+ }`
1304
+ : ""
1305
+ }. If you are running in a headless environment, please do not use the "open" option or related flags like "--open", "--open-target", and "--open-app".`
1306
+ );
1307
+ });
1308
+ })
1309
+ );
1310
+ }
1311
+
1312
+ runBonjour() {
1313
+ const bonjour = require("bonjour")();
1314
+
1315
+ bonjour.publish({
1316
+ name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
1317
+ port: this.options.port,
1318
+ type: this.options.https ? "https" : "http",
1319
+ subtypes: ["webpack"],
1320
+ ...this.options.bonjour,
1321
+ });
1322
+
1323
+ process.on("exit", () => {
1324
+ bonjour.unpublishAll(() => {
1325
+ bonjour.destroy();
1326
+ });
1327
+ });
1328
+ }
1329
+
1330
+ logStatus() {
1331
+ const colorette = require("colorette");
553
1332
 
554
- this.socketServer.onConnection((connection, headers) => {
555
- if (!connection) {
556
- return;
557
- }
558
-
559
- if (!headers) {
560
- this.logger.warn(
561
- 'transportMode.server implementation must pass headers to the callback of onConnection(f) ' +
562
- 'via f(connection, headers) in order for clients to pass a headers security check'
563
- );
564
- }
565
-
566
- if (!headers || !this.checkHost(headers) || !this.checkOrigin(headers)) {
567
- this.sockWrite([connection], 'error', 'Invalid Host/Origin header');
1333
+ const getColorsOption = (compilerOptions) => {
1334
+ let colorsEnabled;
568
1335
 
569
- this.socketServer.close(connection);
570
-
571
- return;
1336
+ if (
1337
+ compilerOptions.stats &&
1338
+ typeof compilerOptions.stats.colors !== "undefined"
1339
+ ) {
1340
+ colorsEnabled = compilerOptions.stats;
1341
+ } else {
1342
+ colorsEnabled = colorette.options.enabled;
572
1343
  }
573
1344
 
574
- this.sockets.push(connection);
1345
+ return colorsEnabled;
1346
+ };
575
1347
 
576
- this.socketServer.onConnectionClose(connection, () => {
577
- const idx = this.sockets.indexOf(connection);
1348
+ const colors = {
1349
+ info(useColor, msg) {
1350
+ if (useColor) {
1351
+ return colorette.cyan(msg);
1352
+ }
578
1353
 
579
- if (idx >= 0) {
580
- this.sockets.splice(idx, 1);
1354
+ return msg;
1355
+ },
1356
+ error(useColor, msg) {
1357
+ if (useColor) {
1358
+ return colorette.red(msg);
581
1359
  }
582
- });
583
1360
 
584
- if (this.options.client.logging) {
585
- this.sockWrite([connection], 'logging', this.options.client.logging);
586
- }
1361
+ return msg;
1362
+ },
1363
+ };
1364
+ const useColor = getColorsOption(this.getCompilerOptions());
587
1365
 
588
- if (this.options.hot === true || this.options.hot === 'only') {
589
- this.sockWrite([connection], 'hot');
590
- }
1366
+ if (this.options.ipc) {
1367
+ this.logger.info(`Project is running at: "${this.server.address()}"`);
1368
+ } else {
1369
+ const protocol = this.options.https ? "https" : "http";
1370
+ const { address, port } = this.server.address();
1371
+ const prettyPrintURL = (newHostname) =>
1372
+ url.format({ protocol, hostname: newHostname, port, pathname: "/" });
1373
+
1374
+ let server;
1375
+ let localhost;
1376
+ let loopbackIPv4;
1377
+ let loopbackIPv6;
1378
+ let networkUrlIPv4;
1379
+ let networkUrlIPv6;
1380
+
1381
+ if (this.options.host) {
1382
+ if (this.options.host === "localhost") {
1383
+ localhost = prettyPrintURL("localhost");
1384
+ } else {
1385
+ let isIP;
591
1386
 
592
- if (this.options.liveReload) {
593
- this.sockWrite([connection], 'liveReload');
594
- }
1387
+ try {
1388
+ isIP = ipaddr.parse(this.options.host);
1389
+ } catch (error) {
1390
+ // Ignore
1391
+ }
595
1392
 
596
- if (this.options.client.progress) {
597
- this.sockWrite([connection], 'progress', this.options.client.progress);
1393
+ if (!isIP) {
1394
+ server = prettyPrintURL(this.options.host);
1395
+ }
1396
+ }
598
1397
  }
599
1398
 
600
- if (this.options.client.overlay) {
601
- this.sockWrite([connection], 'overlay', this.options.client.overlay);
602
- }
1399
+ const parsedIP = ipaddr.parse(address);
603
1400
 
604
- if (!this.stats) {
605
- return;
606
- }
1401
+ if (parsedIP.range() === "unspecified") {
1402
+ localhost = prettyPrintURL("localhost");
607
1403
 
608
- this.sendStats([connection], this.getStats(this.stats), true);
609
- });
610
- }
1404
+ const networkIPv4 = internalIp.v4.sync();
611
1405
 
612
- showStatus() {
613
- const useColor = getColorsOption(getCompilerConfigArray(this.compiler));
614
- const protocol = this.options.https ? 'https' : 'http';
615
- const { address, port } = this.server.address();
616
- const prettyPrintUrl = (newHostname) =>
617
- url.format({ protocol, hostname: newHostname, port, pathname: '/' });
618
-
619
- let server;
620
- let localhost;
621
- let loopbackIPv4;
622
- let loopbackIPv6;
623
- let networkUrlIPv4;
624
- let networkUrlIPv6;
625
-
626
- if (this.hostname) {
627
- if (this.hostname === 'localhost') {
628
- localhost = prettyPrintUrl('localhost');
629
- } else {
630
- let isIP;
1406
+ if (networkIPv4) {
1407
+ networkUrlIPv4 = prettyPrintURL(networkIPv4);
1408
+ }
631
1409
 
632
- try {
633
- isIP = ipaddr.parse(this.hostname);
634
- } catch (error) {
635
- // Ignore
1410
+ const networkIPv6 = internalIp.v6.sync();
1411
+
1412
+ if (networkIPv6) {
1413
+ networkUrlIPv6 = prettyPrintURL(networkIPv6);
1414
+ }
1415
+ } else if (parsedIP.range() === "loopback") {
1416
+ if (parsedIP.kind() === "ipv4") {
1417
+ loopbackIPv4 = prettyPrintURL(parsedIP.toString());
1418
+ } else if (parsedIP.kind() === "ipv6") {
1419
+ loopbackIPv6 = prettyPrintURL(parsedIP.toString());
636
1420
  }
1421
+ } else {
1422
+ networkUrlIPv4 =
1423
+ parsedIP.kind() === "ipv6" && parsedIP.isIPv4MappedAddress()
1424
+ ? prettyPrintURL(parsedIP.toIPv4Address().toString())
1425
+ : prettyPrintURL(address);
637
1426
 
638
- if (!isIP) {
639
- server = prettyPrintUrl(this.hostname);
1427
+ if (parsedIP.kind() === "ipv6") {
1428
+ networkUrlIPv6 = prettyPrintURL(address);
640
1429
  }
641
1430
  }
642
- }
643
1431
 
644
- const parsedIP = ipaddr.parse(address);
1432
+ this.logger.info("Project is running at:");
645
1433
 
646
- if (parsedIP.range() === 'unspecified') {
647
- localhost = prettyPrintUrl('localhost');
648
-
649
- const networkIPv4 = internalIp.v4.sync();
650
-
651
- if (networkIPv4) {
652
- networkUrlIPv4 = prettyPrintUrl(networkIPv4);
1434
+ if (server) {
1435
+ this.logger.info(`Server: ${colors.info(useColor, server)}`);
653
1436
  }
654
1437
 
655
- const networkIPv6 = internalIp.v6.sync();
1438
+ if (localhost || loopbackIPv4 || loopbackIPv6) {
1439
+ const loopbacks = []
1440
+ .concat(localhost ? [colors.info(useColor, localhost)] : [])
1441
+ .concat(loopbackIPv4 ? [colors.info(useColor, loopbackIPv4)] : [])
1442
+ .concat(loopbackIPv6 ? [colors.info(useColor, loopbackIPv6)] : []);
656
1443
 
657
- if (networkIPv6) {
658
- networkUrlIPv6 = prettyPrintUrl(networkIPv6);
1444
+ this.logger.info(`Loopback: ${loopbacks.join(", ")}`);
659
1445
  }
660
- } else if (parsedIP.range() === 'loopback') {
661
- if (parsedIP.kind() === 'ipv4') {
662
- loopbackIPv4 = prettyPrintUrl(parsedIP.toString());
663
- } else if (parsedIP.kind() === 'ipv6') {
664
- loopbackIPv6 = prettyPrintUrl(parsedIP.toString());
665
- }
666
- } else {
667
- networkUrlIPv4 =
668
- parsedIP.kind() === 'ipv6' && parsedIP.isIPv4MappedAddress()
669
- ? prettyPrintUrl(parsedIP.toIPv4Address().toString())
670
- : prettyPrintUrl(address);
671
1446
 
672
- if (parsedIP.kind() === 'ipv6') {
673
- networkUrlIPv6 = prettyPrintUrl(address);
1447
+ if (networkUrlIPv4) {
1448
+ this.logger.info(
1449
+ `On Your Network (IPv4): ${colors.info(useColor, networkUrlIPv4)}`
1450
+ );
674
1451
  }
675
- }
676
-
677
- this.logger.info('Project is running at:');
678
-
679
- if (server) {
680
- this.logger.info(`Server: ${colors.info(useColor, server)}`);
681
- }
682
-
683
- if (localhost || loopbackIPv4 || loopbackIPv6) {
684
- const loopbacks = []
685
- .concat(localhost ? [colors.info(useColor, localhost)] : [])
686
- .concat(loopbackIPv4 ? [colors.info(useColor, loopbackIPv4)] : [])
687
- .concat(loopbackIPv6 ? [colors.info(useColor, loopbackIPv6)] : []);
688
-
689
- this.logger.info(`Loopback: ${loopbacks.join(', ')}`);
690
- }
691
1452
 
692
- if (networkUrlIPv4) {
693
- this.logger.info(
694
- `On Your Network (IPv4): ${colors.info(useColor, networkUrlIPv4)}`
695
- );
696
- }
1453
+ if (networkUrlIPv6) {
1454
+ this.logger.info(
1455
+ `On Your Network (IPv6): ${colors.info(useColor, networkUrlIPv6)}`
1456
+ );
1457
+ }
697
1458
 
698
- if (networkUrlIPv6) {
699
- this.logger.info(
700
- `On Your Network (IPv6): ${colors.info(useColor, networkUrlIPv6)}`
701
- );
702
- }
1459
+ if (this.options.open.length > 0) {
1460
+ const openTarget = prettyPrintURL(this.options.host || "localhost");
703
1461
 
704
- if (
705
- this.options.dev &&
706
- typeof this.options.dev.publicPath !== 'undefined'
707
- ) {
708
- this.logger.info(
709
- `webpack output is served from '${colors.info(
710
- useColor,
711
- this.options.dev.publicPath === 'auto'
712
- ? '/'
713
- : this.options.dev.publicPath
714
- )}' URL`
715
- );
1462
+ this.openBrowser(openTarget);
1463
+ }
716
1464
  }
717
1465
 
718
1466
  if (this.options.static && this.options.static.length > 0) {
@@ -721,7 +1469,7 @@ class Server {
721
1469
  useColor,
722
1470
  this.options.static
723
1471
  .map((staticOption) => staticOption.directory)
724
- .join(', ')
1472
+ .join(", ")
725
1473
  )}' directory`
726
1474
  );
727
1475
  }
@@ -730,153 +1478,45 @@ class Server {
730
1478
  this.logger.info(
731
1479
  `404s will fallback to '${colors.info(
732
1480
  useColor,
733
- this.options.historyApiFallback.index || '/index.html'
1481
+ this.options.historyApiFallback.index || "/index.html"
734
1482
  )}'`
735
1483
  );
736
1484
  }
737
1485
 
738
1486
  if (this.options.bonjour) {
739
- this.logger.info(
740
- 'Broadcasting "http" with subtype of "webpack" via ZeroConf DNS (Bonjour)'
741
- );
742
- }
743
-
744
- if (this.options.open) {
745
- const openTarget = prettyPrintUrl(this.hostname || 'localhost');
746
-
747
- runOpen(openTarget, this.options.open, this.logger);
748
- }
749
- }
750
-
751
- listen(port, hostname, fn) {
752
- if (hostname === 'local-ip') {
753
- this.hostname = internalIp.v4.sync() || internalIp.v6.sync() || '0.0.0.0';
754
- } else if (hostname === 'local-ipv4') {
755
- this.hostname = internalIp.v4.sync() || '0.0.0.0';
756
- } else if (hostname === 'local-ipv6') {
757
- this.hostname = internalIp.v6.sync() || '::';
758
- } else {
759
- this.hostname = hostname;
760
- }
1487
+ const bonjourProtocol =
1488
+ this.options.bonjour.type || this.options.https ? "https" : "http";
761
1489
 
762
- if (typeof port !== 'undefined' && port !== this.options.port) {
763
- this.logger.warn(
764
- 'The port specified in options and the port passed as an argument is different.'
1490
+ this.logger.info(
1491
+ `Broadcasting "${bonjourProtocol}" with subtype of "webpack" via ZeroConf DNS (Bonjour)`
765
1492
  );
766
1493
  }
767
-
768
- return (
769
- findPort(port || this.options.port)
770
- // eslint-disable-next-line no-shadow
771
- .then((port) => {
772
- this.port = port;
773
- return this.server.listen(port, this.hostname, (error) => {
774
- if (this.options.hot || this.options.liveReload) {
775
- this.createSocketServer();
776
- }
777
-
778
- if (this.options.bonjour) {
779
- runBonjour(this.options);
780
- }
781
-
782
- this.showStatus();
783
-
784
- if (fn) {
785
- fn.call(this.server, error);
786
- }
787
-
788
- if (typeof this.options.onListening === 'function') {
789
- this.options.onListening(this);
790
- }
791
- });
792
- })
793
- .catch((error) => {
794
- if (fn) {
795
- fn.call(this.server, error);
796
- }
797
- })
798
- );
799
- }
800
-
801
- close(cb) {
802
- this.sockets.forEach((socket) => {
803
- this.socketServer.close(socket);
804
- });
805
-
806
- this.sockets = [];
807
-
808
- const prom = Promise.all(
809
- this.staticWatchers.map((watcher) => watcher.close())
810
- );
811
- this.staticWatchers = [];
812
-
813
- this.server.kill(() => {
814
- // watchers must be closed before closing middleware
815
- prom.then(() => {
816
- this.middleware.close(cb);
817
- });
818
- });
819
- }
820
-
821
- static get DEFAULT_STATS() {
822
- return {
823
- all: false,
824
- hash: true,
825
- assets: true,
826
- warnings: true,
827
- errors: true,
828
- errorDetails: false,
829
- };
830
1494
  }
831
1495
 
832
- getStats(statsObj) {
833
- const stats = Server.DEFAULT_STATS;
834
-
835
- const configArr = getCompilerConfigArray(this.compiler);
836
- const statsOption = getStatsOption(configArr);
837
-
838
- if (typeof statsOption === 'object' && statsOption.warningsFilter) {
839
- stats.warningsFilter = statsOption.warningsFilter;
840
- }
841
-
842
- return statsObj.toJson(stats);
843
- }
1496
+ setHeaders(req, res, next) {
1497
+ let { headers } = this.options;
844
1498
 
845
- use() {
846
- // eslint-disable-next-line prefer-spread
847
- this.app.use.apply(this.app, arguments);
848
- }
1499
+ if (headers) {
1500
+ if (typeof headers === "function") {
1501
+ headers = headers(req, res, this.middleware.context);
1502
+ }
849
1503
 
850
- setContentHeaders(req, res, next) {
851
- if (this.options.headers) {
852
1504
  // eslint-disable-next-line guard-for-in
853
- for (const name in this.options.headers) {
854
- res.setHeader(name, this.options.headers[name]);
1505
+ for (const name in headers) {
1506
+ res.setHeader(name, headers[name]);
855
1507
  }
856
1508
  }
857
1509
 
858
1510
  next();
859
1511
  }
860
1512
 
861
- checkHost(headers) {
862
- return this.checkHeaders(headers, 'host');
863
- }
864
-
865
- checkOrigin(headers) {
866
- return this.checkHeaders(headers, 'origin');
867
- }
868
-
869
- checkHeaders(headers, headerToCheck) {
1513
+ checkHeader(headers, headerToCheck) {
870
1514
  // allow user to opt out of this security check, at their own risk
871
- // by explicitly disabling firewall
872
- if (!this.options.firewall) {
1515
+ // by explicitly enabling allowedHosts
1516
+ if (this.options.allowedHosts === "all") {
873
1517
  return true;
874
1518
  }
875
1519
 
876
- if (!headerToCheck) {
877
- headerToCheck = 'host';
878
- }
879
-
880
1520
  // get the Host header and extract hostname
881
1521
  // we don't care about port not matching
882
1522
  const hostHeader = headers[headerToCheck];
@@ -900,22 +1540,22 @@ class Server {
900
1540
  // these are removed from the hostname in url.parse(),
901
1541
  // so we have the pure IPv6-address in hostname.
902
1542
  // always allow localhost host, for convenience (hostname === 'localhost')
903
- // allow hostname of listening address (hostname === this.hostname)
1543
+ // allow hostname of listening address (hostname === this.options.host)
904
1544
  const isValidHostname =
905
1545
  ipaddr.IPv4.isValid(hostname) ||
906
1546
  ipaddr.IPv6.isValid(hostname) ||
907
- hostname === 'localhost' ||
908
- hostname === this.hostname;
1547
+ hostname === "localhost" ||
1548
+ hostname === this.options.host;
909
1549
 
910
1550
  if (isValidHostname) {
911
1551
  return true;
912
1552
  }
913
1553
 
914
- const allowedHosts = this.options.firewall;
1554
+ const { allowedHosts } = this.options;
915
1555
 
916
1556
  // always allow localhost host, for convenience
917
1557
  // allow if hostname is in allowedHosts
918
- if (Array.isArray(allowedHosts) && allowedHosts.length) {
1558
+ if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
919
1559
  for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) {
920
1560
  const allowedHost = allowedHosts[hostIdx];
921
1561
 
@@ -925,7 +1565,7 @@ class Server {
925
1565
 
926
1566
  // support "." as a subdomain wildcard
927
1567
  // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
928
- if (allowedHost[0] === '.') {
1568
+ if (allowedHost[0] === ".") {
929
1569
  // "example.com" (hostname === allowedHost.substring(1))
930
1570
  // "*.example.com" (hostname.endsWith(allowedHost))
931
1571
  if (
@@ -938,76 +1578,82 @@ class Server {
938
1578
  }
939
1579
  }
940
1580
 
941
- // also allow public hostname if provided
942
- if (typeof this.options.public === 'string') {
943
- const idxPublic = this.options.public.indexOf(':');
944
- const publicHostname =
945
- idxPublic >= 0
946
- ? this.options.public.substr(0, idxPublic)
947
- : this.options.public;
948
-
949
- if (hostname === publicHostname) {
950
- return true;
951
- }
1581
+ // Also allow if `client.webSocketURL.hostname` provided
1582
+ if (
1583
+ this.options.client &&
1584
+ typeof this.options.client.webSocketURL !== "undefined"
1585
+ ) {
1586
+ return this.options.client.webSocketURL.hostname === hostname;
952
1587
  }
953
1588
 
954
1589
  // disallow
955
1590
  return false;
956
1591
  }
957
1592
 
958
- sockWrite(sockets, type, data) {
959
- sockets.forEach((socket) => {
960
- this.socketServer.send(socket, JSON.stringify({ type, data }));
1593
+ // eslint-disable-next-line class-methods-use-this
1594
+ sendMessage(clients, type, data) {
1595
+ clients.forEach((client) => {
1596
+ // `sockjs` uses `1` to indicate client is ready to accept data
1597
+ // `ws` uses `WebSocket.OPEN`, but it is mean `1` too
1598
+ if (client.readyState === 1) {
1599
+ client.send(JSON.stringify({ type, data }));
1600
+ }
961
1601
  });
962
1602
  }
963
1603
 
964
1604
  serveMagicHtml(req, res, next) {
965
- const _path = req.path;
1605
+ this.middleware.waitUntilValid(() => {
1606
+ const _path = req.path;
966
1607
 
967
- try {
968
- const filename = getFilenameFromUrl(
969
- this.middleware.context,
970
- `${_path}.js`
971
- );
972
- const isFile = this.middleware.context.outputFileSystem
973
- .statSync(filename)
974
- .isFile();
1608
+ try {
1609
+ const filename = this.middleware.getFilenameFromUrl(`${_path}.js`);
1610
+ const isFile = this.middleware.context.outputFileSystem
1611
+ .statSync(filename)
1612
+ .isFile();
975
1613
 
976
- if (!isFile) {
977
- return next();
978
- }
1614
+ if (!isFile) {
1615
+ return next();
1616
+ }
979
1617
 
980
- // Serve a page that executes the javascript
981
- const queries = req._parsedUrl.search || '';
982
- 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>`;
1618
+ // Serve a page that executes the javascript
1619
+ const queries = req._parsedUrl.search || "";
1620
+ 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>`;
983
1621
 
984
- res.send(responsePage);
985
- } catch (error) {
986
- return next();
987
- }
1622
+ res.send(responsePage);
1623
+ } catch (error) {
1624
+ return next();
1625
+ }
1626
+ });
988
1627
  }
989
1628
 
990
- // send stats to a socket or multiple sockets
991
- sendStats(sockets, stats, force) {
1629
+ // Send stats to a socket or multiple sockets
1630
+ sendStats(clients, stats, force) {
992
1631
  const shouldEmit =
993
1632
  !force &&
994
1633
  stats &&
995
1634
  (!stats.errors || stats.errors.length === 0) &&
1635
+ (!stats.warnings || stats.warnings.length === 0) &&
996
1636
  stats.assets &&
997
1637
  stats.assets.every((asset) => !asset.emitted);
998
1638
 
999
1639
  if (shouldEmit) {
1000
- return this.sockWrite(sockets, 'still-ok');
1640
+ this.sendMessage(clients, "still-ok");
1641
+
1642
+ return;
1001
1643
  }
1002
1644
 
1003
- this.sockWrite(sockets, 'hash', stats.hash);
1645
+ this.sendMessage(clients, "hash", stats.hash);
1646
+
1647
+ if (stats.errors.length > 0 || stats.warnings.length > 0) {
1648
+ if (stats.warnings.length > 0) {
1649
+ this.sendMessage(clients, "warnings", stats.warnings);
1650
+ }
1004
1651
 
1005
- if (stats.errors.length > 0) {
1006
- this.sockWrite(sockets, 'errors', stats.errors);
1007
- } else if (stats.warnings.length > 0) {
1008
- this.sockWrite(sockets, 'warnings', stats.warnings);
1652
+ if (stats.errors.length > 0) {
1653
+ this.sendMessage(clients, "errors", stats.errors);
1654
+ }
1009
1655
  } else {
1010
- this.sockWrite(sockets, 'ok');
1656
+ this.sendMessage(clients, "ok");
1011
1657
  }
1012
1658
  }
1013
1659
 
@@ -1016,9 +1662,15 @@ class Server {
1016
1662
  // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
1017
1663
  // this isn't an elegant solution, but we'll improve it in the future
1018
1664
  // eslint-disable-next-line no-undefined
1019
- const usePolling = watchOptions.poll ? true : undefined;
1665
+ const usePolling =
1666
+ typeof watchOptions.usePolling !== "undefined"
1667
+ ? watchOptions.usePolling
1668
+ : Boolean(watchOptions.poll);
1020
1669
  const interval =
1021
- typeof watchOptions.poll === 'number'
1670
+ // eslint-disable-next-line no-nested-ternary
1671
+ typeof watchOptions.interval !== "undefined"
1672
+ ? watchOptions.interval
1673
+ : typeof watchOptions.poll === "number"
1022
1674
  ? watchOptions.poll
1023
1675
  : // eslint-disable-next-line no-undefined
1024
1676
  undefined;
@@ -1035,12 +1687,20 @@ class Server {
1035
1687
  interval,
1036
1688
  };
1037
1689
 
1690
+ const chokidar = require("chokidar");
1691
+
1038
1692
  const watcher = chokidar.watch(watchPath, finalWatchOptions);
1039
1693
 
1040
1694
  // disabling refreshing on changing the content
1041
1695
  if (this.options.liveReload) {
1042
- watcher.on('change', () => {
1043
- this.sockWrite(this.sockets, 'content-changed');
1696
+ watcher.on("change", (item) => {
1697
+ if (this.webSocketServer) {
1698
+ this.sendMessage(
1699
+ this.webSocketServer.clients,
1700
+ "static-changed",
1701
+ item
1702
+ );
1703
+ }
1044
1704
  });
1045
1705
  }
1046
1706
 
@@ -1052,6 +1712,263 @@ class Server {
1052
1712
  this.middleware.invalidate(callback);
1053
1713
  }
1054
1714
  }
1715
+
1716
+ async start() {
1717
+ await this.normalizeOptions();
1718
+
1719
+ if (this.options.ipc) {
1720
+ await new Promise((resolve, reject) => {
1721
+ const net = require("net");
1722
+ const socket = new net.Socket();
1723
+
1724
+ socket.on("error", (error) => {
1725
+ if (error.code === "ECONNREFUSED") {
1726
+ // No other server listening on this socket so it can be safely removed
1727
+ fs.unlinkSync(this.options.ipc);
1728
+
1729
+ resolve();
1730
+
1731
+ return;
1732
+ } else if (error.code === "ENOENT") {
1733
+ resolve();
1734
+
1735
+ return;
1736
+ }
1737
+
1738
+ reject(error);
1739
+ });
1740
+
1741
+ socket.connect({ path: this.options.ipc }, () => {
1742
+ throw new Error(`IPC "${this.options.ipc}" is already used`);
1743
+ });
1744
+ });
1745
+ } else {
1746
+ this.options.host = await Server.getHostname(this.options.host);
1747
+ this.options.port = await Server.getFreePort(this.options.port);
1748
+ }
1749
+
1750
+ await this.initialize();
1751
+
1752
+ const listenOptions = this.options.ipc
1753
+ ? { path: this.options.ipc }
1754
+ : { host: this.options.host, port: this.options.port };
1755
+
1756
+ await new Promise((resolve) => {
1757
+ this.server.listen(listenOptions, () => {
1758
+ resolve();
1759
+ });
1760
+ });
1761
+
1762
+ if (this.options.ipc) {
1763
+ // chmod 666 (rw rw rw)
1764
+ const READ_WRITE = 438;
1765
+
1766
+ fs.chmodSync(this.options.ipc, READ_WRITE);
1767
+ }
1768
+
1769
+ if (this.options.webSocketServer) {
1770
+ this.createWebSocketServer();
1771
+ }
1772
+
1773
+ if (this.options.bonjour) {
1774
+ this.runBonjour();
1775
+ }
1776
+
1777
+ this.logStatus();
1778
+
1779
+ if (typeof this.options.onListening === "function") {
1780
+ this.options.onListening(this);
1781
+ }
1782
+ }
1783
+
1784
+ startCallback(callback) {
1785
+ this.start().then(() => callback(null), callback);
1786
+ }
1787
+
1788
+ async stop() {
1789
+ this.webSocketProxies = [];
1790
+
1791
+ await Promise.all(this.staticWatchers.map((watcher) => watcher.close()));
1792
+
1793
+ this.staticWatchers = [];
1794
+
1795
+ if (this.webSocketServer) {
1796
+ await new Promise((resolve) => {
1797
+ this.webSocketServer.implementation.close(() => {
1798
+ this.webSocketServer = null;
1799
+
1800
+ resolve();
1801
+ });
1802
+
1803
+ for (const client of this.webSocketServer.clients) {
1804
+ client.terminate();
1805
+ }
1806
+
1807
+ this.webSocketServer.clients = [];
1808
+ });
1809
+ }
1810
+
1811
+ if (this.server) {
1812
+ await new Promise((resolve) => {
1813
+ this.server.close(() => {
1814
+ this.server = null;
1815
+
1816
+ resolve();
1817
+ });
1818
+
1819
+ for (const socket of this.sockets) {
1820
+ socket.destroy();
1821
+ }
1822
+
1823
+ this.sockets = [];
1824
+ });
1825
+
1826
+ if (this.middleware) {
1827
+ await new Promise((resolve, reject) => {
1828
+ this.middleware.close((error) => {
1829
+ if (error) {
1830
+ reject(error);
1831
+
1832
+ return;
1833
+ }
1834
+
1835
+ resolve();
1836
+ });
1837
+ });
1838
+
1839
+ this.middleware = null;
1840
+ }
1841
+ }
1842
+ }
1843
+
1844
+ stopCallback(callback) {
1845
+ this.stop().then(() => callback(null), callback);
1846
+ }
1847
+
1848
+ // TODO remove in the next major release
1849
+ listen(port, hostname, fn) {
1850
+ util.deprecate(
1851
+ () => {},
1852
+ "'listen' is deprecated. Please use async 'start' or 'startCallback' methods.",
1853
+ "DEP_WEBPACK_DEV_SERVER_LISTEN"
1854
+ )();
1855
+
1856
+ this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
1857
+
1858
+ if (typeof port === "function") {
1859
+ fn = port;
1860
+ }
1861
+
1862
+ if (
1863
+ typeof port !== "undefined" &&
1864
+ typeof this.options.port !== "undefined" &&
1865
+ port !== this.options.port
1866
+ ) {
1867
+ this.options.port = port;
1868
+
1869
+ this.logger.warn(
1870
+ 'The "port" specified in options is different from the port passed as an argument. Will be used from arguments.'
1871
+ );
1872
+ }
1873
+
1874
+ if (!this.options.port) {
1875
+ this.options.port = port;
1876
+ }
1877
+
1878
+ if (
1879
+ typeof hostname !== "undefined" &&
1880
+ typeof this.options.host !== "undefined" &&
1881
+ hostname !== this.options.host
1882
+ ) {
1883
+ this.options.host = hostname;
1884
+
1885
+ this.logger.warn(
1886
+ 'The "host" specified in options is different from the host passed as an argument. Will be used from arguments.'
1887
+ );
1888
+ }
1889
+
1890
+ if (!this.options.host) {
1891
+ this.options.host = hostname;
1892
+ }
1893
+
1894
+ return this.start()
1895
+ .then(() => {
1896
+ if (fn) {
1897
+ fn.call(this.server);
1898
+ }
1899
+ })
1900
+ .catch((error) => {
1901
+ // Nothing
1902
+ if (fn) {
1903
+ fn.call(this.server, error);
1904
+ }
1905
+ });
1906
+ }
1907
+
1908
+ // TODO remove in the next major release
1909
+ close(callback) {
1910
+ util.deprecate(
1911
+ () => {},
1912
+ "'close' is deprecated. Please use async 'stop' or 'stopCallback' methods.",
1913
+ "DEP_WEBPACK_DEV_SERVER_CLOSE"
1914
+ )();
1915
+
1916
+ return this.stop()
1917
+ .then(() => {
1918
+ if (callback) {
1919
+ callback(null);
1920
+ }
1921
+ })
1922
+ .catch((error) => {
1923
+ if (callback) {
1924
+ callback(error);
1925
+ }
1926
+ });
1927
+ }
1055
1928
  }
1056
1929
 
1057
- module.exports = Server;
1930
+ const mergeExports = (obj, exports) => {
1931
+ const descriptors = Object.getOwnPropertyDescriptors(exports);
1932
+
1933
+ for (const name of Object.keys(descriptors)) {
1934
+ const descriptor = descriptors[name];
1935
+
1936
+ if (descriptor.get) {
1937
+ const fn = descriptor.get;
1938
+
1939
+ Object.defineProperty(obj, name, {
1940
+ configurable: false,
1941
+ enumerable: true,
1942
+ get: fn,
1943
+ });
1944
+ } else if (typeof descriptor.value === "object") {
1945
+ Object.defineProperty(obj, name, {
1946
+ configurable: false,
1947
+ enumerable: true,
1948
+ writable: false,
1949
+ value: mergeExports({}, descriptor.value),
1950
+ });
1951
+ } else {
1952
+ throw new Error(
1953
+ "Exposed values must be either a getter or an nested object"
1954
+ );
1955
+ }
1956
+ }
1957
+
1958
+ return Object.freeze(obj);
1959
+ };
1960
+
1961
+ module.exports = mergeExports(Server, {
1962
+ get schema() {
1963
+ return schema;
1964
+ },
1965
+ // TODO compatibility with webpack v4, remove it after drop
1966
+ cli: {
1967
+ get getArguments() {
1968
+ return () => require("../bin/cli-flags");
1969
+ },
1970
+ get processArguments() {
1971
+ return require("../bin/process-arguments");
1972
+ },
1973
+ },
1974
+ });