UNPKG

16.9 kBJavaScriptView Raw
1'use strict';
2
3function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
4
5var webpack = require('webpack');
6var env = _interopDefault(require('std-env'));
7var prettyTime = _interopDefault(require('pretty-time'));
8var path = require('path');
9var path__default = _interopDefault(path);
10var chalk = _interopDefault(require('chalk'));
11var Consola = _interopDefault(require('consola'));
12var textTable = _interopDefault(require('text-table'));
13var figures = require('figures');
14var ansiEscapes = _interopDefault(require('ansi-escapes'));
15var wrapAnsi = _interopDefault(require('wrap-ansi'));
16
17function first(arr) {
18 return arr[0];
19}
20function last(arr) {
21 return arr.length ? arr[arr.length - 1] : null;
22}
23function startCase(str) {
24 return str[0].toUpperCase() + str.substr(1);
25}
26function firstMatch(regex, str) {
27 const m = regex.exec(str);
28 return m ? m[0] : null;
29}
30function hasValue(s) {
31 return s && s.length;
32}
33function removeAfter(delimiter, str) {
34 return first(str.split(delimiter)) || '';
35}
36function removeBefore(delimiter, str) {
37 return last(str.split(delimiter)) || '';
38}
39function range(len) {
40 const arr = [];
41
42 for (let i = 0; i < len; i++) {
43 arr.push(i);
44 }
45
46 return arr;
47}
48function shortenPath(path$1 = '') {
49 const cwd = process.cwd() + path.sep;
50 return String(path$1).replace(cwd, '');
51}
52function objectValues(obj) {
53 return Object.keys(obj).map(key => obj[key]);
54}
55
56const nodeModules = `${path__default.delimiter}node_modules${path__default.delimiter}`;
57const BAR_LENGTH = 25;
58const BLOCK_CHAR = '█';
59const BLOCK_CHAR2 = '█';
60const NEXT = ' ' + chalk.blue(figures.pointerSmall) + ' ';
61const BULLET = figures.bullet;
62const TICK = figures.tick;
63const CROSS = figures.cross;
64const CIRCLE_OPEN = figures.radioOff;
65
66const consola = Consola.withTag('webpackbar');
67const colorize = color => {
68 if (color[0] === '#') {
69 return chalk.hex(color);
70 }
71
72 return chalk[color] || chalk.keyword(color);
73};
74const renderBar = (progress, color) => {
75 const w = progress * (BAR_LENGTH / 100);
76 const bg = chalk.white(BLOCK_CHAR);
77 const fg = colorize(color)(BLOCK_CHAR2);
78 return range(BAR_LENGTH).map(i => i < w ? fg : bg).join('');
79};
80function createTable(data) {
81 return textTable(data, {
82 align: data[0].map(() => 'l')
83 }).replace(/\n/g, '\n\n');
84}
85function ellipsisLeft(str, n) {
86 if (str.length <= n - 3) {
87 return str;
88 }
89
90 return `...${str.substr(str.length - n - 1)}`;
91}
92
93const parseRequest = requestStr => {
94 const parts = (requestStr || '').split('!');
95 const file = path__default.relative(process.cwd(), removeAfter('?', removeBefore(nodeModules, parts.pop())));
96 const loaders = parts.map(part => firstMatch(/[a-z0-9-@]+-loader/, part)).filter(hasValue);
97 return {
98 file: hasValue(file) ? file : null,
99 loaders
100 };
101};
102const formatRequest = request => {
103 const loaders = request.loaders.join(NEXT);
104
105 if (!loaders.length) {
106 return request.file || '';
107 }
108
109 return `${loaders}${NEXT}${request.file}`;
110}; // Hook helper for webpack 3 + 4 support
111
112function hook(compiler, hookName, fn) {
113 if (compiler.hooks) {
114 compiler.hooks[hookName].tap('WebpackBar:' + hookName, fn);
115 } else {
116 compiler.plugin(hookName, fn);
117 }
118}
119
120const originalWrite = Symbol('webpackbarWrite');
121class LogUpdate {
122 constructor() {
123 this.prevLineCount = 0;
124 this.listening = false;
125 this.extraLines = '';
126 this._onData = this._onData.bind(this);
127 this._streams = [process.stdout, process.stderr];
128 }
129
130 render(lines) {
131 this.listen();
132 const wrappedLines = wrapAnsi(lines, this.columns, {
133 trim: false,
134 hard: true,
135 wordWrap: false
136 });
137 const data = ansiEscapes.eraseLines(this.prevLineCount) + wrappedLines + '\n' + this.extraLines;
138 this.write(data);
139 this.prevLineCount = data.split('\n').length;
140 }
141
142 get columns() {
143 return (process.stderr.columns || 80) - 2;
144 }
145
146 write(data) {
147 const stream = process.stderr;
148
149 if (stream.write[originalWrite]) {
150 stream.write[originalWrite].call(stream, data, 'utf-8');
151 } else {
152 stream.write(data, 'utf-8');
153 }
154 }
155
156 clear() {
157 this.done();
158 this.write(ansiEscapes.eraseLines(this.prevLineCount));
159 }
160
161 done() {
162 this.stopListen();
163 this.prevLineCount = 0;
164 this.extraLines = '';
165 }
166
167 _onData(data) {
168 const str = String(data);
169 const lines = str.split('\n').length - 1;
170
171 if (lines > 0) {
172 this.prevLineCount += lines;
173 this.extraLines += data;
174 }
175 }
176
177 listen() {
178 // Prevent listening more than once
179 if (this.listening) {
180 return;
181 } // Spy on all streams
182
183
184 for (const stream of this._streams) {
185 // Prevent overriding more than once
186 if (stream.write[originalWrite]) {
187 continue;
188 } // Create a wrapper fn
189
190
191 const write = (data, ...args) => {
192 if (!stream.write[originalWrite]) {
193 return stream.write(data, ...args);
194 }
195
196 this._onData(data);
197
198 return stream.write[originalWrite].call(stream, data, ...args);
199 }; // Backup original write fn
200
201
202 write[originalWrite] = stream.write; // Override write fn
203
204 stream.write = write;
205 }
206
207 this.listening = true;
208 }
209
210 stopListen() {
211 // Restore original write fns
212 for (const stream of this._streams) {
213 if (stream.write[originalWrite]) {
214 stream.write = stream.write[originalWrite];
215 }
216 }
217
218 this.listening = false;
219 }
220
221}
222
223/* eslint-disable no-console */
224const logUpdate = new LogUpdate();
225let lastRender = Date.now();
226class FancyReporter {
227 allDone() {
228 logUpdate.done();
229 }
230
231 done(context) {
232 this._renderStates(context.statesArray);
233
234 if (context.hasErrors) {
235 logUpdate.done();
236 }
237 }
238
239 progress(context) {
240 if (Date.now() - lastRender > 50) {
241 this._renderStates(context.statesArray);
242 }
243 }
244
245 _renderStates(statesArray) {
246 lastRender = Date.now();
247 const renderedStates = statesArray.map(c => this._renderState(c)).join('\n\n');
248 logUpdate.render('\n' + renderedStates + '\n');
249 }
250
251 _renderState(state) {
252 const color = colorize(state.color);
253 let line1;
254 let line2;
255
256 if (state.progress >= 0 && state.progress < 100) {
257 // Running
258 line1 = [color(BULLET), color(state.name), renderBar(state.progress, state.color), state.message, `(${state.progress || 0}%)`, chalk.grey(state.details[0] || ''), chalk.grey(state.details[1] || '')].join(' ');
259 line2 = state.request ? ' ' + chalk.grey(ellipsisLeft(formatRequest(state.request), logUpdate.columns)) : '';
260 } else {
261 let icon = ' ';
262
263 if (state.hasErrors) {
264 icon = CROSS;
265 } else if (state.progress === 100) {
266 icon = TICK;
267 } else if (state.progress === -1) {
268 icon = CIRCLE_OPEN;
269 }
270
271 line1 = color(`${icon} ${state.name}`);
272 line2 = chalk.grey(' ' + state.message);
273 }
274
275 return line1 + '\n' + line2;
276 }
277
278}
279
280class SimpleReporter {
281 start(context) {
282 consola.info(`Compiling ${context.state.name}`);
283 }
284
285 change(context, {
286 shortPath
287 }) {
288 consola.debug(`${shortPath} changed.`, `Rebuilding ${context.state.name}`);
289 }
290
291 done(context) {
292 const {
293 hasError,
294 message,
295 name
296 } = context.state;
297 consola[hasError ? 'error' : 'success'](`${name}: ${message}`);
298 }
299
300}
301
302const DB = {
303 loader: {
304 get: loader => startCase(loader)
305 },
306 ext: {
307 get: ext => `${ext} files`,
308 vue: 'Vue Single File components',
309 js: 'JavaScript files',
310 sass: 'SASS files',
311 scss: 'SASS files',
312 unknown: 'Unknown files'
313 }
314};
315function getDescription(category, keyword) {
316 if (!DB[category]) {
317 return startCase(keyword);
318 }
319
320 if (DB[category][keyword]) {
321 return DB[category][keyword];
322 }
323
324 if (DB[category].get) {
325 return DB[category].get(keyword);
326 }
327
328 return '-';
329}
330
331function formatStats(allStats) {
332 const lines = [];
333 Object.keys(allStats).forEach(category => {
334 const stats = allStats[category];
335 lines.push(`> Stats by ${chalk.bold(startCase(category))}`);
336 let totalRequests = 0;
337 const totalTime = [0, 0];
338 const data = [[startCase(category), 'Requests', 'Time', 'Time/Request', 'Description']];
339 Object.keys(stats).forEach(item => {
340 const stat = stats[item];
341 totalRequests += stat.count || 0;
342 const description = getDescription(category, item);
343 totalTime[0] += stat.time[0];
344 totalTime[1] += stat.time[1];
345 const avgTime = [stat.time[0] / stat.count, stat.time[1] / stat.count];
346 data.push([item, stat.count || '-', prettyTime(stat.time), prettyTime(avgTime), description]);
347 });
348 data.push(['Total', totalRequests, prettyTime(totalTime), '', '']);
349 lines.push(createTable(data));
350 });
351 return `${lines.join('\n\n')}\n`;
352}
353
354class Profiler {
355 constructor() {
356 this.requests = [];
357 }
358
359 onRequest(request) {
360 if (!request) {
361 return;
362 } // Measure time for last request
363
364
365 if (this.requests.length) {
366 const lastReq = this.requests[this.requests.length - 1];
367
368 if (lastReq.start) {
369 lastReq.time = process.hrtime(lastReq.start);
370 delete lastReq.start;
371 }
372 } // Ignore requests without any file or loaders
373
374
375 if (!request.file || !request.loaders.length) {
376 return;
377 }
378
379 this.requests.push({
380 request,
381 start: process.hrtime()
382 });
383 }
384
385 getStats() {
386 const loaderStats = {};
387 const extStats = {};
388
389 const getStat = (stats, name) => {
390 if (!stats[name]) {
391 // eslint-disable-next-line no-param-reassign
392 stats[name] = {
393 count: 0,
394 time: [0, 0]
395 };
396 }
397
398 return stats[name];
399 };
400
401 const addToStat = (stats, name, count, time) => {
402 const stat = getStat(stats, name);
403 stat.count += count;
404 stat.time[0] += time[0];
405 stat.time[1] += time[1];
406 };
407
408 this.requests.forEach(({
409 request,
410 time = [0, 0]
411 }) => {
412 request.loaders.forEach(loader => {
413 addToStat(loaderStats, loader, 1, time);
414 });
415 const ext = request.file && path__default.extname(request.file).substr(1);
416 addToStat(extStats, ext && ext.length ? ext : 'unknown', 1, time);
417 });
418 return {
419 ext: extStats,
420 loader: loaderStats
421 };
422 }
423
424 getFormattedStats() {
425 return formatStats(this.getStats());
426 }
427
428}
429
430class ProfileReporter {
431 progress(context) {
432 if (!context.state.profiler) {
433 context.state.profiler = new Profiler();
434 }
435
436 context.state.profiler.onRequest(context.state.request);
437 }
438
439 done(context) {
440 if (context.state.profiler) {
441 context.state.profile = context.state.profiler.getFormattedStats();
442 delete context.state.profiler;
443 }
444 }
445
446 allDone(context) {
447 let str = '';
448
449 for (const state of context.statesArray) {
450 const color = colorize(state.color);
451
452 if (state.profile) {
453 str += color(`\nProfile results for ${chalk.bold(state.name)}\n`) + `\n${state.profile}\n`;
454 delete state.profile;
455 }
456 }
457
458 process.stderr.write(str);
459 }
460
461}
462
463class StatsReporter {
464 constructor(options) {
465 this.options = Object.assign({
466 chunks: false,
467 children: false,
468 modules: false,
469 colors: true,
470 warnings: true,
471 errors: true
472 }, options);
473 }
474
475 done(context, {
476 stats
477 }) {
478 const str = stats.toString(this.options);
479
480 if (context.hasErrors) {
481 process.stderr.write('\n' + str + '\n');
482 } else {
483 context.state.statsString = str;
484 }
485 }
486
487 allDone(context) {
488 let str = '';
489
490 for (const state of context.statesArray) {
491 if (state.statsString) {
492 str += '\n' + state.statsString + '\n';
493 delete state.statsString;
494 }
495 }
496
497 process.stderr.write(str);
498 }
499
500}
501
502
503
504var reporters = /*#__PURE__*/Object.freeze({
505 fancy: FancyReporter,
506 basic: SimpleReporter,
507 profile: ProfileReporter,
508 stats: StatsReporter
509});
510
511const DEFAULTS = {
512 name: 'webpack',
513 color: 'green',
514 reporters: env.minimalCLI ? ['basic'] : ['fancy'],
515 reporter: null // Default state object
516
517};
518const DEFAULT_STATE = {
519 start: null,
520 progress: -1,
521 done: false,
522 message: '',
523 details: [],
524 request: null,
525 hasErrors: false // Mapping from name => State
526
527};
528const globalStates = {};
529class WebpackBarPlugin extends webpack.ProgressPlugin {
530 constructor(options) {
531 super();
532 this.options = Object.assign({}, DEFAULTS, options); // Assign a better handler to base ProgressPlugin
533
534 this.handler = (percent, message, ...details) => {
535 this.updateProgress(percent, message, details);
536 }; // Reporters
537
538
539 this.reporters = Array.from(this.options.reporters || []);
540
541 if (this.options.reporter) {
542 this.reporters.push(this.options.reporter);
543 } // Resolve reporters
544
545
546 this.reporters = this.reporters.filter(Boolean).map(_reporter => {
547 if (this.options[_reporter] === false) {
548 return false;
549 }
550
551 let reporter = _reporter;
552 let reporterOptions = this.options[reporter] || {};
553
554 if (Array.isArray(_reporter)) {
555 reporter = _reporter[0];
556
557 if (_reporter[1] === false) {
558 return false;
559 }
560
561 if (_reporter[1]) {
562 reporterOptions = _reporter[1];
563 }
564 }
565
566 if (typeof reporter === 'string') {
567 if (reporters[reporter]) {
568 // eslint-disable-line import/namespace
569 reporter = reporters[reporter]; // eslint-disable-line import/namespace
570 } else {
571 reporter = require(reporter);
572 }
573 }
574
575 if (typeof reporter === 'function') {
576 if (typeof reporter.constructor === 'function') {
577 const Reporter = reporter;
578 reporter = new Reporter(reporterOptions);
579 } else {
580 reporter = reporter(reporterOptions);
581 }
582 }
583
584 return reporter;
585 }).filter(Boolean);
586 }
587
588 callReporters(fn, payload = {}) {
589 for (const reporter of this.reporters) {
590 if (typeof reporter[fn] === 'function') {
591 try {
592 reporter[fn](this, payload);
593 } catch (e) {
594 process.stdout.write(e.stack + '\n');
595 }
596 }
597 }
598 }
599
600 get hasRunning() {
601 return objectValues(this.states).some(state => !state.done);
602 }
603
604 get hasErrors() {
605 return objectValues(this.states).some(state => state.hasErrors);
606 }
607
608 get statesArray() {
609 return objectValues(this.states).sort((s1, s2) => s1.name.localeCompare(s2.name));
610 }
611
612 get states() {
613 return globalStates;
614 }
615
616 get state() {
617 return globalStates[this.options.name];
618 }
619
620 _ensureState() {
621 // Keep our state in shared object
622 if (!this.states[this.options.name]) {
623 this.states[this.options.name] = { ...DEFAULT_STATE,
624 color: this.options.color,
625 name: startCase(this.options.name)
626 };
627 }
628 }
629
630 apply(compiler) {
631 // Prevent adding multi instances to the same compiler
632 if (compiler.webpackbar) {
633 return;
634 }
635
636 compiler.webpackbar = this; // Apply base hooks
637
638 super.apply(compiler); // Register our state after all plugins initialized
639
640 hook(compiler, 'afterPlugins', () => {
641 this._ensureState();
642 }); // Hook into the compiler before a new compilation is created.
643
644 hook(compiler, 'compile', () => {
645 this._ensureState();
646
647 Object.assign(this.state, { ...DEFAULT_STATE,
648 start: process.hrtime()
649 });
650 this.callReporters('start');
651 }); // Watch compilation has been invalidated.
652
653 hook(compiler, 'invalid', (fileName, changeTime) => {
654 this._ensureState();
655
656 this.callReporters('change', {
657 path: fileName,
658 shortPath: shortenPath(fileName),
659 time: changeTime
660 });
661 }); // Compilation has completed
662
663 hook(compiler, 'done', stats => {
664 this._ensureState(); // Prevent calling done twice
665
666
667 if (this.state.done) {
668 return;
669 }
670
671 const hasErrors = stats.hasErrors();
672 const status = hasErrors ? 'with some errors' : 'successfully';
673 const time = this.state.start ? ' in ' + prettyTime(process.hrtime(this.state.start), 2) : '';
674 Object.assign(this.state, { ...DEFAULT_STATE,
675 progress: 100,
676 done: true,
677 message: `Compiled ${status}${time}`,
678 hasErrors
679 });
680 this.callReporters('progress');
681 this.callReporters('done', {
682 stats
683 });
684
685 if (!this.hasRunning) {
686 this.callReporters('beforeAllDone');
687 this.callReporters('allDone');
688 this.callReporters('afterAllDone');
689 }
690 });
691 }
692
693 updateProgress(percent = 0, message = '', details = []) {
694 const progress = Math.floor(percent * 100);
695 Object.assign(this.state, {
696 progress,
697 message: message || '',
698 details,
699 request: parseRequest(details[2])
700 });
701 this.callReporters('progress');
702 }
703
704}
705
706module.exports = WebpackBarPlugin;
707
\No newline at end of file