Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-client-and-server-defects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-dev-server": minor
---

Fix client progress modes, socket URL credentials, overlay and progress lifecycles, server startup error handling and local IP lookup; export `BaseServer`.
52 changes: 40 additions & 12 deletions client-src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import sendMessage from "./utils/sendMessage.js";
* @typedef {object} Options
* @property {boolean} hot true when hot enabled, otherwise false
* @property {boolean} liveReload true when live reload enabled, otherwise false
* @property {boolean} progress true when need to show progress, otherwise false
* @property {boolean | "linear" | "circular"} progress progress display mode
* @property {boolean | OverlayOptions} overlay overlay options
* @property {LogLevel=} logging logging level
* @property {number=} reconnect count of allowed reconnection
Expand Down Expand Up @@ -129,10 +129,17 @@ const parseURL = (resourceQuery) => {
const searchParams = resourceQuery.slice(1).split("&");

for (let i = 0; i < searchParams.length; i++) {
const pair = searchParams[i].split("=");

/** @type {EXPECTED_ANY} */
(result)[pair[0]] = decodeURIComponent(pair[1]);
const parameter = searchParams[i].replace(/\+/g, " ");
const separator = parameter.indexOf("=");
const key = separator === -1 ? parameter : parameter.slice(0, separator);
const value = separator === -1 ? "" : parameter.slice(separator + 1);

try {
/** @type {EXPECTED_ANY} */
(result)[decodeURIComponent(key)] = decodeURIComponent(value);
} catch {
// Ignore malformed percent escapes without preventing client startup.
}
}
} else {
// Else, get the url from the <script> this file was called with.
Expand Down Expand Up @@ -189,8 +196,15 @@ if (parsedResourceQuery["live-reload"] === "true") {
enabledFeatures["Live Reloading"] = true;
}

if (parsedResourceQuery.progress === "true") {
options.progress = true;
if (
parsedResourceQuery.progress === "true" ||
parsedResourceQuery.progress === "linear" ||
parsedResourceQuery.progress === "circular"
) {
options.progress =
parsedResourceQuery.progress === "true"
? true
: parsedResourceQuery.progress;
enabledFeatures.Progress = true;
}

Expand Down Expand Up @@ -440,7 +454,7 @@ const onSocketMessage = {
options.reconnect = value;
},
/**
* @param {boolean} value progress value
* @param {boolean | "linear" | "circular"} value progress value
*/
progress(value) {
options.progress = value;
Expand Down Expand Up @@ -534,7 +548,7 @@ const onSocketMessage = {
overlay.send({
type: "BUILD_ERROR",
level: "warning",
messages: warnings,
messages: warningsToDisplay,
});
}
}
Expand Down Expand Up @@ -578,7 +592,7 @@ const onSocketMessage = {
overlay.send({
type: "BUILD_ERROR",
level: "error",
messages: errors,
messages: errorsToDisplay,
});
}
}
Expand Down Expand Up @@ -710,16 +724,30 @@ const createSocketURL = (parsedURL) => {

let socketURLAuth = "";

/**
* @param {string} value credential
* @returns {string} decoded credential
*/
const decodeAuth = (value) => {
if (!parsedURL.fromCurrentScript) return value;
try {
return decodeURIComponent(value);
} catch {
// URL accepts literal percent signs that are not valid escape sequences.
return value;
}
};

// `new URL(urlString, [baseURLstring])` doesn't have `auth` property
// Parse authentication credentials in case we need them
if (parsedURL.username) {
socketURLAuth = parsedURL.username;
socketURLAuth = decodeAuth(parsedURL.username);

// Since HTTP basic authentication does not allow empty username,
// we only include password if the username is not empty.
if (parsedURL.password) {
// Result: <username>:<password>
socketURLAuth = socketURLAuth.concat(":", parsedURL.password);
socketURLAuth = socketURLAuth.concat(":", decodeAuth(parsedURL.password));
}
}

Expand Down
124 changes: 59 additions & 65 deletions client-src/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,6 @@ import ansiHTML from "ansi-html-community";

/** @typedef {import("./index.js").EXPECTED_ANY} EXPECTED_ANY */

/**
* @type {(input: string, position: number) => number | undefined}
*/
// @ts-expect-error
const getCodePoint = String.prototype.codePointAt
? // @ts-expect-error
(input, position) => input.codePointAt(position)
: (input, position) =>
(input.charCodeAt(position) - 0xd800) * 0x400 +
input.charCodeAt(position + 1) -
0xdc00 +
0x10000;

/**
* @param {string} macroText macro text
* @param {RegExp} macroRegExp macro reg exp
* @param {(input: string) => string} macroReplacer macro replacer
* @returns {string} result
*/
const replaceUsingRegExp = (macroText, macroRegExp, macroReplacer) => {
macroRegExp.lastIndex = 0;
let replaceMatch = macroRegExp.exec(macroText);
let replaceResult;
if (replaceMatch) {
replaceResult = "";
let replaceLastIndex = 0;
do {
if (replaceLastIndex !== replaceMatch.index) {
replaceResult += macroText.slice(replaceLastIndex, replaceMatch.index);
}
const replaceInput = replaceMatch[0];
replaceResult += macroReplacer(replaceInput);
replaceLastIndex = replaceMatch.index + replaceInput.length;
} while ((replaceMatch = macroRegExp.exec(macroText)));

if (replaceLastIndex !== macroText.length) {
replaceResult += macroText.slice(replaceLastIndex);
}
} else {
replaceResult = macroText;
}
return replaceResult;
};

const references = {
"<": "&lt;",
">": "&gt;",
Expand All @@ -66,15 +22,10 @@ function encode(text) {
return "";
}

return replaceUsingRegExp(text, /[<>'"&]/g, (input) => {
let result = references[/** @type {keyof typeof references} */ (input)];
if (!result) {
const code =
input.length > 1 ? getCodePoint(input, 0) : input.charCodeAt(0);
result = `&#${code};`;
}
return result;
});
return text.replace(
/[<>'"&]/g,
(input) => references[/** @type {keyof typeof references} */ (input)],
);
}

/**
Expand Down Expand Up @@ -450,8 +401,10 @@ const createOverlay = (options) => {
let containerElement;
/** @type {HTMLDivElement | null | undefined} */
let headerElement;
/** @type {((element: HTMLDivElement) => void)[]} */
let onLoadQueue = [];
/** @type {((element: HTMLDivElement) => void) | undefined} */
let onLoad;
/** @type {Element | null | undefined} */
let previousActiveElement;
/** @type {Omit<TrustedTypePolicy, "createScript" | "createScriptURL"> | undefined} */
let overlayTrustedTypesPolicy;

Expand All @@ -474,7 +427,7 @@ const createOverlay = (options) => {
*/
function createContainer(trustedTypesPolicyName) {
// Enable Trusted Types if they are available in the current browser.
if (window.trustedTypes) {
if (window.trustedTypes && !overlayTrustedTypesPolicy) {
overlayTrustedTypesPolicy = window.trustedTypes.createPolicy(
trustedTypesPolicyName || "webpack-dev-server#overlay",
{
Expand All @@ -485,9 +438,14 @@ const createOverlay = (options) => {

iframeContainerElement = document.createElement("iframe");
iframeContainerElement.id = "webpack-dev-server-client-overlay";
iframeContainerElement.title = "Webpack development server errors";
iframeContainerElement.src = "about:blank";
applyStyle(iframeContainerElement, iframeStyle);

previousActiveElement = document.activeElement;
// eslint-disable-next-line no-use-before-define
window.addEventListener("keydown", handleEscapeKey);

iframeContainerElement.onload = () => {
const contentElement =
/** @type {Document} */
Expand Down Expand Up @@ -531,10 +489,21 @@ const createOverlay = (options) => {
(iframeContainerElement).contentDocument
).body.appendChild(contentElement);

onLoadQueue.forEach((onLoad) => {
onLoad(/** @type {HTMLDivElement} */ (contentElement));
});
onLoadQueue = [];
const iframeDocument = /** @type {Document} */ (
/** @type {HTMLIFrameElement} */ (iframeContainerElement)
.contentDocument
);
iframeDocument.documentElement.lang =
document.documentElement.lang || "en";
// eslint-disable-next-line no-use-before-define
iframeDocument.addEventListener("keydown", handleEscapeKey);

if (onLoad) {
onLoad(contentElement);
onLoad = undefined;
}

closeButtonElement.focus();

/** @type {HTMLIFrameElement} */
(iframeContainerElement).onload = null;
Expand All @@ -559,7 +528,8 @@ const createOverlay = (options) => {
return;
}

onLoadQueue.push(callback);
// Each callback renders the complete current message list.
onLoad = callback;

if (iframeContainerElement) {
return;
Expand All @@ -578,10 +548,24 @@ const createOverlay = (options) => {
}

// Clean up and reset internal state.
iframeContainerElement.onload = null;
iframeContainerElement.contentDocument?.removeEventListener(
"keydown",
// eslint-disable-next-line no-use-before-define
handleEscapeKey,
);
const restoreFocus = document.activeElement === iframeContainerElement;
document.body.removeChild(iframeContainerElement);

iframeContainerElement = null;
containerElement = null;
headerElement = null;
onLoad = undefined;

if (restoreFocus && previousActiveElement instanceof HTMLElement) {
previousActiveElement.focus();
}
previousActiveElement = null;
}

// Compilation with errors (e.g. syntax error or missing modules).
Expand All @@ -608,19 +592,29 @@ const createOverlay = (options) => {
padding: "1rem 1rem 1.5rem 1rem",
});

const typeElement = document.createElement("div");
const canOpen = typeof message !== "string" && message.moduleIdentifier;
const typeElement = document.createElement(canOpen ? "button" : "div");
const { header, body } = formatProblem(type, message);

typeElement.innerText = header;
applyStyle(typeElement, msgTypeStyle);

if (typeof message !== "string" && message.moduleIdentifier) {
applyStyle(typeElement, { cursor: "pointer" });
if (canOpen) {
typeElement.setAttribute("type", "button");
applyStyle(typeElement, {
cursor: "pointer",
background: "none",
border: "none",
padding: "0",
textAlign: "left",
display: "block",
lineHeight: "inherit",
});
// element.dataset not supported in IE
typeElement.setAttribute("data-can-open", "true");
typeElement.addEventListener("click", () => {
fetch(
`/webpack-dev-server/open-editor?fileName=${message.moduleIdentifier}`,
`/webpack-dev-server/open-editor?fileName=${encodeURIComponent(canOpen)}`,
);
});
}
Expand Down
Loading
Loading