webpackbar.js 17 KB

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