diff --git a/.cspell.json b/.cspell.json index 77c778432f..eb56d1ecd9 100644 --- a/.cspell.json +++ b/.cspell.json @@ -64,7 +64,8 @@ "omnibox", "swiftshader", "hoge", - "subsubcomain" + "subsubcomain", + "noselect" ], "ignorePaths": [ "CHANGELOG.md", diff --git a/client-src/index.js b/client-src/index.js index 4523ef845c..d6614a7a67 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -9,6 +9,7 @@ import { log, logEnabledFeatures, setLogLevel } from "./utils/log.js"; import sendMessage from "./utils/sendMessage.js"; import reloadApp from "./utils/reloadApp.js"; import createSocketURL from "./utils/createSocketURL.js"; +import { isProgressSupported, defineProgressElement } from "./progress.js"; /** * @typedef {Object} OverlayOptions @@ -236,6 +237,19 @@ const onSocketMessage = { ); } + if (isProgressSupported()) { + if (typeof options.progress === "string") { + let progress = document.querySelector("wds-progress"); + if (!progress) { + defineProgressElement(); + progress = document.createElement("wds-progress"); + document.body.appendChild(progress); + } + progress.setAttribute("progress", data.percent); + progress.setAttribute("type", options.progress); + } + } + sendMessage("Progress", data); }, "still-ok": function stillOk() { diff --git a/client-src/progress.js b/client-src/progress.js new file mode 100644 index 0000000000..21a98e7209 --- /dev/null +++ b/client-src/progress.js @@ -0,0 +1,205 @@ +class WebpackDevServerProgress extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.maxDashOffset = -219.99078369140625; + this.animationTimer = null; + } + + #reset() { + clearTimeout(this.animationTimer); + this.animationTimer = null; + + const typeAttr = this.getAttribute("type")?.toLowerCase(); + this.type = typeAttr === "circular" ? "circular" : "linear"; + + const innerHTML = + this.type === "circular" + ? WebpackDevServerProgress.#circularTemplate() + : WebpackDevServerProgress.#linearTemplate(); + this.shadowRoot.innerHTML = innerHTML; + + this.initialProgress = Number(this.getAttribute("progress")) ?? 0; + + this.#update(this.initialProgress); + } + + static #circularTemplate() { + return ` + + + `; + } + + static #linearTemplate() { + return ` + +
+ `; + } + + connectedCallback() { + this.#reset(); + } + + static get observedAttributes() { + return ["progress", "type"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === "progress") { + this.#update(Number(newValue)); + } else if (name === "type") { + this.#reset(); + } + } + + #update(percent) { + const element = this.shadowRoot.querySelector("#progress"); + if (this.type === "circular") { + const path = this.shadowRoot.querySelector("path"); + const value = this.shadowRoot.querySelector("#percent-value"); + const offset = ((100 - percent) / 100) * this.maxDashOffset; + + path.style.strokeDashoffset = offset; + value.textContent = percent; + } else { + element.style.width = `${percent}%`; + } + + if (percent >= 100) { + this.#hide(); + } else if (percent > 0) { + this.#show(); + } + } + + #show() { + const element = this.shadowRoot.querySelector("#progress"); + element.classList.remove("hidden"); + } + + #hide() { + const element = this.shadowRoot.querySelector("#progress"); + if (this.type === "circular") { + element.classList.add("disappear"); + element.addEventListener( + "animationend", + () => { + element.classList.add("hidden"); + this.#update(0); + }, + { once: true }, + ); + } else if (this.type === "linear") { + element.classList.add("disappear"); + this.animationTimer = setTimeout(() => { + element.classList.remove("disappear"); + element.classList.add("hidden"); + element.style.width = "0%"; + this.animationTimer = null; + }, 800); + } + } +} + +export function isProgressSupported() { + return "customElements" in window && !!HTMLElement.prototype.attachShadow; +} + +export function defineProgressElement() { + if (customElements.get("wds-progress")) { + return; + } + + customElements.define("wds-progress", WebpackDevServerProgress); +} diff --git a/examples/client/progress/README.md b/examples/client/progress/README.md index b0f790a3fc..5f687b2927 100644 --- a/examples/client/progress/README.md +++ b/examples/client/progress/README.md @@ -7,7 +7,7 @@ module.exports = { // ... devServer: { client: { - progress: true, + progress: true | "linear" | "circular", }, }, }; @@ -17,6 +17,8 @@ Usage via CLI: ```shell npx webpack serve --open --client-progress +npx webpack serve --open --client-progress linear +npx webpack serve --open --client-progress circular ``` To disable: diff --git a/lib/options.json b/lib/options.json index 0951aefbcf..e6cffed194 100644 --- a/lib/options.json +++ b/lib/options.json @@ -156,11 +156,12 @@ ] }, "ClientProgress": { - "description": "Prints compilation progress in percentage in the browser.", + "description": "Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators.", "link": "https://webpack.js.org/configuration/dev-server/#progress", - "type": "boolean", + "type": ["boolean", "string"], + "enum": [true, false, "linear", "circular"], "cli": { - "negatedDescription": "Does not print compilation progress in percentage in the browser." + "negatedDescription": "Does not display compilation progress in the browser." } }, "ClientReconnect": { diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5 index 60fa07786f..8c4865a874 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack5 @@ -153,8 +153,9 @@ exports[`options validate should throw an error on the "client" option with '{"o exports[`options validate should throw an error on the "client" option with '{"progress":""}' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - - options.client.progress should be a boolean. - -> Prints compilation progress in percentage in the browser. + - options.client.progress should be one of these: + true | false | "linear" | "circular" + -> Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators. -> Read more at https://webpack.js.org/configuration/dev-server/#progress" `; diff --git a/test/cli/__snapshots__/basic.test.js.snap.webpack5 b/test/cli/__snapshots__/basic.test.js.snap.webpack5 index e39e5be91a..a5e1d16264 100644 --- a/test/cli/__snapshots__/basic.test.js.snap.webpack5 +++ b/test/cli/__snapshots__/basic.test.js.snap.webpack5 @@ -77,8 +77,8 @@ Options: --client-overlay-runtime-errors Enables a full-screen overlay in the browser when there are uncaught runtime errors. --no-client-overlay-runtime-errors Disables the full-screen overlay in the browser when there are uncaught runtime errors. --client-overlay-trusted-types-policy-name