'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var webpack = require('webpack'); var env = _interopDefault(require('std-env')); var prettyTime = _interopDefault(require('pretty-time')); var path = require('path'); var path__default = _interopDefault(path); var chalk = _interopDefault(require('chalk')); var Consola = _interopDefault(require('consola')); var textTable = _interopDefault(require('text-table')); var figures = require('figures'); var ansiEscapes = _interopDefault(require('ansi-escapes')); var wrapAnsi = _interopDefault(require('wrap-ansi')); function first(arr) { return arr[0]; } function last(arr) { return arr.length ? arr[arr.length - 1] : null; } function startCase(str) { return str[0].toUpperCase() + str.substr(1); } function firstMatch(regex, str) { const m = regex.exec(str); return m ? m[0] : null; } function hasValue(s) { return s && s.length; } function removeAfter(delimiter, str) { return first(str.split(delimiter)) || ''; } function removeBefore(delimiter, str) { return last(str.split(delimiter)) || ''; } function range(len) { const arr = []; for (let i = 0; i < len; i++) { arr.push(i); } return arr; } function shortenPath(path$1 = '') { const cwd = process.cwd() + path.sep; return String(path$1).replace(cwd, ''); } function objectValues(obj) { return Object.keys(obj).map(key => obj[key]); } const nodeModules = `${path__default.delimiter}node_modules${path__default.delimiter}`; const BAR_LENGTH = 25; const BLOCK_CHAR = '█'; const BLOCK_CHAR2 = '█'; const NEXT = ' ' + chalk.blue(figures.pointerSmall) + ' '; const BULLET = figures.bullet; const TICK = figures.tick; const CROSS = figures.cross; const CIRCLE_OPEN = figures.radioOff; const consola = Consola.withTag('webpackbar'); const colorize = color => { if (color[0] === '#') { return chalk.hex(color); } return chalk[color] || chalk.keyword(color); }; const renderBar = (progress, color) => { const w = progress * (BAR_LENGTH / 100); const bg = chalk.white(BLOCK_CHAR); const fg = colorize(color)(BLOCK_CHAR2); return range(BAR_LENGTH).map(i => i < w ? fg : bg).join(''); }; function createTable(data) { return textTable(data, { align: data[0].map(() => 'l') }).replace(/\n/g, '\n\n'); } function ellipsisLeft(str, n) { if (str.length <= n - 3) { return str; } return `...${str.substr(str.length - n - 1)}`; } const parseRequest = requestStr => { const parts = (requestStr || '').split('!'); const file = path__default.relative(process.cwd(), removeAfter('?', removeBefore(nodeModules, parts.pop()))); const loaders = parts.map(part => firstMatch(/[a-z0-9-@]+-loader/, part)).filter(hasValue); return { file: hasValue(file) ? file : null, loaders }; }; const formatRequest = request => { const loaders = request.loaders.join(NEXT); if (!loaders.length) { return request.file || ''; } return `${loaders}${NEXT}${request.file}`; }; // Hook helper for webpack 3 + 4 support function hook(compiler, hookName, fn) { if (compiler.hooks) { compiler.hooks[hookName].tap('WebpackBar:' + hookName, fn); } else { compiler.plugin(hookName, fn); } } const originalWrite = Symbol('webpackbarWrite'); class LogUpdate { constructor() { this.prevLineCount = 0; this.listening = false; this.extraLines = ''; this._onData = this._onData.bind(this); this._streams = [process.stdout, process.stderr]; } render(lines) { this.listen(); const wrappedLines = wrapAnsi(lines, this.columns, { trim: false, hard: true, wordWrap: false }); const data = ansiEscapes.eraseLines(this.prevLineCount) + wrappedLines + '\n' + this.extraLines; this.write(data); this.prevLineCount = data.split('\n').length; } get columns() { return (process.stderr.columns || 80) - 2; } write(data) { const stream = process.stderr; if (stream.write[originalWrite]) { stream.write[originalWrite].call(stream, data, 'utf-8'); } else { stream.write(data, 'utf-8'); } } clear() { this.done(); this.write(ansiEscapes.eraseLines(this.prevLineCount)); } done() { this.stopListen(); this.prevLineCount = 0; this.extraLines = ''; } _onData(data) { const str = String(data); const lines = str.split('\n').length - 1; if (lines > 0) { this.prevLineCount += lines; this.extraLines += data; } } listen() { // Prevent listening more than once if (this.listening) { return; } // Spy on all streams for (const stream of this._streams) { // Prevent overriding more than once if (stream.write[originalWrite]) { continue; } // Create a wrapper fn const write = (data, ...args) => { if (!stream.write[originalWrite]) { return stream.write(data, ...args); } this._onData(data); return stream.write[originalWrite].call(stream, data, ...args); }; // Backup original write fn write[originalWrite] = stream.write; // Override write fn stream.write = write; } this.listening = true; } stopListen() { // Restore original write fns for (const stream of this._streams) { if (stream.write[originalWrite]) { stream.write = stream.write[originalWrite]; } } this.listening = false; } } /* eslint-disable no-console */ const logUpdate = new LogUpdate(); let lastRender = Date.now(); class FancyReporter { allDone() { logUpdate.done(); } done(context) { this._renderStates(context.statesArray); if (context.hasErrors) { logUpdate.done(); } } progress(context) { if (Date.now() - lastRender > 50) { this._renderStates(context.statesArray); } } _renderStates(statesArray) { lastRender = Date.now(); const renderedStates = statesArray.map(c => this._renderState(c)).join('\n\n'); logUpdate.render('\n' + renderedStates + '\n'); } _renderState(state) { const color = colorize(state.color); let line1; let line2; if (state.progress >= 0 && state.progress < 100) { // Running 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(' '); line2 = state.request ? ' ' + chalk.grey(ellipsisLeft(formatRequest(state.request), logUpdate.columns)) : ''; } else { let icon = ' '; if (state.hasErrors) { icon = CROSS; } else if (state.progress === 100) { icon = TICK; } else if (state.progress === -1) { icon = CIRCLE_OPEN; } line1 = color(`${icon} ${state.name}`); line2 = chalk.grey(' ' + state.message); } return line1 + '\n' + line2; } } class SimpleReporter { start(context) { consola.info(`Compiling ${context.state.name}`); } change(context, { shortPath }) { consola.debug(`${shortPath} changed.`, `Rebuilding ${context.state.name}`); } done(context) { const { hasError, message, name } = context.state; consola[hasError ? 'error' : 'success'](`${name}: ${message}`); } } const DB = { loader: { get: loader => startCase(loader) }, ext: { get: ext => `${ext} files`, vue: 'Vue Single File components', js: 'JavaScript files', sass: 'SASS files', scss: 'SASS files', unknown: 'Unknown files' } }; function getDescription(category, keyword) { if (!DB[category]) { return startCase(keyword); } if (DB[category][keyword]) { return DB[category][keyword]; } if (DB[category].get) { return DB[category].get(keyword); } return '-'; } function formatStats(allStats) { const lines = []; Object.keys(allStats).forEach(category => { const stats = allStats[category]; lines.push(`> Stats by ${chalk.bold(startCase(category))}`); let totalRequests = 0; const totalTime = [0, 0]; const data = [[startCase(category), 'Requests', 'Time', 'Time/Request', 'Description']]; Object.keys(stats).forEach(item => { const stat = stats[item]; totalRequests += stat.count || 0; const description = getDescription(category, item); totalTime[0] += stat.time[0]; totalTime[1] += stat.time[1]; const avgTime = [stat.time[0] / stat.count, stat.time[1] / stat.count]; data.push([item, stat.count || '-', prettyTime(stat.time), prettyTime(avgTime), description]); }); data.push(['Total', totalRequests, prettyTime(totalTime), '', '']); lines.push(createTable(data)); }); return `${lines.join('\n\n')}\n`; } class Profiler { constructor() { this.requests = []; } onRequest(request) { if (!request) { return; } // Measure time for last request if (this.requests.length) { const lastReq = this.requests[this.requests.length - 1]; if (lastReq.start) { lastReq.time = process.hrtime(lastReq.start); delete lastReq.start; } } // Ignore requests without any file or loaders if (!request.file || !request.loaders.length) { return; } this.requests.push({ request, start: process.hrtime() }); } getStats() { const loaderStats = {}; const extStats = {}; const getStat = (stats, name) => { if (!stats[name]) { // eslint-disable-next-line no-param-reassign stats[name] = { count: 0, time: [0, 0] }; } return stats[name]; }; const addToStat = (stats, name, count, time) => { const stat = getStat(stats, name); stat.count += count; stat.time[0] += time[0]; stat.time[1] += time[1]; }; this.requests.forEach(({ request, time = [0, 0] }) => { request.loaders.forEach(loader => { addToStat(loaderStats, loader, 1, time); }); const ext = request.file && path__default.extname(request.file).substr(1); addToStat(extStats, ext && ext.length ? ext : 'unknown', 1, time); }); return { ext: extStats, loader: loaderStats }; } getFormattedStats() { return formatStats(this.getStats()); } } class ProfileReporter { progress(context) { if (!context.state.profiler) { context.state.profiler = new Profiler(); } context.state.profiler.onRequest(context.state.request); } done(context) { if (context.state.profiler) { context.state.profile = context.state.profiler.getFormattedStats(); delete context.state.profiler; } } allDone(context) { let str = ''; for (const state of context.statesArray) { const color = colorize(state.color); if (state.profile) { str += color(`\nProfile results for ${chalk.bold(state.name)}\n`) + `\n${state.profile}\n`; delete state.profile; } } process.stderr.write(str); } } class StatsReporter { constructor(options) { this.options = Object.assign({ chunks: false, children: false, modules: false, colors: true, warnings: true, errors: true }, options); } done(context, { stats }) { const str = stats.toString(this.options); if (context.hasErrors) { process.stderr.write('\n' + str + '\n'); } else { context.state.statsString = str; } } allDone(context) { let str = ''; for (const state of context.statesArray) { if (state.statsString) { str += '\n' + state.statsString + '\n'; delete state.statsString; } } process.stderr.write(str); } } var reporters = /*#__PURE__*/Object.freeze({ fancy: FancyReporter, basic: SimpleReporter, profile: ProfileReporter, stats: StatsReporter }); const DEFAULTS = { name: 'webpack', color: 'green', reporters: env.minimalCLI ? ['basic'] : ['fancy'], reporter: null // Default state object }; const DEFAULT_STATE = { start: null, progress: -1, done: false, message: '', details: [], request: null, hasErrors: false // Mapping from name => State }; const globalStates = {}; class WebpackBarPlugin extends webpack.ProgressPlugin { constructor(options) { super(); this.options = Object.assign({}, DEFAULTS, options); // Assign a better handler to base ProgressPlugin this.handler = (percent, message, ...details) => { this.updateProgress(percent, message, details); }; // Reporters this.reporters = Array.from(this.options.reporters || []); if (this.options.reporter) { this.reporters.push(this.options.reporter); } // Resolve reporters this.reporters = this.reporters.filter(Boolean).map(_reporter => { if (this.options[_reporter] === false) { return false; } let reporter = _reporter; let reporterOptions = this.options[reporter] || {}; if (Array.isArray(_reporter)) { reporter = _reporter[0]; if (_reporter[1] === false) { return false; } if (_reporter[1]) { reporterOptions = _reporter[1]; } } if (typeof reporter === 'string') { if (reporters[reporter]) { // eslint-disable-line import/namespace reporter = reporters[reporter]; // eslint-disable-line import/namespace } else { reporter = require(reporter); } } if (typeof reporter === 'function') { if (typeof reporter.constructor === 'function') { const Reporter = reporter; reporter = new Reporter(reporterOptions); } else { reporter = reporter(reporterOptions); } } return reporter; }).filter(Boolean); } callReporters(fn, payload = {}) { for (const reporter of this.reporters) { if (typeof reporter[fn] === 'function') { try { reporter[fn](this, payload); } catch (e) { process.stdout.write(e.stack + '\n'); } } } } get hasRunning() { return objectValues(this.states).some(state => !state.done); } get hasErrors() { return objectValues(this.states).some(state => state.hasErrors); } get statesArray() { return objectValues(this.states).sort((s1, s2) => s1.name.localeCompare(s2.name)); } get states() { return globalStates; } get state() { return globalStates[this.options.name]; } _ensureState() { // Keep our state in shared object if (!this.states[this.options.name]) { this.states[this.options.name] = { ...DEFAULT_STATE, color: this.options.color, name: startCase(this.options.name) }; } } apply(compiler) { // Prevent adding multi instances to the same compiler if (compiler.webpackbar) { return; } compiler.webpackbar = this; // Apply base hooks super.apply(compiler); // Register our state after all plugins initialized hook(compiler, 'afterPlugins', () => { this._ensureState(); }); // Hook into the compiler before a new compilation is created. hook(compiler, 'compile', () => { this._ensureState(); Object.assign(this.state, { ...DEFAULT_STATE, start: process.hrtime() }); this.callReporters('start'); }); // Watch compilation has been invalidated. hook(compiler, 'invalid', (fileName, changeTime) => { this._ensureState(); this.callReporters('change', { path: fileName, shortPath: shortenPath(fileName), time: changeTime }); }); // Compilation has completed hook(compiler, 'done', stats => { this._ensureState(); // Prevent calling done twice if (this.state.done) { return; } const hasErrors = stats.hasErrors(); const status = hasErrors ? 'with some errors' : 'successfully'; const time = this.state.start ? ' in ' + prettyTime(process.hrtime(this.state.start), 2) : ''; Object.assign(this.state, { ...DEFAULT_STATE, progress: 100, done: true, message: `Compiled ${status}${time}`, hasErrors }); this.callReporters('progress'); this.callReporters('done', { stats }); if (!this.hasRunning) { this.callReporters('beforeAllDone'); this.callReporters('allDone'); this.callReporters('afterAllDone'); } }); } updateProgress(percent = 0, message = '', details = []) { const progress = Math.floor(percent * 100); Object.assign(this.state, { progress, message: message || '', details, request: parseRequest(details[2]) }); this.callReporters('progress'); } } module.exports = WebpackBarPlugin;