Skip to content

Commit a8f40b7

Browse files
authored
feat: add visual progress indicators
1 parent 774120f commit a8f40b7

File tree

8 files changed

+267
-35
lines changed

8 files changed

+267
-35
lines changed

.cspell.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@
6464
"omnibox",
6565
"swiftshader",
6666
"hoge",
67-
"subsubcomain"
67+
"subsubcomain",
68+
"noselect"
6869
],
6970
"ignorePaths": [
7071
"CHANGELOG.md",

client-src/index.js

+14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { log, logEnabledFeatures, setLogLevel } from "./utils/log.js";
99
import sendMessage from "./utils/sendMessage.js";
1010
import reloadApp from "./utils/reloadApp.js";
1111
import createSocketURL from "./utils/createSocketURL.js";
12+
import { isProgressSupported, defineProgressElement } from "./progress.js";
1213

1314
/**
1415
* @typedef {Object} OverlayOptions
@@ -236,6 +237,19 @@ const onSocketMessage = {
236237
);
237238
}
238239

240+
if (isProgressSupported()) {
241+
if (typeof options.progress === "string") {
242+
let progress = document.querySelector("wds-progress");
243+
if (!progress) {
244+
defineProgressElement();
245+
progress = document.createElement("wds-progress");
246+
document.body.appendChild(progress);
247+
}
248+
progress.setAttribute("progress", data.percent);
249+
progress.setAttribute("type", options.progress);
250+
}
251+
}
252+
239253
sendMessage("Progress", data);
240254
},
241255
"still-ok": function stillOk() {

client-src/progress.js

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
class WebpackDevServerProgress extends HTMLElement {
2+
constructor() {
3+
super();
4+
this.attachShadow({ mode: "open" });
5+
this.maxDashOffset = -219.99078369140625;
6+
this.animationTimer = null;
7+
}
8+
9+
#reset() {
10+
clearTimeout(this.animationTimer);
11+
this.animationTimer = null;
12+
13+
const typeAttr = this.getAttribute("type")?.toLowerCase();
14+
this.type = typeAttr === "circular" ? "circular" : "linear";
15+
16+
const innerHTML =
17+
this.type === "circular"
18+
? WebpackDevServerProgress.#circularTemplate()
19+
: WebpackDevServerProgress.#linearTemplate();
20+
this.shadowRoot.innerHTML = innerHTML;
21+
22+
this.initialProgress = Number(this.getAttribute("progress")) ?? 0;
23+
24+
this.#update(this.initialProgress);
25+
}
26+
27+
static #circularTemplate() {
28+
return `
29+
<style>
30+
:host {
31+
width: 200px;
32+
height: 200px;
33+
position: fixed;
34+
right: 5%;
35+
top: 5%;
36+
transition: opacity .25s ease-in-out;
37+
z-index: 2147483645;
38+
}
39+
40+
circle {
41+
fill: #282d35;
42+
}
43+
44+
path {
45+
fill: rgba(0, 0, 0, 0);
46+
stroke: rgb(186, 223, 172);
47+
stroke-dasharray: 219.99078369140625;
48+
stroke-dashoffset: -219.99078369140625;
49+
stroke-width: 10;
50+
transform: rotate(90deg) translate(0px, -80px);
51+
}
52+
53+
text {
54+
font-family: 'Open Sans', sans-serif;
55+
font-size: 18px;
56+
fill: #ffffff;
57+
dominant-baseline: middle;
58+
text-anchor: middle;
59+
}
60+
61+
tspan#percent-super {
62+
fill: #bdc3c7;
63+
font-size: 0.45em;
64+
baseline-shift: 10%;
65+
}
66+
67+
@keyframes fade {
68+
0% { opacity: 1; transform: scale(1); }
69+
100% { opacity: 0; transform: scale(0); }
70+
}
71+
72+
.disappear {
73+
animation: fade 0.3s;
74+
animation-fill-mode: forwards;
75+
animation-delay: 0.5s;
76+
}
77+
78+
.hidden {
79+
display: none;
80+
}
81+
</style>
82+
<svg id="progress" class="hidden noselect" viewBox="0 0 80 80">
83+
<circle cx="50%" cy="50%" r="35"></circle>
84+
<path d="M5,40a35,35 0 1,0 70,0a35,35 0 1,0 -70,0"></path>
85+
<text x="50%" y="51%">
86+
<tspan id="percent-value">0</tspan>
87+
<tspan id="percent-super">%</tspan>
88+
</text>
89+
</svg>
90+
`;
91+
}
92+
93+
static #linearTemplate() {
94+
return `
95+
<style>
96+
:host {
97+
position: fixed;
98+
top: 0;
99+
left: 0;
100+
height: 4px;
101+
width: 100vw;
102+
z-index: 2147483645;
103+
}
104+
105+
#bar {
106+
width: 0%;
107+
height: 4px;
108+
background-color: rgb(186, 223, 172);
109+
}
110+
111+
@keyframes fade {
112+
0% { opacity: 1; }
113+
100% { opacity: 0; }
114+
}
115+
116+
.disappear {
117+
animation: fade 0.3s;
118+
animation-fill-mode: forwards;
119+
animation-delay: 0.5s;
120+
}
121+
122+
.hidden {
123+
display: none;
124+
}
125+
</style>
126+
<div id="progress"></div>
127+
`;
128+
}
129+
130+
connectedCallback() {
131+
this.#reset();
132+
}
133+
134+
static get observedAttributes() {
135+
return ["progress", "type"];
136+
}
137+
138+
attributeChangedCallback(name, oldValue, newValue) {
139+
if (name === "progress") {
140+
this.#update(Number(newValue));
141+
} else if (name === "type") {
142+
this.#reset();
143+
}
144+
}
145+
146+
#update(percent) {
147+
const element = this.shadowRoot.querySelector("#progress");
148+
if (this.type === "circular") {
149+
const path = this.shadowRoot.querySelector("path");
150+
const value = this.shadowRoot.querySelector("#percent-value");
151+
const offset = ((100 - percent) / 100) * this.maxDashOffset;
152+
153+
path.style.strokeDashoffset = offset;
154+
value.textContent = percent;
155+
} else {
156+
element.style.width = `${percent}%`;
157+
}
158+
159+
if (percent >= 100) {
160+
this.#hide();
161+
} else if (percent > 0) {
162+
this.#show();
163+
}
164+
}
165+
166+
#show() {
167+
const element = this.shadowRoot.querySelector("#progress");
168+
element.classList.remove("hidden");
169+
}
170+
171+
#hide() {
172+
const element = this.shadowRoot.querySelector("#progress");
173+
if (this.type === "circular") {
174+
element.classList.add("disappear");
175+
element.addEventListener(
176+
"animationend",
177+
() => {
178+
element.classList.add("hidden");
179+
this.#update(0);
180+
},
181+
{ once: true },
182+
);
183+
} else if (this.type === "linear") {
184+
element.classList.add("disappear");
185+
this.animationTimer = setTimeout(() => {
186+
element.classList.remove("disappear");
187+
element.classList.add("hidden");
188+
element.style.width = "0%";
189+
this.animationTimer = null;
190+
}, 800);
191+
}
192+
}
193+
}
194+
195+
export function isProgressSupported() {
196+
return "customElements" in window && !!HTMLElement.prototype.attachShadow;
197+
}
198+
199+
export function defineProgressElement() {
200+
if (customElements.get("wds-progress")) {
201+
return;
202+
}
203+
204+
customElements.define("wds-progress", WebpackDevServerProgress);
205+
}

examples/client/progress/README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module.exports = {
77
// ...
88
devServer: {
99
client: {
10-
progress: true,
10+
progress: true | "linear" | "circular",
1111
},
1212
},
1313
};
@@ -17,6 +17,8 @@ Usage via CLI:
1717

1818
```shell
1919
npx webpack serve --open --client-progress
20+
npx webpack serve --open --client-progress linear
21+
npx webpack serve --open --client-progress circular
2022
```
2123

2224
To disable:

lib/options.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,12 @@
156156
]
157157
},
158158
"ClientProgress": {
159-
"description": "Prints compilation progress in percentage in the browser.",
159+
"description": "Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators.",
160160
"link": "https://webpack.js.org/configuration/dev-server/#progress",
161-
"type": "boolean",
161+
"type": ["boolean", "string"],
162+
"enum": [true, false, "linear", "circular"],
162163
"cli": {
163-
"negatedDescription": "Does not print compilation progress in percentage in the browser."
164+
"negatedDescription": "Does not display compilation progress in the browser."
164165
}
165166
},
166167
"ClientReconnect": {

test/__snapshots__/validate-options.test.js.snap.webpack5

+3-2
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,9 @@ exports[`options validate should throw an error on the "client" option with '{"o
153153

154154
exports[`options validate should throw an error on the "client" option with '{"progress":""}' value 1`] = `
155155
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
156-
- options.client.progress should be a boolean.
157-
-> Prints compilation progress in percentage in the browser.
156+
- options.client.progress should be one of these:
157+
true | false | "linear" | "circular"
158+
-> Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators.
158159
-> Read more at https://webpack.js.org/configuration/dev-server/#progress"
159160
`;
160161

test/cli/__snapshots__/basic.test.js.snap.webpack5

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ Options:
7777
--client-overlay-runtime-errors Enables a full-screen overlay in the browser when there are uncaught runtime errors.
7878
--no-client-overlay-runtime-errors Disables the full-screen overlay in the browser when there are uncaught runtime errors.
7979
--client-overlay-trusted-types-policy-name <value> The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'.
80-
--client-progress Prints compilation progress in percentage in the browser.
81-
--no-client-progress Does not print compilation progress in percentage in the browser.
80+
--client-progress [value] Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators.
81+
--no-client-progress Does not display compilation progress in the browser.
8282
--client-reconnect [value] Tells dev-server the number of times it should try to reconnect the client.
8383
--no-client-reconnect Tells dev-server to not to try to reconnect the client.
8484
--client-web-socket-transport <value> Allows to set custom web socket transport to communicate with dev server.

0 commit comments

Comments
 (0)