UNPKG

webpack-dev-middleware

Version:
601 lines (559 loc) 24.5 kB
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); }