webpack-dev-server
Version:
Serves a webpack app. Updates the browser on changes.
1,612 lines (1,401 loc) • 104 kB
JavaScript
"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 { 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("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").Server} HTTPServer*/
/** @typedef {import("http").IncomingMessage} IncomingMessage */
/** @typedef {import("http").ServerResponse} ServerResponse */
/** @typedef {import("open").Options} OpenOptions */
/** @typedef {import("express").Application} ExpressApplication */
/** @typedef {import("express").RequestHandler} ExpressRequestHandler */
/** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */
/** @typedef {import("express").Request} ExpressRequest */
/** @typedef {import("express").Response} ExpressResponse */
/** @typedef {(err?: any) => void} NextFunction */
/** @typedef {(req: IncomingMessage, res: ServerResponse) => void} SimpleHandleFunction */
/** @typedef {(req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} NextHandleFunction */
/** @typedef {(err: any, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} ErrorHandleFunction */
/** @typedef {SimpleHandleFunction | NextHandleFunction | ErrorHandleFunction} HandleFunction */
/** @typedef {import("https").ServerOptions & { spdy?: { plain?: boolean | undefined, ssl?: boolean | undefined, 'x-forwarded-for'?: string | undefined, protocol?: string | undefined, protocols?: string[] | undefined }}} ServerOptions */
/**
* @template {BasicApplication} [T=ExpressApplication]
* @typedef {T extends ExpressApplication ? ExpressRequest : IncomingMessage} Request
*/
/**
* @template {BasicApplication} [T=ExpressApplication]
* @typedef {T extends ExpressApplication ? ExpressResponse : ServerResponse} Response
*/
/**
* @template {Request} T
* @template {Response} U
* @typedef {import("webpack-dev-middleware").Options<T, U>} DevMiddlewareOptions
*/
/**
* @template {Request} T
* @template {Response} U
* @typedef {import("webpack-dev-middleware").Context<T, U>} DevMiddlewareContext
*/
/**
* @typedef {"local-ip" | "local-ipv4" | "local-ipv6" | string} Host
*/
/**
* @typedef {number | string | "auto"} Port
*/
/**
* @typedef {Object} WatchFiles
* @property {string | string[]} paths
* @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
*/
/**
* @template {BasicApplication} [A=ExpressApplication]
* @template {BasicServer} [S=import("http").Server]
* @typedef {"http" | "https" | "spdy" | "http2" | string | function(ServerOptions, A): S} ServerType
*/
/**
* @template {BasicApplication} [A=ExpressApplication]
* @template {BasicServer} [S=import("http").Server]
* @typedef {Object} ServerConfiguration
* @property {ServerType<A, S>} [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 {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 {boolean | ((error: Error) => void)} OverlayMessageOptions
*/
/**
* @typedef {Object} ClientConfiguration
* @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
* @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [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
*/
/**
* @template {BasicApplication} [T=ExpressApplication]
* @typedef {T extends ExpressApplication ? ExpressRequestHandler | ExpressErrorRequestHandler : HandleFunction} MiddlewareHandler
*/
/**
* @typedef {{ name?: string, path?: string, middleware: MiddlewareHandler }} MiddlewareObject
*/
/**
* @typedef {MiddlewareObject | MiddlewareHandler } Middleware
*/
/** @typedef {import("net").Server | import("tls").Server} BasicServer */
/**
* @template {BasicApplication} [A=ExpressApplication]
* @template {BasicServer} [S=import("http").Server]
* @typedef {Object} Configuration
* @property {boolean | string} [ipc]
* @property {Host} [host]
* @property {Port} [port]
* @property {boolean | "only"} [hot]
* @property {boolean} [liveReload]
* @property {DevMiddlewareOptions<Request, Response>} [devMiddleware]
* @property {boolean} [compress]
* @property {"auto" | "all" | string | string[]} [allowedHosts]
* @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback]
* @property {boolean | Record<string, never> | BonjourOptions} [bonjour]
* @property {string | string[] | WatchFiles | Array<string | WatchFiles>} [watchFiles]
* @property {boolean | string | Static | Array<string | Static>} [static]
* @property {ServerType<A, S> | ServerConfiguration<A, S>} [server]
* @property {() => Promise<A>} [app]
* @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer]
* @property {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> | undefined) => Headers)} [headers]
* @property {(devServer: Server<A, S>) => void} [onListening]
* @property {(middlewares: Middleware[], devServer: Server<A, S>) => Middleware[]} [setupMiddlewares]
*/
if (!process.env.WEBPACK_SERVE) {
process.env.WEBPACK_SERVE = "true";
}
/**
* @template T
* @param fn {(function(): any) | undefined}
* @returns {function(): T}
*/
const memoize = (fn) => {
let cache = false;
/** @type {T} */
let result;
return () => {
if (cache) {
return result;
}
result = /** @type {function(): any} */ (fn)();
cache = true;
// Allow to clean up memory for fn
// and all dependent resources
// eslint-disable-next-line no-undefined
fn = undefined;
return result;
};
};
const getExpress = memoize(() => require("express"));
/**
*
* @param {OverlayMessageOptions} [setting]
* @returns
*/
const encodeOverlaySettings = (setting) =>
typeof setting === "function"
? encodeURIComponent(setting.toString())
: setting;
// Working for overload, because typescript doesn't support this yes
/**
* @overload
* @param {NextHandleFunction} fn
* @returns {BasicApplication}
*/
/**
* @overload
* @param {HandleFunction} fn
* @returns {BasicApplication}
*/
/**
* @overload
* @param {string} route
* @param {NextHandleFunction} fn
* @returns {BasicApplication}
*/
/**
* @param {string} route
* @param {HandleFunction} fn
* @returns {BasicApplication}
*/
// eslint-disable-next-line no-unused-vars
function useFn(route, fn) {
return /** @type {BasicApplication} */ ({});
}
const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i;
/**
* @typedef {Object} BasicApplication
* @property {typeof useFn} use
*/
/**
* @template {BasicApplication} [A=ExpressApplication]
* @template {BasicServer} [S=HTTPServer]
*/
class Server {
/**
* @param {Configuration<A, S>} options
* @param {Compiler | MultiCompiler} compiler
*/
constructor(options = {}, compiler) {
validate(/** @type {Schema} */ (schema), options, {
name: "Dev Server",
baseDataPath: "options",
});
this.compiler = compiler;
/**
* @type {ReturnType<Compiler["getInfrastructureLogger"]>}
* */
this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
this.options = 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;
}
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} gatewayOrFamily or family
* @param {boolean} [isInternal] ip should be internal
* @returns {string | undefined}
*/
static findIp(gatewayOrFamily, isInternal) {
if (gatewayOrFamily === "v4" || gatewayOrFamily === "v6") {
let host;
const networks = Object.values(os.networkInterfaces())
// eslint-disable-next-line no-shadow
.flatMap((networks) => networks ?? [])
.filter((network) => {
if (!network || !network.address) {
return false;
}
if (network.family !== `IP${gatewayOrFamily}`) {
return false;
}
if (
typeof isInternal !== "undefined" &&
network.internal !== isInternal
) {
return false;
}
if (gatewayOrFamily === "v6") {
const range = ipaddr.parse(network.address).range();
if (
range !== "ipv4Mapped" &&
range !== "uniqueLocal" &&
range !== "loopback"
) {
return false;
}
}
return network.address;
});
if (networks.length > 0) {
// Take the first network found
host = networks[0].address;
if (host.includes(":")) {
host = `[${host}]`;
}
}
return host;
}
const gatewayIp = ipaddr.parse(gatewayOrFamily);
// 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();
}
}
}
}
// TODO remove me in the next major release, we have `findIp`
/**
* @param {"v4" | "v6"} family
* @returns {Promise<string | undefined>}
*/
static async internalIP(family) {
return Server.findIp(family, false);
}
// TODO remove me in the next major release, we have `findIp`
/**
* @param {"v4" | "v6"} family
* @returns {string | undefined}
*/
static internalIPSync(family) {
return Server.findIp(family, false);
}
/**
* @param {Host} hostname
* @returns {Promise<string>}
*/
static async getHostname(hostname) {
if (hostname === "local-ip") {
return (
Server.findIp("v4", false) || Server.findIp("v6", false) || "0.0.0.0"
);
} else if (hostname === "local-ipv4") {
return Server.findIp("v4", false) || "0.0.0.0";
} else if (hostname === "local-ipv6") {
return Server.findIp("v6", false) || "::";
}
return hostname;
}
/**
* @param {Port} port
* @param {string} host
* @returns {Promise<number | string>}
*/
static async getFreePort(port, host) {
if (typeof port !== "undefined" && port !== null && port !== "auto") {
return port;
}
const pRetry = (await import("p-retry")).default;
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
* @returns bool
*/
static isWebTarget(compiler) {
if (compiler.platform && compiler.platform.web) {
return compiler.platform.web;
}
// TODO improve for the next major version and keep only `webTargets` to fallback for old versions
if (
compiler.options.externalsPresets &&
compiler.options.externalsPresets.web
) {
return true;
}
if (
compiler.options.resolve.conditionNames &&
compiler.options.resolve.conditionNames.includes("browser")
) {
return true;
}
const webTargets = [
"web",
"webworker",
"electron-preload",
"electron-renderer",
"nwjs",
"node-webkit",
// eslint-disable-next-line no-undefined
undefined,
null,
];
if (Array.isArray(compiler.options.target)) {
return compiler.options.target.some((r) => webTargets.includes(r));
}
return webTargets.includes(/** @type {string} */ (compiler.options.target));
}
/**
* @private
* @param {Compiler} compiler
*/
addAdditionalEntries(compiler) {
/**
* @type {string[]}
*/
const additionalEntries = [];
const isWebTarget = Server.isWebTarget(compiler);
// TODO maybe empty client
if (this.options.client && isWebTarget) {
let webSocketURLStr = "";
if (this.options.webSocketServer) {
const webSocketURL =
/** @type {WebSocketURL} */
(
/** @type {ClientConfiguration} */
(this.options.client).webSocketURL
);
const webSocketServer =
/** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable<WebSocketServerConfiguration["options"]> }} */
(this.options.webSocketServer);
const searchParams = new URLSearchParams();
/** @type {string} */
let protocol;
// We are proxying dev server and need to specify custom `hostname`
if (typeof webSocketURL.protocol !== "undefined") {
protocol = webSocketURL.protocol;
} else {
protocol = this.isTlsServer ? "wss:" : "ws:";
}
searchParams.set("protocol", protocol);
if (typeof webSocketURL.username !== "undefined") {
searchParams.set("username", webSocketURL.username);
}
if (typeof webSocketURL.password !== "undefined") {
searchParams.set("password", webSocketURL.password);
}
/** @type {string} */
let hostname;
// SockJS is not supported server mode, so `hostname` and `port` can't specified, let's ignore them
const isSockJSType = webSocketServer.type === "sockjs";
const isWebSocketServerHostDefined =
typeof webSocketServer.options.host !== "undefined";
const isWebSocketServerPortDefined =
typeof webSocketServer.options.port !== "undefined";
if (
isSockJSType &&
(isWebSocketServerHostDefined || isWebSocketServerPortDefined)
) {
this.logger.warn(
"SockJS only supports client mode and does not support custom hostname and port options. Please consider using 'ws' if you need to customize these options.",
);
}
// We are proxying dev server and need to specify custom `hostname`
if (typeof webSocketURL.hostname !== "undefined") {
hostname = webSocketURL.hostname;
}
// Web socket server works on custom `hostname`, only for `ws` because `sock-js` is not support custom `hostname`
else if (isWebSocketServerHostDefined && !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 (isWebSocketServerPortDefined && !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") {
const overlayString =
typeof client.overlay === "boolean"
? String(client.overlay)
: JSON.stringify({
...client.overlay,
errors: encodeOverlaySettings(client.overlay.errors),
warnings: encodeOverlaySettings(client.overlay.warnings),
runtimeErrors: encodeOverlaySettings(
client.overlay.runtimeErrors,
),
});
searchParams.set("overlay", overlayString);
}
if (typeof client.reconnect !== "undefined") {
searchParams.set(
"reconnect",
typeof client.reconnect === "number"
? String(client.reconnect)
: "10",
);
}
if (typeof this.options.hot !== "undefined") {
searchParams.set("hot", String(this.options.hot));
}
if (typeof this.options.liveReload !== "undefined") {
searchParams.set("live-reload", String(this.options.liveReload));
}
webSocketURLStr = searchParams.toString();
}
additionalEntries.push(`${this.getClientEntry()}?${webSocketURLStr}`);
}
const clientHotEntry = this.getClientHotEntry();
if (clientHotEntry) {
additionalEntries.push(clientHotEntry);
}
const webpack = compiler.webpack || require("webpack");
// use a hook to add entries if available
for (const additionalEntry of additionalEntries) {
new webpack.EntryPlugin(compiler.context, additionalEntry, {
// eslint-disable-next-line no-undefined
name: undefined,
}).apply(compiler);
}
}
/**
* @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();
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,
staticOptions:
typeof optionsForStatic.staticOptions !== "undefined"
? { ...def.staticOptions, ...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,
serveIndex:
// Check if 'serveIndex' property is defined in 'optionsForStatic'
// If 'serveIndex' is a boolean and true, use default 'serveIndex'
// If 'serveIndex' is an object, merge its properties with default 'serveIndex'
// If 'serveIndex' is neither a boolean true nor an object, use it as-is
// If 'serveIndex' is not defined in 'optionsForStatic', use default 'serveIndex'
// eslint-disable-next-line no-nested-ternary
typeof optionsForStatic.serveIndex !== "undefined"
? // eslint-disable-next-line no-nested-ternary
typeof optionsForStatic.serveIndex === "boolean" &&
optionsForStatic.serveIndex
? def.serveIndex
: typeof optionsForStatic.serveIndex === "object"
? { ...def.serveIndex, ...optionsForStatic.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;
if (
typeof options.server === "function" ||
typeof options.server === "string"
) {
options.server = {
type: options.server,
options: {},
};
} else {
const serverOptions =
/** @type {ServerConfiguration<A, S>} */
(options.server || {});
options.server = {
type: serverOptions.type || "http",
options: { ...serverOptions.options },
};
}
const serverOptions = /** @type {ServerOptions} */ (options.server.options);
if (
options.server.type === "spdy" &&
typeof serverOptions.spdy === "undefined"
) {
serverOptions.spdy = { protocols: ["h2", "http/1.1"] };
}
if (
options.server.type === "https" ||
options.server.type === "http2" ||
options.server.type === "spdy"
) {
if (typeof serverOptions.requestCert === "undefined") {
serverOptions.requestCert = false;
}
const httpsProperties =
/** @type {Array<keyof ServerOptions>} */
(["ca", "cert", "crl", "key", "pfx"]);
for (const property of httpsProperties) {
if (typeof serverOptions[property] === "undefined") {
// eslint-disable-next-line no-continue
continue;
}
/** @type {any} */
const value = serverOptions[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 a file
return stats ? fs.readFileSync(item) : item;
}
};
/** @type {any} */
(serverOptions)[property] = Array.isArray(value)
? value.map((item) => readFile(item))
: readFile(value);
}
let fakeCert;
if (!serverOptions.key || !serverOptions.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) {
this.logger.info(
"SSL certificate is more than 30 days old. Removing...",
);
await fs.promises.rm(certificatePath, { recursive: true });
certificateExists = false;
}
}
if (!certificateExists) {
this.logger.info("Generating SSL certificate...");
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}`);
}
serverOptions.key = serverOptions.key || fakeCert;
serverOptions.cert = serverOptions.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;
// https://github.com/webpack/webpack-dev-server/issues/1990
const defaultOpenOptions = { wait: false };
/**
* @param {any} target
* @returns {NormalizedOpen[]}
*/
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 = [];
for (const item of options.open) {
if (typeof item === "string") {
result.push({ target: item, options: defaultOpenOptions });
// eslint-disable-next-line no-continue
continue;
}
result.push(...getOpenItemsFromObject(item));
}
/** @type {NormalizedOpen[]} */
(options.open) = result;
} else {
/** @type {NormalizedOpen[]} */
(options.open) = [...getOpenItemsFromObject(options.open)];
}
if (typeof options.port === "string" && options.port !== "auto") {
options.port = Number(options.port);
}
/**
* Assume a proxy configuration specified as:
* proxy: {
* 'context': { options }
* }
* OR
* proxy: {
* 'context': 'target'
* }
*/
if (typeof options.proxy !== "undefined") {
options.proxy = options.proxy.map((item) => {
if (typeof item === "function") {
return item;
}
/**
* @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