@rspack/dev-server 1.1.5 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,42 +1,1104 @@
1
1
  "use strict";
2
+ /**
3
+ * The following code is modified based on
4
+ * https://github.com/webpack/webpack-dev-server
5
+ *
6
+ * MIT Licensed
7
+ * Author Tobias Koppers @sokra
8
+ * Copyright (c) JS Foundation and other contributors
9
+ * https://github.com/webpack/webpack-dev-server/blob/main/LICENSE
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
2
34
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
36
  };
5
37
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.RspackDevServer = void 0;
7
- const webpack_dev_server_1 = __importDefault(require("webpack-dev-server"));
8
- // @ts-ignore 'package.json' is not under 'rootDir'
9
- const package_json_1 = require("../package.json");
10
- const patch_1 = require("./patch");
11
- (0, patch_1.applyDevServerPatch)();
12
- const getFreePort = async function getFreePort(port, host) {
13
- if (typeof port !== 'undefined' && port !== null && port !== 'auto') {
14
- return port;
15
- }
16
- const { default: pRetry } = await import('p-retry');
17
- const getPort = require('webpack-dev-server/lib/getPort');
18
- const basePort = typeof process.env.WEBPACK_DEV_SERVER_BASE_PORT !== 'undefined'
19
- ? Number.parseInt(process.env.WEBPACK_DEV_SERVER_BASE_PORT, 10)
20
- : 8080;
21
- // Try to find unused port and listen on it for 3 times,
22
- // if port is not specified in options.
23
- const defaultPortRetry = typeof process.env.WEBPACK_DEV_SERVER_PORT_RETRY !== 'undefined'
24
- ? Number.parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10)
25
- : 3;
26
- return pRetry(() => getPort(basePort, host), {
27
- retries: defaultPortRetry,
28
- });
38
+ exports.Server = void 0;
39
+ const os = __importStar(require("node:os"));
40
+ const path = __importStar(require("node:path"));
41
+ const url = __importStar(require("node:url"));
42
+ const util = __importStar(require("node:util"));
43
+ const fs = __importStar(require("graceful-fs"));
44
+ const ipaddr = __importStar(require("ipaddr.js"));
45
+ const schema_utils_1 = require("schema-utils");
46
+ const options_json_1 = __importDefault(require("./options.json"));
47
+ // Define BasicApplication and Server as ambient, or import them
48
+ if (!process.env.WEBPACK_SERVE) {
49
+ process.env.WEBPACK_SERVE = 'true';
50
+ }
51
+ const memoize = (fn) => {
52
+ let cache = false;
53
+ let result;
54
+ let fnRef = fn;
55
+ return () => {
56
+ if (cache) {
57
+ return result;
58
+ }
59
+ result = fnRef();
60
+ cache = true;
61
+ // Allow to clean up memory for fn and all dependent resources
62
+ fnRef = undefined;
63
+ return result;
64
+ };
65
+ };
66
+ const getExpress = memoize(() => require('express'));
67
+ const encodeOverlaySettings = (setting) => {
68
+ return typeof setting === 'function'
69
+ ? encodeURIComponent(setting.toString())
70
+ : setting;
29
71
  };
30
- webpack_dev_server_1.default.getFreePort = getFreePort;
72
+ const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i;
31
73
  function isMultiCompiler(compiler) {
32
74
  return Array.isArray(compiler.compilers);
33
75
  }
34
- class RspackDevServer extends webpack_dev_server_1.default {
76
+ class Server {
35
77
  constructor(options, compiler) {
36
- // biome-ignore lint/suspicious/noExplicitAny: _
37
- super(options, compiler);
38
- // override
78
+ this.isTlsServer = false;
79
+ (0, schema_utils_1.validate)(options_json_1.default, options, {
80
+ name: 'Dev Server',
81
+ baseDataPath: 'options',
82
+ });
83
+ this.compiler = compiler;
84
+ this.logger = this.compiler.getInfrastructureLogger('webpack-dev-server');
85
+ this.options = options;
86
+ this.staticWatchers = [];
87
+ this.listeners = [];
88
+ this.webSocketProxies = [];
89
+ this.sockets = [];
90
+ this.currentHash = undefined;
91
+ }
92
+ static get schema() {
93
+ return options_json_1.default;
94
+ }
95
+ static get DEFAULT_STATS() {
96
+ return {
97
+ all: false,
98
+ hash: true,
99
+ warnings: true,
100
+ errors: true,
101
+ errorDetails: false,
102
+ };
103
+ }
104
+ static isAbsoluteURL(URL) {
105
+ // Don't match Windows paths `c:\`
106
+ if (/^[a-zA-Z]:\\/.test(URL)) {
107
+ return false;
108
+ }
109
+ // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
110
+ // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
111
+ return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL);
112
+ }
113
+ static findIp(gatewayOrFamily, isInternal) {
114
+ if (gatewayOrFamily === 'v4' || gatewayOrFamily === 'v6') {
115
+ let host;
116
+ const networks = Object.values(os.networkInterfaces())
117
+ .flatMap((networks) => networks ?? [])
118
+ .filter((network) => {
119
+ if (!network || !network.address) {
120
+ return false;
121
+ }
122
+ if (network.family !== `IP${gatewayOrFamily}`) {
123
+ return false;
124
+ }
125
+ if (typeof isInternal !== 'undefined' &&
126
+ network.internal !== isInternal) {
127
+ return false;
128
+ }
129
+ if (gatewayOrFamily === 'v6') {
130
+ const range = ipaddr.parse(network.address).range();
131
+ if (range !== 'ipv4Mapped' &&
132
+ range !== 'uniqueLocal' &&
133
+ range !== 'loopback') {
134
+ return false;
135
+ }
136
+ }
137
+ return network.address;
138
+ });
139
+ if (networks.length > 0) {
140
+ // Take the first network found
141
+ host = networks[0].address;
142
+ if (host.includes(':')) {
143
+ host = `[${host}]`;
144
+ }
145
+ }
146
+ return host;
147
+ }
148
+ const gatewayIp = ipaddr.parse(gatewayOrFamily);
149
+ // Look for the matching interface in all local interfaces.
150
+ for (const addresses of Object.values(os.networkInterfaces())) {
151
+ for (const { cidr } of addresses) {
152
+ const net = ipaddr.parseCIDR(cidr);
153
+ if (net[0] &&
154
+ net[0].kind() === gatewayIp.kind() &&
155
+ gatewayIp.match(net)) {
156
+ return net[0].toString();
157
+ }
158
+ }
159
+ }
160
+ }
161
+ // TODO remove me in the next major release, we have `findIp`
162
+ static async internalIP(family) {
163
+ return Server.findIp(family, false);
164
+ }
165
+ // TODO remove me in the next major release, we have `findIp`
166
+ static internalIPSync(family) {
167
+ return Server.findIp(family, false);
168
+ }
169
+ static async getHostname(hostname) {
170
+ if (hostname === 'local-ip') {
171
+ return (Server.findIp('v4', false) || Server.findIp('v6', false) || '0.0.0.0');
172
+ }
173
+ if (hostname === 'local-ipv4') {
174
+ return Server.findIp('v4', false) || '0.0.0.0';
175
+ }
176
+ if (hostname === 'local-ipv6') {
177
+ return Server.findIp('v6', false) || '::';
178
+ }
179
+ return hostname;
180
+ }
181
+ static async getFreePort(port, host) {
182
+ if (typeof port !== 'undefined' && port !== null && port !== 'auto') {
183
+ return port;
184
+ }
185
+ const { default: pRetry } = await import('p-retry');
186
+ const getPort = require('./getPort');
187
+ const basePort = typeof process.env.WEBPACK_DEV_SERVER_BASE_PORT !== 'undefined'
188
+ ? Number.parseInt(process.env.WEBPACK_DEV_SERVER_BASE_PORT, 10)
189
+ : 8080;
190
+ // Try to find unused port and listen on it for 3 times,
191
+ // if port is not specified in options.
192
+ const defaultPortRetry = typeof process.env.WEBPACK_DEV_SERVER_PORT_RETRY !== 'undefined'
193
+ ? Number.parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10)
194
+ : 3;
195
+ return pRetry(() => getPort(basePort, host), {
196
+ retries: defaultPortRetry,
197
+ });
198
+ }
199
+ static findCacheDir() {
200
+ const cwd = process.cwd();
201
+ let dir = cwd;
202
+ for (;;) {
203
+ try {
204
+ if (fs.statSync(path.join(dir, 'package.json')).isFile())
205
+ break;
206
+ // eslint-disable-next-line no-empty
207
+ }
208
+ catch { }
209
+ const parent = path.dirname(dir);
210
+ if (dir === parent) {
211
+ dir = undefined;
212
+ break;
213
+ }
214
+ dir = parent;
215
+ }
216
+ if (!dir) {
217
+ return path.resolve(cwd, '.cache/webpack-dev-server');
218
+ }
219
+ if (process.versions.pnp === '1') {
220
+ return path.resolve(dir, '.pnp/.cache/webpack-dev-server');
221
+ }
222
+ if (process.versions.pnp === '3') {
223
+ return path.resolve(dir, '.yarn/.cache/webpack-dev-server');
224
+ }
225
+ return path.resolve(dir, 'node_modules/.cache/webpack-dev-server');
226
+ }
227
+ static isWebTarget(compiler) {
228
+ if (compiler.platform?.web) {
229
+ return compiler.platform.web;
230
+ }
231
+ // TODO improve for the next major version and keep only `webTargets` to fallback for old versions
232
+ if (compiler.options.externalsPresets?.web) {
233
+ return true;
234
+ }
235
+ if (compiler.options.resolve?.conditionNames?.includes('browser')) {
236
+ return true;
237
+ }
238
+ const webTargets = [
239
+ 'web',
240
+ 'webworker',
241
+ 'electron-preload',
242
+ 'electron-renderer',
243
+ 'nwjs',
244
+ 'node-webkit',
245
+ undefined,
246
+ null,
247
+ ];
248
+ if (Array.isArray(compiler.options.target)) {
249
+ return compiler.options.target.some((r) => webTargets.includes(r));
250
+ }
251
+ return webTargets.includes(compiler.options.target);
252
+ }
253
+ addAdditionalEntries(compiler) {
254
+ const additionalEntries = [];
255
+ const isWebTarget = Server.isWebTarget(compiler);
256
+ // TODO maybe empty client
257
+ if (this.options.client && isWebTarget) {
258
+ let webSocketURLStr = '';
259
+ if (this.options.webSocketServer) {
260
+ const webSocketURL = this.options.client
261
+ .webSocketURL;
262
+ const webSocketServer = this.options.webSocketServer;
263
+ const searchParams = new URLSearchParams();
264
+ let protocol;
265
+ // We are proxying dev server and need to specify custom `hostname`
266
+ if (typeof webSocketURL.protocol !== 'undefined') {
267
+ protocol = webSocketURL.protocol;
268
+ }
269
+ else {
270
+ protocol = this.isTlsServer ? 'wss:' : 'ws:';
271
+ }
272
+ searchParams.set('protocol', protocol);
273
+ if (typeof webSocketURL.username !== 'undefined') {
274
+ searchParams.set('username', webSocketURL.username);
275
+ }
276
+ if (typeof webSocketURL.password !== 'undefined') {
277
+ searchParams.set('password', webSocketURL.password);
278
+ }
279
+ let hostname;
280
+ // SockJS is not supported server mode, so `hostname` and `port` can't specified, let's ignore them
281
+ const isSockJSType = webSocketServer.type === 'sockjs';
282
+ const isWebSocketServerHostDefined = typeof webSocketServer.options.host !== 'undefined';
283
+ const isWebSocketServerPortDefined = typeof webSocketServer.options.port !== 'undefined';
284
+ if (isSockJSType &&
285
+ (isWebSocketServerHostDefined || isWebSocketServerPortDefined)) {
286
+ this.logger.warn("SockJS only supports client mode and does not support custom hostname and port options. Please consider using 'ws' if you need to customize these options.");
287
+ }
288
+ // We are proxying dev server and need to specify custom `hostname`
289
+ if (typeof webSocketURL.hostname !== 'undefined') {
290
+ hostname = webSocketURL.hostname;
291
+ }
292
+ // Web socket server works on custom `hostname`, only for `ws` because `sock-js` is not support custom `hostname`
293
+ else if (isWebSocketServerHostDefined && !isSockJSType) {
294
+ hostname = webSocketServer.options.host;
295
+ }
296
+ // The `host` option is specified
297
+ else if (typeof this.options.host !== 'undefined') {
298
+ hostname = this.options.host;
299
+ }
300
+ // The `port` option is not specified
301
+ else {
302
+ hostname = '0.0.0.0';
303
+ }
304
+ searchParams.set('hostname', hostname);
305
+ let port;
306
+ // We are proxying dev server and need to specify custom `port`
307
+ if (typeof webSocketURL.port !== 'undefined') {
308
+ port = webSocketURL.port;
309
+ }
310
+ // Web socket server works on custom `port`, only for `ws` because `sock-js` is not support custom `port`
311
+ else if (isWebSocketServerPortDefined && !isSockJSType) {
312
+ port = webSocketServer.options.port;
313
+ }
314
+ // The `port` option is specified
315
+ else if (typeof this.options.port === 'number') {
316
+ port = this.options.port;
317
+ }
318
+ // The `port` option is specified using `string`
319
+ else if (typeof this.options.port === 'string' &&
320
+ this.options.port !== 'auto') {
321
+ port = Number(this.options.port);
322
+ }
323
+ // The `port` option is not specified or set to `auto`
324
+ else {
325
+ port = '0';
326
+ }
327
+ searchParams.set('port', String(port));
328
+ let pathname = '';
329
+ // We are proxying dev server and need to specify custom `pathname`
330
+ if (typeof webSocketURL.pathname !== 'undefined') {
331
+ pathname = webSocketURL.pathname;
332
+ }
333
+ // Web socket server works on custom `path`
334
+ else if (typeof webSocketServer.options.prefix !== 'undefined' ||
335
+ typeof webSocketServer.options.path !== 'undefined') {
336
+ pathname =
337
+ webSocketServer.options.prefix || webSocketServer.options.path;
338
+ }
339
+ searchParams.set('pathname', pathname);
340
+ const client = this.options.client;
341
+ if (typeof client.logging !== 'undefined') {
342
+ searchParams.set('logging', client.logging);
343
+ }
344
+ if (typeof client.progress !== 'undefined') {
345
+ searchParams.set('progress', String(client.progress));
346
+ }
347
+ if (typeof client.overlay !== 'undefined') {
348
+ const overlayString = typeof client.overlay === 'boolean'
349
+ ? String(client.overlay)
350
+ : JSON.stringify({
351
+ ...client.overlay,
352
+ errors: encodeOverlaySettings(client.overlay.errors),
353
+ warnings: encodeOverlaySettings(client.overlay.warnings),
354
+ runtimeErrors: encodeOverlaySettings(client.overlay.runtimeErrors),
355
+ });
356
+ searchParams.set('overlay', overlayString);
357
+ }
358
+ if (typeof client.reconnect !== 'undefined') {
359
+ searchParams.set('reconnect', typeof client.reconnect === 'number'
360
+ ? String(client.reconnect)
361
+ : '10');
362
+ }
363
+ if (typeof this.options.hot !== 'undefined') {
364
+ searchParams.set('hot', String(this.options.hot));
365
+ }
366
+ if (typeof this.options.liveReload !== 'undefined') {
367
+ searchParams.set('live-reload', String(this.options.liveReload));
368
+ }
369
+ webSocketURLStr = searchParams.toString();
370
+ }
371
+ additionalEntries.push(`${this.getClientEntry()}?${webSocketURLStr}`);
372
+ }
373
+ const clientHotEntry = this.getClientHotEntry();
374
+ if (clientHotEntry) {
375
+ additionalEntries.push(clientHotEntry);
376
+ }
377
+ const webpack = compiler.webpack || require('webpack');
378
+ // use a hook to add entries if available
379
+ for (const additionalEntry of additionalEntries) {
380
+ new webpack.EntryPlugin(compiler.context, additionalEntry, {
381
+ name: undefined,
382
+ }).apply(compiler);
383
+ }
384
+ }
385
+ /**
386
+ * @private
387
+ * @returns {Compiler["options"]} compiler options
388
+ */
389
+ getCompilerOptions() {
390
+ if (typeof this.compiler.compilers !== 'undefined') {
391
+ if (this.compiler.compilers.length === 1) {
392
+ return this.compiler.compilers[0].options;
393
+ }
394
+ // Configuration with the `devServer` options
395
+ const compilerWithDevServer = this.compiler.compilers.find((config) => config.options.devServer);
396
+ if (compilerWithDevServer) {
397
+ return compilerWithDevServer.options;
398
+ }
399
+ // Configuration with `web` preset
400
+ const compilerWithWebPreset = this.compiler.compilers.find((config) => config.options.externalsPresets?.web ||
401
+ [
402
+ 'web',
403
+ 'webworker',
404
+ 'electron-preload',
405
+ 'electron-renderer',
406
+ 'node-webkit',
407
+ undefined,
408
+ null,
409
+ ].includes(config.options.target));
410
+ if (compilerWithWebPreset) {
411
+ return compilerWithWebPreset.options;
412
+ }
413
+ // Fallback
414
+ return this.compiler.compilers[0].options;
415
+ }
416
+ return this.compiler.options;
417
+ }
418
+ async normalizeOptions() {
419
+ const { options } = this;
420
+ const compilerOptions = this.getCompilerOptions();
421
+ const compilerWatchOptions = compilerOptions.watchOptions;
422
+ const getWatchOptions = (watchOptions = {}) => {
423
+ const getPolling = () => {
424
+ if (typeof watchOptions.usePolling !== 'undefined') {
425
+ return watchOptions.usePolling;
426
+ }
427
+ if (typeof watchOptions.poll !== 'undefined') {
428
+ return Boolean(watchOptions.poll);
429
+ }
430
+ if (typeof compilerWatchOptions.poll !== 'undefined') {
431
+ return Boolean(compilerWatchOptions.poll);
432
+ }
433
+ return false;
434
+ };
435
+ const getInterval = () => {
436
+ if (typeof watchOptions.interval !== 'undefined') {
437
+ return watchOptions.interval;
438
+ }
439
+ if (typeof watchOptions.poll === 'number') {
440
+ return watchOptions.poll;
441
+ }
442
+ if (typeof compilerWatchOptions.poll === 'number') {
443
+ return compilerWatchOptions.poll;
444
+ }
445
+ };
446
+ const usePolling = getPolling();
447
+ const interval = getInterval();
448
+ const { poll, ...rest } = watchOptions;
449
+ return {
450
+ ignoreInitial: true,
451
+ persistent: true,
452
+ followSymlinks: false,
453
+ atomic: false,
454
+ alwaysStat: true,
455
+ ignorePermissionErrors: true,
456
+ // Respect options from compiler watchOptions
457
+ usePolling,
458
+ interval,
459
+ ignored: watchOptions.ignored,
460
+ // 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
461
+ ...rest,
462
+ };
463
+ };
464
+ const getStaticItem = (optionsForStatic) => {
465
+ const getDefaultStaticOptions = () => {
466
+ return {
467
+ directory: path.join(process.cwd(), 'public'),
468
+ staticOptions: {},
469
+ publicPath: ['/'],
470
+ serveIndex: { icons: true },
471
+ watch: getWatchOptions(),
472
+ };
473
+ };
474
+ let item;
475
+ if (typeof optionsForStatic === 'undefined') {
476
+ item = getDefaultStaticOptions();
477
+ }
478
+ else if (typeof optionsForStatic === 'string') {
479
+ item = {
480
+ ...getDefaultStaticOptions(),
481
+ directory: optionsForStatic,
482
+ };
483
+ }
484
+ else {
485
+ const def = getDefaultStaticOptions();
486
+ item = {
487
+ directory: typeof optionsForStatic.directory !== 'undefined'
488
+ ? optionsForStatic.directory
489
+ : def.directory,
490
+ staticOptions: typeof optionsForStatic.staticOptions !== 'undefined'
491
+ ? { ...def.staticOptions, ...optionsForStatic.staticOptions }
492
+ : def.staticOptions,
493
+ publicPath:
494
+ // eslint-disable-next-line no-nested-ternary
495
+ typeof optionsForStatic.publicPath !== 'undefined'
496
+ ? Array.isArray(optionsForStatic.publicPath)
497
+ ? optionsForStatic.publicPath
498
+ : [optionsForStatic.publicPath]
499
+ : def.publicPath,
500
+ serveIndex:
501
+ // Check if 'serveIndex' property is defined in 'optionsForStatic'
502
+ // If 'serveIndex' is a boolean and true, use default 'serveIndex'
503
+ // If 'serveIndex' is an object, merge its properties with default 'serveIndex'
504
+ // If 'serveIndex' is neither a boolean true nor an object, use it as-is
505
+ // If 'serveIndex' is not defined in 'optionsForStatic', use default 'serveIndex'
506
+ // eslint-disable-next-line no-nested-ternary
507
+ typeof optionsForStatic.serveIndex !== 'undefined'
508
+ ? // eslint-disable-next-line no-nested-ternary
509
+ typeof optionsForStatic.serveIndex === 'boolean' &&
510
+ optionsForStatic.serveIndex
511
+ ? def.serveIndex
512
+ : typeof optionsForStatic.serveIndex === 'object'
513
+ ? { ...def.serveIndex, ...optionsForStatic.serveIndex }
514
+ : optionsForStatic.serveIndex
515
+ : def.serveIndex,
516
+ watch:
517
+ // eslint-disable-next-line no-nested-ternary
518
+ typeof optionsForStatic.watch !== 'undefined'
519
+ ? // eslint-disable-next-line no-nested-ternary
520
+ typeof optionsForStatic.watch === 'boolean'
521
+ ? optionsForStatic.watch
522
+ ? def.watch
523
+ : false
524
+ : getWatchOptions(optionsForStatic.watch)
525
+ : def.watch,
526
+ };
527
+ }
528
+ if (Server.isAbsoluteURL(item.directory)) {
529
+ throw new Error('Using a URL as static.directory is not supported');
530
+ }
531
+ return item;
532
+ };
533
+ if (typeof options.allowedHosts === 'undefined') {
534
+ // AllowedHosts allows some default hosts picked from `options.host` or `webSocketURL.hostname` and `localhost`
535
+ options.allowedHosts = 'auto';
536
+ }
537
+ // We store allowedHosts as array when supplied as string
538
+ else if (typeof options.allowedHosts === 'string' &&
539
+ options.allowedHosts !== 'auto' &&
540
+ options.allowedHosts !== 'all') {
541
+ options.allowedHosts = [options.allowedHosts];
542
+ }
543
+ // CLI pass options as array, we should normalize them
544
+ else if (Array.isArray(options.allowedHosts) &&
545
+ options.allowedHosts.includes('all')) {
546
+ options.allowedHosts = 'all';
547
+ }
548
+ if (typeof options.bonjour === 'undefined') {
549
+ options.bonjour = false;
550
+ }
551
+ else if (typeof options.bonjour === 'boolean') {
552
+ options.bonjour = options.bonjour ? {} : false;
553
+ }
554
+ if (typeof options.client === 'undefined' ||
555
+ (typeof options.client === 'object' && options.client !== null)) {
556
+ if (!options.client) {
557
+ options.client = {};
558
+ }
559
+ if (typeof options.client.webSocketURL === 'undefined') {
560
+ options.client.webSocketURL = {};
561
+ }
562
+ else if (typeof options.client.webSocketURL === 'string') {
563
+ const parsedURL = new URL(options.client.webSocketURL);
564
+ options.client.webSocketURL = {
565
+ protocol: parsedURL.protocol,
566
+ hostname: parsedURL.hostname,
567
+ port: parsedURL.port.length > 0 ? Number(parsedURL.port) : '',
568
+ pathname: parsedURL.pathname,
569
+ username: parsedURL.username,
570
+ password: parsedURL.password,
571
+ };
572
+ }
573
+ else if (typeof options.client.webSocketURL.port === 'string') {
574
+ options.client.webSocketURL.port = Number(options.client.webSocketURL.port);
575
+ }
576
+ // Enable client overlay by default
577
+ if (typeof options.client.overlay === 'undefined') {
578
+ options.client.overlay = true;
579
+ }
580
+ else if (typeof options.client.overlay !== 'boolean') {
581
+ options.client.overlay = {
582
+ errors: true,
583
+ warnings: true,
584
+ ...options.client.overlay,
585
+ };
586
+ }
587
+ if (typeof options.client.reconnect === 'undefined') {
588
+ options.client.reconnect = 10;
589
+ }
590
+ else if (options.client.reconnect === true) {
591
+ options.client.reconnect = Number.POSITIVE_INFINITY;
592
+ }
593
+ else if (options.client.reconnect === false) {
594
+ options.client.reconnect = 0;
595
+ }
596
+ // Respect infrastructureLogging.level
597
+ if (typeof options.client.logging === 'undefined') {
598
+ options.client.logging = compilerOptions.infrastructureLogging
599
+ ? compilerOptions.infrastructureLogging.level
600
+ : 'info';
601
+ }
602
+ }
603
+ if (typeof options.compress === 'undefined') {
604
+ options.compress = true;
605
+ }
606
+ if (typeof options.devMiddleware === 'undefined') {
607
+ options.devMiddleware = {};
608
+ }
609
+ // No need to normalize `headers`
610
+ if (typeof options.historyApiFallback === 'undefined') {
611
+ options.historyApiFallback = false;
612
+ }
613
+ else if (typeof options.historyApiFallback === 'boolean' &&
614
+ options.historyApiFallback) {
615
+ options.historyApiFallback = {};
616
+ }
617
+ // No need to normalize `host`
618
+ options.hot =
619
+ typeof options.hot === 'boolean' || options.hot === 'only'
620
+ ? options.hot
621
+ : true;
622
+ if (typeof options.server === 'function' ||
623
+ typeof options.server === 'string') {
624
+ options.server = {
625
+ type: options.server,
626
+ options: {},
627
+ };
628
+ }
629
+ else {
630
+ const serverOptions = options.server || {};
631
+ options.server = {
632
+ type: serverOptions.type || 'http',
633
+ options: { ...serverOptions.options },
634
+ };
635
+ }
636
+ const serverOptions = options.server.options;
637
+ if (options.server.type === 'spdy' &&
638
+ typeof serverOptions.spdy === 'undefined') {
639
+ serverOptions.spdy = { protocols: ['h2', 'http/1.1'] };
640
+ }
641
+ if (options.server.type === 'https' ||
642
+ options.server.type === 'http2' ||
643
+ options.server.type === 'spdy') {
644
+ if (typeof serverOptions.requestCert === 'undefined') {
645
+ serverOptions.requestCert = false;
646
+ }
647
+ const httpsProperties = [
648
+ 'ca',
649
+ 'cert',
650
+ 'crl',
651
+ 'key',
652
+ 'pfx',
653
+ ];
654
+ for (const property of httpsProperties) {
655
+ if (typeof serverOptions[property] === 'undefined') {
656
+ // eslint-disable-next-line no-continue
657
+ continue;
658
+ }
659
+ const value = serverOptions[property];
660
+ const readFile = (item) => {
661
+ if (Buffer.isBuffer(item) ||
662
+ (typeof item === 'object' && item !== null && !Array.isArray(item))) {
663
+ return item;
664
+ }
665
+ if (item) {
666
+ let stats = null;
667
+ try {
668
+ stats = fs.lstatSync(fs.realpathSync(item)).isFile();
669
+ }
670
+ catch (error) {
671
+ // Ignore error
672
+ }
673
+ // It is a file
674
+ return stats ? fs.readFileSync(item) : item;
675
+ }
676
+ };
677
+ // @ts-expect-error too complex
678
+ serverOptions[property] = (Array.isArray(value)
679
+ ? value.map((item) => readFile(item))
680
+ : readFile(value));
681
+ }
682
+ let fakeCert;
683
+ if (!serverOptions.key || !serverOptions.cert) {
684
+ const certificateDir = Server.findCacheDir();
685
+ const certificatePath = path.join(certificateDir, 'server.pem');
686
+ let certificateExists;
687
+ try {
688
+ const certificate = await fs.promises.stat(certificatePath);
689
+ certificateExists = certificate.isFile();
690
+ }
691
+ catch {
692
+ certificateExists = false;
693
+ }
694
+ if (certificateExists) {
695
+ const certificateTtl = 1000 * 60 * 60 * 24;
696
+ const certificateStat = await fs.promises.stat(certificatePath);
697
+ const now = Number(new Date());
698
+ // cert is more than 30 days old, kill it with fire
699
+ if ((now - Number(certificateStat.ctime)) / certificateTtl > 30) {
700
+ this.logger.info('SSL certificate is more than 30 days old. Removing...');
701
+ await fs.promises.rm(certificatePath, { recursive: true });
702
+ certificateExists = false;
703
+ }
704
+ }
705
+ if (!certificateExists) {
706
+ this.logger.info('Generating SSL certificate...');
707
+ const selfsigned = require('selfsigned');
708
+ const attributes = [{ name: 'commonName', value: 'localhost' }];
709
+ const pems = selfsigned.generate(attributes, {
710
+ algorithm: 'sha256',
711
+ days: 30,
712
+ keySize: 2048,
713
+ extensions: [
714
+ {
715
+ name: 'basicConstraints',
716
+ cA: true,
717
+ },
718
+ {
719
+ name: 'keyUsage',
720
+ keyCertSign: true,
721
+ digitalSignature: true,
722
+ nonRepudiation: true,
723
+ keyEncipherment: true,
724
+ dataEncipherment: true,
725
+ },
726
+ {
727
+ name: 'extKeyUsage',
728
+ serverAuth: true,
729
+ clientAuth: true,
730
+ codeSigning: true,
731
+ timeStamping: true,
732
+ },
733
+ {
734
+ name: 'subjectAltName',
735
+ altNames: [
736
+ {
737
+ // type 2 is DNS
738
+ type: 2,
739
+ value: 'localhost',
740
+ },
741
+ {
742
+ type: 2,
743
+ value: 'localhost.localdomain',
744
+ },
745
+ {
746
+ type: 2,
747
+ value: 'lvh.me',
748
+ },
749
+ {
750
+ type: 2,
751
+ value: '*.lvh.me',
752
+ },
753
+ {
754
+ type: 2,
755
+ value: '[::1]',
756
+ },
757
+ {
758
+ // type 7 is IP
759
+ type: 7,
760
+ ip: '127.0.0.1',
761
+ },
762
+ {
763
+ type: 7,
764
+ ip: 'fe80::1',
765
+ },
766
+ ],
767
+ },
768
+ ],
769
+ });
770
+ await fs.promises.mkdir(certificateDir, { recursive: true });
771
+ await fs.promises.writeFile(certificatePath, pems.private + pems.cert, {
772
+ encoding: 'utf8',
773
+ });
774
+ }
775
+ fakeCert = await fs.promises.readFile(certificatePath);
776
+ this.logger.info(`SSL certificate: ${certificatePath}`);
777
+ }
778
+ serverOptions.key = serverOptions.key || fakeCert;
779
+ serverOptions.cert = serverOptions.cert || fakeCert;
780
+ }
781
+ if (typeof options.ipc === 'boolean') {
782
+ const isWindows = process.platform === 'win32';
783
+ const pipePrefix = isWindows ? '\\\\.\\pipe\\' : os.tmpdir();
784
+ const pipeName = 'webpack-dev-server.sock';
785
+ options.ipc = path.join(pipePrefix, pipeName);
786
+ }
787
+ options.liveReload =
788
+ typeof options.liveReload !== 'undefined' ? options.liveReload : true;
789
+ // https://github.com/webpack/webpack-dev-server/issues/1990
790
+ const defaultOpenOptions = { wait: false };
791
+ const getOpenItemsFromObject = ({ target, ...rest }) => {
792
+ const normalizedOptions = {
793
+ ...defaultOpenOptions,
794
+ ...rest,
795
+ };
796
+ if (typeof normalizedOptions.app === 'string') {
797
+ normalizedOptions.app = {
798
+ name: normalizedOptions.app,
799
+ };
800
+ }
801
+ const normalizedTarget = typeof target === 'undefined' ? '<url>' : target;
802
+ if (Array.isArray(normalizedTarget)) {
803
+ return normalizedTarget.map((singleTarget) => {
804
+ return { target: singleTarget, options: normalizedOptions };
805
+ });
806
+ }
807
+ return [{ target: normalizedTarget, options: normalizedOptions }];
808
+ };
809
+ if (typeof options.open === 'undefined') {
810
+ options.open = [];
811
+ }
812
+ else if (typeof options.open === 'boolean') {
813
+ options.open = options.open
814
+ ? [
815
+ {
816
+ target: '<url>',
817
+ options: defaultOpenOptions,
818
+ },
819
+ ]
820
+ : [];
821
+ }
822
+ else if (typeof options.open === 'string') {
823
+ options.open = [
824
+ { target: options.open, options: defaultOpenOptions },
825
+ ];
826
+ }
827
+ else if (Array.isArray(options.open)) {
828
+ const result = [];
829
+ for (const item of options.open) {
830
+ if (typeof item === 'string') {
831
+ result.push({ target: item, options: defaultOpenOptions });
832
+ // eslint-disable-next-line no-continue
833
+ continue;
834
+ }
835
+ result.push(...getOpenItemsFromObject(item));
836
+ }
837
+ options.open = result;
838
+ }
839
+ else {
840
+ options.open = [
841
+ ...getOpenItemsFromObject(options.open),
842
+ ];
843
+ }
844
+ if (typeof options.port === 'string' && options.port !== 'auto') {
845
+ options.port = Number(options.port);
846
+ }
847
+ /**
848
+ * Assume a proxy configuration specified as:
849
+ * proxy: {
850
+ * 'context': { options }
851
+ * }
852
+ * OR
853
+ * proxy: {
854
+ * 'context': 'target'
855
+ * }
856
+ */
857
+ if (typeof options.proxy !== 'undefined') {
858
+ options.proxy = options.proxy.map((item) => {
859
+ if (typeof item === 'function') {
860
+ return item;
861
+ }
862
+ const getLogLevelForProxy = (level) => {
863
+ if (level === 'none') {
864
+ return 'silent';
865
+ }
866
+ if (level === 'log') {
867
+ return 'info';
868
+ }
869
+ if (level === 'verbose') {
870
+ return 'debug';
871
+ }
872
+ return level;
873
+ };
874
+ if (typeof item.logLevel === 'undefined') {
875
+ item.logLevel = getLogLevelForProxy(compilerOptions.infrastructureLogging
876
+ ? compilerOptions.infrastructureLogging.level
877
+ : 'info');
878
+ }
879
+ if (typeof item.logProvider === 'undefined') {
880
+ item.logProvider = () => this.logger;
881
+ }
882
+ return item;
883
+ });
884
+ }
885
+ if (typeof options.setupExitSignals === 'undefined') {
886
+ options.setupExitSignals = true;
887
+ }
888
+ if (typeof options.static === 'undefined') {
889
+ options.static = [getStaticItem()];
890
+ }
891
+ else if (typeof options.static === 'boolean') {
892
+ options.static = options.static ? [getStaticItem()] : false;
893
+ }
894
+ else if (typeof options.static === 'string') {
895
+ options.static = [getStaticItem(options.static)];
896
+ }
897
+ else if (Array.isArray(options.static)) {
898
+ options.static = options.static.map((item) => getStaticItem(item));
899
+ }
900
+ else {
901
+ options.static = [getStaticItem(options.static)];
902
+ }
903
+ if (typeof options.watchFiles === 'string') {
904
+ options.watchFiles = [
905
+ { paths: options.watchFiles, options: getWatchOptions() },
906
+ ];
907
+ }
908
+ else if (typeof options.watchFiles === 'object' &&
909
+ options.watchFiles !== null &&
910
+ !Array.isArray(options.watchFiles)) {
911
+ options.watchFiles = [
912
+ {
913
+ paths: options.watchFiles.paths,
914
+ options: getWatchOptions(options.watchFiles.options || {}),
915
+ },
916
+ ];
917
+ }
918
+ else if (Array.isArray(options.watchFiles)) {
919
+ options.watchFiles = options.watchFiles.map((item) => {
920
+ if (typeof item === 'string') {
921
+ return { paths: item, options: getWatchOptions() };
922
+ }
923
+ return {
924
+ paths: item.paths,
925
+ options: getWatchOptions(item.options || {}),
926
+ };
927
+ });
928
+ }
929
+ else {
930
+ options.watchFiles = [];
931
+ }
932
+ const defaultWebSocketServerType = 'ws';
933
+ const defaultWebSocketServerOptions = { path: '/ws' };
934
+ if (typeof options.webSocketServer === 'undefined') {
935
+ options.webSocketServer = {
936
+ type: defaultWebSocketServerType,
937
+ options: defaultWebSocketServerOptions,
938
+ };
939
+ }
940
+ else if (typeof options.webSocketServer === 'boolean' &&
941
+ !options.webSocketServer) {
942
+ options.webSocketServer = false;
943
+ }
944
+ else if (typeof options.webSocketServer === 'string' ||
945
+ typeof options.webSocketServer === 'function') {
946
+ options.webSocketServer = {
947
+ type: options.webSocketServer,
948
+ options: defaultWebSocketServerOptions,
949
+ };
950
+ }
951
+ else {
952
+ options.webSocketServer = {
953
+ type: options.webSocketServer.type ||
954
+ defaultWebSocketServerType,
955
+ options: {
956
+ ...defaultWebSocketServerOptions,
957
+ ...options.webSocketServer.options,
958
+ },
959
+ };
960
+ const webSocketServer = options.webSocketServer;
961
+ if (typeof webSocketServer.options.port === 'string') {
962
+ webSocketServer.options.port = Number(webSocketServer.options.port);
963
+ }
964
+ }
965
+ }
966
+ /**
967
+ * @private
968
+ * @returns {string} client transport
969
+ */
970
+ getClientTransport() {
971
+ let clientImplementation;
972
+ let clientImplementationFound = true;
973
+ const isKnownWebSocketServerImplementation = this.options.webSocketServer &&
974
+ typeof this.options.webSocketServer
975
+ .type === 'string' &&
976
+ // @ts-expect-error
977
+ (this.options.webSocketServer.type === 'ws' ||
978
+ this.options.webSocketServer.type ===
979
+ 'sockjs');
980
+ let clientTransport;
981
+ if (this.options.client) {
982
+ if (typeof this.options.client
983
+ .webSocketTransport !== 'undefined') {
984
+ clientTransport = this.options.client
985
+ .webSocketTransport;
986
+ }
987
+ else if (isKnownWebSocketServerImplementation) {
988
+ clientTransport = this.options.webSocketServer.type;
989
+ }
990
+ else {
991
+ clientTransport = 'ws';
992
+ }
993
+ }
994
+ else {
995
+ clientTransport = 'ws';
996
+ }
997
+ switch (typeof clientTransport) {
998
+ case 'string':
999
+ // could be 'sockjs', 'ws', or a path that should be required
1000
+ if (clientTransport === 'sockjs') {
1001
+ clientImplementation = require.resolve('../client/clients/SockJSClient');
1002
+ }
1003
+ else if (clientTransport === 'ws') {
1004
+ clientImplementation = require.resolve('../client/clients/WebSocketClient');
1005
+ }
1006
+ else {
1007
+ try {
1008
+ clientImplementation = require.resolve(clientTransport);
1009
+ }
1010
+ catch {
1011
+ clientImplementationFound = false;
1012
+ }
1013
+ }
1014
+ break;
1015
+ default:
1016
+ clientImplementationFound = false;
1017
+ }
1018
+ if (!clientImplementationFound) {
1019
+ throw new Error(`${!isKnownWebSocketServerImplementation
1020
+ ? 'When you use custom web socket implementation you must explicitly specify client.webSocketTransport. '
1021
+ : ''}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 `);
1022
+ }
1023
+ return clientImplementation;
1024
+ }
1025
+ getServerTransport() {
1026
+ let implementation;
1027
+ let implementationFound = true;
1028
+ switch (typeof this.options.webSocketServer.type) {
1029
+ case 'string':
1030
+ // Could be 'sockjs', in the future 'ws', or a path that should be required
1031
+ if (this.options.webSocketServer
1032
+ .type === 'sockjs') {
1033
+ implementation = require('./servers/SockJSServer');
1034
+ }
1035
+ else if (this.options.webSocketServer
1036
+ .type === 'ws') {
1037
+ implementation = require('./servers/WebsocketServer');
1038
+ }
1039
+ else {
1040
+ try {
1041
+ implementation = require(this.options.webSocketServer
1042
+ .type);
1043
+ }
1044
+ catch {
1045
+ implementationFound = false;
1046
+ }
1047
+ }
1048
+ break;
1049
+ case 'function':
1050
+ implementation = this.options.webSocketServer.type;
1051
+ break;
1052
+ default:
1053
+ implementationFound = false;
1054
+ }
1055
+ if (!implementationFound) {
1056
+ throw new Error("webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. 'ws', 'sockjs'), a full path to " +
1057
+ 'a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) ' +
1058
+ 'via require.resolve(...), or the class itself which extends BaseServer');
1059
+ }
1060
+ return implementation;
1061
+ }
1062
+ getClientEntry() {
1063
+ return require.resolve('@rspack/dev-server/client/index');
1064
+ }
1065
+ getClientHotEntry() {
1066
+ if (this.options.hot === 'only') {
1067
+ return require.resolve('@rspack/core/hot/only-dev-server');
1068
+ }
1069
+ if (this.options.hot) {
1070
+ return require.resolve('@rspack/core/hot/dev-server');
1071
+ }
1072
+ }
1073
+ setupProgressPlugin() {
1074
+ const { ProgressPlugin } = this.compiler.compilers
1075
+ ? this.compiler.compilers[0].webpack
1076
+ : this.compiler.webpack;
1077
+ new ProgressPlugin((percent, msg, addInfo, pluginName) => {
1078
+ const percentValue = Math.floor(percent * 100);
1079
+ let msgValue = msg;
1080
+ if (percentValue === 100) {
1081
+ msgValue = 'Compilation completed';
1082
+ }
1083
+ if (addInfo) {
1084
+ msgValue = `${msgValue} (${addInfo})`;
1085
+ }
1086
+ if (this.webSocketServer) {
1087
+ this.sendMessage(this.webSocketServer.clients, 'progress-update', {
1088
+ percent: percentValue,
1089
+ msg: msgValue,
1090
+ pluginName,
1091
+ });
1092
+ }
1093
+ if (this.server) {
1094
+ this.server.emit('progress-update', { percent, msg, pluginName });
1095
+ }
1096
+ }).apply(this.compiler);
39
1097
  }
1098
+ /**
1099
+ * @private
1100
+ * @returns {Promise<void>}
1101
+ */
40
1102
  async initialize() {
41
1103
  const compilers = isMultiCompiler(this.compiler)
42
1104
  ? this.compiler.compilers
@@ -54,21 +1116,1107 @@ class RspackDevServer extends webpack_dev_server_1.default {
54
1116
  };
55
1117
  }
56
1118
  }
57
- // @ts-expect-error
58
- await super.initialize();
1119
+ this.setupHooks();
1120
+ await this.setupApp();
1121
+ await this.createServer();
1122
+ if (this.options.webSocketServer) {
1123
+ const compilers = this.compiler.compilers ||
1124
+ [this.compiler];
1125
+ for (const compiler of compilers) {
1126
+ if (compiler.options.devServer === false) {
1127
+ continue;
1128
+ }
1129
+ this.addAdditionalEntries(compiler);
1130
+ const webpack = compiler.webpack || require('webpack');
1131
+ new webpack.ProvidePlugin({
1132
+ __webpack_dev_server_client__: this.getClientTransport(),
1133
+ }).apply(compiler);
1134
+ if (this.options.hot) {
1135
+ const HMRPluginExists = compiler.options.plugins.find((plugin) => plugin &&
1136
+ plugin.constructor === webpack.HotModuleReplacementPlugin);
1137
+ if (HMRPluginExists) {
1138
+ this.logger.warn('"hot: true" automatically applies HMR plugin, you don\'t have to add it manually to your webpack configuration.');
1139
+ }
1140
+ else {
1141
+ // Apply the HMR plugin
1142
+ const plugin = new webpack.HotModuleReplacementPlugin();
1143
+ plugin.apply(compiler);
1144
+ }
1145
+ }
1146
+ }
1147
+ if (this.options.client &&
1148
+ this.options.client.progress) {
1149
+ this.setupProgressPlugin();
1150
+ }
1151
+ }
1152
+ this.setupWatchFiles();
1153
+ this.setupWatchStaticFiles();
1154
+ this.setupMiddlewares();
1155
+ if (this.options.setupExitSignals) {
1156
+ const signals = ['SIGINT', 'SIGTERM'];
1157
+ let needForceShutdown = false;
1158
+ for (const signal of signals) {
1159
+ // eslint-disable-next-line no-loop-func
1160
+ const listener = () => {
1161
+ if (needForceShutdown) {
1162
+ // eslint-disable-next-line n/no-process-exit
1163
+ process.exit();
1164
+ }
1165
+ this.logger.info('Gracefully shutting down. To force exit, press ^C again. Please wait...');
1166
+ needForceShutdown = true;
1167
+ this.stopCallback(() => {
1168
+ if (typeof this.compiler.close === 'function') {
1169
+ this.compiler.close(() => {
1170
+ // eslint-disable-next-line n/no-process-exit
1171
+ process.exit();
1172
+ });
1173
+ }
1174
+ else {
1175
+ // eslint-disable-next-line n/no-process-exit
1176
+ process.exit();
1177
+ }
1178
+ });
1179
+ };
1180
+ this.listeners.push({ name: signal, listener });
1181
+ process.on(signal, listener);
1182
+ }
1183
+ }
1184
+ // Proxy WebSocket without the initial http request
1185
+ // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
1186
+ const webSocketProxies = this.webSocketProxies;
1187
+ for (const webSocketProxy of webSocketProxies) {
1188
+ this.server.on('upgrade', webSocketProxy.upgrade);
1189
+ }
59
1190
  }
60
- getClientEntry() {
61
- return require.resolve('@rspack/dev-server/client/index');
1191
+ async setupApp() {
1192
+ this.app = (typeof this.options.app === 'function'
1193
+ ? await this.options.app()
1194
+ : getExpress()());
62
1195
  }
63
- getClientHotEntry() {
64
- if (this.options.hot === 'only') {
65
- return require.resolve('@rspack/core/hot/only-dev-server');
1196
+ getStats(statsObj) {
1197
+ const stats = Server.DEFAULT_STATS;
1198
+ const compilerOptions = this.getCompilerOptions();
1199
+ if (compilerOptions.stats &&
1200
+ compilerOptions.stats
1201
+ .warningsFilter) {
1202
+ stats.warningsFilter = compilerOptions.stats.warningsFilter;
66
1203
  }
67
- if (this.options.hot) {
68
- return require.resolve('@rspack/core/hot/dev-server');
1204
+ return statsObj.toJson(stats);
1205
+ }
1206
+ setupHooks() {
1207
+ this.compiler.hooks.invalid.tap('webpack-dev-server', () => {
1208
+ if (this.webSocketServer) {
1209
+ this.sendMessage(this.webSocketServer.clients, 'invalid');
1210
+ }
1211
+ });
1212
+ this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {
1213
+ if (this.webSocketServer) {
1214
+ this.sendStats(this.webSocketServer.clients, this.getStats(stats));
1215
+ }
1216
+ this.stats = stats;
1217
+ });
1218
+ }
1219
+ setupWatchStaticFiles() {
1220
+ const watchFiles = this.options.static;
1221
+ if (watchFiles.length > 0) {
1222
+ for (const item of watchFiles) {
1223
+ if (item.watch) {
1224
+ this.watchFiles(item.directory, item.watch);
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+ setupWatchFiles() {
1230
+ const watchFiles = this.options.watchFiles;
1231
+ if (watchFiles.length > 0) {
1232
+ for (const item of watchFiles) {
1233
+ this.watchFiles(item.paths, item.options);
1234
+ }
1235
+ }
1236
+ }
1237
+ setupMiddlewares() {
1238
+ let middlewares = [];
1239
+ // Register setup host header check for security
1240
+ middlewares.push({
1241
+ name: 'host-header-check',
1242
+ middleware: (req, res, next) => {
1243
+ const headers = req.headers;
1244
+ const headerName = headers[':authority'] ? ':authority' : 'host';
1245
+ if (this.isValidHost(headers, headerName)) {
1246
+ next();
1247
+ return;
1248
+ }
1249
+ res.statusCode = 403;
1250
+ res.end('Invalid Host header');
1251
+ },
1252
+ });
1253
+ // Register setup cross origin request check for security
1254
+ middlewares.push({
1255
+ name: 'cross-origin-header-check',
1256
+ middleware: (req, res, next) => {
1257
+ const headers = req.headers;
1258
+ const headerName = headers[':authority'] ? ':authority' : 'host';
1259
+ if (this.isValidHost(headers, headerName, false)) {
1260
+ next();
1261
+ return;
1262
+ }
1263
+ if (headers['sec-fetch-mode'] === 'no-cors' &&
1264
+ headers['sec-fetch-site'] === 'cross-site') {
1265
+ res.statusCode = 403;
1266
+ res.end('Cross-Origin request blocked');
1267
+ return;
1268
+ }
1269
+ next();
1270
+ },
1271
+ });
1272
+ const isHTTP2 = this.options.server.type === 'http2';
1273
+ if (isHTTP2) {
1274
+ // TODO patch for https://github.com/pillarjs/finalhandler/pull/45, need remove then will be resolved
1275
+ middlewares.push({
1276
+ name: 'http2-status-message-patch',
1277
+ middleware: (_req, res, next) => {
1278
+ Object.defineProperty(res, 'statusMessage', {
1279
+ get() {
1280
+ return '';
1281
+ },
1282
+ set() { },
1283
+ });
1284
+ next();
1285
+ },
1286
+ });
1287
+ }
1288
+ // compress is placed last and uses unshift so that it will be the first middleware used
1289
+ if (this.options.compress && !isHTTP2) {
1290
+ const compression = require('compression');
1291
+ middlewares.push({ name: 'compression', middleware: compression() });
1292
+ }
1293
+ if (typeof this.options.headers !== 'undefined') {
1294
+ middlewares.push({
1295
+ name: 'set-headers',
1296
+ middleware: this.setHeaders.bind(this),
1297
+ });
1298
+ }
1299
+ middlewares.push({
1300
+ name: 'webpack-dev-middleware',
1301
+ middleware: this.middleware,
1302
+ });
1303
+ // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
1304
+ middlewares.push({
1305
+ name: 'webpack-dev-server-sockjs-bundle',
1306
+ path: '/__webpack_dev_server__/sockjs.bundle.js',
1307
+ middleware: (req, res, next) => {
1308
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1309
+ next();
1310
+ return;
1311
+ }
1312
+ const clientPath = path.join(__dirname, '../', 'client/modules/sockjs-client/index.js');
1313
+ // Express send Etag and other headers by default, so let's keep them for compatibility reasons
1314
+ if (typeof res.sendFile === 'function') {
1315
+ res.sendFile(clientPath);
1316
+ return;
1317
+ }
1318
+ let stats;
1319
+ try {
1320
+ // TODO implement `inputFileSystem.createReadStream` in webpack
1321
+ stats = fs.statSync(clientPath);
1322
+ }
1323
+ catch {
1324
+ next();
1325
+ return;
1326
+ }
1327
+ res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
1328
+ res.setHeader('Content-Length', stats.size);
1329
+ if (req.method === 'HEAD') {
1330
+ res.end();
1331
+ return;
1332
+ }
1333
+ fs.createReadStream(clientPath).pipe(res);
1334
+ },
1335
+ });
1336
+ middlewares.push({
1337
+ name: 'webpack-dev-server-invalidate',
1338
+ path: '/webpack-dev-server/invalidate',
1339
+ middleware: (req, res, next) => {
1340
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1341
+ next();
1342
+ return;
1343
+ }
1344
+ this.invalidate();
1345
+ res.end();
1346
+ },
1347
+ });
1348
+ middlewares.push({
1349
+ name: 'webpack-dev-server-open-editor',
1350
+ path: '/webpack-dev-server/open-editor',
1351
+ middleware: (req, res, next) => {
1352
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1353
+ next();
1354
+ return;
1355
+ }
1356
+ if (!req.url) {
1357
+ next();
1358
+ return;
1359
+ }
1360
+ const resolveUrl = new URL(req.url, `http://${req.headers.host}`);
1361
+ const params = new URLSearchParams(resolveUrl.search);
1362
+ const fileName = params.get('fileName');
1363
+ if (typeof fileName === 'string') {
1364
+ const launchEditor = require('launch-editor');
1365
+ launchEditor(fileName);
1366
+ }
1367
+ res.end();
1368
+ },
1369
+ });
1370
+ middlewares.push({
1371
+ name: 'webpack-dev-server-assets',
1372
+ path: '/webpack-dev-server',
1373
+ middleware: (req, res, next) => {
1374
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1375
+ next();
1376
+ return;
1377
+ }
1378
+ if (!this.middleware) {
1379
+ next();
1380
+ return;
1381
+ }
1382
+ this.middleware.waitUntilValid((stats) => {
1383
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1384
+ // HEAD requests should not return body content
1385
+ if (req.method === 'HEAD') {
1386
+ res.end();
1387
+ return;
1388
+ }
1389
+ res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>');
1390
+ const statsForPrint = typeof stats.stats !== 'undefined'
1391
+ ? stats.toJson({})
1392
+ .children
1393
+ : [stats.toJson()];
1394
+ res.write('<h1>Assets Report:</h1>');
1395
+ for (const [index, item] of statsForPrint?.entries() ?? []) {
1396
+ res.write('<div>');
1397
+ const name = typeof item.name !== 'undefined'
1398
+ ? item.name
1399
+ : stats.stats
1400
+ ? `unnamed[${index}]`
1401
+ : 'unnamed';
1402
+ res.write(`<h2>Compilation: ${name}</h2>`);
1403
+ res.write('<ul>');
1404
+ const publicPath = item.publicPath === 'auto' ? '' : item.publicPath;
1405
+ const assets = item.assets;
1406
+ for (const asset of assets ?? []) {
1407
+ const assetName = asset.name;
1408
+ const assetURL = `${publicPath}${assetName}`;
1409
+ res.write(`<li>
1410
+ <strong><a href="${assetURL}" target="_blank">${assetName}</a></strong>
1411
+ </li>`);
1412
+ }
1413
+ res.write('</ul>');
1414
+ res.write('</div>');
1415
+ }
1416
+ res.end('</body></html>');
1417
+ });
1418
+ },
1419
+ });
1420
+ if (this.options.proxy) {
1421
+ const { createProxyMiddleware } = require('http-proxy-middleware');
1422
+ const getProxyMiddleware = (proxyConfig) => {
1423
+ // It is possible to use the `bypass` method without a `target` or `router`.
1424
+ // However, the proxy middleware has no use in this case, and will fail to instantiate.
1425
+ if (proxyConfig.target) {
1426
+ const context = proxyConfig.context || proxyConfig.path;
1427
+ return createProxyMiddleware(context, proxyConfig);
1428
+ }
1429
+ if (proxyConfig.router) {
1430
+ return createProxyMiddleware(proxyConfig);
1431
+ }
1432
+ // TODO improve me after drop `bypass` to always generate error when configuration is bad
1433
+ if (!proxyConfig.bypass) {
1434
+ util.deprecate(() => { }, `Invalid proxy configuration:\n\n${JSON.stringify(proxyConfig, null, 2)}\n\nThe use of proxy object notation as proxy routes has been removed.\nPlease use the 'router' or 'context' options. Read more at https://github.com/chimurai/http-proxy-middleware/tree/v2.0.6#http-proxy-middleware-options`, 'DEP_WEBPACK_DEV_SERVER_PROXY_ROUTES_ARGUMENT')();
1435
+ }
1436
+ };
1437
+ /**
1438
+ * @example
1439
+ * Assume a proxy configuration specified as:
1440
+ * proxy: [
1441
+ * {
1442
+ * context: "value",
1443
+ * ...options,
1444
+ * },
1445
+ * // or:
1446
+ * function() {
1447
+ * return {
1448
+ * context: "context",
1449
+ * ...options,
1450
+ * };
1451
+ * }
1452
+ * ]
1453
+ */
1454
+ for (const proxyConfigOrCallback of this.options.proxy) {
1455
+ let proxyMiddleware;
1456
+ let proxyConfig = typeof proxyConfigOrCallback === 'function'
1457
+ ? proxyConfigOrCallback()
1458
+ : proxyConfigOrCallback;
1459
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
1460
+ if (proxyConfig.ws && proxyMiddleware) {
1461
+ this.webSocketProxies.push(proxyMiddleware);
1462
+ }
1463
+ const handler = async (req, res, next) => {
1464
+ if (typeof proxyConfigOrCallback === 'function') {
1465
+ const newProxyConfig = proxyConfigOrCallback(req, res, next);
1466
+ if (newProxyConfig !== proxyConfig) {
1467
+ proxyConfig = newProxyConfig;
1468
+ const socket = req.socket || req.connection;
1469
+ const server = socket ? socket.server : null;
1470
+ if (server) {
1471
+ server.removeAllListeners('close');
1472
+ }
1473
+ proxyMiddleware = getProxyMiddleware(proxyConfig);
1474
+ }
1475
+ }
1476
+ // - Check if we have a bypass function defined
1477
+ // - In case the bypass function is defined we'll retrieve the
1478
+ // bypassUrl from it otherwise bypassUrl would be null
1479
+ // TODO remove in the next major in favor `context` and `router` options
1480
+ const isByPassFuncDefined = typeof proxyConfig.bypass === 'function';
1481
+ if (isByPassFuncDefined) {
1482
+ util.deprecate(() => { }, "Using the 'bypass' option is deprecated. Please use the 'router' or 'context' options. Read more at https://github.com/chimurai/http-proxy-middleware/tree/v2.0.6#http-proxy-middleware-options", 'DEP_WEBPACK_DEV_SERVER_PROXY_BYPASS_ARGUMENT')();
1483
+ }
1484
+ const bypassUrl = isByPassFuncDefined
1485
+ ? await proxyConfig.bypass(req, res, proxyConfig)
1486
+ : null;
1487
+ if (typeof bypassUrl === 'boolean') {
1488
+ // skip the proxy
1489
+ res.statusCode = 404;
1490
+ req.url = '';
1491
+ next();
1492
+ }
1493
+ else if (typeof bypassUrl === 'string') {
1494
+ // byPass to that url
1495
+ req.url = bypassUrl;
1496
+ next();
1497
+ }
1498
+ else if (proxyMiddleware) {
1499
+ return proxyMiddleware(req, res, next);
1500
+ }
1501
+ else {
1502
+ next();
1503
+ }
1504
+ };
1505
+ middlewares.push({
1506
+ name: 'http-proxy-middleware',
1507
+ middleware: handler,
1508
+ });
1509
+ // Also forward error requests to the proxy so it can handle them.
1510
+ middlewares.push({
1511
+ name: 'http-proxy-middleware-error-handler',
1512
+ middleware: (error, req, res, next) => handler(req, res, next),
1513
+ });
1514
+ }
1515
+ middlewares.push({
1516
+ name: 'webpack-dev-middleware',
1517
+ middleware: this.middleware,
1518
+ });
1519
+ }
1520
+ const staticOptions = this.options.static;
1521
+ if (staticOptions.length > 0) {
1522
+ for (const staticOption of staticOptions) {
1523
+ for (const publicPath of staticOption.publicPath) {
1524
+ middlewares.push({
1525
+ name: 'express-static',
1526
+ path: publicPath,
1527
+ middleware: getExpress().static(staticOption.directory, staticOption.staticOptions),
1528
+ });
1529
+ }
1530
+ }
1531
+ }
1532
+ if (this.options.historyApiFallback) {
1533
+ const connectHistoryApiFallback = require('connect-history-api-fallback');
1534
+ const { historyApiFallback } = this.options;
1535
+ if (typeof historyApiFallback ===
1536
+ 'undefined' &&
1537
+ !historyApiFallback.verbose) {
1538
+ historyApiFallback.logger = this.logger.log.bind(this.logger, '[connect-history-api-fallback]');
1539
+ }
1540
+ // Fall back to /index.html if nothing else matches.
1541
+ middlewares.push({
1542
+ name: 'connect-history-api-fallback',
1543
+ middleware: connectHistoryApiFallback(historyApiFallback),
1544
+ });
1545
+ // include our middleware to ensure
1546
+ // it is able to handle '/index.html' request after redirect
1547
+ middlewares.push({
1548
+ name: 'webpack-dev-middleware',
1549
+ middleware: this.middleware,
1550
+ });
1551
+ if (staticOptions.length > 0) {
1552
+ for (const staticOption of staticOptions) {
1553
+ for (const publicPath of staticOption.publicPath) {
1554
+ middlewares.push({
1555
+ name: 'express-static',
1556
+ path: publicPath,
1557
+ middleware: getExpress().static(staticOption.directory, staticOption.staticOptions),
1558
+ });
1559
+ }
1560
+ }
1561
+ }
1562
+ }
1563
+ if (staticOptions.length > 0) {
1564
+ const serveIndex = require('serve-index');
1565
+ for (const staticOption of staticOptions) {
1566
+ for (const publicPath of staticOption.publicPath) {
1567
+ if (staticOption.serveIndex) {
1568
+ middlewares.push({
1569
+ name: 'serve-index',
1570
+ path: publicPath,
1571
+ middleware: (req, res, next) => {
1572
+ // serve-index doesn't fallthrough non-get/head request to next middleware
1573
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1574
+ return next();
1575
+ }
1576
+ serveIndex(staticOption.directory, staticOption.serveIndex)(req, res, next);
1577
+ },
1578
+ });
1579
+ }
1580
+ }
1581
+ }
1582
+ }
1583
+ // Register this middleware always as the last one so that it's only used as a
1584
+ // fallback when no other middleware responses.
1585
+ middlewares.push({
1586
+ name: 'options-middleware',
1587
+ middleware: (req, res, next) => {
1588
+ if (req.method === 'OPTIONS') {
1589
+ res.statusCode = 204;
1590
+ res.setHeader('Content-Length', '0');
1591
+ res.end();
1592
+ return;
1593
+ }
1594
+ next();
1595
+ },
1596
+ });
1597
+ if (typeof this.options.setupMiddlewares === 'function') {
1598
+ middlewares = this.options.setupMiddlewares(middlewares, this);
1599
+ }
1600
+ // Lazy init webpack dev middleware
1601
+ const lazyInitDevMiddleware = () => {
1602
+ if (!this.middleware) {
1603
+ const webpackDevMiddleware = require('webpack-dev-middleware');
1604
+ // middleware for serving webpack bundle
1605
+ this.middleware = webpackDevMiddleware(this.compiler, this.options.devMiddleware);
1606
+ }
1607
+ return this.middleware;
1608
+ };
1609
+ for (const i of middlewares) {
1610
+ if (i.name === 'webpack-dev-middleware') {
1611
+ const item = i;
1612
+ if (typeof item.middleware === 'undefined') {
1613
+ item.middleware =
1614
+ lazyInitDevMiddleware();
1615
+ }
1616
+ }
1617
+ }
1618
+ for (const middleware of middlewares) {
1619
+ if (typeof middleware === 'function') {
1620
+ this.app.use(middleware);
1621
+ }
1622
+ else if (typeof middleware.path !== 'undefined') {
1623
+ this.app.use(middleware.path, middleware.middleware);
1624
+ }
1625
+ else {
1626
+ this.app.use(middleware.middleware);
1627
+ }
1628
+ }
1629
+ }
1630
+ /**
1631
+ * @private
1632
+ * @returns {Promise<void>}
1633
+ */
1634
+ async createServer() {
1635
+ const { type, options } = this.options.server;
1636
+ if (typeof type === 'function') {
1637
+ this.server = await type(options, this.app);
1638
+ }
1639
+ else {
1640
+ const serverType = require(type);
1641
+ this.server =
1642
+ type === 'http2'
1643
+ ? serverType.createSecureServer({ ...options, allowHTTP1: true }, this.app)
1644
+ : serverType.createServer(options, this.app);
1645
+ }
1646
+ this.isTlsServer =
1647
+ typeof this.server
1648
+ .setSecureContext !== 'undefined';
1649
+ this.server.on('connection', (socket) => {
1650
+ // Add socket to list
1651
+ this.sockets.push(socket);
1652
+ socket.once('close', () => {
1653
+ // Remove socket from list
1654
+ this.sockets.splice(this.sockets.indexOf(socket), 1);
1655
+ });
1656
+ });
1657
+ this.server.on('error', (error) => {
1658
+ throw error;
1659
+ });
1660
+ }
1661
+ createWebSocketServer() {
1662
+ // @ts-expect-error constructor
1663
+ this.webSocketServer = new (this.getServerTransport())(this);
1664
+ (this.webSocketServer?.implementation).on('connection', (client, request) => {
1665
+ const headers = typeof request !== 'undefined'
1666
+ ? request.headers
1667
+ : typeof client
1668
+ .headers !== 'undefined'
1669
+ ? client.headers
1670
+ : undefined;
1671
+ if (!headers) {
1672
+ this.logger.warn('webSocketServer implementation must pass headers for the "connection" event');
1673
+ }
1674
+ if (!headers ||
1675
+ !this.isValidHost(headers, 'host') ||
1676
+ !this.isValidHost(headers, 'origin') ||
1677
+ !this.isSameOrigin(headers)) {
1678
+ this.sendMessage([client], 'error', 'Invalid Host/Origin header');
1679
+ // With https enabled, the sendMessage above is encrypted asynchronously so not yet sent
1680
+ // Terminate would prevent it sending, so use close to allow it to be sent
1681
+ client.close();
1682
+ return;
1683
+ }
1684
+ if (this.options.hot === true || this.options.hot === 'only') {
1685
+ this.sendMessage([client], 'hot');
1686
+ }
1687
+ if (this.options.liveReload) {
1688
+ this.sendMessage([client], 'liveReload');
1689
+ }
1690
+ if (this.options.client &&
1691
+ this.options.client.progress) {
1692
+ this.sendMessage([client], 'progress', this.options.client.progress);
1693
+ }
1694
+ if (this.options.client &&
1695
+ this.options.client.reconnect) {
1696
+ this.sendMessage([client], 'reconnect', this.options.client.reconnect);
1697
+ }
1698
+ if (this.options.client &&
1699
+ this.options.client.overlay) {
1700
+ const overlayConfig = this.options.client
1701
+ .overlay;
1702
+ this.sendMessage([client], 'overlay', typeof overlayConfig === 'object'
1703
+ ? {
1704
+ ...overlayConfig,
1705
+ errors: overlayConfig.errors &&
1706
+ encodeOverlaySettings(overlayConfig.errors),
1707
+ warnings: overlayConfig.warnings &&
1708
+ encodeOverlaySettings(overlayConfig.warnings),
1709
+ runtimeErrors: overlayConfig.runtimeErrors &&
1710
+ encodeOverlaySettings(overlayConfig.runtimeErrors),
1711
+ }
1712
+ : overlayConfig);
1713
+ }
1714
+ if (!this.stats) {
1715
+ return;
1716
+ }
1717
+ this.sendStats([client], this.getStats(this.stats), true);
1718
+ });
1719
+ }
1720
+ async openBrowser(defaultOpenTarget) {
1721
+ const open = (await import('open')).default;
1722
+ Promise.all(this.options.open.map((item) => {
1723
+ let openTarget;
1724
+ if (item.target === '<url>') {
1725
+ openTarget = defaultOpenTarget;
1726
+ }
1727
+ else {
1728
+ openTarget = Server.isAbsoluteURL(item.target)
1729
+ ? item.target
1730
+ : new URL(item.target, defaultOpenTarget).toString();
1731
+ }
1732
+ // Type assertion needed: OpenOptions is compatible at runtime but TypeScript can't verify
1733
+ // the type match between our type definition and the ES module's type in CommonJS context
1734
+ return open(openTarget, item.options).catch(() => {
1735
+ const app = item.options.app;
1736
+ this.logger.warn(`Unable to open "${openTarget}" page${app
1737
+ ? ` in "${app.name}" app${app.arguments
1738
+ ? ` with "${app.arguments.join(' ')}" arguments`
1739
+ : ''}`
1740
+ : ''}. 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-name".`);
1741
+ });
1742
+ }));
1743
+ }
1744
+ runBonjour() {
1745
+ const { Bonjour } = require('bonjour-service');
1746
+ const type = this.isTlsServer ? 'https' : 'http';
1747
+ this.bonjour = new Bonjour();
1748
+ this.bonjour?.publish({
1749
+ name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
1750
+ port: this.options.port,
1751
+ type,
1752
+ subtypes: ['webpack'],
1753
+ ...this.options.bonjour,
1754
+ });
1755
+ }
1756
+ stopBonjour(callback = () => { }) {
1757
+ this.bonjour?.unpublishAll(() => {
1758
+ this.bonjour?.destroy();
1759
+ if (callback) {
1760
+ callback();
1761
+ }
1762
+ });
1763
+ }
1764
+ async logStatus() {
1765
+ const { cyan, isColorSupported, red } = require('colorette');
1766
+ const getColorsOption = (compilerOptions) => {
1767
+ let colorsEnabled;
1768
+ if (compilerOptions.stats &&
1769
+ typeof compilerOptions.stats.colors !==
1770
+ 'undefined') {
1771
+ colorsEnabled = compilerOptions.stats
1772
+ .colors;
1773
+ }
1774
+ else {
1775
+ colorsEnabled = isColorSupported;
1776
+ }
1777
+ return colorsEnabled;
1778
+ };
1779
+ const colors = {
1780
+ info(useColor, msg) {
1781
+ if (useColor) {
1782
+ return cyan(msg);
1783
+ }
1784
+ return msg;
1785
+ },
1786
+ error(useColor, msg) {
1787
+ if (useColor) {
1788
+ return red(msg);
1789
+ }
1790
+ return msg;
1791
+ },
1792
+ };
1793
+ const useColor = getColorsOption(this.getCompilerOptions());
1794
+ const server = this.server;
1795
+ if (this.options.ipc) {
1796
+ this.logger.info(`Project is running at: "${server?.address()}"`);
1797
+ }
1798
+ else {
1799
+ const protocol = this.isTlsServer ? 'https' : 'http';
1800
+ const addressInfo = server?.address();
1801
+ if (!addressInfo) {
1802
+ return;
1803
+ }
1804
+ const { address, port } = addressInfo;
1805
+ const prettyPrintURL = (newHostname) => url.format({ protocol, hostname: newHostname, port, pathname: '/' });
1806
+ let host;
1807
+ let localhost;
1808
+ let loopbackIPv4;
1809
+ let loopbackIPv6;
1810
+ let networkUrlIPv4;
1811
+ let networkUrlIPv6;
1812
+ if (this.options.host) {
1813
+ if (this.options.host === 'localhost') {
1814
+ localhost = prettyPrintURL('localhost');
1815
+ }
1816
+ else {
1817
+ let isIP;
1818
+ try {
1819
+ isIP = ipaddr.parse(this.options.host);
1820
+ }
1821
+ catch {
1822
+ // Ignore
1823
+ }
1824
+ if (!isIP) {
1825
+ host = prettyPrintURL(this.options.host);
1826
+ }
1827
+ }
1828
+ }
1829
+ const parsedIP = ipaddr.parse(address);
1830
+ if (parsedIP.range() === 'unspecified') {
1831
+ localhost = prettyPrintURL('localhost');
1832
+ loopbackIPv6 = prettyPrintURL('::1');
1833
+ const networkIPv4 = Server.findIp('v4', false);
1834
+ if (networkIPv4) {
1835
+ networkUrlIPv4 = prettyPrintURL(networkIPv4);
1836
+ }
1837
+ const networkIPv6 = Server.findIp('v6', false);
1838
+ if (networkIPv6) {
1839
+ networkUrlIPv6 = prettyPrintURL(networkIPv6);
1840
+ }
1841
+ }
1842
+ else if (parsedIP.range() === 'loopback') {
1843
+ if (parsedIP.kind() === 'ipv4') {
1844
+ loopbackIPv4 = prettyPrintURL(parsedIP.toString());
1845
+ }
1846
+ else if (parsedIP.kind() === 'ipv6') {
1847
+ loopbackIPv6 = prettyPrintURL(parsedIP.toString());
1848
+ }
1849
+ }
1850
+ else {
1851
+ networkUrlIPv4 =
1852
+ parsedIP.kind() === 'ipv6' && parsedIP.isIPv4MappedAddress()
1853
+ ? prettyPrintURL(parsedIP.toIPv4Address().toString())
1854
+ : prettyPrintURL(address);
1855
+ if (parsedIP.kind() === 'ipv6') {
1856
+ networkUrlIPv6 = prettyPrintURL(address);
1857
+ }
1858
+ }
1859
+ this.logger.info('Project is running at:');
1860
+ if (host) {
1861
+ this.logger.info(`Server: ${colors.info(useColor, host)}`);
1862
+ }
1863
+ if (localhost || loopbackIPv4 || loopbackIPv6) {
1864
+ const loopbacks = [];
1865
+ if (localhost) {
1866
+ loopbacks.push([colors.info(useColor, localhost)]);
1867
+ }
1868
+ if (loopbackIPv4) {
1869
+ loopbacks.push([colors.info(useColor, loopbackIPv4)]);
1870
+ }
1871
+ if (loopbackIPv6) {
1872
+ loopbacks.push([colors.info(useColor, loopbackIPv6)]);
1873
+ }
1874
+ this.logger.info(`Loopback: ${loopbacks.join(', ')}`);
1875
+ }
1876
+ if (networkUrlIPv4) {
1877
+ this.logger.info(`On Your Network (IPv4): ${colors.info(useColor, networkUrlIPv4)}`);
1878
+ }
1879
+ if (networkUrlIPv6) {
1880
+ this.logger.info(`On Your Network (IPv6): ${colors.info(useColor, networkUrlIPv6)}`);
1881
+ }
1882
+ if (this.options.open?.length > 0) {
1883
+ const openTarget = prettyPrintURL(!this.options.host ||
1884
+ this.options.host === '0.0.0.0' ||
1885
+ this.options.host === '::'
1886
+ ? 'localhost'
1887
+ : this.options.host);
1888
+ await this.openBrowser(openTarget);
1889
+ }
1890
+ }
1891
+ if (this.options.static?.length > 0) {
1892
+ this.logger.info(`Content not from webpack is served from '${colors.info(useColor, this.options.static
1893
+ .map((staticOption) => staticOption.directory)
1894
+ .join(', '))}' directory`);
1895
+ }
1896
+ if (this.options.historyApiFallback) {
1897
+ this.logger.info(`404s will fallback to '${colors.info(useColor, this.options.historyApiFallback
1898
+ .index || '/index.html')}'`);
1899
+ }
1900
+ if (this.options.bonjour) {
1901
+ const bonjourProtocol = this.options.bonjour?.type ||
1902
+ this.isTlsServer
1903
+ ? 'https'
1904
+ : 'http';
1905
+ this.logger.info(`Broadcasting "${bonjourProtocol}" with subtype of "webpack" via ZeroConf DNS (Bonjour)`);
1906
+ }
1907
+ }
1908
+ setHeaders(req, res, next) {
1909
+ let { headers } = this.options;
1910
+ if (headers) {
1911
+ if (typeof headers === 'function') {
1912
+ headers = headers(req, res, this.middleware ? this.middleware.context : undefined);
1913
+ }
1914
+ const allHeaders = [];
1915
+ if (!Array.isArray(headers)) {
1916
+ for (const name in headers) {
1917
+ allHeaders.push({
1918
+ key: name,
1919
+ value: headers[name],
1920
+ });
1921
+ }
1922
+ headers = allHeaders;
1923
+ }
1924
+ for (const { key, value } of headers) {
1925
+ res.setHeader(key, value);
1926
+ }
1927
+ }
1928
+ next();
1929
+ }
1930
+ isHostAllowed(value) {
1931
+ const { allowedHosts } = this.options;
1932
+ // allow user to opt out of this security check, at their own risk
1933
+ // by explicitly enabling allowedHosts
1934
+ if (allowedHosts === 'all') {
1935
+ return true;
1936
+ }
1937
+ // always allow localhost host, for convenience
1938
+ // allow if value is in allowedHosts
1939
+ if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
1940
+ for (const allowedHost of allowedHosts) {
1941
+ if (allowedHost === value) {
1942
+ return true;
1943
+ }
1944
+ // support "." as a subdomain wildcard
1945
+ // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
1946
+ if (allowedHost.startsWith('.') && // "example.com" (value === allowedHost.substring(1))
1947
+ // "*.example.com" (value.endsWith(allowedHost))
1948
+ (value === allowedHost.slice(1) || value.endsWith(allowedHost))) {
1949
+ return true;
1950
+ }
1951
+ }
1952
+ }
1953
+ // Also allow if `client.webSocketURL.hostname` provided
1954
+ if (this.options.client &&
1955
+ typeof this.options.client.webSocketURL !==
1956
+ 'undefined') {
1957
+ return (this.options.client.webSocketURL === value);
1958
+ }
1959
+ return false;
1960
+ }
1961
+ isValidHost(headers, headerToCheck, validateHost = true) {
1962
+ if (this.options.allowedHosts === 'all') {
1963
+ return true;
1964
+ }
1965
+ // get the Host header and extract hostname
1966
+ // we don't care about port not matching
1967
+ const header = headers[headerToCheck];
1968
+ if (!header) {
1969
+ return false;
69
1970
  }
1971
+ if (DEFAULT_ALLOWED_PROTOCOLS.test(header)) {
1972
+ return true;
1973
+ }
1974
+ // use the node url-parser to retrieve the hostname from the host-header.
1975
+ // TODO resolve me in the next major release
1976
+ // eslint-disable-next-line n/no-deprecated-api
1977
+ const { hostname } = url.parse(
1978
+ // if header doesn't have scheme, add // for parsing.
1979
+ /^(.+:)?\/\//.test(header) ? header : `//${header}`, false, true);
1980
+ if (hostname === null) {
1981
+ return false;
1982
+ }
1983
+ if (this.isHostAllowed(hostname)) {
1984
+ return true;
1985
+ }
1986
+ // always allow requests with explicit IPv4 or IPv6-address.
1987
+ // A note on IPv6 addresses:
1988
+ // header will always contain the brackets denoting
1989
+ // an IPv6-address in URLs,
1990
+ // these are removed from the hostname in url.parse(),
1991
+ // so we have the pure IPv6-address in hostname.
1992
+ // For convenience, always allow localhost (hostname === 'localhost')
1993
+ // and its subdomains (hostname.endsWith(".localhost")).
1994
+ // allow hostname of listening address (hostname === this.options.host)
1995
+ const isValidHostname = validateHost
1996
+ ? ipaddr.IPv4.isValid(hostname) ||
1997
+ ipaddr.IPv6.isValid(hostname) ||
1998
+ hostname === 'localhost' ||
1999
+ hostname.endsWith('.localhost') ||
2000
+ hostname === this.options.host
2001
+ : false;
2002
+ return isValidHostname;
2003
+ }
2004
+ isSameOrigin(headers) {
2005
+ if (this.options.allowedHosts === 'all') {
2006
+ return true;
2007
+ }
2008
+ const originHeader = headers.origin;
2009
+ if (!originHeader) {
2010
+ return this.options.allowedHosts === 'all';
2011
+ }
2012
+ if (DEFAULT_ALLOWED_PROTOCOLS.test(originHeader)) {
2013
+ return true;
2014
+ }
2015
+ // TODO resolve me in the next major release
2016
+ // eslint-disable-next-line n/no-deprecated-api
2017
+ const origin = url.parse(originHeader, false, true).hostname;
2018
+ if (origin === null) {
2019
+ return false;
2020
+ }
2021
+ if (this.isHostAllowed(origin)) {
2022
+ return true;
2023
+ }
2024
+ const hostHeader = headers.host;
2025
+ if (!hostHeader) {
2026
+ return this.options.allowedHosts === 'all';
2027
+ }
2028
+ if (DEFAULT_ALLOWED_PROTOCOLS.test(hostHeader)) {
2029
+ return true;
2030
+ }
2031
+ // eslint-disable-next-line n/no-deprecated-api
2032
+ const host = url.parse(
2033
+ // if hostHeader doesn't have scheme, add // for parsing.
2034
+ /^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`, false, true).hostname;
2035
+ if (host === null) {
2036
+ return false;
2037
+ }
2038
+ if (this.isHostAllowed(host)) {
2039
+ return true;
2040
+ }
2041
+ return origin === host;
2042
+ }
2043
+ sendMessage(clients, type, data, params) {
2044
+ for (const client of clients) {
2045
+ // `sockjs` uses `1` to indicate client is ready to accept data
2046
+ // `ws` uses `WebSocket.OPEN`, but it is mean `1` too
2047
+ if (client.readyState === 1) {
2048
+ client.send(JSON.stringify({ type, data, params }));
2049
+ }
2050
+ }
2051
+ }
2052
+ // Send stats to a socket or multiple sockets
2053
+ sendStats(clients, stats, force) {
2054
+ if (!stats) {
2055
+ return;
2056
+ }
2057
+ const shouldEmit = !force &&
2058
+ stats &&
2059
+ (!stats.errors || stats.errors.length === 0) &&
2060
+ (!stats.warnings || stats.warnings.length === 0) &&
2061
+ this.currentHash === stats.hash;
2062
+ if (shouldEmit) {
2063
+ this.sendMessage(clients, 'still-ok');
2064
+ return;
2065
+ }
2066
+ this.currentHash = stats.hash;
2067
+ this.sendMessage(clients, 'hash', stats.hash);
2068
+ if (stats.errors?.length > 0 ||
2069
+ stats.warnings?.length > 0) {
2070
+ const hasErrors = stats.errors?.length > 0;
2071
+ if (stats.warnings?.length >
2072
+ 0) {
2073
+ let params;
2074
+ if (hasErrors) {
2075
+ params = { preventReloading: true };
2076
+ }
2077
+ this.sendMessage(clients, 'warnings', stats.warnings, params);
2078
+ }
2079
+ if (stats.errors?.length > 0) {
2080
+ this.sendMessage(clients, 'errors', stats.errors);
2081
+ }
2082
+ }
2083
+ else {
2084
+ this.sendMessage(clients, 'ok');
2085
+ }
2086
+ }
2087
+ watchFiles(watchPath, watchOptions) {
2088
+ const chokidar = require('chokidar');
2089
+ const watcher = chokidar.watch(watchPath, watchOptions);
2090
+ // disabling refreshing on changing the content
2091
+ if (this.options.liveReload) {
2092
+ watcher.on('change', (item) => {
2093
+ if (this.webSocketServer) {
2094
+ this.sendMessage(this.webSocketServer.clients, 'static-changed', item);
2095
+ }
2096
+ });
2097
+ }
2098
+ this.staticWatchers.push(watcher);
2099
+ }
2100
+ invalidate(callback = () => { }) {
2101
+ if (this.middleware) {
2102
+ this.middleware.invalidate(callback);
2103
+ }
2104
+ }
2105
+ async start() {
2106
+ await this.normalizeOptions();
2107
+ if (this.options.ipc) {
2108
+ await new Promise((resolve, reject) => {
2109
+ const net = require('node:net');
2110
+ const socket = new net.Socket();
2111
+ socket.on('error', (error) => {
2112
+ if (error.code === 'ECONNREFUSED') {
2113
+ // No other server listening on this socket, so it can be safely removed
2114
+ fs.unlinkSync(this.options.ipc);
2115
+ resolve();
2116
+ return;
2117
+ }
2118
+ if (error.code === 'ENOENT') {
2119
+ resolve();
2120
+ return;
2121
+ }
2122
+ reject(error);
2123
+ });
2124
+ socket.connect({ path: this.options.ipc }, () => {
2125
+ throw new Error(`IPC "${this.options.ipc}" is already used`);
2126
+ });
2127
+ });
2128
+ }
2129
+ else {
2130
+ this.options.host = await Server.getHostname(this.options.host);
2131
+ this.options.port = await Server.getFreePort(this.options.port, this.options.host);
2132
+ }
2133
+ await this.initialize();
2134
+ const listenOptions = this.options.ipc
2135
+ ? { path: this.options.ipc }
2136
+ : { host: this.options.host, port: this.options.port };
2137
+ await new Promise((resolve) => {
2138
+ this.server.listen(listenOptions, () => {
2139
+ resolve();
2140
+ });
2141
+ });
2142
+ if (this.options.ipc) {
2143
+ // chmod 666 (rw rw rw)
2144
+ const READ_WRITE = 438;
2145
+ await fs.promises.chmod(this.options.ipc, READ_WRITE);
2146
+ }
2147
+ if (this.options.webSocketServer) {
2148
+ this.createWebSocketServer();
2149
+ }
2150
+ if (this.options.bonjour) {
2151
+ this.runBonjour();
2152
+ }
2153
+ await this.logStatus();
2154
+ if (typeof this.options.onListening === 'function') {
2155
+ this.options.onListening(this);
2156
+ }
2157
+ }
2158
+ startCallback(callback = () => { }) {
2159
+ this.start()
2160
+ .then(() => callback(), callback)
2161
+ .catch(callback);
2162
+ }
2163
+ async stop() {
2164
+ if (this.bonjour) {
2165
+ await new Promise((resolve) => {
2166
+ this.stopBonjour(() => {
2167
+ resolve();
2168
+ });
2169
+ });
2170
+ }
2171
+ this.webSocketProxies = [];
2172
+ await Promise.all(this.staticWatchers.map((watcher) => watcher.close()));
2173
+ this.staticWatchers = [];
2174
+ if (this.webSocketServer) {
2175
+ await new Promise((resolve) => {
2176
+ this.webSocketServer.implementation.close(() => {
2177
+ this.webSocketServer = null;
2178
+ resolve();
2179
+ });
2180
+ for (const client of this.webSocketServer.clients) {
2181
+ client.terminate();
2182
+ }
2183
+ this.webSocketServer.clients = [];
2184
+ });
2185
+ }
2186
+ if (this.server) {
2187
+ await new Promise((resolve) => {
2188
+ this.server.close(() => {
2189
+ this.server = undefined;
2190
+ resolve();
2191
+ });
2192
+ for (const socket of this.sockets) {
2193
+ socket.destroy();
2194
+ }
2195
+ this.sockets = [];
2196
+ });
2197
+ if (this.middleware) {
2198
+ await new Promise((resolve, reject) => {
2199
+ this.middleware.close((error) => {
2200
+ if (error) {
2201
+ reject(error);
2202
+ return;
2203
+ }
2204
+ resolve();
2205
+ });
2206
+ });
2207
+ this.middleware = undefined;
2208
+ }
2209
+ }
2210
+ // We add listeners to signals when creating a new Server instance
2211
+ // So ensure they are removed to prevent EventEmitter memory leak warnings
2212
+ for (const item of this.listeners) {
2213
+ process.removeListener(item.name, item.listener);
2214
+ }
2215
+ }
2216
+ stopCallback(callback = () => { }) {
2217
+ this.stop()
2218
+ .then(() => callback(), callback)
2219
+ .catch(callback);
70
2220
  }
71
2221
  }
72
- RspackDevServer.getFreePort = getFreePort;
73
- RspackDevServer.version = package_json_1.version;
74
- exports.RspackDevServer = RspackDevServer;
2222
+ exports.Server = Server;