| "use strict"; |
| |
| const os = require("os"); |
| const path = require("path"); |
| const url = require("url"); |
| const util = require("util"); |
| const fs = require("graceful-fs"); |
| const ipaddr = require("ipaddr.js"); |
| const defaultGateway = require("default-gateway"); |
| const express = require("express"); |
| const { validate } = require("schema-utils"); |
| const schema = require("./options.json"); |
| |
| /** @typedef {import("schema-utils/declarations/validate").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("express").Request} Request */ |
| /** @typedef {import("express").Response} Response */ |
| /** @typedef {import("express").NextFunction} NextFunction */ |
| /** @typedef {import("express").RequestHandler} ExpressRequestHandler */ |
| /** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */ |
| /** @typedef {import("chokidar").WatchOptions} WatchOptions */ |
| /** @typedef {import("chokidar").FSWatcher} FSWatcher */ |
| /** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */ |
| /** @typedef {import("bonjour").Bonjour} Bonjour */ |
| /** @typedef {import("bonjour").BonjourOptions} 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").IncomingMessage} IncomingMessage */ |
| /** @typedef {import("open").Options} OpenOptions */ |
| |
| /** @typedef {import("https").ServerOptions & { spdy?: { plain?: boolean | undefined, ssl?: boolean | undefined, 'x-forwarded-for'?: string | undefined, protocol?: string | undefined, protocols?: string[] | undefined }}} ServerOptions */ |
| |
| /** |
| * @template Request, Response |
| * @typedef {import("webpack-dev-middleware").Options<Request, Response>} DevMiddlewareOptions |
| */ |
| |
| /** |
| * @template Request, Response |
| * @typedef {import("webpack-dev-middleware").Context<Request, Response>} DevMiddlewareContext |
| */ |
| |
| /** |
| * @typedef {"local-ip" | "local-ipv4" | "local-ipv6" | string} Host |
| */ |
| |
| /** |
| * @typedef {number | string | "auto"} Port |
| */ |
| |
| /** |
| * @typedef {Object} WatchFiles |
| * @property {string | string[]} paths |
| * @property {WatchOptions & { aggregateTimeout?: number, ignored?: string | RegExp | string[], poll?: number | boolean }} [options] |
| */ |
| |
| /** |
| * @typedef {Object} Static |
| * @property {string} [directory] |
| * @property {string | string[]} [publicPath] |
| * @property {boolean | ServeIndexOptions} [serveIndex] |
| * @property {ServeStaticOptions} [staticOptions] |
| * @property {boolean | WatchOptions & { aggregateTimeout?: number, ignored?: string | RegExp | string[], poll?: number | boolean }} [watch] |
| */ |
| |
| /** |
| * @typedef {Object} NormalizedStatic |
| * @property {string} directory |
| * @property {string[]} publicPath |
| * @property {false | ServeIndexOptions} serveIndex |
| * @property {ServeStaticOptions} staticOptions |
| * @property {false | WatchOptions} watch |
| */ |
| |
| /** |
| * @typedef {Object} ServerConfiguration |
| * @property {"http" | "https" | "spdy" | string} [type] |
| * @property {ServerOptions} [options] |
| */ |
| |
| /** |
| * @typedef {Object} WebSocketServerConfiguration |
| * @property {"sockjs" | "ws" | string | Function} [type] |
| * @property {Record<string, any>} [options] |
| */ |
| |
| /** |
| * @typedef {(import("ws").WebSocket | import("sockjs").Connection & { send: import("ws").WebSocket["send"], terminate: import("ws").WebSocket["terminate"], ping: import("ws").WebSocket["ping"] }) & { isAlive?: boolean }} ClientConnection |
| */ |
| |
| /** |
| * @typedef {import("ws").WebSocketServer | import("sockjs").Server & { close: import("ws").WebSocketServer["close"] }} WebSocketServer |
| */ |
| |
| /** |
| * @typedef {{ implementation: WebSocketServer, clients: ClientConnection[] }} WebSocketServerImplementation |
| */ |
| |
| /** |
| * @typedef {{ [url: string]: string | HttpProxyMiddlewareOptions }} ProxyConfigMap |
| */ |
| |
| /** |
| * @typedef {HttpProxyMiddlewareOptions[]} ProxyArray |
| */ |
| |
| /** |
| * @callback ByPass |
| * @param {Request} req |
| * @param {Response} res |
| * @param {ProxyConfigArray} proxyConfig |
| */ |
| |
| /** |
| * @typedef {{ path?: string | string[] | undefined, context?: string | string[] | HttpProxyMiddlewareOptionsFilter | undefined } & HttpProxyMiddlewareOptions & ByPass} ProxyConfigArray |
| */ |
| |
| /** |
| * @typedef {Object} OpenApp |
| * @property {string} [name] |
| * @property {string[]} [arguments] |
| */ |
| |
| /** |
| * @typedef {Object} Open |
| * @property {string | string[] | OpenApp} [app] |
| * @property {string | string[]} [target] |
| */ |
| |
| /** |
| * @typedef {Object} NormalizedOpen |
| * @property {string} target |
| * @property {import("open").Options} options |
| */ |
| |
| /** |
| * @typedef {Object} WebSocketURL |
| * @property {string} [hostname] |
| * @property {string} [password] |
| * @property {string} [pathname] |
| * @property {number | string} [port] |
| * @property {string} [protocol] |
| * @property {string} [username] |
| */ |
| |
| /** |
| * @typedef {Object} ClientConfiguration |
| * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging] |
| * @property {boolean | { warnings?: boolean, errors?: boolean }} [overlay] |
| * @property {boolean} [progress] |
| * @property {boolean | number} [reconnect] |
| * @property {"ws" | "sockjs" | string} [webSocketTransport] |
| * @property {string | WebSocketURL} [webSocketURL] |
| */ |
| |
| /** |
| * @typedef {Array<{ key: string; value: string }> | Record<string, string | string[]>} Headers |
| */ |
| |
| /** |
| * @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware |
| */ |
| |
| /** |
| * @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 {boolean} [magicHtml] |
| * @property {"auto" | "all" | string | string[]} [allowedHosts] |
| * @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback] |
| * @property {boolean} [setupExitSignals] |
| * @property {boolean | BonjourOptions} [bonjour] |
| * @property {string | string[] | WatchFiles | Array<string | WatchFiles>} [watchFiles] |
| * @property {boolean | string | Static | Array<string | Static>} [static] |
| * @property {boolean | ServerOptions} [https] |
| * @property {boolean} [http2] |
| * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server] |
| * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer] |
| * @property {ProxyConfigMap | ProxyConfigArray | ProxyArray} [proxy] |
| * @property {boolean | string | Open | Array<string | Open>} [open] |
| * @property {boolean} [setupExitSignals] |
| * @property {boolean | ClientConfiguration} [client] |
| * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext<Request, Response>) => Headers)} [headers] |
| * @property {(devServer: Server) => void} [onAfterSetupMiddleware] |
| * @property {(devServer: Server) => void} [onBeforeSetupMiddleware] |
| * @property {(devServer: Server) => void} [onListening] |
| * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares] |
| */ |
| |
| if (!process.env.WEBPACK_SERVE) { |
| // TODO fix me in the next major release |
| // @ts-ignore |
| process.env.WEBPACK_SERVE = true; |
| } |
| |
| class Server { |
| /** |
| * @param {Configuration | Compiler | MultiCompiler} options |
| * @param {Compiler | MultiCompiler | Configuration} compiler |
| */ |
| constructor(options = {}, compiler) { |
| // TODO: remove this after plugin support is published |
| if (/** @type {Compiler | MultiCompiler} */ (options).hooks) { |
| util.deprecate( |
| () => {}, |
| "Using 'compiler' as the first argument is deprecated. Please use 'options' as the first argument and 'compiler' as the second argument.", |
| "DEP_WEBPACK_DEV_SERVER_CONSTRUCTOR" |
| )(); |
| |
| [options = {}, compiler] = [compiler, options]; |
| } |
| |
| validate(/** @type {Schema} */ (schema), options, { |
| name: "Dev Server", |
| baseDataPath: "options", |
| }); |
| |
| this.compiler = /** @type {Compiler | MultiCompiler} */ (compiler); |
| /** |
| * @type {ReturnType<Compiler["getInfrastructureLogger"]>} |
| * */ |
| this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server"); |
| this.options = /** @type {Configuration} */ (options); |
| /** |
| * @type {FSWatcher[]} |
| */ |
| this.staticWatchers = []; |
| /** |
| * @private |
| * @type {{ name: string | symbol, listener: (...args: 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} |
| */ |
| // eslint-disable-next-line no-undefined |
| this.currentHash = undefined; |
| } |
| |
| // TODO compatibility with webpack v4, remove it after drop |
| static get cli() { |
| return { |
| get getArguments() { |
| return () => require("../bin/cli-flags"); |
| }, |
| get processArguments() { |
| return require("../bin/process-arguments"); |
| }, |
| }; |
| } |
| |
| static get schema() { |
| return schema; |
| } |
| |
| /** |
| * @private |
| * @returns {StatsOptions} |
| * @constructor |
| */ |
| static get DEFAULT_STATS() { |
| return { |
| all: false, |
| hash: true, |
| warnings: true, |
| errors: true, |
| errorDetails: false, |
| }; |
| } |
| |
| /** |
| * @param {string} URL |
| * @returns {boolean} |
| */ |
| 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} gateway |
| * @returns {string | undefined} |
| */ |
| static findIp(gateway) { |
| const gatewayIp = ipaddr.parse(gateway); |
| |
| // Look for the matching interface in all local interfaces. |
| for (const addresses of Object.values(os.networkInterfaces())) { |
| for (const { cidr } of /** @type {NetworkInterfaceInfo[]} */ ( |
| addresses |
| )) { |
| const net = ipaddr.parseCIDR(/** @type {string} */ (cidr)); |
| |
| if ( |
| net[0] && |
| net[0].kind() === gatewayIp.kind() && |
| gatewayIp.match(net) |
| ) { |
| return net[0].toString(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param {"v4" | "v6"} family |
| * @returns {Promise<string | undefined>} |
| */ |
| static async internalIP(family) { |
| try { |
| const { gateway } = await defaultGateway[family](); |
| return Server.findIp(gateway); |
| } catch { |
| // ignore |
| } |
| } |
| |
| /** |
| * @param {"v4" | "v6"} family |
| * @returns {string | undefined} |
| */ |
| static internalIPSync(family) { |
| try { |
| const { gateway } = defaultGateway[family].sync(); |
| return Server.findIp(gateway); |
| } catch { |
| // ignore |
| } |
| } |
| |
| /** |
| * @param {Host} hostname |
| * @returns {Promise<string>} |
| */ |
| static async getHostname(hostname) { |
| if (hostname === "local-ip") { |
| return ( |
| (await Server.internalIP("v4")) || |
| (await Server.internalIP("v6")) || |
| "0.0.0.0" |
| ); |
| } else if (hostname === "local-ipv4") { |
| return (await Server.internalIP("v4")) || "0.0.0.0"; |
| } else if (hostname === "local-ipv6") { |
| return (await Server.internalIP("v6")) || "::"; |
| } |
| |
| return hostname; |
| } |
| |
| /** |
| * @param {Port} port |
| * @returns {Promise<number | string>} |
| */ |
| static async getFreePort(port) { |
| if (typeof port !== "undefined" && port !== null && port !== "auto") { |
| return port; |
| } |
| |
| const pRetry = require("p-retry"); |
| const portfinder = require("portfinder"); |
| |
| portfinder.basePort = |
| typeof process.env.WEBPACK_DEV_SERVER_BASE_PORT !== "undefined" |
| ? 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" |
| ? parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10) |
| : 3; |
| |
| return pRetry(() => portfinder.getPortPromise(), { |
| retries: defaultPortRetry, |
| }); |
| } |
| |
| /** |
| * @returns {string} |
| */ |
| static findCacheDir() { |
| const cwd = process.cwd(); |
| |
| /** |
| * @type {string | undefined} |
| */ |
| let dir = cwd; |
| |
| for (;;) { |
| try { |
| if (fs.statSync(path.join(dir, "package.json")).isFile()) break; |
| // eslint-disable-next-line no-empty |
| } catch (e) {} |
| |
| const parent = path.dirname(dir); |
| |
| if (dir === parent) { |
| // eslint-disable-next-line no-undefined |
| dir = undefined; |
| break; |
| } |
| |
| dir = parent; |
| } |
| |
| if (!dir) { |
| return path.resolve(cwd, ".cache/webpack-dev-server"); |
| } else if (process.versions.pnp === "1") { |
| return path.resolve(dir, ".pnp/.cache/webpack-dev-server"); |
| } else if (process.versions.pnp === "3") { |
| return path.resolve(dir, ".yarn/.cache/webpack-dev-server"); |
| } |
| |
| return path.resolve(dir, "node_modules/.cache/webpack-dev-server"); |
| } |
| |
| /** |
| * @private |
| * @param {Compiler} compiler |
| */ |
| addAdditionalEntries(compiler) { |
| /** |
| * @type {string[]} |
| */ |
| const additionalEntries = []; |
| |
| const isWebTarget = compiler.options.externalsPresets |
| ? compiler.options.externalsPresets.web |
| : [ |
| "web", |
| "webworker", |
| "electron-preload", |
| "electron-renderer", |
| "node-webkit", |
| // eslint-disable-next-line no-undefined |
| undefined, |
| null, |
| ].includes(/** @type {string} */ (compiler.options.target)); |
| |
| // TODO maybe empty 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 = |
| /** @type {ServerConfiguration} */ |
| (this.options.server).type === "http" ? "ws:" : "wss:"; |
| } |
| |
| 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; |
| |
| // SockJS is not supported server mode, so `hostname` and `port` can't specified, let's ignore them |
| // TODO show warning about this |
| const isSockJSType = webSocketServer.type === "sockjs"; |
| |
| // 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 ( |
| typeof webSocketServer.options.host !== "undefined" && |
| !isSockJSType |
| ) { |
| 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 ( |
| typeof webSocketServer.options.port !== "undefined" && |
| !isSockJSType |
| ) { |
| 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.reconnect !== "undefined") { |
| searchParams.set( |
| "reconnect", |
| typeof client.reconnect === "number" |
| ? String(client.reconnect) |
| : "10" |
| ); |
| } |
| |
| webSocketURLStr = searchParams.toString(); |
| } |
| |
| additionalEntries.push( |
| `${require.resolve("../client/index.js")}?${webSocketURLStr}` |
| ); |
| } |
| |
| if (this.options.hot === "only") { |
| additionalEntries.push(require.resolve("webpack/hot/only-dev-server")); |
| } else if (this.options.hot) { |
| additionalEntries.push(require.resolve("webpack/hot/dev-server")); |
| } |
| |
| const webpack = compiler.webpack || require("webpack"); |
| |
| // use a hook to add entries if available |
| if (typeof webpack.EntryPlugin !== "undefined") { |
| for (const additionalEntry of additionalEntries) { |
| new webpack.EntryPlugin(compiler.context, additionalEntry, { |
| // eslint-disable-next-line no-undefined |
| name: undefined, |
| }).apply(compiler); |
| } |
| } |
| // TODO remove after drop webpack v4 support |
| else { |
| /** |
| * prependEntry Method for webpack 4 |
| * @param {any} originalEntry |
| * @param {any} newAdditionalEntries |
| * @returns {any} |
| */ |
| const prependEntry = (originalEntry, newAdditionalEntries) => { |
| if (typeof originalEntry === "function") { |
| return () => |
| Promise.resolve(originalEntry()).then((entry) => |
| prependEntry(entry, newAdditionalEntries) |
| ); |
| } |
| |
| if ( |
| typeof originalEntry === "object" && |
| !Array.isArray(originalEntry) |
| ) { |
| /** @type {Object<string,string>} */ |
| const clone = {}; |
| |
| Object.keys(originalEntry).forEach((key) => { |
| // entry[key] should be a string here |
| const entryDescription = originalEntry[key]; |
| |
| clone[key] = prependEntry(entryDescription, newAdditionalEntries); |
| }); |
| |
| return clone; |
| } |
| |
| // in this case, entry is a string or an array. |
| // make sure that we do not add duplicates. |
| /** @type {any} */ |
| const entriesClone = additionalEntries.slice(0); |
| |
| [].concat(originalEntry).forEach((newEntry) => { |
| if (!entriesClone.includes(newEntry)) { |
| entriesClone.push(newEntry); |
| } |
| }); |
| |
| return entriesClone; |
| }; |
| |
| compiler.options.entry = prependEntry( |
| compiler.options.entry || "./src", |
| additionalEntries |
| ); |
| compiler.hooks.entryOption.call( |
| /** @type {string} */ (compiler.options.context), |
| compiler.options.entry |
| ); |
| } |
| } |
| |
| /** |
| * @private |
| * @returns {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) => |
| (config.options.externalsPresets && |
| config.options.externalsPresets.web) || |
| [ |
| "web", |
| "webworker", |
| "electron-preload", |
| "electron-renderer", |
| "node-webkit", |
| // eslint-disable-next-line no-undefined |
| undefined, |
| null, |
| ].includes(/** @type {string} */ (config.options.target)) |
| ); |
| |
| 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(); |
| // TODO remove `{}` after drop webpack v4 support |
| const compilerWatchOptions = compilerOptions.watchOptions || {}; |
| /** |
| * @param {WatchOptions & WebpackConfiguration["watchOptions"]} watchOptions |
| * @returns {WatchOptions} |
| */ |
| 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, ...rest } = watchOptions; |
| |
| return { |
| ignoreInitial: true, |
| persistent: true, |
| followSymlinks: false, |
| atomic: false, |
| alwaysStat: true, |
| ignorePermissionErrors: true, |
| // Respect options from compiler watchOptions |
| usePolling, |
| interval, |
| ignored: watchOptions.ignored, |
| // 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 |
| ...rest, |
| }; |
| }; |
| /** |
| * @param {string | Static | undefined} [optionsForStatic] |
| * @returns {NormalizedStatic} |
| */ |
| const getStaticItem = (optionsForStatic) => { |
| const getDefaultStaticOptions = () => { |
| return { |
| directory: path.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, |
| // TODO: do merge in the next major release |
| staticOptions: |
| typeof optionsForStatic.staticOptions !== "undefined" |
| ? optionsForStatic.staticOptions |
| : def.staticOptions, |
| publicPath: |
| // eslint-disable-next-line no-nested-ternary |
| typeof optionsForStatic.publicPath !== "undefined" |
| ? Array.isArray(optionsForStatic.publicPath) |
| ? optionsForStatic.publicPath |
| : [optionsForStatic.publicPath] |
| : def.publicPath, |
| // TODO: do merge in the next major release |
| serveIndex: |
| // eslint-disable-next-line no-nested-ternary |
| typeof optionsForStatic.serveIndex !== "undefined" |
| ? typeof optionsForStatic.serveIndex === "boolean" && |
| optionsForStatic.serveIndex |
| ? def.serveIndex |
| : optionsForStatic.serveIndex |
| : def.serveIndex, |
| watch: |
| // eslint-disable-next-line no-nested-ternary |
| typeof optionsForStatic.watch !== "undefined" |
| ? // eslint-disable-next-line no-nested-ternary |
| 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; |
| |
| const isHTTPs = Boolean(options.https); |
| const isSPDY = Boolean(options.http2); |
| |
| if (isHTTPs) { |
| // TODO: remove in the next major release |
| util.deprecate( |
| () => {}, |
| "'https' option is deprecated. Please use the 'server' option.", |
| "DEP_WEBPACK_DEV_SERVER_HTTPS" |
| )(); |
| } |
| |
| if (isSPDY) { |
| // TODO: remove in the next major release |
| util.deprecate( |
| () => {}, |
| "'http2' option is deprecated. Please use the 'server' option.", |
| "DEP_WEBPACK_DEV_SERVER_HTTP2" |
| )(); |
| } |
| |
| options.server = { |
| type: |
| // eslint-disable-next-line no-nested-ternary |
| typeof options.server === "string" |
| ? options.server |
| : // eslint-disable-next-line no-nested-ternary |
| typeof (options.server || {}).type === "string" |
| ? /** @type {ServerConfiguration} */ (options.server).type || "http" |
| : // eslint-disable-next-line no-nested-ternary |
| isSPDY |
| ? "spdy" |
| : isHTTPs |
| ? "https" |
| : "http", |
| options: { |
| .../** @type {ServerOptions} */ (options.https), |
| .../** @type {ServerConfiguration} */ (options.server || {}).options, |
| }, |
| }; |
| |
| if ( |
| options.server.type === "spdy" && |
| typeof (/** @type {ServerOptions} */ (options.server.options).spdy) === |
| "undefined" |
| ) { |
| /** @type {ServerOptions} */ |
| (options.server.options).spdy = { |
| protocols: ["h2", "http/1.1"], |
| }; |
| } |
| |
| if (options.server.type === "https" || options.server.type === "spdy") { |
| if ( |
| typeof ( |
| /** @type {ServerOptions} */ (options.server.options).requestCert |
| ) === "undefined" |
| ) { |
| /** @type {ServerOptions} */ |
| (options.server.options).requestCert = false; |
| } |
| |
| const httpsProperties = |
| /** @type {Array<keyof ServerOptions>} */ |
| (["cacert", "ca", "cert", "crl", "key", "pfx"]); |
| |
| for (const property of httpsProperties) { |
| if ( |
| typeof ( |
| /** @type {ServerOptions} */ (options.server.options)[property] |
| ) === "undefined" |
| ) { |
| // eslint-disable-next-line no-continue |
| continue; |
| } |
| |
| // @ts-ignore |
| if (property === "cacert") { |
| // TODO remove the `cacert` option in favor `ca` in the next major release |
| util.deprecate( |
| () => {}, |
| "The 'cacert' option is deprecated. Please use the 'ca' option.", |
| "DEP_WEBPACK_DEV_SERVER_CACERT" |
| )(); |
| } |
| |
| /** @type {any} */ |
| const value = |
| /** @type {ServerOptions} */ |
| (options.server.options)[property]; |
| /** |
| * @param {string | Buffer | undefined} item |
| * @returns {string | Buffer | undefined} |
| */ |
| const readFile = (item) => { |
| if ( |
| Buffer.isBuffer(item) || |
| (typeof item === "object" && item !== null && !Array.isArray(item)) |
| ) { |
| return item; |
| } |
| |
| if (item) { |
| let stats = null; |
| |
| try { |
| stats = fs.lstatSync(fs.realpathSync(item)).isFile(); |
| } catch (error) { |
| // Ignore error |
| } |
| |
| // It is file |
| return stats ? fs.readFileSync(item) : item; |
| } |
| }; |
| |
| /** @type {any} */ |
| (options.server.options)[property] = Array.isArray(value) |
| ? value.map((item) => readFile(item)) |
| : readFile(value); |
| } |
| |
| let fakeCert; |
| |
| if ( |
| !(/** @type {ServerOptions} */ (options.server.options).key) || |
| /** @type {ServerOptions} */ (!options.server.options).cert |
| ) { |
| const certificateDir = Server.findCacheDir(); |
| const certificatePath = path.join(certificateDir, "server.pem"); |
| let certificateExists; |
| |
| try { |
| const certificate = await fs.promises.stat(certificatePath); |
| certificateExists = certificate.isFile(); |
| } catch { |
| certificateExists = false; |
| } |
| |
| if (certificateExists) { |
| const certificateTtl = 1000 * 60 * 60 * 24; |
| const certificateStat = await fs.promises.stat(certificatePath); |
| const now = Number(new Date()); |
| |
| // cert is more than 30 days old, kill it with fire |
| if ((now - Number(certificateStat.ctime)) / certificateTtl > 30) { |
| const del = require("del"); |
| |
| this.logger.info( |
| "SSL certificate is more than 30 days old. Removing..." |
| ); |
| |
| await del([certificatePath], { force: true }); |
| |
| certificateExists = false; |
| } |
| } |
| |
| if (!certificateExists) { |
| this.logger.info("Generating SSL certificate..."); |
| |
| // @ts-ignore |
| const selfsigned = require("selfsigned"); |
| const attributes = [{ name: "commonName", value: "localhost" }]; |
| const pems = selfsigned.generate(attributes, { |
| algorithm: "sha256", |
| days: 30, |
| keySize: 2048, |
| 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 fs.promises.mkdir(certificateDir, { recursive: true }); |
| |
| await fs.promises.writeFile( |
| certificatePath, |
| pems.private + pems.cert, |
| { |
| encoding: "utf8", |
| } |
| ); |
| } |
| |
| fakeCert = await fs.promises.readFile(certificatePath); |
| |
| this.logger.info(`SSL certificate: ${certificatePath}`); |
| } |
| |
| if ( |
| /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */ ( |
| options.server.options |
| ).cacert |
| ) { |
| if (/** @type {ServerOptions} */ (options.server.options).ca) { |
| this.logger.warn( |
| "Do not specify 'ca' and 'cacert' options together, the 'ca' option will be used." |
| ); |
| } else { |
| /** @type {ServerOptions} */ |
| (options.server.options).ca = |
| /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */ |
| (options.server.options).cacert; |
| } |
| |
| delete ( |
| /** @type {ServerOptions & { cacert?: ServerOptions["ca"] }} */ ( |
| options.server.options |
| ).cacert |
| ); |
| } |
| |
| /** @type {ServerOptions} */ |
| (options.server.options).key = |
| /** @type {ServerOptions} */ |
| (options.server.options).key || fakeCert; |
| /** @type {ServerOptions} */ |
| (options.server.options).cert = |
| /** @type {ServerOptions} */ |
| (options.server.options).cert || fakeCert; |
| } |
| |
| if (typeof options.ipc === "boolean") { |
| const isWindows = process.platform === "win32"; |
| const pipePrefix = isWindows ? "\\\\.\\pipe\\" : os.tmpdir(); |
| const pipeName = "webpack-dev-server.sock"; |
| |
| options.ipc = path.join(pipePrefix, pipeName); |
| } |
| |
| options.liveReload = |
| typeof options.liveReload !== "undefined" ? options.liveReload : true; |
| |
| options.magicHtml = |
| typeof options.magicHtml !== "undefined" ? options.magicHtml : true; |
| |
| // https://github.com/webpack/webpack-dev-server/issues/1990 |
| const defaultOpenOptions = { wait: false }; |
| /** |
| * @param {any} target |
| * @returns {NormalizedOpen[]} |
| */ |
| // TODO: remove --open-app in favor of --open-app-name |
| 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) => { |
| return { 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 = []; |
| |
| options.open.forEach((item) => { |
| if (typeof item === "string") { |
| result.push({ target: item, options: defaultOpenOptions }); |
| |
| return; |
| } |
| |
| result.push(...getOpenItemsFromObject(item)); |
| }); |
| |
| /** @type {NormalizedOpen[]} */ |
| (options.open) = result; |
| } else { |
| /** @type {NormalizedOpen[]} */ |
| (options.open) = [...getOpenItemsFromObject(options.open)]; |
| } |
| |
| if (options.onAfterSetupMiddleware) { |
| // TODO: remove in the next major release |
| util.deprecate( |
| () => {}, |
| "'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.", |
| `DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE` |
| )(); |
| } |
| |
| if (options.onBeforeSetupMiddleware) { |
| // TODO: remove in the next major release |
| util.deprecate( |
| () => {}, |
| "'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.", |
| `DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE` |
| )(); |
| } |
| |
| 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") { |
| // TODO remove in the next major release, only accept `Array` |
| if (!Array.isArray(options.proxy)) { |
| if ( |
| Object.prototype.hasOwnProperty.call(options.proxy, "target") || |
| Object.prototype.hasOwnProperty.call(options.proxy, "router") |
| ) { |
| /** @type {ProxyArray} */ |
| (options.proxy) = [/** @type {ProxyConfigMap} */ (options.proxy)]; |
| } else { |
| /** @type {ProxyArray} */ |
| (options.proxy) = Object.keys(options.proxy).map( |
| /** |
| * @param {string} context |
| * @returns {HttpProxyMiddlewareOptions} |
| */ |
| (context) => { |
| let proxyOptions; |
| // For backwards compatibility reasons. |
| const correctedContext = context |
| .replace(/^\*$/, "**") |
| .replace(/\/\*$/, ""); |
| |
| if ( |
| typeof ( |
| /** @type {ProxyConfigMap} */ (options.proxy)[context] |
| ) === "string" |
| ) { |
| proxyOptions = { |
| context: correctedContext, |
| target: |
| /** @type {ProxyConfigMap} */ |
| (options.proxy)[context], |
| }; |
| } else { |
| proxyOptions = { |
| // @ts-ignore |
| .../** @type {ProxyConfigMap} */ (options.proxy)[context], |
| }; |
| proxyOptions.context = correctedContext; |
| } |
| |
| return proxyOptions; |
| } |
| ); |
| } |
| } |
| |
| /** @type {ProxyArray} */ |
| (options.proxy) = |
| /** @type {ProxyArray} */ |
| (options.proxy).map( |
| /** |
| * @param {HttpProxyMiddlewareOptions} item |
| * @returns {HttpProxyMiddlewareOptions} |
| */ |
| (item) => { |
| /** |
| * @param {"info" | "warn" | "error" | "debug" | "silent" | undefined | "none" | "log" | "verbose"} level |
| * @returns {"info" | "warn" | "error" | "debug" | "silent" | undefined} |
| */ |
| const getLogLevelForProxy = (level) => { |
| if (level === "none") { |
| return "silent"; |
| } |
| |
| if (level === "log") { |
| return "info"; |
| } |
| |
| if (level === "verbose") { |
| return "debug"; |
| } |
| |
| return level; |
| }; |
| |
| if (typeof item.logLevel === "undefined") { |
| item.logLevel = getLogLevelForProxy( |
| compilerOptions.infrastructureLogging |
| ? compilerOptions.infrastructureLogging.level |
| : "info" |
| ); |
| } |
| |
| if (typeof item.logProvider === "undefined") { |
| item.logProvider = () => this.logger; |
| } |
| |
| return item; |
| } |
| ); |
| } |
| |
| if (typeof options.setupExitSignals === "undefined") { |
| options.setupExitSignals = true; |
| } |
| |
| 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); |
| } |
| } |
| } |
| |
| /** |
| * @private |
| * @returns {string} |
| */ |
| getClientTransport() { |
| let clientImplementation; |
| let clientImplementationFound = true; |
| |
| const isKnownWebSocketServerImplementation = |
| this.options.webSocketServer && |
| typeof ( |
| /** @type {WebSocketServerConfiguration} */ |
| (this.options.webSocketServer).type |
| ) === "string" && |
| // @ts-ignore |
| (this.options.webSocketServer.type === "ws" || |
| /** @type {WebSocketServerConfiguration} */ |
| (this.options.webSocketServer).type === "sockjs"); |
| |
| 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 'sockjs', 'ws', or a path that should be required |
| if (clientTransport === "sockjs") { |
| clientImplementation = require.resolve( |
| "../client/clients/SockJSClient" |
| ); |
| } else if (clientTransport === "ws") { |
| clientImplementation = require.resolve( |
| "../client/clients/WebSocketClient" |
| ); |
| } else { |
| try { |
| clientImplementation = require.resolve(clientTransport); |
| } catch (e) { |
| 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. 'sockjs', 'ws') or a full path to a JS file via require.resolve(...) which exports a class ` |
| ); |
| } |
| |
| return /** @type {string} */ (clientImplementation); |
| } |
| |
| /** |
| * @private |
| * @returns {string} |
| */ |
| getServerTransport() { |
| let implementation; |
| let implementationFound = true; |
| |
| switch ( |
| typeof ( |
| /** @type {WebSocketServerConfiguration} */ |
| (this.options.webSocketServer).type |
| ) |
| ) { |
| case "string": |
| // Could be 'sockjs', in the future 'ws', or a path that should be required |
| if ( |
| /** @type {WebSocketServerConfiguration} */ ( |
| this.options.webSocketServer |
| ).type === "sockjs" |
| ) { |
| implementation = require("./servers/SockJSServer"); |
| } else if ( |
| /** @type {WebSocketServerConfiguration} */ ( |
| this.options.webSocketServer |
| ).type === "ws" |
| ) { |
| implementation = require("./servers/WebsocketServer"); |
| } else { |
| try { |
| // eslint-disable-next-line import/no-dynamic-require |
| implementation = require(/** @type {WebSocketServerConfiguration} */ ( |
| this.options.webSocketServer |
| ).type); |
| } catch (error) { |
| 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', 'sockjs'), a full path to " + |
| "a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) " + |
| "via require.resolve(...), or the class itself which extends BaseServer" |
| ); |
| } |
| |
| return implementation; |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupProgressPlugin() { |
| const { ProgressPlugin } = |
| /** @type {MultiCompiler}*/ |
| (this.compiler).compilers |
| ? /** @type {MultiCompiler}*/ (this.compiler).compilers[0].webpack |
| : /** @type {Compiler}*/ (this.compiler).webpack || |
| // TODO remove me after drop webpack v4 |
| require("webpack"); |
| |
| new ProgressPlugin( |
| /** |
| * @param {number} percent |
| * @param {string} msg |
| * @param {string} addInfo |
| * @param {string} pluginName |
| */ |
| (percent, msg, addInfo, pluginName) => { |
| percent = Math.floor(percent * 100); |
| |
| if (percent === 100) { |
| msg = "Compilation completed"; |
| } |
| |
| if (addInfo) { |
| msg = `${msg} (${addInfo})`; |
| } |
| |
| if (this.webSocketServer) { |
| this.sendMessage(this.webSocketServer.clients, "progress-update", { |
| percent, |
| msg, |
| pluginName, |
| }); |
| } |
| |
| if (this.server) { |
| this.server.emit("progress-update", { percent, msg, pluginName }); |
| } |
| } |
| ).apply(this.compiler); |
| } |
| |
| /** |
| * @private |
| * @returns {Promise<void>} |
| */ |
| async initialize() { |
| if (this.options.webSocketServer) { |
| const compilers = |
| /** @type {MultiCompiler} */ |
| (this.compiler).compilers || [this.compiler]; |
| |
| compilers.forEach((compiler) => { |
| this.addAdditionalEntries(compiler); |
| |
| const webpack = compiler.webpack || require("webpack"); |
| |
| new webpack.ProvidePlugin({ |
| __webpack_dev_server_client__: this.getClientTransport(), |
| }).apply(compiler); |
| |
| // TODO remove after drop webpack v4 support |
| compiler.options.plugins = compiler.options.plugins || []; |
| |
| if (this.options.hot) { |
| const HMRPluginExists = compiler.options.plugins.find( |
| (p) => p.constructor === webpack.HotModuleReplacementPlugin |
| ); |
| |
| if (HMRPluginExists) { |
| this.logger.warn( |
| `"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.` |
| ); |
| } else { |
| // Apply the HMR plugin |
| const plugin = new webpack.HotModuleReplacementPlugin(); |
| |
| plugin.apply(compiler); |
| } |
| } |
| }); |
| |
| if ( |
| this.options.client && |
| /** @type {ClientConfiguration} */ (this.options.client).progress |
| ) { |
| this.setupProgressPlugin(); |
| } |
| } |
| |
| this.setupHooks(); |
| this.setupApp(); |
| this.setupHostHeaderCheck(); |
| this.setupDevMiddleware(); |
| // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response |
| this.setupBuiltInRoutes(); |
| this.setupWatchFiles(); |
| this.setupWatchStaticFiles(); |
| this.setupMiddlewares(); |
| this.createServer(); |
| |
| if (this.options.setupExitSignals) { |
| const signals = ["SIGINT", "SIGTERM"]; |
| |
| let needForceShutdown = false; |
| |
| signals.forEach((signal) => { |
| const listener = () => { |
| if (needForceShutdown) { |
| process.exit(); |
| } |
| |
| this.logger.info( |
| "Gracefully shutting down. To force exit, press ^C again. Please wait..." |
| ); |
| |
| needForceShutdown = true; |
| |
| this.stopCallback(() => { |
| if (typeof this.compiler.close === "function") { |
| this.compiler.close(() => { |
| process.exit(); |
| }); |
| } else { |
| process.exit(); |
| } |
| }); |
| }; |
| |
| this.listeners.push({ name: signal, listener }); |
| |
| process.on(signal, listener); |
| }); |
| } |
| |
| // Proxy WebSocket without the initial http request |
| // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade |
| /** @type {RequestHandler[]} */ |
| (this.webSocketProxies).forEach((webSocketProxy) => { |
| /** @type {import("http").Server} */ |
| (this.server).on( |
| "upgrade", |
| /** @type {RequestHandler & { upgrade: NonNullable<RequestHandler["upgrade"]> }} */ |
| (webSocketProxy).upgrade |
| ); |
| }, this); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupApp() { |
| /** @type {import("express").Application | undefined}*/ |
| // eslint-disable-next-line new-cap |
| this.app = new /** @type {any} */ (express)(); |
| } |
| |
| /** |
| * @private |
| * @param {Stats | MultiStats} statsObj |
| * @returns {StatsCompilation} |
| */ |
| getStats(statsObj) { |
| const stats = Server.DEFAULT_STATS; |
| const compilerOptions = this.getCompilerOptions(); |
| |
| // @ts-ignore |
| if (compilerOptions.stats && compilerOptions.stats.warningsFilter) { |
| // @ts-ignore |
| stats.warningsFilter = compilerOptions.stats.warningsFilter; |
| } |
| |
| return statsObj.toJson(stats); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupHooks() { |
| this.compiler.hooks.invalid.tap("webpack-dev-server", () => { |
| if (this.webSocketServer) { |
| this.sendMessage(this.webSocketServer.clients, "invalid"); |
| } |
| }); |
| this.compiler.hooks.done.tap( |
| "webpack-dev-server", |
| /** |
| * @param {Stats | MultiStats} stats |
| */ |
| (stats) => { |
| if (this.webSocketServer) { |
| this.sendStats(this.webSocketServer.clients, this.getStats(stats)); |
| } |
| |
| /** |
| * @private |
| * @type {Stats | MultiStats} |
| */ |
| this.stats = stats; |
| } |
| ); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupHostHeaderCheck() { |
| /** @type {import("express").Application} */ |
| (this.app).all( |
| "*", |
| /** |
| * @param {Request} req |
| * @param {Response} res |
| * @param {NextFunction} next |
| * @returns {void} |
| */ |
| (req, res, next) => { |
| if ( |
| this.checkHeader( |
| /** @type {{ [key: string]: string | undefined }} */ |
| (req.headers), |
| "host" |
| ) |
| ) { |
| return next(); |
| } |
| |
| res.send("Invalid Host header"); |
| } |
| ); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupDevMiddleware() { |
| const webpackDevMiddleware = require("webpack-dev-middleware"); |
| |
| // middleware for serving webpack bundle |
| this.middleware = webpackDevMiddleware( |
| this.compiler, |
| this.options.devMiddleware |
| ); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupBuiltInRoutes() { |
| const { app, middleware } = this; |
| |
| /** @type {import("express").Application} */ |
| (app).get( |
| "/__webpack_dev_server__/sockjs.bundle.js", |
| /** |
| * @param {Request} req |
| * @param {Response} res |
| * @returns {void} |
| */ |
| (req, res) => { |
| res.setHeader("Content-Type", "application/javascript"); |
| |
| const { createReadStream } = fs; |
| const clientPath = path.join(__dirname, "..", "client"); |
| |
| createReadStream( |
| path.join(clientPath, "modules/sockjs-client/index.js") |
| ).pipe(res); |
| } |
| ); |
| |
| /** @type {import("express").Application} */ |
| (app).get( |
| "/webpack-dev-server/invalidate", |
| /** |
| * @param {Request} _req |
| * @param {Response} res |
| * @returns {void} |
| */ |
| (_req, res) => { |
| this.invalidate(); |
| |
| res.end(); |
| } |
| ); |
| |
| /** @type {import("express").Application} */ |
| (app).get( |
| "/webpack-dev-server", |
| /** |
| * @param {Request} req |
| * @param {Response} res |
| * @returns {void} |
| */ |
| (req, res) => { |
| /** @type {import("webpack-dev-middleware").API<Request, Response>}*/ |
| (middleware).waitUntilValid((stats) => { |
| res.setHeader("Content-Type", "text/html"); |
| res.write( |
| '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>' |
| ); |
| |
| const statsForPrint = |
| typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined" |
| ? /** @type {MultiStats} */ (stats).toJson().children |
| : [/** @type {Stats} */ (stats).toJson()]; |
| |
| res.write(`<h1>Assets Report:</h1>`); |
| |
| /** |
| * @type {StatsCompilation[]} |
| */ |
| (statsForPrint).forEach((item, index) => { |
| res.write("<div>"); |
| |
| const name = |
| // eslint-disable-next-line no-nested-ternary |
| typeof item.name !== "undefined" |
| ? item.name |
| : /** @type {MultiStats} */ (stats).stats |
| ? `unnamed[${index}]` |
| : "unnamed"; |
| |
| res.write(`<h2>Compilation: ${name}</h2>`); |
| res.write("<ul>"); |
| |
| const publicPath = |
| item.publicPath === "auto" ? "" : item.publicPath; |
| |
| for (const asset of /** @type {NonNullable<StatsCompilation["assets"]>} */ ( |
| item.assets |
| )) { |
| const assetName = asset.name; |
| const assetURL = `${publicPath}${assetName}`; |
| |
| res.write( |
| `<li> |
| <strong><a href="${assetURL}" target="_blank">${assetName}</a></strong> |
| </li>` |
| ); |
| } |
| |
| res.write("</ul>"); |
| res.write("</div>"); |
| }); |
| |
| res.end("</body></html>"); |
| }); |
| } |
| ); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupWatchStaticFiles() { |
| if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { |
| /** @type {NormalizedStatic[]} */ |
| (this.options.static).forEach((staticOption) => { |
| if (staticOption.watch) { |
| this.watchFiles(staticOption.directory, staticOption.watch); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupWatchFiles() { |
| const { watchFiles } = this.options; |
| |
| if (/** @type {WatchFiles[]} */ (watchFiles).length > 0) { |
| /** @type {WatchFiles[]} */ |
| (watchFiles).forEach((item) => { |
| this.watchFiles(item.paths, item.options); |
| }); |
| } |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| setupMiddlewares() { |
| /** |
| * @type {Array<Middleware>} |
| */ |
| let middlewares = []; |
| |
| // compress is placed last and uses unshift so that it will be the first middleware used |
| if (this.options.compress) { |
| const compression = require("compression"); |
| |
| middlewares.push({ name: "compression", middleware: compression() }); |
| } |
| |
| if (typeof this.options.onBeforeSetupMiddleware === "function") { |
| this.options.onBeforeSetupMiddleware(this); |
| } |
| |
| if (typeof this.options.headers !== "undefined") { |
| middlewares.push({ |
| name: "set-headers", |
| path: "*", |
| middleware: this.setHeaders.bind(this), |
| }); |
| } |
| |
| middlewares.push({ |
| name: "webpack-dev-middleware", |
| middleware: |
| /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/ |
| (this.middleware), |
| }); |
| |
| if (this.options.proxy) { |
| const { createProxyMiddleware } = require("http-proxy-middleware"); |
| |
| /** |
| * @param {ProxyConfigArray} proxyConfig |
| * @returns {RequestHandler | undefined} |
| */ |
| const getProxyMiddleware = (proxyConfig) => { |
| // It is possible to use the `bypass` method without a `target` or `router`. |
| // However, the proxy middleware has no use in this case, and will fail to instantiate. |
| if (proxyConfig.target) { |
| const context = proxyConfig.context || proxyConfig.path; |
| |
| return createProxyMiddleware( |
| /** @type {string} */ (context), |
| proxyConfig |
| ); |
| } |
| |
| if (proxyConfig.router) { |
| return createProxyMiddleware(proxyConfig); |
| } |
| }; |
| |
| /** |
| * Assume a proxy configuration specified as: |
| * proxy: [ |
| * { |
| * context: "value", |
| * ...options, |
| * }, |
| * // or: |
| * function() { |
| * return { |
| * context: "context", |
| * ...options, |
| * }; |
| * } |
| * ] |
| */ |
| /** @type {ProxyArray} */ |
| (this.options.proxy).forEach( |
| /** |
| * @param {any} proxyConfigOrCallback |
| */ |
| (proxyConfigOrCallback) => { |
| /** |
| * @type {RequestHandler} |
| */ |
| let proxyMiddleware; |
| |
| let proxyConfig = |
| typeof proxyConfigOrCallback === "function" |
| ? proxyConfigOrCallback() |
| : proxyConfigOrCallback; |
| |
| proxyMiddleware = |
| /** @type {RequestHandler} */ |
| (getProxyMiddleware(proxyConfig)); |
| |
| if (proxyConfig.ws) { |
| this.webSocketProxies.push(proxyMiddleware); |
| } |
| |
| /** |
| * @param {Request} req |
| * @param {Response} res |
| * @param {NextFunction} next |
| * @returns {Promise<void>} |
| */ |
| const handler = async (req, res, next) => { |
| if (typeof proxyConfigOrCallback === "function") { |
| const newProxyConfig = proxyConfigOrCallback(req, res, next); |
| |
| if (newProxyConfig !== proxyConfig) { |
| proxyConfig = newProxyConfig; |
| proxyMiddleware = |
| /** @type {RequestHandler} */ |
| (getProxyMiddleware(proxyConfig)); |
| } |
| } |
| |
| // - Check if we have a bypass function defined |
| // - In case the bypass function is defined we'll retrieve the |
| // bypassUrl from it otherwise bypassUrl would be null |
| // TODO remove in the next major in favor `context` and `router` options |
| const isByPassFuncDefined = |
| typeof proxyConfig.bypass === "function"; |
| const bypassUrl = isByPassFuncDefined |
| ? await proxyConfig.bypass(req, res, proxyConfig) |
| : null; |
| |
| if (typeof bypassUrl === "boolean") { |
| // skip the proxy |
| // @ts-ignore |
| req.url = null; |
| next(); |
| } else if (typeof bypassUrl === "string") { |
| // byPass to that url |
| req.url = bypassUrl; |
| next(); |
| } else if (proxyMiddleware) { |
| return proxyMiddleware(req, res, next); |
| } else { |
| next(); |
| } |
| }; |
| |
| middlewares.push({ |
| name: "http-proxy-middleware", |
| middleware: handler, |
| }); |
| // Also forward error requests to the proxy so it can handle them. |
| middlewares.push({ |
| name: "http-proxy-middleware-error-handler", |
| middleware: |
| /** |
| * @param {Error} error |
| * @param {Request} req |
| * @param {Response} res |
| * @param {NextFunction} next |
| * @returns {any} |
| */ |
| (error, req, res, next) => handler(req, res, next), |
| }); |
| } |
| ); |
| |
| middlewares.push({ |
| name: "webpack-dev-middleware", |
| middleware: |
| /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/ |
| (this.middleware), |
| }); |
| } |
| |
| if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { |
| /** @type {NormalizedStatic[]} */ |
| (this.options.static).forEach((staticOption) => { |
| staticOption.publicPath.forEach((publicPath) => { |
| middlewares.push({ |
| name: "express-static", |
| path: publicPath, |
| middleware: express.static( |
| staticOption.directory, |
| staticOption.staticOptions |
| ), |
| }); |
| }); |
| }); |
| } |
| |
| if (this.options.historyApiFallback) { |
| const connectHistoryApiFallback = require("connect-history-api-fallback"); |
| const { historyApiFallback } = this.options; |
| |
| if ( |
| typeof ( |
| /** @type {ConnectHistoryApiFallbackOptions} */ |
| (historyApiFallback).logger |
| ) === "undefined" && |
| !( |
| /** @type {ConnectHistoryApiFallbackOptions} */ |
| (historyApiFallback).verbose |
| ) |
| ) { |
| // @ts-ignore |
| historyApiFallback.logger = this.logger.log.bind( |
| this.logger, |
| "[connect-history-api-fallback]" |
| ); |
| } |
| |
| // Fall back to /index.html if nothing else matches. |
| middlewares.push({ |
| name: "connect-history-api-fallback", |
| middleware: connectHistoryApiFallback( |
| /** @type {ConnectHistoryApiFallbackOptions} */ |
| (historyApiFallback) |
| ), |
| }); |
| |
| // include our middleware to ensure |
| // it is able to handle '/index.html' request after redirect |
| middlewares.push({ |
| name: "webpack-dev-middleware", |
| middleware: |
| /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/ |
| (this.middleware), |
| }); |
| |
| if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { |
| /** @type {NormalizedStatic[]} */ |
| (this.options.static).forEach((staticOption) => { |
| staticOption.publicPath.forEach((publicPath) => { |
| middlewares.push({ |
| name: "express-static", |
| path: publicPath, |
| middleware: express.static( |
| staticOption.directory, |
| staticOption.staticOptions |
| ), |
| }); |
| }); |
| }); |
| } |
| } |
| |
| if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { |
| const serveIndex = require("serve-index"); |
| |
| /** @type {NormalizedStatic[]} */ |
| (this.options.static).forEach((staticOption) => { |
| staticOption.publicPath.forEach((publicPath) => { |
| if (staticOption.serveIndex) { |
| middlewares.push({ |
| name: "serve-index", |
| path: publicPath, |
| /** |
| * @param {Request} req |
| * @param {Response} res |
| * @param {NextFunction} next |
| * @returns {void} |
| */ |
| middleware: (req, res, next) => { |
| // serve-index doesn't fallthrough non-get/head request to next middleware |
| if (req.method !== "GET" && req.method !== "HEAD") { |
| return next(); |
| } |
| |
| serveIndex( |
| staticOption.directory, |
| /** @type {ServeIndexOptions} */ |
| (staticOption.serveIndex) |
| )(req, res, next); |
| }, |
| }); |
| } |
| }); |
| }); |
| } |
| |
| if (this.options.magicHtml) { |
| middlewares.push({ |
| name: "serve-magic-html", |
| middleware: this.serveMagicHtml.bind(this), |
| }); |
| } |
| |
| if (typeof this.options.setupMiddlewares === "function") { |
| middlewares = this.options.setupMiddlewares(middlewares, this); |
| } |
| |
| middlewares.forEach((middleware) => { |
| if (typeof middleware === "function") { |
| /** @type {import("express").Application} */ |
| (this.app).use(middleware); |
| } else if (typeof middleware.path !== "undefined") { |
| /** @type {import("express").Application} */ |
| (this.app).use(middleware.path, middleware.middleware); |
| } else { |
| /** @type {import("express").Application} */ |
| (this.app).use(middleware.middleware); |
| } |
| }); |
| |
| if (typeof this.options.onAfterSetupMiddleware === "function") { |
| this.options.onAfterSetupMiddleware(this); |
| } |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| createServer() { |
| const { type, options } = /** @type {ServerConfiguration} */ ( |
| this.options.server |
| ); |
| |
| /** @type {import("http").Server | undefined | null} */ |
| // eslint-disable-next-line import/no-dynamic-require |
| this.server = require(/** @type {string} */ (type)).createServer( |
| options, |
| this.app |
| ); |
| |
| /** @type {import("http").Server} */ |
| (this.server).on( |
| "connection", |
| /** |
| * @param {Socket} socket |
| */ |
| (socket) => { |
| // Add socket to list |
| this.sockets.push(socket); |
| |
| socket.once("close", () => { |
| // Remove socket from list |
| this.sockets.splice(this.sockets.indexOf(socket), 1); |
| }); |
| } |
| ); |
| |
| /** @type {import("http").Server} */ |
| (this.server).on( |
| "error", |
| /** |
| * @param {Error} error |
| */ |
| (error) => { |
| throw error; |
| } |
| ); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| // TODO: remove `--web-socket-server` in favor of `--web-socket-server-type` |
| createWebSocketServer() { |
| /** @type {WebSocketServerImplementation | undefined | null} */ |
| this.webSocketServer = new /** @type {any} */ (this.getServerTransport())( |
| this |
| ); |
| /** @type {WebSocketServerImplementation} */ |
| (this.webSocketServer).implementation.on( |
| "connection", |
| /** |
| * @param {ClientConnection} client |
| * @param {IncomingMessage} request |
| */ |
| (client, request) => { |
| /** @type {{ [key: string]: string | undefined } | undefined} */ |
| const headers = |
| // eslint-disable-next-line no-nested-ternary |
| typeof request !== "undefined" |
| ? /** @type {{ [key: string]: string | undefined }} */ |
| (request.headers) |
| : typeof ( |
| /** @type {import("sockjs").Connection} */ (client).headers |
| ) !== "undefined" |
| ? /** @type {import("sockjs").Connection} */ (client).headers |
| : // eslint-disable-next-line no-undefined |
| undefined; |
| |
| if (!headers) { |
| this.logger.warn( |
| 'webSocketServer implementation must pass headers for the "connection" event' |
| ); |
| } |
| |
| if ( |
| !headers || |
| !this.checkHeader(headers, "host") || |
| !this.checkHeader(headers, "origin") |
| ) { |
| this.sendMessage([client], "error", "Invalid Host/Origin header"); |
| |
| // With https enabled, the sendMessage above is encrypted asynchronously so not yet sent |
| // Terminate would prevent it sending, so use close to allow it to be sent |
| client.close(); |
| |
| return; |
| } |
| |
| if (this.options.hot === true || this.options.hot === "only") { |
| this.sendMessage([client], "hot"); |
| } |
| |
| if (this.options.liveReload) { |
| this.sendMessage([client], "liveReload"); |
| } |
| |
| if ( |
| this.options.client && |
| /** @type {ClientConfiguration} */ |
| (this.options.client).progress |
| ) { |
| this.sendMessage( |
| [client], |
| "progress", |
| /** @type {ClientConfiguration} */ |
| (this.options.client).progress |
| ); |
| } |
| |
| if ( |
| this.options.client && |
| /** @type {ClientConfiguration} */ (this.options.client).reconnect |
| ) { |
| this.sendMessage( |
| [client], |
| "reconnect", |
| /** @type {ClientConfiguration} */ |
| (this.options.client).reconnect |
| ); |
| } |
| |
| if ( |
| this.options.client && |
| /** @type {ClientConfiguration} */ |
| (this.options.client).overlay |
| ) { |
| this.sendMessage( |
| [client], |
| "overlay", |
| /** @type {ClientConfiguration} */ |
| (this.options.client).overlay |
| ); |
| } |
| |
| if (!this.stats) { |
| return; |
| } |
| |
| this.sendStats([client], this.getStats(this.stats), true); |
| } |
| ); |
| } |
| |
| /** |
| * @private |
| * @param {string} defaultOpenTarget |
| * @returns {void} |
| */ |
| openBrowser(defaultOpenTarget) { |
| const open = require("open"); |
| |
| Promise.all( |
| /** @type {NormalizedOpen[]} */ |
| (this.options.open).map((item) => { |
| /** |
| * @type {string} |
| */ |
| let openTarget; |
| |
| if (item.target === "<url>") { |
| openTarget = defaultOpenTarget; |
| } else { |
| openTarget = Server.isAbsoluteURL(item.target) |
| ? item.target |
| : new URL(item.target, defaultOpenTarget).toString(); |
| } |
| |
| return open(openTarget, item.options).catch(() => { |
| this.logger.warn( |
| `Unable to open "${openTarget}" page${ |
| item.options.app |
| ? ` in "${ |
| /** @type {import("open").App} */ |
| (item.options.app).name |
| }" app${ |
| /** @type {import("open").App} */ |
| (item.options.app).arguments |
| ? ` with "${ |
| /** @type {import("open").App} */ |
| (item.options.app).arguments.join(" ") |
| }" arguments` |
| : "" |
| }` |
| : "" |
| }. 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".` |
| ); |
| }); |
| }) |
| ); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| runBonjour() { |
| /** |
| * @private |
| * @type {import("bonjour").Bonjour | undefined} |
| */ |
| this.bonjour = require("bonjour")(); |
| this.bonjour.publish({ |
| name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`, |
| port: /** @type {number} */ (this.options.port), |
| type: |
| /** @type {ServerConfiguration} */ |
| (this.options.server).type === "http" ? "http" : "https", |
| subtypes: ["webpack"], |
| .../** @type {BonjourOptions} */ (this.options.bonjour), |
| }); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| stopBonjour(callback = () => {}) { |
| /** @type {Bonjour} */ |
| (this.bonjour).unpublishAll(() => { |
| /** @type {Bonjour} */ |
| (this.bonjour).destroy(); |
| |
| if (callback) { |
| callback(); |
| } |
| }); |
| } |
| |
| /** |
| * @private |
| * @returns {void} |
| */ |
| logStatus() { |
| const { isColorSupported, cyan, red } = require("colorette"); |
| |
| /** |
| * @param {Compiler["options"]} compilerOptions |
| * @returns {boolean} |
| */ |
| const getColorsOption = (compilerOptions) => { |
| /** |
| * @type {boolean} |
| */ |
| let colorsEnabled; |
| |
| if ( |
| compilerOptions.stats && |
| typeof (/** @type {StatsOptions} */ (compilerOptions.stats).colors) !== |
| "undefined" |
| ) { |
| colorsEnabled = |
| /** @type {boolean} */ |
| (/** @type {StatsOptions} */ (compilerOptions.stats).colors); |
| } else { |
| colorsEnabled = isColorSupported; |
| } |
| |
| return colorsEnabled; |
| }; |
| |
| const colors = { |
| /** |
| * @param {boolean} useColor |
| * @param {string} msg |
| * @returns {string} |
| */ |
| info(useColor, msg) { |
| if (useColor) { |
| return cyan(msg); |
| } |
| |
| return msg; |
| }, |
| /** |
| * @param {boolean} useColor |
| * @param {string} msg |
| * @returns {string} |
| */ |
| error(useColor, msg) { |
| if (useColor) { |
| return red(msg); |
| } |
| |
| return msg; |
| }, |
| }; |
| const useColor = getColorsOption(this.getCompilerOptions()); |
| |
| if (this.options.ipc) { |
| this.logger.info( |
| `Project is running at: "${ |
| /** @type {import("http").Server} */ |
| (this.server).address() |
| }"` |
| ); |
| } else { |
| const protocol = |
| /** @type {ServerConfiguration} */ |
| (this.options.server).type === "http" ? "http" : "https"; |
| const { address, port } = |
| /** @type {import("net").AddressInfo} */ |
| ( |
| /** @type {import("http").Server} */ |
| (this.server).address() |
| ); |
| /** |
| * @param {string} newHostname |
| * @returns {string} |
| */ |
| const prettyPrintURL = (newHostname) => |
| url.format({ protocol, hostname: newHostname, port, pathname: "/" }); |
| |
| let server; |
| let localhost; |
| let loopbackIPv4; |
| let loopbackIPv6; |
| let networkUrlIPv4; |
| let networkUrlIPv6; |
| |
| if (this.options.host) { |
| if (this.options.host === "localhost") { |
| localhost = prettyPrintURL("localhost"); |
| } else { |
| let isIP; |
| |
| try { |
| isIP = ipaddr.parse(this.options.host); |
| } catch (error) { |
| // Ignore |
| } |
| |
| if (!isIP) { |
| server = prettyPrintURL(this.options.host); |
| } |
| } |
| } |
| |
| const parsedIP = ipaddr.parse(address); |
| |
| if (parsedIP.range() === "unspecified") { |
| localhost = prettyPrintURL("localhost"); |
| |
| const networkIPv4 = Server.internalIPSync("v4"); |
| |
| if (networkIPv4) { |
| networkUrlIPv4 = prettyPrintURL(networkIPv4); |
| } |
| |
| const networkIPv6 = Server.internalIPSync("v6"); |
| |
| if (networkIPv6) { |
| networkUrlIPv6 = prettyPrintURL(networkIPv6); |
| } |
| } else if (parsedIP.range() === "loopback") { |
| if (parsedIP.kind() === "ipv4") { |
| loopbackIPv4 = prettyPrintURL(parsedIP.toString()); |
| } else if (parsedIP.kind() === "ipv6") { |
| loopbackIPv6 = prettyPrintURL(parsedIP.toString()); |
| } |
| } else { |
| networkUrlIPv4 = |
| parsedIP.kind() === "ipv6" && |
| /** @type {IPv6} */ |
| (parsedIP).isIPv4MappedAddress() |
| ? prettyPrintURL( |
| /** @type {IPv6} */ |
| (parsedIP).toIPv4Address().toString() |
| ) |
| : prettyPrintURL(address); |
| |
| if (parsedIP.kind() === "ipv6") { |
| networkUrlIPv6 = prettyPrintURL(address); |
| } |
| } |
| |
| this.logger.info("Project is running at:"); |
| |
| if (server) { |
| this.logger.info(`Server: ${colors.info(useColor, server)}`); |
| } |
| |
| if (localhost || loopbackIPv4 || loopbackIPv6) { |
| const loopbacks = []; |
| |
| if (localhost) { |
| loopbacks.push([colors.info(useColor, localhost)]); |
| } |
| |
| if (loopbackIPv4) { |
| loopbacks.push([colors.info(useColor, loopbackIPv4)]); |
| } |
| |
| if (loopbackIPv6) { |
| loopbacks.push([colors.info(useColor, loopbackIPv6)]); |
| } |
| |
| this.logger.info(`Loopback: ${loopbacks.join(", ")}`); |
| } |
| |
| if (networkUrlIPv4) { |
| this.logger.info( |
| `On Your Network (IPv4): ${colors.info(useColor, networkUrlIPv4)}` |
| ); |
| } |
| |
| if (networkUrlIPv6) { |
| this.logger.info( |
| `On Your Network (IPv6): ${colors.info(useColor, networkUrlIPv6)}` |
| ); |
| } |
| |
| if (/** @type {NormalizedOpen[]} */ (this.options.open).length > 0) { |
| const openTarget = prettyPrintURL(this.options.host || "localhost"); |
| |
| this.openBrowser(openTarget); |
| } |
| } |
| |
| if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { |
| this.logger.info( |
| `Content not from webpack is served from '${colors.info( |
| useColor, |
| /** @type {NormalizedStatic[]} */ |
| (this.options.static) |
| .map((staticOption) => staticOption.directory) |
| .join(", ") |
| )}' directory` |
| ); |
| } |
| |
| if (this.options.historyApiFallback) { |
| this.logger.info( |
| `404s will fallback to '${colors.info( |
| useColor, |
| /** @type {ConnectHistoryApiFallbackOptions} */ ( |
| this.options.historyApiFallback |
| ).index || "/index.html" |
| )}'` |
| ); |
| } |
| |
| if (this.options.bonjour) { |
| const bonjourProtocol = |
| /** @type {BonjourOptions} */ |
| (this.options.bonjour).type || |
| /** @type {ServerConfiguration} */ |
| (this.options.server).type === "http" |
| ? "http" |
| : "https"; |
| |
| this.logger.info( |
| `Broadcasting "${bonjourProtocol}" with subtype of "webpack" via ZeroConf DNS (Bonjour)` |
| ); |
| } |
| } |
| |
| /** |
| * @private |
| * @param {Request} req |
| * @param {Response} res |
| * @param {NextFunction} next |
| */ |
| setHeaders(req, res, next) { |
| let { headers } = this.options; |
| |
| if (headers) { |
| if (typeof headers === "function") { |
| headers = headers( |
| req, |
| res, |
| /** @type {import("webpack-dev-middleware").API<Request, Response>}*/ |
| (this.middleware).context |
| ); |
| } |
| |
| /** |
| * @type {{key: string, value: string}[]} |
| */ |
| const allHeaders = []; |
| |
| if (!Array.isArray(headers)) { |
| // eslint-disable-next-line guard-for-in |
| for (const name in headers) { |
| // @ts-ignore |
| allHeaders.push({ key: name, value: headers[name] }); |
| } |
| |
| headers = allHeaders; |
| } |
| |
| headers.forEach( |
| /** |
| * @param {{key: string, value: any}} header |
| */ |
| (header) => { |
| res.setHeader(header.key, header.value); |
| } |
| ); |
| } |
| |
| next(); |
| } |
| |
| /** |
| * @private |
| * @param {{ [key: string]: string | undefined }} headers |
| * @param {string} headerToCheck |
| * @returns {boolean} |
| */ |
| checkHeader(headers, headerToCheck) { |
| // allow user to opt out of this security check, at their own risk |
| // by explicitly enabling allowedHosts |
| if (this.options.allowedHosts === "all") { |
| return true; |
| } |
| |
| // get the Host header and extract hostname |
| // we don't care about port not matching |
| const hostHeader = headers[headerToCheck]; |
| |
| if (!hostHeader) { |
| return false; |
| } |
| |
| if (/^(file|.+-extension):/i.test(hostHeader)) { |
| return true; |
| } |
| |
| // use the node url-parser to retrieve the hostname from the host-header. |
| const hostname = url.parse( |
| // if hostHeader doesn't have scheme, add // for parsing. |
| /^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`, |
| false, |
| true |
| ).hostname; |
| |
| // always allow requests with explicit IPv4 or IPv6-address. |
| // A note on IPv6 addresses: |
| // hostHeader will always contain the brackets denoting |
| // an IPv6-address in URLs, |
| // these are removed from the hostname in url.parse(), |
| // so we have the pure IPv6-address in hostname. |
| // always allow localhost host, for convenience (hostname === 'localhost') |
| // allow hostname of listening address (hostname === this.options.host) |
| const isValidHostname = |
| (hostname !== null && ipaddr.IPv4.isValid(hostname)) || |
| (hostname !== null && ipaddr.IPv6.isValid(hostname)) || |
| hostname === "localhost" || |
| hostname === this.options.host; |
| |
| if (isValidHostname) { |
| return true; |
| } |
| |
| const { allowedHosts } = this.options; |
| |
| // always allow localhost host, for convenience |
| // allow if hostname is in allowedHosts |
| if (Array.isArray(allowedHosts) && allowedHosts.length > 0) { |
| for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) { |
| const allowedHost = allowedHosts[hostIdx]; |
| |
| if (allowedHost === hostname) { |
| return true; |
| } |
| |
| // support "." as a subdomain wildcard |
| // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc |
| if (allowedHost[0] === ".") { |
| // "example.com" (hostname === allowedHost.substring(1)) |
| // "*.example.com" (hostname.endsWith(allowedHost)) |
| if ( |
| hostname === allowedHost.substring(1) || |
| /** @type {string} */ (hostname).endsWith(allowedHost) |
| ) { |
| return true; |
| } |
| } |
| } |
| } |
| |
| // Also allow if `client.webSocketURL.hostname` provided |
| if ( |
| this.options.client && |
| typeof ( |
| /** @type {ClientConfiguration} */ (this.options.client).webSocketURL |
| ) !== "undefined" |
| ) { |
| return ( |
| /** @type {WebSocketURL} */ |
| (/** @type {ClientConfiguration} */ (this.options.client).webSocketURL) |
| .hostname === hostname |
| ); |
| } |
| |
| // disallow |
| return false; |
| } |
| |
| /** |
| * @param {ClientConnection[]} clients |
| * @param {string} type |
| * @param {any} [data] |
| * @param {any} [params] |
| */ |
| // eslint-disable-next-line class-methods-use-this |
| sendMessage(clients, type, data, params) { |
| for (const client of clients) { |
| // `sockjs` uses `1` to indicate client is ready to accept data |
| // `ws` uses `WebSocket.OPEN`, but it is mean `1` too |
| if (client.readyState === 1) { |
| client.send(JSON.stringify({ type, data, params })); |
| } |
| } |
| } |
| |
| /** |
| * @private |
| * @param {Request} req |
| * @param {Response} res |
| * @param {NextFunction} next |
| * @returns {void} |
| */ |
| serveMagicHtml(req, res, next) { |
| if (req.method !== "GET" && req.method !== "HEAD") { |
| return next(); |
| } |
| |
| /** @type {import("webpack-dev-middleware").API<Request, Response>}*/ |
| (this.middleware).waitUntilValid(() => { |
| const _path = req.path; |
| |
| try { |
| const filename = |
| /** @type {import("webpack-dev-middleware").API<Request, Response>}*/ |
| (this.middleware).getFilenameFromUrl(`${_path}.js`); |
| const isFile = |
| /** @type {Compiler["outputFileSystem"] & { statSync: import("fs").StatSyncFn }}*/ |
| ( |
| /** @type {import("webpack-dev-middleware").API<Request, Response>}*/ |
| (this.middleware).context.outputFileSystem |
| ) |
| .statSync(/** @type {import("fs").PathLike} */ (filename)) |
| .isFile(); |
| |
| if (!isFile) { |
| return next(); |
| } |
| |
| // Serve a page that executes the javascript |
| // @ts-ignore |
| const queries = req._parsedUrl.search || ""; |
| const responsePage = `<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="${_path}.js${queries}"></script></body></html>`; |
| |
| res.send(responsePage); |
| } catch (error) { |
| return next(); |
| } |
| }); |
| } |
| |
| // Send stats to a socket or multiple sockets |
| /** |
| * @private |
| * @param {ClientConnection[]} clients |
| * @param {StatsCompilation} stats |
| * @param {boolean} [force] |
| */ |
| sendStats(clients, stats, force) { |
| const shouldEmit = |
| !force && |
| stats && |
| (!stats.errors || stats.errors.length === 0) && |
| (!stats.warnings || stats.warnings.length === 0) && |
| this.currentHash === stats.hash; |
| |
| if (shouldEmit) { |
| this.sendMessage(clients, "still-ok"); |
| |
| return; |
| } |
| |
| this.currentHash = stats.hash; |
| this.sendMessage(clients, "hash", stats.hash); |
| |
| if ( |
| /** @type {NonNullable<StatsCompilation["errors"]>} */ |
| (stats.errors).length > 0 || |
| /** @type {NonNullable<StatsCompilation["warnings"]>} */ |
| (stats.warnings).length > 0 |
| ) { |
| const hasErrors = |
| /** @type {NonNullable<StatsCompilation["errors"]>} */ |
| (stats.errors).length > 0; |
| |
| if ( |
| /** @type {NonNullable<StatsCompilation["warnings"]>} */ |
| (stats.warnings).length > 0 |
| ) { |
| let params; |
| |
| if (hasErrors) { |
| params = { preventReloading: true }; |
| } |
| |
| this.sendMessage(clients, "warnings", stats.warnings, params); |
| } |
| |
| if ( |
| /** @type {NonNullable<StatsCompilation["errors"]>} */ (stats.errors) |
| .length > 0 |
| ) { |
| this.sendMessage(clients, "errors", stats.errors); |
| } |
| } else { |
| this.sendMessage(clients, "ok"); |
| } |
| } |
| |
| /** |
| * @param {string | string[]} watchPath |
| * @param {WatchOptions} [watchOptions] |
| */ |
| watchFiles(watchPath, watchOptions) { |
| const chokidar = require("chokidar"); |
| const watcher = chokidar.watch(watchPath, watchOptions); |
| |
| // disabling refreshing on changing the content |
| if (this.options.liveReload) { |
| watcher.on("change", (item) => { |
| if (this.webSocketServer) { |
| this.sendMessage( |
| this.webSocketServer.clients, |
| "static-changed", |
| item |
| ); |
| } |
| }); |
| } |
| |
| this.staticWatchers.push(watcher); |
| } |
| |
| /** |
| * @param {import("webpack-dev-middleware").Callback} [callback] |
| */ |
| invalidate(callback = () => {}) { |
| if (this.middleware) { |
| this.middleware.invalidate(callback); |
| } |
| } |
| |
| /** |
| * @returns {Promise<void>} |
| */ |
| async start() { |
| await this.normalizeOptions(); |
| |
| if (this.options.ipc) { |
| await /** @type {Promise<void>} */ ( |
| new Promise((resolve, reject) => { |
| const net = require("net"); |
| const socket = new net.Socket(); |
| |
| socket.on( |
| "error", |
| /** |
| * @param {Error & { code?: string }} error |
| */ |
| (error) => { |
| if (error.code === "ECONNREFUSED") { |
| // No other server listening on this socket so it can be safely removed |
| fs.unlinkSync(/** @type {string} */ (this.options.ipc)); |
| |
| resolve(); |
| |
| return; |
| } else if (error.code === "ENOENT") { |
| resolve(); |
| |
| return; |
| } |
| |
| reject(error); |
| } |
| ); |
| |
| socket.connect( |
| { path: /** @type {string} */ (this.options.ipc) }, |
| () => { |
| throw new Error(`IPC "${this.options.ipc}" is already used`); |
| } |
| ); |
| }) |
| ); |
| } else { |
| this.options.host = await Server.getHostname( |
| /** @type {Host} */ (this.options.host) |
| ); |
| this.options.port = await Server.getFreePort( |
| /** @type {Port} */ (this.options.port) |
| ); |
| } |
| |
| await this.initialize(); |
| |
| const listenOptions = this.options.ipc |
| ? { path: this.options.ipc } |
| : { host: this.options.host, port: this.options.port }; |
| |
| await /** @type {Promise<void>} */ ( |
| new Promise((resolve) => { |
| /** @type {import("http").Server} */ |
| (this.server).listen(listenOptions, () => { |
| resolve(); |
| }); |
| }) |
| ); |
| |
| if (this.options.ipc) { |
| // chmod 666 (rw rw rw) |
| const READ_WRITE = 438; |
| |
| await fs.promises.chmod( |
| /** @type {string} */ (this.options.ipc), |
| READ_WRITE |
| ); |
| } |
| |
| if (this.options.webSocketServer) { |
| this.createWebSocketServer(); |
| } |
| |
| if (this.options.bonjour) { |
| this.runBonjour(); |
| } |
| |
| this.logStatus(); |
| |
| if (typeof this.options.onListening === "function") { |
| this.options.onListening(this); |
| } |
| } |
| |
| /** |
| * @param {(err?: Error) => void} [callback] |
| */ |
| startCallback(callback = () => {}) { |
| this.start() |
| .then(() => callback(), callback) |
| .catch(callback); |
| } |
| |
| /** |
| * @returns {Promise<void>} |
| */ |
| async stop() { |
| if (this.bonjour) { |
| await /** @type {Promise<void>} */ ( |
| new Promise((resolve) => { |
| this.stopBonjour(() => { |
| resolve(); |
| }); |
| }) |
| ); |
| } |
| |
| this.webSocketProxies = []; |
| |
| await Promise.all(this.staticWatchers.map((watcher) => watcher.close())); |
| |
| this.staticWatchers = []; |
| |
| if (this.webSocketServer) { |
| await /** @type {Promise<void>} */ ( |
| new Promise((resolve) => { |
| /** @type {WebSocketServerImplementation} */ |
| (this.webSocketServer).implementation.close(() => { |
| this.webSocketServer = null; |
| |
| resolve(); |
| }); |
| |
| for (const client of /** @type {WebSocketServerImplementation} */ ( |
| this.webSocketServer |
| ).clients) { |
| client.terminate(); |
| } |
| |
| /** @type {WebSocketServerImplementation} */ |
| (this.webSocketServer).clients = []; |
| }) |
| ); |
| } |
| |
| if (this.server) { |
| await /** @type {Promise<void>} */ ( |
| new Promise((resolve) => { |
| /** @type {import("http").Server} */ |
| (this.server).close(() => { |
| this.server = null; |
| |
| resolve(); |
| }); |
| |
| for (const socket of this.sockets) { |
| socket.destroy(); |
| } |
| |
| this.sockets = []; |
| }) |
| ); |
| |
| if (this.middleware) { |
| await /** @type {Promise<void>} */ ( |
| new Promise((resolve, reject) => { |
| /** @type {import("webpack-dev-middleware").API<Request, Response>}*/ |
| (this.middleware).close((error) => { |
| if (error) { |
| reject(error); |
| |
| return; |
| } |
| |
| resolve(); |
| }); |
| }) |
| ); |
| |
| this.middleware = null; |
| } |
| } |
| |
| // We add listeners to signals when creating a new Server instance |
| // So ensure they are removed to prevent EventEmitter memory leak warnings |
| for (const item of this.listeners) { |
| process.removeListener(item.name, item.listener); |
| } |
| } |
| |
| /** |
| * @param {(err?: Error) => void} [callback] |
| */ |
| stopCallback(callback = () => {}) { |
| this.stop() |
| .then(() => callback(), callback) |
| .catch(callback); |
| } |
| |
| // TODO remove in the next major release |
| /** |
| * @param {Port} port |
| * @param {Host} hostname |
| * @param {(err?: Error) => void} fn |
| * @returns {void} |
| */ |
| listen(port, hostname, fn) { |
| util.deprecate( |
| () => {}, |
| "'listen' is deprecated. Please use the async 'start' or 'startCallback' method.", |
| "DEP_WEBPACK_DEV_SERVER_LISTEN" |
| )(); |
| |
| if (typeof port === "function") { |
| fn = port; |
| } |
| |
| if ( |
| typeof port !== "undefined" && |
| typeof this.options.port !== "undefined" && |
| port !== this.options.port |
| ) { |
| this.options.port = port; |
| |
| this.logger.warn( |
| 'The "port" specified in options is different from the port passed as an argument. Will be used from arguments.' |
| ); |
| } |
| |
| if (!this.options.port) { |
| this.options.port = port; |
| } |
| |
| if ( |
| typeof hostname !== "undefined" && |
| typeof this.options.host !== "undefined" && |
| hostname !== this.options.host |
| ) { |
| this.options.host = hostname; |
| |
| this.logger.warn( |
| 'The "host" specified in options is different from the host passed as an argument. Will be used from arguments.' |
| ); |
| } |
| |
| if (!this.options.host) { |
| this.options.host = hostname; |
| } |
| |
| this.start() |
| .then(() => { |
| if (fn) { |
| fn.call(this.server); |
| } |
| }) |
| .catch((error) => { |
| // Nothing |
| if (fn) { |
| fn.call(this.server, error); |
| } |
| }); |
| } |
| |
| /** |
| * @param {(err?: Error) => void} [callback] |
| * @returns {void} |
| */ |
| // TODO remove in the next major release |
| close(callback) { |
| util.deprecate( |
| () => {}, |
| "'close' is deprecated. Please use the async 'stop' or 'stopCallback' method.", |
| "DEP_WEBPACK_DEV_SERVER_CLOSE" |
| )(); |
| |
| this.stop() |
| .then(() => { |
| if (callback) { |
| callback(); |
| } |
| }) |
| .catch((error) => { |
| if (callback) { |
| callback(error); |
| } |
| }); |
| } |
| } |
| |
| module.exports = Server; |