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