UNPKG

webpack-dev-server

Version:

Serves a webpack app. Updates the browser on changes.

1,374 lines (1,280 loc) 107 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _nodeModule = require("node:module"); var _nodeOs = _interopRequireDefault(require("node:os")); var _nodePath = _interopRequireDefault(require("node:path")); var _nodeUrl = _interopRequireDefault(require("node:url")); var _gracefulFs = _interopRequireDefault(require("graceful-fs")); var _ipaddr = _interopRequireDefault(require("ipaddr.js")); var _schemaUtils = require("schema-utils"); var _options = _interopRequireDefault(require("./options.json")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } // Named `cjsRequire` (not `require`) so it doesn't shadow the implicit CommonJS // `require` in the transpiled `dist/cjs` build, which would collide with the // `require("url")` that `babel-plugin-transform-import-meta` injects here. const cjsRequire = (0, _nodeModule.createRequire)(require('url').pathToFileURL(__filename).toString()); /** @type {Promise<typeof import("webpack") | undefined> | undefined} */ let webpackPeer; /** * Lazily load the optional `webpack` peer dependency. * @returns {Promise<typeof import("webpack") | undefined>} resolved webpack module, or undefined when not installed */ function loadWebpackPeer() { webpackPeer ??= import("webpack").then(m => m.default).catch(() => undefined); return webpackPeer; } /** @typedef {import("schema-utils").Schema} Schema */ /** @typedef {import("webpack").Compiler} Compiler */ /** @typedef {import("webpack").MultiCompiler} MultiCompiler */ /** @typedef {import("webpack").Configuration} WebpackConfiguration */ /** @typedef {import("webpack").StatsOptions} StatsOptions */ /** @typedef {import("webpack").StatsCompilation} StatsCompilation */ /** @typedef {import("webpack").Stats} Stats */ /** @typedef {import("webpack").MultiStats} MultiStats */ /** @typedef {import("os").NetworkInterfaceInfo} NetworkInterfaceInfo */ /** @typedef {import("chokidar").ChokidarOptions} WatchOptions */ /** @typedef {import("chokidar").FSWatcher} FSWatcher */ /** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */ /** @typedef {import("bonjour-service").Bonjour} Bonjour */ /** @typedef {import("bonjour-service").Service} BonjourOptions */ /** @typedef {import("http-proxy-middleware").RequestHandler} RequestHandler */ /** @typedef {import("http-proxy-middleware").Options} HttpProxyMiddlewareOptions */ /** @typedef {import("http-proxy-middleware").Filter} HttpProxyMiddlewareOptionsFilter */ /** @typedef {import("serve-index").Options} ServeIndexOptions */ /** @typedef {import("serve-static").ServeStaticOptions} ServeStaticOptions */ /** @typedef {import("ipaddr.js").IPv4} IPv4 */ /** @typedef {import("ipaddr.js").IPv6} IPv6 */ /** @typedef {import("net").Socket} Socket */ /** @typedef {import("http").Server} HTTPServer */ /** @typedef {import("http").IncomingMessage} IncomingMessage */ /** @typedef {import("http").ServerResponse} ServerResponse */ /** @typedef {import("open").Options} OpenOptions */ /** @typedef {import("express").Application} ExpressApplication */ /** @typedef {import("express").RequestHandler} ExpressRequestHandler */ /** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */ /** @typedef {import("express").Request} ExpressRequest */ /** @typedef {import("express").Response} ExpressResponse */ // eslint-disable-next-line jsdoc/reject-any-type /** @typedef {any} EXPECTED_ANY */ /** @typedef {(err?: EXPECTED_ANY) => void} NextFunction */ /** @typedef {(req: IncomingMessage, res: ServerResponse) => void} SimpleHandleFunction */ /** @typedef {(req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} NextHandleFunction */ /** @typedef {(err: EXPECTED_ANY, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} ErrorHandleFunction */ /** @typedef {SimpleHandleFunction | NextHandleFunction | ErrorHandleFunction} HandleFunction */ /** @typedef {import("https").ServerOptions} ServerOptions */ /** * @template {BasicApplication} [T=ExpressApplication] * @typedef {T extends ExpressApplication ? ExpressRequest : IncomingMessage} Request */ /** * @template {BasicApplication} [T=ExpressApplication] * @typedef {T extends ExpressApplication ? ExpressResponse : ServerResponse} Response */ /** * @template {Request} T * @template {Response} U * @typedef {import("webpack-dev-middleware").Options<T, U>} DevMiddlewareOptions */ /** * @template {Request} T * @template {Response} U * @typedef {import("webpack-dev-middleware").Context<T, U>} DevMiddlewareContext */ /** * @typedef {"local-ip" | "local-ipv4" | "local-ipv6" | string} Host */ /** * @typedef {number | string | "auto"} Port */ /** * @typedef {object} WatchFiles * @property {string | string[]} paths paths * @property {(WatchOptions & { aggregateTimeout?: number, ignored?: WatchOptions["ignored"], poll?: number | boolean })=} options options */ /** * @typedef {object} Static * @property {string=} directory directory * @property {(string | string[])=} publicPath public path * @property {(boolean | ServeIndexOptions)=} serveIndex serve index * @property {ServeStaticOptions=} staticOptions static options * @property {(boolean | WatchOptions & { aggregateTimeout?: number, ignored?: WatchOptions["ignored"], poll?: number | boolean })=} watch watch and watch options */ /** * @typedef {object} NormalizedStatic * @property {string} directory * @property {string[]} publicPath * @property {false | ServeIndexOptions} serveIndex * @property {ServeStaticOptions} staticOptions * @property {false | WatchOptions} watch */ /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=import("http").Server] * @typedef {"http" | "https" | "http2" | string | ((serverOptions: ServerOptions, application: A) => S)} ServerType */ /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=import("http").Server] * @typedef {object} ServerConfiguration * @property {ServerType<A, S>=} type type * @property {ServerOptions=} options options */ /** * @typedef {object} WebSocketServerConfiguration * @property {("ws" | string | (() => WebSocketServerConfiguration))=} type type * @property {Record<string, EXPECTED_ANY>=} options options */ /** * @typedef {(import("ws").WebSocket & { send: import("ws").WebSocket["send"], terminate: import("ws").WebSocket["terminate"], ping: import("ws").WebSocket["ping"] }) & { isAlive?: boolean }} ClientConnection */ /** * @typedef {import("ws").WebSocketServer & { close: import("ws").WebSocketServer["close"] }} WebSocketServer */ /** * @typedef {{ implementation: WebSocketServer, clients: ClientConnection[] }} WebSocketServerImplementation */ /** * @typedef {{ path?: HttpProxyMiddlewareOptionsFilter | undefined, context?: HttpProxyMiddlewareOptionsFilter | undefined } & HttpProxyMiddlewareOptions} ProxyConfigArrayItem */ /** * @typedef {(ProxyConfigArrayItem | ((req?: Request | undefined, res?: Response | undefined, next?: NextFunction | undefined) => ProxyConfigArrayItem))[]} ProxyConfigArray */ /** * @typedef {object} OpenApp * @property {string=} name * @property {string[]=} arguments */ /** * @typedef {object} Open * @property {(string | string[] | OpenApp)=} app * @property {(string | string[])=} target target */ /** * @typedef {object} NormalizedOpen * @property {string} target * @property {import("open").Options} options */ /** * @typedef {object} WebSocketURL * @property {string=} hostname hostname * @property {string=} password password * @property {string=} pathname pathname * @property {(number | string)=} port port * @property {string=} protocol protocol * @property {string=} username username */ /** * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions */ /** * @typedef {object} ClientConfiguration * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"=} logging logging * @property {(boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions })=} overlay overlay * @property {boolean=} progress progress * @property {(boolean | number)=} reconnect reconnect * @property {("ws" | string)=} webSocketTransport web socket transport * @property {(string | WebSocketURL)=} webSocketURL web socket URL */ /** * @typedef {{ key: string, value: string }[] | Record<string, string | string[]>} Headers */ /** * @template {BasicApplication} [T=ExpressApplication] * @typedef {T extends ExpressApplication ? ExpressRequestHandler | ExpressErrorRequestHandler : HandleFunction} MiddlewareHandler */ /** * @typedef {{ name?: string, path?: string, middleware: MiddlewareHandler }} MiddlewareObject */ /** * @typedef {MiddlewareObject | MiddlewareHandler} Middleware */ /** @typedef {import("net").Server | import("tls").Server} BasicServer */ /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=import("http").Server] * @typedef {object} Configuration * @property {(boolean | string)=} ipc * @property {Host=} host * @property {Port=} port * @property {(boolean | "only")=} hot * @property {boolean=} liveReload * @property {DevMiddlewareOptions<Request, Response>=} devMiddleware * @property {boolean=} compress * @property {("auto" | "all" | string | string[])=} allowedHosts * @property {(boolean | ConnectHistoryApiFallbackOptions)=} historyApiFallback * @property {(boolean | Record<string, never> | BonjourOptions)=} bonjour * @property {(string | string[] | WatchFiles | (string | WatchFiles)[])=} watchFiles * @property {(boolean | string | Static | (string | Static)[])=} static * @property {(ServerType<A, S> | ServerConfiguration<A, S>)=} server * @property {(() => Promise<A>)=} app * @property {(boolean | "ws" | string | WebSocketServerConfiguration)=} webSocketServer * @property {ProxyConfigArray=} proxy * @property {(boolean | string | Open | (string | Open)[])=} open * @property {boolean=} setupExitSignals * @property {(boolean | ClientConfiguration)=} client * @property {(Headers | ((req: Request, res: Response, context: DevMiddlewareContext<Request, Response> | undefined) => Headers))=} headers * @property {((devServer: Server<A, S>) => void)=} onListening * @property {((middlewares: Middleware[], devServer: Server<A, S>) => Middleware[])=} setupMiddlewares */ if (!process.env.WEBPACK_SERVE) { process.env.WEBPACK_SERVE = "true"; } /** * @template T * @typedef {() => T} FunctionReturning */ /** * @template T * @param {FunctionReturning<T>} fn memorized function * @returns {FunctionReturning<T>} new function */ const memoize = fn => { let cache = false; /** @type {T | undefined} */ let result; return () => { if (cache) { return /** @type {T} */result; } result = fn(); cache = true; // Allow to clean up memory for fn // and all dependent resources /** @type {FunctionReturning<T> | undefined} */ fn = undefined; return /** @type {T} */result; }; }; const getExpress = memoize(async () => (await import("express")).default); /** * @param {OverlayMessageOptions=} setting overlay settings * @returns {undefined | string | boolean} encoded overlay settings */ const encodeOverlaySettings = setting => typeof setting === "function" ? encodeURIComponent(setting.toString()) : setting; // Working for overload, because typescript doesn't support this yes /** * @overload * @param {NextHandleFunction} fn function * @returns {BasicApplication} application */ /** * @overload * @param {HandleFunction} fn function * @returns {BasicApplication} application */ /** * @overload * @param {string} route route * @param {NextHandleFunction} fn function * @returns {BasicApplication} application */ /** * @param {string} route route * @param {HandleFunction} fn function * @returns {BasicApplication} application */ // eslint-disable-next-line no-unused-vars function useFn(route, fn) { return /** @type {BasicApplication} */{}; } const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i; /** * @typedef {object} BasicApplication * @property {typeof useFn} use */ const pluginName = "webpack-dev-server"; /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=HTTPServer] */ class Server { /** * @param {Configuration<A, S> | undefined} options options * @param {(Compiler | MultiCompiler)=} compiler compiler, omitted when the server is used as a plugin via `apply()` */ constructor(options, compiler) { options = options === undefined ? {} : options; (0, _schemaUtils.validate)(/** @type {Schema} */_options.default, options, { name: "Dev Server", baseDataPath: "options" }); if (compiler) { this.compiler = compiler; /** * @type {ReturnType<Compiler["getInfrastructureLogger"]>} */ this.logger = this.compiler.getInfrastructureLogger(pluginName); } this.options = options; /** * @type {FSWatcher[]} */ this.staticWatchers = []; /** * @private * @type {{ name: string | symbol, listener: (...args: EXPECTED_ANY[]) => void }[]} } */ this.listeners = []; // Keep track of websocket proxies for external websocket upgrade. /** * @private * @type {RequestHandler[]} */ this.webSocketProxies = []; /** * @type {Socket[]} */ this.sockets = []; /** * @private * @type {string | undefined} */ this.currentHash = undefined; /** * @private * @type {boolean} */ this.isPlugin = false; } static get schema() { return _options.default; } /** * @private * @returns {StatsOptions} default stats options */ static get DEFAULT_STATS() { return { all: false, hash: true, warnings: true, errors: true, errorDetails: false }; } /** * @param {string} URL url * @returns {boolean} true when URL is absolute, otherwise false */ static isAbsoluteURL(URL) { // Don't match Windows paths `c:\` if (/^[a-zA-Z]:\\/.test(URL)) { return false; } // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL); } /** * @param {string} gatewayOrFamily gateway or family * @param {boolean=} isInternal ip should be internal * @returns {string | undefined} resolved IP */ static findIp(gatewayOrFamily, isInternal) { if (gatewayOrFamily === "v4" || gatewayOrFamily === "v6") { let host; const networks = Object.values(_nodeOs.default.networkInterfaces()).flatMap(networks => networks ?? []).filter(network => { if (!network || !network.address) { return false; } if (network.family !== `IP${gatewayOrFamily}`) { return false; } if (typeof isInternal !== "undefined" && network.internal !== isInternal) { return false; } if (gatewayOrFamily === "v6") { const range = _ipaddr.default.parse(network.address).range(); if (range !== "ipv4Mapped" && range !== "uniqueLocal" && range !== "loopback") { return false; } } return network.address; }); if (networks.length > 0) { // Take the first network found host = networks[0].address; if (host.includes(":")) { host = `[${host}]`; } } return host; } const gatewayIp = _ipaddr.default.parse(gatewayOrFamily); // Look for the matching interface in all local interfaces. for (const addresses of Object.values(_nodeOs.default.networkInterfaces())) { for (const { cidr } of (/** @type {NetworkInterfaceInfo[]} */ addresses)) { const net = _ipaddr.default.parseCIDR(/** @type {string} */cidr); if (net[0] && net[0].kind() === gatewayIp.kind() && // eslint-disable-next-line unicorn/prefer-regexp-test gatewayIp.match(net)) { return net[0].toString(); } } } } /** * @param {Host} hostname hostname * @returns {Promise<string>} resolved hostname */ static async getHostname(hostname) { if (hostname === "local-ip") { return Server.findIp("v4", false) || Server.findIp("v6", false) || "0.0.0.0"; } else if (hostname === "local-ipv4") { return Server.findIp("v4", false) || "0.0.0.0"; } else if (hostname === "local-ipv6") { return Server.findIp("v6", false) || "::"; } return hostname; } /** * @param {Port} port port * @param {string} host host * @returns {Promise<number | string>} free port */ static async getFreePort(port, host) { if (typeof port !== "undefined" && port !== null && port !== "auto") { return port; } const { default: pRetry } = await import("p-retry"); const { default: getPort } = await Promise.resolve().then(() => require("./getPort.js")); const basePort = typeof process.env.WEBPACK_DEV_SERVER_BASE_PORT !== "undefined" ? Number.parseInt(process.env.WEBPACK_DEV_SERVER_BASE_PORT, 10) : 8080; // Try to find unused port and listen on it for 3 times, // if port is not specified in options. const defaultPortRetry = typeof process.env.WEBPACK_DEV_SERVER_PORT_RETRY !== "undefined" ? Number.parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10) : 3; return pRetry(() => getPort(basePort, host), { retries: defaultPortRetry }); } /** * @returns {string} path to cache dir */ static findCacheDir() { const cwd = process.cwd(); /** * @type {string | undefined} */ let dir = cwd; for (;;) { try { if (_gracefulFs.default.statSync(_nodePath.default.join(dir, "package.json")).isFile()) break; // eslint-disable-next-line no-empty } catch {} const parent = _nodePath.default.dirname(dir); if (dir === parent) { dir = undefined; break; } dir = parent; } if (!dir) { return _nodePath.default.resolve(cwd, `.cache/${pluginName}`); } else if (process.versions.pnp === "1") { return _nodePath.default.resolve(dir, `.pnp/.cache/${pluginName}`); } else if (process.versions.pnp === "3") { return _nodePath.default.resolve(dir, `.yarn/.cache/${pluginName}`); } return _nodePath.default.resolve(dir, `node_modules/.cache/${pluginName}`); } /** * @private * @param {Compiler} compiler compiler * @returns {boolean} true when target is `web` or `universal`, otherwise false */ static isWebTarget(compiler) { const { platform } = compiler; // A `web` or universal target (`web` and `node` both `null`) injects the // client. `target: false` is `null` everywhere, so it is excluded. return Boolean(platform.web || /** @type {{ universal?: boolean | null }} */platform?.universal || compiler.options.target !== false && platform.web === null && platform.node === null); } /** * @private * @param {Compiler} compiler compiler * @returns {Promise<void>} */ async addAdditionalEntries(compiler) { /** * @type {string[]} */ const additionalEntries = []; const isWebTarget = Server.isWebTarget(compiler); // TODO maybe empty client if (this.options.client && isWebTarget) { let webSocketURLStr = ""; if (this.options.webSocketServer) { const webSocketURL = /** @type {WebSocketURL} */ /** @type {ClientConfiguration} */ this.options.client.webSocketURL; const webSocketServer = /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable<WebSocketServerConfiguration["options"]> }} */ this.options.webSocketServer; const searchParams = new URLSearchParams(); /** @type {string} */ let protocol; // We are proxying dev server and need to specify custom `hostname` if (typeof webSocketURL.protocol !== "undefined") { protocol = webSocketURL.protocol; } else { protocol = this.isTlsServer ? "wss:" : "ws:"; } searchParams.set("protocol", protocol); if (typeof webSocketURL.username !== "undefined") { searchParams.set("username", webSocketURL.username); } if (typeof webSocketURL.password !== "undefined") { searchParams.set("password", webSocketURL.password); } /** @type {string} */ let hostname; const isWebSocketServerHostDefined = typeof webSocketServer.options.host !== "undefined"; const isWebSocketServerPortDefined = typeof webSocketServer.options.port !== "undefined"; // We are proxying dev server and need to specify custom `hostname` if (typeof webSocketURL.hostname !== "undefined") { hostname = webSocketURL.hostname; } // Web socket server works on custom `hostname`, only for `ws` because `sock-js` is not support custom `hostname` else if (isWebSocketServerHostDefined) { hostname = webSocketServer.options.host; } // The `host` option is specified else if (typeof this.options.host !== "undefined") { hostname = this.options.host; } // The `port` option is not specified else { hostname = "0.0.0.0"; } searchParams.set("hostname", hostname); /** @type {number | string} */ let port; // We are proxying dev server and need to specify custom `port` if (typeof webSocketURL.port !== "undefined") { port = webSocketURL.port; } // Web socket server works on custom `port`, only for `ws` because `sock-js` is not support custom `port` else if (isWebSocketServerPortDefined) { port = webSocketServer.options.port; } // The `port` option is specified else if (typeof this.options.port === "number") { port = this.options.port; } // The `port` option is specified using `string` else if (typeof this.options.port === "string" && this.options.port !== "auto") { port = Number(this.options.port); } // The `port` option is not specified or set to `auto` else { port = "0"; } searchParams.set("port", String(port)); /** @type {string} */ let pathname = ""; // We are proxying dev server and need to specify custom `pathname` if (typeof webSocketURL.pathname !== "undefined") { pathname = webSocketURL.pathname; } // Web socket server works on custom `path` else if (typeof webSocketServer.options.prefix !== "undefined" || typeof webSocketServer.options.path !== "undefined") { pathname = webSocketServer.options.prefix || webSocketServer.options.path; } searchParams.set("pathname", pathname); const client = /** @type {ClientConfiguration} */this.options.client; if (typeof client.logging !== "undefined") { searchParams.set("logging", client.logging); } if (typeof client.progress !== "undefined") { searchParams.set("progress", String(client.progress)); } if (typeof client.overlay !== "undefined") { const overlayString = typeof client.overlay === "boolean" ? String(client.overlay) : JSON.stringify({ ...client.overlay, errors: encodeOverlaySettings(client.overlay.errors), warnings: encodeOverlaySettings(client.overlay.warnings), runtimeErrors: encodeOverlaySettings(client.overlay.runtimeErrors) }); searchParams.set("overlay", overlayString); } if (typeof client.reconnect !== "undefined") { searchParams.set("reconnect", typeof client.reconnect === "number" ? String(client.reconnect) : "10"); } if (typeof this.options.hot !== "undefined") { searchParams.set("hot", String(this.options.hot)); } if (typeof this.options.liveReload !== "undefined") { searchParams.set("live-reload", String(this.options.liveReload)); } webSocketURLStr = searchParams.toString(); } additionalEntries.push(`${this.getClientEntry()}?${webSocketURLStr}`); } const clientHotEntry = this.getClientHotEntry(); if (clientHotEntry) { additionalEntries.push(clientHotEntry); } const webpack = compiler.webpack || (/** @type {NonNullable<Awaited<ReturnType<typeof loadWebpackPeer>>>} */ await loadWebpackPeer()); // use a hook to add entries if available for (const additionalEntry of additionalEntries) { new webpack.EntryPlugin(compiler.context, additionalEntry, { name: undefined }).apply(compiler); } } /** * @private * @returns {Compiler["options"]} compiler options */ getCompilerOptions() { if (typeof (/** @type {MultiCompiler} */this.compiler.compilers) !== "undefined") { if (/** @type {MultiCompiler} */this.compiler.compilers.length === 1) { return /** @type {MultiCompiler} */this.compiler.compilers[0].options; } // Configuration with the `devServer` options const compilerWithDevServer = /** @type {MultiCompiler} */ this.compiler.compilers.find(config => config.options.devServer); if (compilerWithDevServer) { return compilerWithDevServer.options; } // Configuration with `web` preset const compilerWithWebPreset = /** @type {MultiCompiler} */ this.compiler.compilers.find(config => Server.isWebTarget(config)); if (compilerWithWebPreset) { return compilerWithWebPreset.options; } // Fallback return /** @type {MultiCompiler} */this.compiler.compilers[0].options; } return /** @type {Compiler} */this.compiler.options; } /** * @private * @returns {Promise<void>} */ async normalizeOptions() { const { options } = this; const compilerOptions = this.getCompilerOptions(); const compilerWatchOptions = compilerOptions.watchOptions; /** * @param {WatchOptions & { aggregateTimeout?: number, ignored?: WatchOptions["ignored"], poll?: number | boolean }} watchOptions watch options * @returns {WatchOptions} normalized watch options */ const getWatchOptions = (watchOptions = {}) => { const getPolling = () => { if (typeof watchOptions.usePolling !== "undefined") { return watchOptions.usePolling; } if (typeof watchOptions.poll !== "undefined") { return Boolean(watchOptions.poll); } if (typeof compilerWatchOptions.poll !== "undefined") { return Boolean(compilerWatchOptions.poll); } return false; }; const getInterval = () => { if (typeof watchOptions.interval !== "undefined") { return watchOptions.interval; } if (typeof watchOptions.poll === "number") { return watchOptions.poll; } if (typeof compilerWatchOptions.poll === "number") { return compilerWatchOptions.poll; } }; const usePolling = getPolling(); const interval = getInterval(); const { poll: _poll, interval: _interval, ...rest } = watchOptions; return { ignoreInitial: true, persistent: true, followSymlinks: false, atomic: false, alwaysStat: true, ignorePermissionErrors: true, // Respect options from compiler watchOptions usePolling, ...(interval !== undefined ? { interval } : {}), ...rest }; }; /** * @param {(string | Static | undefined)=} optionsForStatic for static * @returns {NormalizedStatic} normalized options for static */ const getStaticItem = optionsForStatic => { const getDefaultStaticOptions = () => ({ directory: _nodePath.default.join(process.cwd(), "public"), staticOptions: {}, publicPath: ["/"], serveIndex: { icons: true }, watch: getWatchOptions() }); /** @type {NormalizedStatic} */ let item; if (typeof optionsForStatic === "undefined") { item = getDefaultStaticOptions(); } else if (typeof optionsForStatic === "string") { item = { ...getDefaultStaticOptions(), directory: optionsForStatic }; } else { const def = getDefaultStaticOptions(); item = { directory: typeof optionsForStatic.directory !== "undefined" ? optionsForStatic.directory : def.directory, staticOptions: typeof optionsForStatic.staticOptions !== "undefined" ? { ...def.staticOptions, ...optionsForStatic.staticOptions } : def.staticOptions, publicPath: typeof optionsForStatic.publicPath !== "undefined" ? Array.isArray(optionsForStatic.publicPath) ? optionsForStatic.publicPath : [optionsForStatic.publicPath] : def.publicPath, serveIndex: // Check if 'serveIndex' property is defined in 'optionsForStatic' // If 'serveIndex' is a boolean and true, use default 'serveIndex' // If 'serveIndex' is an object, merge its properties with default 'serveIndex' // If 'serveIndex' is neither a boolean true nor an object, use it as-is // If 'serveIndex' is not defined in 'optionsForStatic', use default 'serveIndex' typeof optionsForStatic.serveIndex !== "undefined" ? typeof optionsForStatic.serveIndex === "boolean" && optionsForStatic.serveIndex ? def.serveIndex : typeof optionsForStatic.serveIndex === "object" ? { ...def.serveIndex, ...optionsForStatic.serveIndex } : optionsForStatic.serveIndex : def.serveIndex, watch: typeof optionsForStatic.watch !== "undefined" ? typeof optionsForStatic.watch === "boolean" ? optionsForStatic.watch ? def.watch : false : getWatchOptions(optionsForStatic.watch) : def.watch }; } if (Server.isAbsoluteURL(item.directory)) { throw new Error("Using a URL as static.directory is not supported"); } return item; }; if (typeof options.allowedHosts === "undefined") { // AllowedHosts allows some default hosts picked from `options.host` or `webSocketURL.hostname` and `localhost` options.allowedHosts = "auto"; } // We store allowedHosts as array when supplied as string else if (typeof options.allowedHosts === "string" && options.allowedHosts !== "auto" && options.allowedHosts !== "all") { options.allowedHosts = [options.allowedHosts]; } // CLI pass options as array, we should normalize them else if (Array.isArray(options.allowedHosts) && options.allowedHosts.includes("all")) { options.allowedHosts = "all"; } if (typeof options.bonjour === "undefined") { options.bonjour = false; } else if (typeof options.bonjour === "boolean") { options.bonjour = options.bonjour ? {} : false; } if (typeof options.client === "undefined" || typeof options.client === "object" && options.client !== null) { if (!options.client) { options.client = {}; } if (typeof options.client.webSocketURL === "undefined") { options.client.webSocketURL = {}; } else if (typeof options.client.webSocketURL === "string") { const parsedURL = new URL(options.client.webSocketURL); options.client.webSocketURL = { protocol: parsedURL.protocol, hostname: parsedURL.hostname, port: parsedURL.port.length > 0 ? Number(parsedURL.port) : "", pathname: parsedURL.pathname, username: parsedURL.username, password: parsedURL.password }; } else if (typeof options.client.webSocketURL.port === "string") { options.client.webSocketURL.port = Number(options.client.webSocketURL.port); } // Enable client overlay by default if (typeof options.client.overlay === "undefined") { options.client.overlay = true; } else if (typeof options.client.overlay !== "boolean") { options.client.overlay = { errors: true, warnings: true, ...options.client.overlay }; } if (typeof options.client.reconnect === "undefined") { options.client.reconnect = 10; } else if (options.client.reconnect === true) { options.client.reconnect = Infinity; } else if (options.client.reconnect === false) { options.client.reconnect = 0; } // Respect infrastructureLogging.level if (typeof options.client.logging === "undefined") { options.client.logging = compilerOptions.infrastructureLogging ? compilerOptions.infrastructureLogging.level : "info"; } } if (typeof options.compress === "undefined") { options.compress = true; } if (typeof options.devMiddleware === "undefined") { options.devMiddleware = {}; } // No need to normalize `headers` if (typeof options.historyApiFallback === "undefined") { options.historyApiFallback = false; } else if (typeof options.historyApiFallback === "boolean" && options.historyApiFallback) { options.historyApiFallback = {}; } // No need to normalize `host` options.hot = typeof options.hot === "boolean" || options.hot === "only" ? options.hot : true; if (typeof options.server === "function" || typeof options.server === "string") { options.server = { type: options.server, options: {} }; } else { const serverOptions = /** @type {ServerConfiguration<A, S>} */ options.server || {}; options.server = { type: serverOptions.type || "http", options: { ...serverOptions.options } }; } const serverOptions = /** @type {ServerOptions} */options.server.options; if (options.server.type === "https" || options.server.type === "http2") { if (typeof serverOptions.requestCert === "undefined") { serverOptions.requestCert = false; } const httpsProperties = /** @type {(keyof ServerOptions)[]} */ ["ca", "cert", "crl", "key", "pfx"]; for (const property_ of httpsProperties) { const property = /** @type {keyof ServerOptions} */property_; if (typeof serverOptions[property] === "undefined") { continue; } const value = serverOptions[property]; /** * @param {string | Buffer | undefined} item file to read * @returns {string | Buffer | undefined} content of file */ const readFile = item => { if (Buffer.isBuffer(item) || typeof item === "object" && item !== null && !Array.isArray(item)) { return item; } if (item) { let stats = null; try { stats = _gracefulFs.default.lstatSync(_gracefulFs.default.realpathSync(item)).isFile(); } catch { // Ignore error } // It is a file return stats ? _gracefulFs.default.readFileSync(item) : item; } }; /** @type {EXPECTED_ANY} */ serverOptions[property] = Array.isArray(value) ? value.map(item => readFile(/** @type {string | Buffer | undefined} */ item)) : readFile(/** @type {string | Buffer | undefined} */ value); } let fakeCert; if (!serverOptions.key || !serverOptions.cert) { const certificateDir = Server.findCacheDir(); const certificatePath = _nodePath.default.join(certificateDir, "server.pem"); let certificateExists; try { const certificate = await _gracefulFs.default.promises.stat(certificatePath); certificateExists = certificate.isFile(); } catch { certificateExists = false; } if (certificateExists) { const certificateTtl = 1000 * 60 * 60 * 24; const certificateStat = await _gracefulFs.default.promises.stat(certificatePath); const now = Date.now(); // cert is more than 30 days old, kill it with fire if ((now - Number(certificateStat.ctime)) / certificateTtl > 30) { this.logger.info("SSL certificate is more than 30 days old. Removing..."); await _gracefulFs.default.promises.rm(certificatePath, { recursive: true }); certificateExists = false; } } if (!certificateExists) { this.logger.info("Generating SSL certificate..."); const { default: selfsigned } = await import("selfsigned"); const attributes = [{ name: "commonName", value: "localhost" }]; const notBeforeDate = new Date(); const notAfterDate = new Date(); notAfterDate.setDate(notAfterDate.getDate() + 30); const pems = await selfsigned.generate(attributes, { algorithm: "sha256", keySize: 2048, notBeforeDate, notAfterDate, extensions: [{ name: "basicConstraints", cA: true }, { name: "keyUsage", keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true }, { name: "extKeyUsage", serverAuth: true, clientAuth: true, codeSigning: true, timeStamping: true }, { name: "subjectAltName", altNames: [{ // type 2 is DNS type: 2, value: "localhost" }, { type: 2, value: "localhost.localdomain" }, { type: 2, value: "lvh.me" }, { type: 2, value: "*.lvh.me" }, { type: 2, value: "[::1]" }, { // type 7 is IP type: 7, ip: "127.0.0.1" }, { type: 7, ip: "fe80::1" }] }] }); await _gracefulFs.default.promises.mkdir(certificateDir, { recursive: true }); await _gracefulFs.default.promises.writeFile(certificatePath, pems.private + pems.cert, { encoding: "utf8" }); } fakeCert = await _gracefulFs.default.promises.readFile(certificatePath); this.logger.info(`SSL certificate: ${certificatePath}`); } serverOptions.key ||= fakeCert; serverOptions.cert ||= fakeCert; } if (typeof options.ipc === "boolean") { const isWindows = process.platform === "win32"; const pipePrefix = isWindows ? "\\\\.\\pipe\\" : _nodeOs.default.tmpdir(); const pipeName = `${pluginName}.sock`; options.ipc = _nodePath.default.join(pipePrefix, pipeName); } options.liveReload = typeof options.liveReload !== "undefined" ? options.liveReload : true; // https://github.com/webpack/webpack-dev-server/issues/1990 const defaultOpenOptions = { wait: false }; /** * @param {import("open").Options & { target?: string | string[] } & EXPECTED_ANY} target target * @returns {NormalizedOpen[]} normalized open options */ const getOpenItemsFromObject = ({ target, ...rest }) => { const normalizedOptions = { ...defaultOpenOptions, ...rest }; if (typeof normalizedOptions.app === "string") { normalizedOptions.app = { name: normalizedOptions.app }; } const normalizedTarget = typeof target === "undefined" ? "<url>" : target; if (Array.isArray(normalizedTarget)) { return normalizedTarget.map(singleTarget => ({ target: singleTarget, options: normalizedOptions })); } return [{ target: normalizedTarget, options: normalizedOptions }]; }; if (typeof options.open === "undefined") { /** @type {NormalizedOpen[]} */ options.open = []; } else if (typeof options.open === "boolean") { /** @type {NormalizedOpen[]} */ options.open = options.open ? [{ target: "<url>", options: (/** @type {OpenOptions} */defaultOpenOptions) }] : []; } else if (typeof options.open === "string") { /** @type {NormalizedOpen[]} */ options.open = [{ target: options.open, options: defaultOpenOptions }]; } else if (Array.isArray(options.open)) { /** * @type {NormalizedOpen[]} */ const result = []; for (const item of options.open) { if (typeof item === "string") { result.push({ target: item, options: defaultOpenOptions }); continue; } result.push(...getOpenItemsFromObject(item)); } /** @type {NormalizedOpen[]} */ options.open = result; } else { /** @type {NormalizedOpen[]} */ options.open = [...getOpenItemsFromObject(options.open)]; } if (typeof options.port === "string" && options.port !== "auto") { options.port = Number(options.port); } /** * Assume a proxy configuration specified as: * proxy: { 'context': { options } } * OR * proxy: { 'context': 'target' } */ if (typeof options.proxy !== "undefined") { options.proxy = options.proxy.map(item => { if (typeof item === "function") { return item; } if (typeof item.logger === "undefined") { item.logger = this.logger; } return item; }); } if (typeof options.setupExitSignals === "undefined") { // In plugin mode, the host (e.g. `webpack-cli`) usually owns process // signal handling and calls `compiler.close()` on shutdown, which fires // our `shutdown` hook. Adding our own SIGINT/SIGTERM listeners on top of // that would race with the host's handler and call `compiler.close()` // twice. options.setupExitSignals = !this.isPlugin; } if (typeof options.static === "undefined") { options.static = [getStaticItem()]; } else if (typeof options.static === "boolean") { options.static = options.static ? [getStaticItem()] : false; } else if (typeof options.static === "string") { options.static = [getStaticItem(options.static)]; } else if (Array.isArray(options.static)) { options.static = options.static.map(item => getStaticItem(item)); } else { options.static = [getStaticItem(options.static)]; } if (typeof options.watchFiles === "string") { options.watchFiles = [{ paths: options.watchFiles, options: getWatchOptions() }]; } else if (typeof options.watchFiles === "object" && options.watchFiles !== null && !Array.isArray(options.watchFiles)) { options.watchFiles = [{ paths: options.watchFiles.paths, options: getWatchOptions(options.watchFiles.options || {}) }]; } else if (Array.isArray(options.watchFiles)) { options.watchFiles = options.watchFiles.map(item => { if (typeof item === "string") { return { paths: item, options: getWatchOptions() }; } return { paths: item.paths, options: getWatchOptions(item.options || {}) }; }); } else { options.watchFiles = []; } const defaultWebSocketServerType = "ws"; const defaultWebSocketServerOptions = { path: "/ws" }; if (typeof options.webSocketServer === "undefined") { options.webSocketServer = { type: defaultWebSocketServerType, options: defaultWebSocketServerOptions }; } else if (typeof options.webSocketServer === "boolean" && !options.webSocketServer) { options.webSocketServer = false; } else if (typeof options.webSocketServer === "string" || typeof options.webSocketServer === "function") { options.webSocketServer = { type: options.webSocketServer, options: defaultWebSocketServerOptions }; } else { options.webSocketServer = { type: /** @type {WebSocketServerConfiguration} */ options.webSocketServer.type || defaultWebSocketServerType, options: { ...defaultWebSocketServerOptions, ... /** @type {WebSocketServerConfiguration} */ options.webSocketServer.options } }; const webSocketServer = /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable<WebSocketServerConfiguration["options"]> }} */ options.webSocketServer; if (typeof webSocketServer.options.port === "string") { webSocketServer.options.port = Number(webSocketServer.options.port); } } } /** * @returns {{ isColorSupported: () => boolean, colors: import("webpack").Colors }} colors support */ #getColors() { const compilerOptions = /** @type {MultiCompiler} */ this.compiler.compilers ? /** @type {MultiCompiler} */this.compiler.compilers[0].webpack : /** @type {Compiler} */this.compiler.webpack; const colors = compilerOptions.cli.createColors({ useColor: compilerOptions.cli.isColorSupported() }); return { isColorSupported: compilerOptions.cli.isColorSupported, colors }; } /** * @private * @returns {string} client transport */ getClientTransport() { let clientImplementation; let clientImplementationFound = true; const isKnownWebSocketServerImplementation = this.options.webSocketServer && typeof (/** @type {WebSocketServerConfiguration} */ this.options.webSocketServer.type) === "string" && // @ts-expect-error this.options.webSocketServer.type === "ws"; let clientTransport; if (this.options.client) { if (typeof (/** @type {ClientConfiguration} */ this.options.client.webSocketTransport) !== "undefined") { clientTransport = /** @type {ClientConfiguration} */ this.options.client.webSocketTransport; } else if (isKnownWebSocketServerImplementation) { clientTransport = /** @type {WebSocketServerConfiguration} */ this.options.webSocketServer.type; } else { clientTransport = "ws"; } } else { clientTransport = "ws"; } switch (typeof clientTransport) { case "string": // could be 'ws', or a path that should be resolved if (clientTransport === "ws") { clientImplementation = cjsRequire.resolve("../client/clients/WebSocketClient.js"); } else { try { clientImplementation = cjsRequire.resolve(clientTransport); } catch { clientImplementationFound = false; } } break; default: clientImplementationFound = false; } if (!clientImplementationFound) { throw new Error(`${!isKnownWebSocketServerImplementation ? "When you use custom web socket implementation you must explicitly specify client.webSocketTransport. " : ""}client.webSocketTransport must be a string denoting a default implementation (e.g. 'ws') or a resolvable module specifier or absolute file path which exports a class `); } return /** @type {string} */clientImplementation; } /** * @template T * @private * @returns {Promise<T>} server transport */ async getServerTransport() { let implementation; let implementationFound = true; switch (typeof (/** @type {WebSocketServerConfiguration} */ this.options.webSocketServer.type)) { case "string": // Could be 'ws', or a path that should be required if (/** @type {WebSocketServerConfiguration} */this.options.webSocketServer.type === "ws") { implementation = (await Promise.resolve().then(() => require("./servers/WebsocketServer.js"))).default; } else { try { const mod = cjsRequire(/** @type {string} */ /** @type {WebSocketServerConfiguration} */ this.options.webSocketServer.type); implementation = mod.default || mod; } catch { implementationFound = false; } } break; case "function": implementation = /** @type {WebSocketServerConfiguration} */ this.options.webSocketServer.type; break; default: implementationFound = false; } if (!implementationFound) { throw new Error("webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. 'ws'), a resolvable module specifier or absolute file path " + "which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js), " + "or the class itself which extends BaseServer"); } return implementation; } /** * @returns {string} */ getClientEntry() { return cjsRequire.resolve("../client/index.js"); } /** * @returns {string | void} client hot entry */ getClientHotEntry() { if (this.options.hot === "only") { return cjsRequire.resolve("webpack/hot/only-dev-server"); } else if (this.options.hot) { return cjsRequire.resolve("webpack/hot/dev-server"); } }