UNPKG

12.7 kBJavaScriptView Raw
1"use strict";
2
3const chalk = require("chalk");
4const blessed = require("neo-blessed");
5
6const { formatOutput } = require("../utils/format-output");
7const { formatModules } = require("../utils/format-modules");
8const { formatAssets } = require("../utils/format-assets");
9const { formatProblems } = require("../utils/format-problems");
10const { deserializeError } = require("../utils/error-serialization");
11
12const PERCENT_MULTIPLIER = 100;
13
14const DEFAULT_SCROLL_OPTIONS = {
15 scrollable: true,
16 input: true,
17 alwaysScroll: true,
18 scrollbar: {
19 ch: " ",
20 inverse: true
21 },
22 keys: true,
23 vi: true,
24 mouse: true
25};
26
27class Dashboard {
28 // eslint-disable-next-line max-statements
29 constructor(options) {
30 // Options, params
31 options = options || {};
32 const title = options.title || "webpack-dashboard";
33
34 this.color = options.color || "green";
35 this.minimal = options.minimal || false;
36 this.stats = null;
37
38 // Data binding, lookup tables.
39 this.setData = this.setData.bind(this);
40 this.actionForMessageType = {
41 progress: this.setProgress.bind(this),
42 operations: this.setOperations.bind(this),
43 status: this.setStatus.bind(this),
44 stats: this.setStats.bind(this),
45 log: this.setLog.bind(this),
46 clear: this.clear.bind(this),
47 sizes: _data => {
48 if (this.minimal) {
49 return;
50 }
51 if (_data.value instanceof Error) {
52 this.setSizesError(_data.value);
53 } else {
54 this.setSizes(_data);
55 }
56 },
57 problems: _data => {
58 if (this.minimal) {
59 return;
60 }
61 if (_data.value instanceof Error) {
62 this.setProblemsError(_data.value);
63 } else {
64 this.setProblems(_data);
65 }
66 }
67 };
68
69 // Start UI stuff.
70 this.screen = blessed.screen({
71 title,
72 smartCSR: true,
73 dockBorders: false,
74 fullUnicode: true,
75 autoPadding: true
76 });
77
78 this.layoutLog();
79 this.layoutStatus();
80
81 if (!this.minimal) {
82 this.layoutModules();
83 this.layoutAssets();
84 this.layoutProblems();
85 }
86
87 this.screen.key(["escape", "q", "C-c"], () => {
88 process.kill(process.pid, "SIGINT");
89 });
90
91 this.screen.render();
92 }
93
94 setData(dataArray, ack) {
95 dataArray
96 .map(data =>
97 data.error
98 ? Object.assign({}, data, {
99 value: deserializeError(data.value)
100 })
101 : data
102 )
103 .forEach(data => {
104 this.actionForMessageType[data.type](data);
105 });
106
107 this.screen.render();
108
109 // Send ack back if requested.
110 if (ack) {
111 ack();
112 }
113 }
114
115 setProgress(data) {
116 const percent = parseInt(data.value * PERCENT_MULTIPLIER, 10);
117 const formattedPercent = `${percent.toString()}%`;
118
119 if (!percent) {
120 this.progressbar.setProgress(percent);
121 }
122
123 if (this.minimal) {
124 this.progress.setContent(formattedPercent);
125 } else {
126 this.progressbar.setContent(formattedPercent);
127 this.progressbar.setProgress(percent);
128 }
129 }
130
131 setOperations(data) {
132 this.operations.setContent(data.value);
133 }
134
135 setStatus(data) {
136 let content;
137
138 switch (data.value) {
139 case "Success":
140 content = `{green-fg}{bold}${data.value}{/}`;
141 break;
142 case "Failed":
143 content = `{red-fg}{bold}${data.value}{/}`;
144 break;
145 case "Error":
146 content = `{red-fg}{bold}${data.value}{/}`;
147 break;
148 default:
149 content = `{bold}${data.value}{/}`;
150 }
151 this.status.setContent(content);
152 }
153
154 setStats(data) {
155 const stats = {
156 hasErrors: () => data.value.errors,
157 hasWarnings: () => data.value.warnings,
158 toJson: () => data.value.data
159 };
160
161 // Save for later when merging inspectpack sizes into the asset list
162 this.stats = stats;
163
164 if (stats.hasErrors()) {
165 this.status.setContent("{red-fg}{bold}Failed{/}");
166 }
167
168 this.logText.log(formatOutput(stats));
169
170 if (!this.minimal) {
171 this.modulesMenu.setLabel(chalk.yellow("Modules (loading...)"));
172 this.assets.setLabel(chalk.yellow("Assets (loading...)"));
173 this.problemsMenu.setLabel(chalk.yellow("Problems (loading...)"));
174 }
175 }
176
177 setSizes(data) {
178 const { assets } = data.value;
179
180 // Start with top-level assets.
181 this.assets.setLabel("Assets");
182 this.assetTable.setData(formatAssets(assets));
183
184 // Then split modules across assets.
185 const previousSelection = this.modulesMenu.selected;
186 const modulesItems = Object.keys(assets).reduce(
187 (memo, name) =>
188 Object.assign({}, memo, {
189 [name]: () => {
190 this.moduleTable.setData(formatModules(assets[name].files));
191 this.screen.render();
192 }
193 }),
194 {}
195 );
196
197 this.modulesMenu.setLabel("Modules");
198 this.modulesMenu.setItems(modulesItems);
199 this.modulesMenu.selectTab(previousSelection);
200
201 // Final render.
202 this.screen.render();
203 }
204
205 setSizesError(err) {
206 this.modulesMenu.setLabel(chalk.red("Modules (error)"));
207 this.assets.setLabel(chalk.red("Assets (error)"));
208 this.logText.log(chalk.red("Could not load module/asset sizes."));
209 this.logText.log(chalk.red(err));
210 }
211
212 setProblems(data) {
213 const { duplicates, versions } = data.value;
214
215 // Separate across assets.
216 // Use duplicates as the "canary" to get asset names.
217 const assetNames = Object.keys(duplicates.assets);
218
219 const previousSelection = this.problemsMenu.selected;
220 const problemsItems = assetNames.reduce(
221 (memo, name) =>
222 Object.assign({}, memo, {
223 [name]: () => {
224 this.problems.setContent(
225 formatProblems({
226 duplicates: duplicates.assets[name],
227 versions: versions.assets[name]
228 })
229 );
230 this.screen.render();
231 }
232 }),
233 {}
234 );
235
236 this.problemsMenu.setLabel("Problems");
237 this.problemsMenu.setItems(problemsItems);
238 this.problemsMenu.selectTab(previousSelection);
239
240 this.screen.render();
241 }
242
243 setProblemsError(err) {
244 this.problemsMenu.setLabel(chalk.red("Problems (error)"));
245 this.logText.log(chalk.red("Could not analyze bundle problems."));
246 this.logText.log(chalk.red(err.stack));
247 }
248
249 setLog(data) {
250 if (this.stats && this.stats.hasErrors()) {
251 return;
252 }
253 this.logText.log(data.value.replace(/[{}]/g, ""));
254 }
255
256 clear() {
257 this.logText.setContent("");
258 }
259
260 layoutLog() {
261 this.log = blessed.box({
262 label: "Log",
263 padding: 1,
264 width: this.minimal ? "100%" : "75%",
265 height: this.minimal ? "70%" : "36%",
266 left: "0%",
267 top: "0%",
268 border: {
269 type: "line"
270 },
271 style: {
272 fg: -1,
273 border: {
274 fg: this.color
275 }
276 }
277 });
278
279 this.logText = blessed.log(
280 Object.assign({}, DEFAULT_SCROLL_OPTIONS, {
281 parent: this.log,
282 tags: true,
283 width: "100%-5"
284 })
285 );
286
287 this.screen.append(this.log);
288 this.mapNavigationKeysToScrollLog();
289 }
290
291 mapNavigationKeysToScrollLog() {
292 this.screen.key(["pageup"], () => {
293 this.logText.setScrollPerc(0);
294 this.logText.screen.render();
295 });
296 this.screen.key(["pagedown"], () => {
297 // eslint-disable-next-line no-magic-numbers
298 this.logText.setScrollPerc(100);
299 this.logText.screen.render();
300 });
301 this.screen.key(["up"], () => {
302 this.logText.scroll(-1);
303 this.logText.screen.render();
304 });
305 this.screen.key(["down"], () => {
306 this.logText.scroll(1);
307 this.logText.screen.render();
308 });
309 this.screen.key(["left"], () => {
310 const currentIndex = this.modulesMenu.selected;
311 this.modulesMenu.selectTab(currentIndex - 1);
312 this.problemsMenu.selectTab(currentIndex - 1);
313 this.problemsMenu.screen.render();
314 this.modulesMenu.screen.render();
315 });
316 this.screen.key(["right"], () => {
317 const currentIndex = this.modulesMenu.selected;
318 this.modulesMenu.selectTab(currentIndex + 1);
319 this.problemsMenu.selectTab(currentIndex + 1);
320 this.problemsMenu.screen.render();
321 this.modulesMenu.screen.render();
322 });
323 }
324
325 layoutModules() {
326 this.modulesMenu = blessed.listbar({
327 label: "Modules",
328 mouse: true,
329 tags: true,
330 width: "50%",
331 height: "66%",
332 left: "0%",
333 top: "36%",
334 border: {
335 type: "line"
336 },
337 padding: 1,
338 style: {
339 fg: -1,
340 border: {
341 fg: this.color
342 },
343 prefix: {
344 fg: -1
345 },
346 item: {
347 fg: "white"
348 },
349 selected: {
350 fg: "black",
351 bg: this.color
352 }
353 },
354 autoCommandKeys: true
355 });
356
357 this.moduleTable = blessed.table(
358 Object.assign({}, DEFAULT_SCROLL_OPTIONS, {
359 parent: this.modulesMenu,
360 height: "100%",
361 width: "100%-5",
362 padding: {
363 top: 2,
364 right: 1,
365 left: 1
366 },
367 align: "left",
368 data: [["Name", "Size", "Percent"]],
369 tags: true
370 })
371 );
372
373 this.screen.append(this.modulesMenu);
374 }
375
376 layoutAssets() {
377 this.assets = blessed.box({
378 label: "Assets",
379 tags: true,
380 padding: 1,
381 width: "50%",
382 height: "28%",
383 left: "50%",
384 top: "36%",
385 border: {
386 type: "line"
387 },
388 style: {
389 fg: -1,
390 border: {
391 fg: this.color
392 }
393 }
394 });
395
396 this.assetTable = blessed.table(
397 Object.assign({}, DEFAULT_SCROLL_OPTIONS, {
398 parent: this.assets,
399 height: "100%",
400 width: "100%-5",
401 align: "left",
402 padding: 1,
403 data: [["Name", "Size"]]
404 })
405 );
406
407 this.screen.append(this.assets);
408 }
409
410 layoutProblems() {
411 this.problemsMenu = blessed.listbar({
412 label: "Problems",
413 mouse: true,
414 width: "50%",
415 height: "38%",
416 left: "50%",
417 top: "63%",
418 border: {
419 type: "line"
420 },
421 padding: {
422 top: 1
423 },
424 style: {
425 border: {
426 fg: this.color
427 },
428 prefix: {
429 fg: -1
430 },
431 item: {
432 fg: "white"
433 },
434 selected: {
435 fg: "black",
436 bg: this.color
437 }
438 },
439 autoCommandKeys: true
440 });
441
442 this.problems = blessed.box(
443 Object.assign({}, DEFAULT_SCROLL_OPTIONS, {
444 parent: this.problemsMenu,
445 padding: 1,
446 border: {
447 fg: -1
448 },
449 style: {
450 fg: -1,
451 border: {
452 fg: this.color
453 }
454 },
455 tags: true
456 })
457 );
458
459 this.screen.append(this.problemsMenu);
460 }
461
462 // eslint-disable-next-line complexity
463 layoutStatus() {
464 this.wrapper = blessed.layout({
465 width: this.minimal ? "100%" : "25%",
466 height: this.minimal ? "30%" : "36%",
467 top: this.minimal ? "70%" : "0%",
468 left: this.minimal ? "0%" : "75%",
469 layout: "grid"
470 });
471
472 this.status = blessed.box({
473 parent: this.wrapper,
474 label: "Status",
475 tags: true,
476 padding: {
477 left: 1
478 },
479 width: this.minimal ? "34%-1" : "100%",
480 height: this.minimal ? "100%" : "34%",
481 valign: "middle",
482 border: {
483 type: "line"
484 },
485 style: {
486 fg: -1,
487 border: {
488 fg: this.color
489 }
490 }
491 });
492
493 this.operations = blessed.box({
494 parent: this.wrapper,
495 label: "Operation",
496 tags: true,
497 padding: {
498 left: 1
499 },
500 width: this.minimal ? "34%-1" : "100%",
501 height: this.minimal ? "100%" : "34%",
502 valign: "middle",
503 border: {
504 type: "line"
505 },
506 style: {
507 fg: -1,
508 border: {
509 fg: this.color
510 }
511 }
512 });
513
514 this.progress = blessed.box({
515 parent: this.wrapper,
516 label: "Progress",
517 tags: true,
518 padding: this.minimal
519 ? {
520 left: 1
521 }
522 : 1,
523 width: this.minimal ? "33%" : "100%",
524 height: this.minimal ? "100%" : "34%",
525 valign: "middle",
526 border: {
527 type: "line"
528 },
529 style: {
530 fg: -1,
531 border: {
532 fg: this.color
533 }
534 }
535 });
536
537 this.progressbar = new blessed.ProgressBar({
538 parent: this.progress,
539 height: 1,
540 width: "90%",
541 top: "center",
542 left: "center",
543 hidden: this.minimal,
544 orientation: "horizontal",
545 style: {
546 bar: {
547 bg: this.color
548 }
549 }
550 });
551
552 this.screen.append(this.wrapper);
553 }
554}
555
556module.exports = Dashboard;