1 | 'use strict';
|
2 |
|
3 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
|
4 |
|
5 | var webpack = require('webpack');
|
6 | var env = _interopDefault(require('std-env'));
|
7 | var prettyTime = _interopDefault(require('pretty-time'));
|
8 | var path = require('path');
|
9 | var path__default = _interopDefault(path);
|
10 | var chalk = _interopDefault(require('chalk'));
|
11 | var Consola = _interopDefault(require('consola'));
|
12 | var textTable = _interopDefault(require('text-table'));
|
13 | var figures = require('figures');
|
14 | var ansiEscapes = _interopDefault(require('ansi-escapes'));
|
15 | var wrapAnsi = _interopDefault(require('wrap-ansi'));
|
16 |
|
17 | function first(arr) {
|
18 | return arr[0];
|
19 | }
|
20 | function last(arr) {
|
21 | return arr.length ? arr[arr.length - 1] : null;
|
22 | }
|
23 | function startCase(str) {
|
24 | return str[0].toUpperCase() + str.substr(1);
|
25 | }
|
26 | function firstMatch(regex, str) {
|
27 | const m = regex.exec(str);
|
28 | return m ? m[0] : null;
|
29 | }
|
30 | function hasValue(s) {
|
31 | return s && s.length;
|
32 | }
|
33 | function removeAfter(delimiter, str) {
|
34 | return first(str.split(delimiter)) || '';
|
35 | }
|
36 | function removeBefore(delimiter, str) {
|
37 | return last(str.split(delimiter)) || '';
|
38 | }
|
39 | function range(len) {
|
40 | const arr = [];
|
41 |
|
42 | for (let i = 0; i < len; i++) {
|
43 | arr.push(i);
|
44 | }
|
45 |
|
46 | return arr;
|
47 | }
|
48 | function shortenPath(path$1 = '') {
|
49 | const cwd = process.cwd() + path.sep;
|
50 | return String(path$1).replace(cwd, '');
|
51 | }
|
52 | function objectValues(obj) {
|
53 | return Object.keys(obj).map(key => obj[key]);
|
54 | }
|
55 |
|
56 | const nodeModules = `${path__default.delimiter}node_modules${path__default.delimiter}`;
|
57 | const BAR_LENGTH = 25;
|
58 | const BLOCK_CHAR = '█';
|
59 | const BLOCK_CHAR2 = '█';
|
60 | const NEXT = ' ' + chalk.blue(figures.pointerSmall) + ' ';
|
61 | const BULLET = figures.bullet;
|
62 | const TICK = figures.tick;
|
63 | const CROSS = figures.cross;
|
64 | const CIRCLE_OPEN = figures.radioOff;
|
65 |
|
66 | const consola = Consola.withTag('webpackbar');
|
67 | const colorize = color => {
|
68 | if (color[0] === '#') {
|
69 | return chalk.hex(color);
|
70 | }
|
71 |
|
72 | return chalk[color] || chalk.keyword(color);
|
73 | };
|
74 | const 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 | };
|
80 | function createTable(data) {
|
81 | return textTable(data, {
|
82 | align: data[0].map(() => 'l')
|
83 | }).replace(/\n/g, '\n\n');
|
84 | }
|
85 | function ellipsisLeft(str, n) {
|
86 | if (str.length <= n - 3) {
|
87 | return str;
|
88 | }
|
89 |
|
90 | return `...${str.substr(str.length - n - 1)}`;
|
91 | }
|
92 |
|
93 | const 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 | };
|
102 | const 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 | };
|
111 |
|
112 | function 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 |
|
120 | const originalWrite = Symbol('webpackbarWrite');
|
121 | class 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 |
|
179 | if (this.listening) {
|
180 | return;
|
181 | }
|
182 |
|
183 |
|
184 | for (const stream of this._streams) {
|
185 |
|
186 | if (stream.write[originalWrite]) {
|
187 | continue;
|
188 | }
|
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 | };
|
200 |
|
201 |
|
202 | write[originalWrite] = stream.write;
|
203 |
|
204 | stream.write = write;
|
205 | }
|
206 |
|
207 | this.listening = true;
|
208 | }
|
209 |
|
210 | stopListen() {
|
211 |
|
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 |
|
224 | const logUpdate = new LogUpdate();
|
225 | let lastRender = Date.now();
|
226 | class 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 |
|
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 |
|
280 | class 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 |
|
302 | const 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 | };
|
315 | function 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 |
|
331 | function 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 |
|
354 | class 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 |
|
430 | class 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 |
|
463 | class 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 |
|
504 | var reporters = /*#__PURE__*/Object.freeze({
|
505 | fancy: FancyReporter,
|
506 | basic: SimpleReporter,
|
507 | profile: ProfileReporter,
|
508 | stats: StatsReporter
|
509 | });
|
510 |
|
511 | const DEFAULTS = {
|
512 | name: 'webpack',
|
513 | color: 'green',
|
514 | reporters: env.minimalCLI ? ['basic'] : ['fancy'],
|
515 | reporter: null // Default state object
|
516 |
|
517 | };
|
518 | const 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 | };
|
528 | const globalStates = {};
|
529 | class 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 |
|
706 | module.exports = WebpackBarPlugin;
|
707 |
|
\ | No newline at end of file |