UNPKG

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