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