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 The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'. - --client-progress Prints compilation progress in percentage in the browser. - --no-client-progress Does not print compilation progress in percentage in the browser. + --client-progress [value] Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators. + --no-client-progress Does not display compilation progress in the browser. --client-reconnect [value] Tells dev-server the number of times it should try to reconnect the client. --no-client-reconnect Tells dev-server to not to try to reconnect the client. --client-web-socket-transport Allows to set custom web socket transport to communicate with dev server. diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 5742ee0b33..26a17da2c5 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -732,7 +732,8 @@ declare class Server< ClientProgress: { description: string; link: string; - type: string; + type: string[]; + enum: (string | boolean)[]; cli: { negatedDescription: string; }; @@ -764,7 +765,9 @@ declare class Server< }; ClientWebSocketTransportEnum: { enum: string[]; - }; + } /** + * @typedef {Array<{ key: string; value: string }> | Record} Headers + */; ClientWebSocketTransportString: { type: string; minLength: number; @@ -835,15 +838,10 @@ declare class Server< }; Compress: { type: string; - /** - * @template T - * @param fn {(function(): any) | undefined} - * @returns {function(): T} - */ description: string; link: string; cli: { - negatedDescription: string /** @type {function(): any} */; + negatedDescription: string; }; }; DevMiddleware: { @@ -866,6 +864,11 @@ declare class Server< }; }; cli: { + /** + * @param {string} route + * @param {HandleFunction} fn + * @returns {BasicApplication} + */ exclude: boolean; }; }; @@ -873,6 +876,11 @@ declare class Server< anyOf: ( | { type: string; + /** + * @param {string} route + * @param {HandleFunction} fn + * @returns {BasicApplication} + */ items: { $ref: string; }; @@ -892,7 +900,9 @@ declare class Server< minItems?: undefined; } )[]; - description: string; + description: string /** + * @template {BasicApplication} [T=ExpressApplication] + */; link: string; }; HistoryApiFallback: { @@ -917,17 +927,10 @@ declare class Server< }; Host: { description: string; - /** - * @private - * @type {RequestHandler[]} - */ link: string; anyOf: ( | { enum: string[]; - /** - * @type {Socket[]} - */ type?: undefined; minLength?: undefined; } @@ -954,6 +957,10 @@ declare class Server< } )[]; description: string; + /** + * @param {string} URL + * @returns {boolean} + */ link: string; }; IPC: { @@ -980,6 +987,7 @@ declare class Server< }; link: string; }; + /** @type {string} */ OnListening: { instanceof: string; description: string; @@ -1001,7 +1009,10 @@ declare class Server< type?: undefined; items?: undefined; } - )[]; + )[] /** + * @param {Host} hostname + * @returns {Promise} + */; description: string; link: string; }; @@ -1137,10 +1148,7 @@ declare class Server< description: string; }; ServerType: { - enum: string[] /** - * @private - * @param {Compiler} compiler - */; + enum: string[]; }; ServerEnum: { enum: string[]; @@ -1164,7 +1172,7 @@ declare class Server< }[]; }; options: { - $ref: string /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable }} */; + $ref: string; }; }; additionalProperties: boolean; @@ -1175,14 +1183,14 @@ declare class Server< properties: { passphrase: { type: string; - /** @type {string} */ description: string; }; requestCert: { type: string; description: string; + /** @type {ServerConfiguration} */ cli: { - negatedDescription: string /** @type {ServerConfiguration} */; + negatedDescription: string; }; }; ca: { @@ -1334,7 +1342,7 @@ declare class Server< } | { type: string; - additionalProperties: boolean; + /** @type {string} */ additionalProperties: boolean; instanceof?: undefined; } )[]; @@ -1485,7 +1493,7 @@ declare class Server< $ref: string; }[]; }; - /** @type {MultiCompiler} */ $ref?: undefined; + $ref?: undefined; } | { $ref: string;