webpack-dev-middleware
Version:
A development middleware for webpack
601 lines (559 loc) • 24.5 kB
JavaScript
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
/* global __resourceQuery, __webpack_public_path__ */
// This file is bundled by webpack into a browser bundle, so it is compiled to
// ES5 (see `babel.config.js`) and sticks to ES5 runtime APIs — `EventSource`
// and `Promise` (both required by HMR itself) are the only exceptions.
// TODO in the next major release add an `exports` field to package.json
// (`.`, `./client`, `./client/indicator`, `./client/overlay`, `./package.json`).
// Adding it now is a breaking change: it would hide every other path of the
// package (e.g. `webpack-dev-middleware/dist/...`) from existing users.
import * as indicator from "./indicator.js";
import configureOverlay from "./overlay.js";
import applyUpdate from "./process-update.js";
import { log, setLogLevel } from "./utils/log.js";
import stripAnsi from "./utils/strip-ansi.js";
/** @typedef {import("./utils/log.js").LogLevel} LogLevel */
/**
* Superset of webpack-dev-server's `client.overlay` object; `styles`,
* `ansiColors`, `openEditorEndpoint` and `paginate` are webpack-dev-middleware
* extensions.
* @typedef {object} OverlayOptions
* @property {(boolean | ((error: string) => boolean))=} errors show build errors in the overlay
* @property {(boolean | ((warning: string) => boolean))=} warnings show build warnings in the overlay
* @property {(boolean | ((error: Error) => boolean))=} runtimeErrors show uncaught runtime errors and unhandled rejections in the overlay
* @property {string=} trustedTypesPolicyName Trusted Types policy name used for the overlay's HTML
* @property {Record<string, string | number>=} styles overrides for the overlay card CSS
* @property {Record<string, string | string[]>=} ansiColors overrides for ANSI → HTML color mapping
* @property {string=} openEditorEndpoint endpoint the overlay calls (GET `?fileName=file:line:column`) when a file reference is clicked; empty disables it
* @property {boolean=} paginate show one problem at a time with prev/next navigation
*/
/**
* @typedef {object} ClientOptions
* @property {string} path SSE endpoint path
* @property {number} timeout reconnection timeout in milliseconds
* @property {boolean | OverlayOptions} overlay enable the in-page error overlay (same value shape as webpack-dev-server's `client.overlay`)
* @property {boolean} reload reload the page when HMR cannot apply the update
* @property {LogLevel} logging logger level
* @property {string} name limit updates to this compilation name
* @property {boolean} autoConnect connect immediately when the entry runs
* @property {boolean} progress show a small badge while a rebuild is in progress
*/
/** @type {ClientOptions} */
var options = {
path: "/__webpack_hmr",
timeout: 20 * 1000,
overlay: true,
reload: true,
logging: "info",
name: "",
autoConnect: true,
progress: true
};
/**
* Parse the entry's resource query. Hand-rolled (rather than through
* `URLSearchParams`, which an ES5 browser does not have) with the same
* decoding: `+` is a space and values are percent-decoded.
* @param {string} query resource query, starting with `?`
* @returns {Record<string, string>} query parameters
*/
function parseQuery(query) {
/** @type {Record<string, string>} */
var parameters = {};
/**
* @param {string} value raw value
* @returns {string} decoded value
*/
var decode = function decode(value) {
return decodeURIComponent(value.replace(/\+/g, " "));
};
var _iterator = _createForOfIteratorHelper(query.slice(1).split("&")),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var pair = _step.value;
if (!pair) {
continue;
}
var separator = pair.indexOf("=");
parameters[decode(separator === -1 ? pair : pair.slice(0, separator))] = separator === -1 ? "" : decode(pair.slice(separator + 1));
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return parameters;
}
/**
* Turn the string values that `errors`/`warnings`/`runtimeErrors` may carry
* in the resource query into filter functions (same behavior as
* webpack-dev-server).
* @param {boolean | OverlayOptions} overlayOptions overlay options
*/
function decodeOverlayOptions(overlayOptions) {
if (_typeof(overlayOptions) === "object") {
for (var _i = 0, _arr = ["errors", "warnings", "runtimeErrors"]; _i < _arr.length; _i++) {
var property = _arr[_i];
var value = overlayOptions[(/** @type {keyof OverlayOptions} */property)];
if (typeof value === "string") {
var filterFunctionString = decodeURIComponent(value);
/** @type {EXPECTED_ANY} */
overlayOptions[property] =
// eslint-disable-next-line no-new-func
new Function("message", "var callback = ".concat(filterFunctionString, "\n return callback(message)"));
}
}
}
}
setLogLevel(options.logging);
/**
* @param {Record<string, string>} overrides parsed query-string overrides
*/
function setOverrides(overrides) {
if (overrides.autoConnect) {
options.autoConnect = overrides.autoConnect === "true";
}
if (overrides.path) options.path = overrides.path;
if (overrides.timeout) {
var timeout = Number(overrides.timeout);
// A non-numeric value would make the watchdog fire in a loop (`NaN` never
// compares greater), so it is ignored rather than applied.
if (timeout > 0) {
options.timeout = timeout;
}
}
if (overrides.overlay) {
// Same value shape as webpack-dev-server's `client.overlay`: a boolean or
// a JSON object with `errors`, `warnings`, `runtimeErrors` (booleans or
// encoded filter functions) and `trustedTypesPolicyName`.
try {
options.overlay = JSON.parse(overrides.overlay);
} catch (_unused) {
options.overlay = overrides.overlay !== "false";
}
// Fill in default "true" params for partially-specified objects.
if (_typeof(options.overlay) === "object") {
options.overlay = _objectSpread({
errors: true,
warnings: true,
runtimeErrors: true
}, options.overlay);
decodeOverlayOptions(options.overlay);
}
}
if (overrides.reload) options.reload = overrides.reload !== "false";
if (overrides.logging) {
options.logging = /** @type {LogLevel} */overrides.logging;
}
if (overrides.name) {
options.name = overrides.name;
}
if (overrides.progress) {
options.progress = overrides.progress !== "false";
}
if (overrides.dynamicPublicPath && overrides.dynamicPublicPath !== "false") {
// `path` is appended like a filename (no leading slash); the public path
// itself is not normalized.
options.path = __webpack_public_path__ + options.path.replace(/^\//, "");
}
setLogLevel(options.logging);
}
/**
* @typedef {(event: { data: string }) => void} MessageListener
*/
/**
* @returns {{ addMessageListener: (fn: MessageListener) => void, close: () => void }} event source wrapper
*/
function createEventSourceWrapper() {
/** @type {EventSource} */
var source;
var lastActivity = Date.now();
/** @type {MessageListener[]} */
var listeners = [];
/** @type {ReturnType<typeof setInterval>} */
var timer;
/** @type {ReturnType<typeof setTimeout>} */
var reconnectTimer;
// Set once the wrapper is closed for good, so an `error` event the
// EventSource had already queued cannot schedule a reconnection after it.
var closed = false;
var handleOnline = function handleOnline() {
log.info("connected");
lastActivity = Date.now();
};
/**
* @param {{ data: string }} event event
*/
var handleMessage = function handleMessage(event) {
lastActivity = Date.now();
for (var _i2 = 0, _listeners = listeners; _i2 < _listeners.length; _i2++) {
var listener = _listeners[_i2];
listener(event);
}
};
/**
* Close the connection and stop the activity timer without scheduling a
* reconnection. A reconnection that is already pending is cancelled too, so
* closing during the reconnect window really is final.
*/
var close = function close() {
closed = true;
clearInterval(timer);
clearTimeout(reconnectTimer);
source.close();
};
/**
* Open the EventSource connection and (re)start the inactivity watchdog —
* disconnecting stops the watchdog, so a reconnected source has to bring its
* own. The disconnect handler belongs to one connection, so it is created
* here rather than shared between reconnections.
*/
var _init = function init() {
closed = false;
var handleDisconnect = function handleDisconnect() {
// Reached only by an `error` event the EventSource had already queued
// when `close()` ran — a race the browser will not stage on demand.
/* istanbul ignore next -- @preserve */
if (closed) {
return;
}
close();
reconnectTimer = setTimeout(_init, /** @type {number} */options.timeout);
};
source = new window.EventSource(/** @type {string} */options.path);
source.addEventListener("open", handleOnline);
source.addEventListener("error", handleDisconnect);
source.addEventListener("message", handleMessage);
lastActivity = Date.now();
clearInterval(timer);
timer = setInterval(function () {
if (Date.now() - lastActivity > (/** @type {number} */options.timeout)) {
handleDisconnect();
}
}, /** @type {number} */options.timeout / 2);
};
_init();
return {
addMessageListener: function addMessageListener(fn) {
listeners.push(fn);
},
close: close
};
}
var WRAPPER_KEY = "__wdmEventSourceWrapper";
/**
* @returns {ReturnType<typeof createEventSourceWrapper>} cached event source wrapper for this path
*/
function getEventSourceWrapper() {
var path = /** @type {string} */options.path;
if (!window[WRAPPER_KEY]) {
window[WRAPPER_KEY] = {};
}
if (!window[WRAPPER_KEY][path]) {
// Cache the wrapper so multiple entries on the same page sharing the same
// `options.path` reuse a single SSE connection.
window[WRAPPER_KEY][path] = createEventSourceWrapper();
}
return window[WRAPPER_KEY][path];
}
// eslint-disable-next-line jsdoc/reject-any-type
/** @typedef {any} EXPECTED_ANY */
/** @typedef {{ name?: string, errors: string[], warnings: string[], hash: string, time?: number, action?: string, file?: string, percent?: number, message?: string }} HMRPayload */
/**
* @returns {{
* cleanProblemsCache: (name: string) => void,
* problems: (type: "errors" | "warnings", obj: HMRPayload) => boolean,
* success: (obj?: HMRPayload) => void,
* useCustomOverlay: (customOverlay: EXPECTED_ANY) => void,
* }} reporter
*/
function createReporter() {
/** @type {EXPECTED_ANY} */
var overlay;
if (typeof document !== "undefined" && options.overlay) {
// Same mapping as webpack-dev-server's createOverlay call, extended with
// the webpack-dev-middleware-specific keys.
overlay = configureOverlay(_typeof(options.overlay) === "object" ? {
catchRuntimeError: options.overlay.runtimeErrors,
trustedTypesPolicyName: options.overlay.trustedTypesPolicyName,
ansiColors: options.overlay.ansiColors,
overlayStyles: options.overlay.styles,
openEditorEndpoint: options.overlay.openEditorEndpoint,
paginate: options.overlay.paginate
} : {
catchRuntimeError: options.overlay
});
}
// Console de-duplication cache, keyed per bundle name and type so interleaved
// multi-compiler payloads do not defeat it. A null-prototype object rather
// than a `Map`, which an ES5 browser does not have.
/** @type {Record<string, string>} */
var previousProblems = Object.create(null);
// Live problems per compilation name. A multi-compiler publishes one event
// per bundle; a success from one bundle must not wipe another bundle's
// still-valid errors from the overlay.
/** @type {Record<string, { errors: string[], warnings: string[] }>} */
var problemsByName = Object.create(null);
/**
* Resolve the show/hide/filter setting for a problem type. Same resolution
* as webpack-dev-server: a boolean overlay applies to both types; an object
* carries a boolean or a filter function per type.
* @param {"errors" | "warnings"} type problem type
* @param {string[]} problems problems of one bundle
* @returns {string[]} the problems the overlay should show
*/
var filterForOverlay = function filterForOverlay(type, problems) {
var setting = typeof options.overlay === "boolean" ? options.overlay : options.overlay && options.overlay[type];
if (!setting) {
return [];
}
return typeof setting === "function" ? problems.filter(function (message) {
return setting(message);
}) : problems;
};
/**
* Render the union of every bundle's live problems, or clear the overlay
* when nothing is left.
* @returns {boolean} true when nothing is shown
*/
var renderOverlay = function renderOverlay() {
if (!overlay) {
return true;
}
/** @type {string[]} */
var errors = [];
/** @type {string[]} */
var warnings = [];
for (var _i3 = 0, _Object$keys = Object.keys(problemsByName); _i3 < _Object$keys.length; _i3++) {
var name = _Object$keys[_i3];
var entry = problemsByName[name];
errors.push.apply(errors, _toConsumableArray(filterForOverlay("errors", entry.errors)));
warnings.push.apply(warnings, _toConsumableArray(filterForOverlay("warnings", entry.warnings)));
}
if (errors.length > 0) {
overlay.showProblems("errors", errors);
return false;
}
if (warnings.length > 0) {
overlay.showProblems("warnings", warnings);
return false;
}
// Clear only this client's problems and the runtime errors — other
// clients sharing the overlay keep theirs.
overlay.clear("");
overlay.clear("runtime");
return true;
};
/**
* @param {"errors" | "warnings"} type problem type
* @param {HMRPayload} obj payload
*/
var logProblems = function logProblems(type, obj) {
var cacheKey = "".concat(obj.name || "", "|").concat(type);
var newProblems = obj[type].map(stripAnsi).join("\n");
if (previousProblems[cacheKey] === newProblems) {
return;
}
previousProblems[cacheKey] = newProblems;
var name = obj.name ? "'".concat(obj.name, "' ") : "";
var title = "bundle ".concat(name, "has ").concat(obj[type].length, " ").concat(type);
if (type === "errors") {
log.error(title);
log.error(newProblems);
} else {
log.warn(title);
log.warn(newProblems);
}
};
return {
cleanProblemsCache: function cleanProblemsCache(name) {
// Scoped to one bundle so a sibling's unchanged problems do not re-log.
delete previousProblems["".concat(name, "|errors")];
delete previousProblems["".concat(name, "|warnings")];
},
problems: function problems(type, obj) {
logProblems(type, obj);
problemsByName[obj.name || ""] = {
errors: obj.errors || [],
warnings: obj.warnings || []
};
return renderOverlay();
},
success: function success(obj) {
delete problemsByName[obj && obj.name || ""];
renderOverlay();
},
useCustomOverlay: function useCustomOverlay(customOverlay) {
overlay = customOverlay;
}
};
}
// The reporter is a singleton on the page so that, when multiple bundles
// include the client, errors are reported once but all clients receive them.
var REPORTER_KEY = "__webpack_dev_middleware_hot_reporter__";
/** @type {ReturnType<typeof createReporter> | undefined} */
var reporter;
/** @type {((obj: HMRPayload) => void) | undefined} */
var customHandler;
/** @type {((obj: HMRPayload) => void) | undefined} */
var subscribeAllHandler;
// Name of the build that most recently reported `building` — progress
// payloads carry no name, so they are attributed to it.
var lastBuildingName = "";
/**
* @param {HMRPayload} obj payload
*/
function processMessage(obj) {
switch (obj.action) {
case "building":
{
log.info("bundle ".concat(obj.name ? "'".concat(obj.name, "' ") : "", "rebuilding").concat(obj.file ? " (".concat(obj.file, " changed)") : ""));
if (options.progress && typeof document !== "undefined") {
lastBuildingName = obj.name || "";
indicator.show(obj.file ? "Rebuilding\u2026 (".concat(obj.file, ")") : undefined, undefined, lastBuildingName);
}
break;
}
case "progress":
{
// Progress payloads carry no name — attribute them to the build that
// most recently reported `building`.
if (options.progress && typeof document !== "undefined") {
indicator.show("Rebuilding\u2026 ".concat(obj.percent, "%").concat(obj.message ? " (".concat(obj.message, ")") : ""), obj.percent, lastBuildingName);
}
break;
}
case "built":
case "sync":
{
if (options.progress && typeof document !== "undefined") {
indicator.hide(obj.name || "");
}
if (obj.action === "built") {
log.info("bundle ".concat(obj.name ? "'".concat(obj.name, "' ") : "", "rebuilt in ").concat(obj.time, "ms"));
}
// Not a `return`: a sibling bundle's event is still delivered to a
// `subscribeAll` handler, which is documented to see every message.
if (obj.name && options.name && obj.name !== options.name) {
break;
}
var shouldApply = true;
if (obj.errors.length > 0) {
if (reporter) reporter.problems("errors", obj);
shouldApply = false;
} else if (obj.warnings.length > 0) {
// Warnings are reported (and possibly shown in the overlay) but do
// not block the update, matching webpack-dev-server.
if (reporter) {
reporter.problems("warnings", obj);
}
} else if (reporter) {
reporter.cleanProblemsCache(obj.name || "");
reporter.success(obj);
}
if (shouldApply) {
applyUpdate(obj.hash, options, obj.name);
}
break;
}
default:
{
if (customHandler) {
customHandler(obj);
}
}
}
if (subscribeAllHandler) {
subscribeAllHandler(obj);
}
}
// Path this copy of the client subscribed to, so a `setOptionsAndConnect`
// call after the automatic connect does not add a second listener (every
// message would be processed twice) while a call that changed `path` still
// subscribes to the new connection.
/** @type {string | undefined} */
var subscribedPath;
/**
* Subscribe the message handler to the shared event source wrapper.
*/
function connect() {
if (subscribedPath === options.path) {
return;
}
subscribedPath = /** @type {string} */options.path;
getEventSourceWrapper().addMessageListener(function (event) {
if (event.data === "💓") {
return;
}
try {
processMessage(JSON.parse(event.data));
} catch (err) {
log.warn("Invalid HMR message: ".concat(event.data, "\n").concat(err));
}
});
}
/**
* @param {Record<string, string>} overrides overrides
*/
export function setOptionsAndConnect(overrides) {
setOverrides(overrides);
connect();
}
/**
* Close the SSE connection for the current path and stop reconnecting. A
* later `setOptionsAndConnect` call opens a fresh connection.
*/
export function disconnect() {
var path = /** @type {string} */options.path;
var wrappers = window[WRAPPER_KEY];
if (wrappers && wrappers[path]) {
wrappers[path].close();
delete wrappers[path];
}
subscribedPath = undefined;
}
// Bootstrap: parse query string overrides, then connect (if enabled).
if (typeof __resourceQuery === "string" && __resourceQuery.length > 0) {
setOverrides(parseQuery(__resourceQuery));
}
if (typeof window !== "undefined") {
if (!window[REPORTER_KEY]) {
window[REPORTER_KEY] = createReporter();
}
reporter = window[REPORTER_KEY];
if (typeof window.EventSource === "undefined") {
log.warn("webpack-dev-middleware's hot client requires EventSource to work. " + "Include a polyfill if you want to support this browser: " + "https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events#Tools");
} else if (options.autoConnect) {
connect();
}
}
/**
* @param {(obj: HMRPayload) => void} handler called for every incoming HMR message
*/
export function subscribeAll(handler) {
subscribeAllHandler = handler;
}
/**
* @param {(obj: HMRPayload) => void} handler called for messages whose `action` is not recognized
*/
export function subscribe(handler) {
customHandler = handler;
}
/**
* @param {EXPECTED_ANY} customOverlay replacement for the default error overlay
*/
export function useCustomOverlay(customOverlay) {
if (reporter) reporter.useCustomOverlay(customOverlay);
}