Last active
September 5, 2022 12:37
-
-
Save alan-agius4/156a7c36c2927986ef4e1dc6ffcdc97a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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-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").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?: WatchOptions["ignored"], 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?: WatchOptions["ignored"], 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 | |
*/ | |
/** | |
* @callback ByPass | |
* @param {Request} req | |
* @param {Response} res | |
* @param {ProxyConfigArrayItem} proxyConfig | |
*/ | |
/** | |
* @typedef {{ path?: HttpProxyMiddlewareOptionsFilter | undefined, context?: HttpProxyMiddlewareOptionsFilter | undefined } & { bypass?: ByPass } & HttpProxyMiddlewareOptions } ProxyConfigArrayItem | |
*/ | |
/** | |
* @typedef {(ProxyConfigArrayItem | ((req?: Request | undefined, res?: Response | undefined, next?: NextFunction | undefined) => ProxyConfigArrayItem))[]} ProxyConfigArray | |
*/ | |
/** | |
* @typedef {{ [url: string]: string | ProxyConfigArrayItem }} ProxyConfigMap | |
*/ | |
/** | |
* @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 | Record<string, never> | 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 | ProxyConfigArrayItem | ProxyConfigArray} [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 | |
* @param {string} host | |
* @returns {Promise<number | string>} | |
*/ | |
static async getFreePort(port, host) { | |
if (typeof port !== "undefined" && port !== null && port !== "auto") { | |
return port; | |
} | |
const pRetry = require("p-retry"); | |
const getPort = require("./getPort"); | |
const 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(() => getPort(basePort, host), { | |
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.progress !== "undefined") { | |
searchParams.set("progress", String(client.progress)); | |
} | |
if (typeof client.overlay !== "undefined") { | |
searchParams.set( | |
"overlay", | |
typeof client.overlay === "boolean" | |
? String(client.overlay) | |
: JSON.stringify(client.overlay) | |
); | |
} | |
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( | |
`${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 & { aggregateTimeout?: number, ignored?: WatchOptions["ignored"], poll?: number | boolean }} 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 { promisify } = require("util"); | |
const rimraf = require("rimraf"); | |
const del = promisify(rimraf); | |
this.logger.info( | |
"SSL certificate is more than 30 days old. Removing..." | |
); | |
await del(certificatePath); | |
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 {ProxyConfigArray} */ | |
(options.proxy) = [/** @type {ProxyConfigMap} */ (options.proxy)]; | |
} else { | |
/** @type {ProxyConfigArray} */ | |
(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 {ProxyConfigArray} */ | |
(options.proxy) = | |
/** @type {ProxyConfigArray} */ | |
(options.proxy).map((item) => { | |
if (typeof item === "function") { | |
return 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 clientPath = path.join(__dirname, "..", "client"); | |
res.sendFile(path.join(clientPath, "modules/sockjs-client/index.js")); | |
} | |
); | |
/** @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 {ProxyConfigArrayItem} 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 {ProxyConfigArray} */ | |
(this.options.proxy).forEach((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 /** @type {ByPass} */ (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), | |
}); | |
} | |
const optionsRequestResponseMiddleware = (req, res, next) => { | |
if (req.method === "OPTIONS") { | |
res.statusCode = 204; | |
res.setHeader("Content-Length", "0"); | |
res.end(); | |
return; | |
} | |
next(); | |
}; | |
middlewares.push({ | |
name: "options-middleware", | |
path: "*", | |
middleware: optionsRequestResponseMiddleware, | |
}); | |
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() { | |
const { Bonjour } = require("bonjour-service"); | |
/** | |
* @private | |
* @type {Bonjour | undefined} | |
*/ | |
this.bonjour = new Bonjour(); | |
this.bonjour.publish({ | |
// @ts-expect-error | |
name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`, | |
// @ts-expect-error | |
port: /** @type {number} */ (this.options.port), | |
// @ts-expect-error | |
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), | |
this.options.host | |
); | |
} | |
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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment