├── .npmignore ├── nyanDark.png ├── nyanLight.png ├── .gitignore ├── .eslintrc ├── LICENSE ├── package.json ├── README.md ├── test └── index.js └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | *.png -------------------------------------------------------------------------------- /nyanDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexkuz/nyan-progress-webpack-plugin/HEAD/nyanDark.png -------------------------------------------------------------------------------- /nyanLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexkuz/nyan-progress-webpack-plugin/HEAD/nyanLight.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-undef": [2], 4 | "no-trailing-spaces": [1], 5 | "space-before-blocks": [2, "always"], 6 | "no-unused-expressions": [0], 7 | "no-underscore-dangle": [0], 8 | "quote-props": [1, "as-needed"], 9 | "no-multi-spaces": [0], 10 | "no-unused-vars": [1], 11 | "no-loop-func": [0], 12 | "key-spacing": [0], 13 | "max-len": [1, 100], 14 | "strict": [0], 15 | "eol-last": [1], 16 | "no-console": [1], 17 | "indent": [1, 2], 18 | "quotes": [1, "single", "avoid-escape"], 19 | "curly": [0] 20 | }, 21 | "env": { 22 | "browser": true, 23 | "node": true, 24 | "mocha": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Alexander Kuznetsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nyan-progress-webpack-plugin", 3 | "version": "1.2.0", 4 | "description": "Meow", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha test", 8 | "preversion": "npm test", 9 | "version": "git add -A .", 10 | "postversion": "git push" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/alexkuz/nyan-progress-webpack-plugin.git" 15 | }, 16 | "keywords": [ 17 | "webpack", 18 | "plugin", 19 | "progress", 20 | "nyan", 21 | "meow" 22 | ], 23 | "author": "Alexander (http://kuzya.org/)", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/alexkuz/nyan-progress-webpack-plugin/issues" 27 | }, 28 | "homepage": "https://github.com/alexkuz/nyan-progress-webpack-plugin#readme", 29 | "dependencies": { 30 | "ansi-escapes": "^1.1.0", 31 | "ansi-styles": "^3.0.0", 32 | "object.assign": "^4.0.4" 33 | }, 34 | "peerDependencies": { 35 | "webpack": "^2.2.1" 36 | }, 37 | "devDependencies": { 38 | "babel-eslint": "^7.1.1", 39 | "eslint": "^3.17.1", 40 | "mocha": "^3.2.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nyan-progress-webpack-plugin 2 | Meow 3 | 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm i -D nyan-progress-webpack-plugin 10 | ``` 11 | 12 | ## Usage 13 | 14 | Add this to your webpack plugin: 15 | 16 | ``` 17 | var NyanProgressPlugin = require('nyan-progress-webpack-plugin'); 18 | 19 | ... 20 | 21 | plugins: [ 22 | new NyanProgressPlugin() 23 | ] 24 | 25 | ... 26 | ``` 27 | 28 | **NB**: use `webpack.ProgressPlugin` carefully with this plugin. If you used it just for progress logging, you can remove it. 29 | 30 | ## Options 31 | ``` 32 | new NyanProgressPlugin(options) 33 | ``` 34 | | Name | Signature | Default Value | Description | 35 | |------|-----------|---------------|-------------| 36 | | logger | `function(message, ...)` | `console.log` | Function used for logging | 37 | | hookStdout | Boolean | `true` | If `true`, patches `process.stdout.write` during progress and counts extraneous log messages, to position Nyan Cat properly | 38 | | getProgressMessage | `function(progress, messages, styles)` | `...` | Gets custom progress message. `styles` is provided for convenience (exported from [ansi-styles](https://github.com/chalk/ansi-styles) module) | 39 | | debounceInterval | Number | `180` | Defines how often `getProgressMessage` is called (in milliseconds) | 40 | | nyanCatSays | `function(progress, messages)` | `progress === 1 && 'Nyan!'` | Override this function to define what nyan cat is saying | 41 | | restoreCursorPosition | Boolean | `false` | Enable this flag, if your terminal supports saving/restoring cursor position, for better output handling | 42 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var NyanProgressPlugin = require('../index'); 2 | 3 | function loop(progress, onLoop) { 4 | var finished = onLoop(progress); 5 | 6 | if (!finished) { 7 | progress += 0.000003; 8 | process.nextTick(loop.bind(null, progress, onLoop)); 9 | } 10 | } 11 | 12 | describe('Nyan Plugin', function() { 13 | this.timeout(10000); 14 | 15 | it('works with default options', function(done) { 16 | var plugin = new NyanProgressPlugin(); 17 | plugin.handler(0, 'started'); 18 | 19 | loop(0.01, function(progress) { 20 | if (progress < 1) { 21 | plugin.handler(progress, 'progress: ' + progress); 22 | } else { 23 | plugin.handler(1, 'finished'); 24 | done(); 25 | } 26 | 27 | return progress >= 1; 28 | }); 29 | }); 30 | 31 | it('works with large debounce interval', function(done) { 32 | var plugin = new NyanProgressPlugin({ debounceInterval: 1000 }); 33 | plugin.handler(0, 'started'); 34 | 35 | loop(0.01, function(progress) { 36 | if (progress < 1) { 37 | plugin.handler(progress, 'progress: ' + progress); 38 | } else { 39 | plugin.handler(1, 'finished'); 40 | done(); 41 | } 42 | 43 | return progress >= 1; 44 | }); 45 | }); 46 | 47 | it('works with small debounce interval', function(done) { 48 | var plugin = new NyanProgressPlugin({ debounceInterval: 50 }); 49 | plugin.handler(0, 'started'); 50 | 51 | loop(0.01, function(progress) { 52 | if (progress < 1) { 53 | plugin.handler(progress, 'progress: ' + progress); 54 | } else { 55 | plugin.handler(1, 'finished'); 56 | done(); 57 | } 58 | 59 | return progress >= 1; 60 | }); 61 | }); 62 | 63 | /* eslint-disable no-console */ 64 | it('works with extraneous console output', function(done) { 65 | var plugin = new NyanProgressPlugin({ debounceInterval: 50 }); 66 | plugin.handler(0, 'started'); 67 | var i = 0; 68 | 69 | console.log('extraneous message on start 1 of 2'); 70 | console.log('extraneous message on start 2 of 2'); 71 | 72 | loop(0.01, function(progress) { 73 | if (progress < 1) { 74 | plugin.handler(progress, 'progress: ' + progress); 75 | } else { 76 | plugin.handler(1, 'finished'); 77 | console.log('extraneous message on end 1 of 2'); 78 | console.log('extraneous message on end 2 of 2'); 79 | done(); 80 | } 81 | if (i === 1000) { 82 | console.log('extraneous message on progress 1 of 4'); 83 | console.log('extraneous message on progress 2 of 4'); 84 | console.log('extraneous message on progress 3 of 4'); 85 | console.log('extraneous message on progress 4 of 4'); 86 | } 87 | i++; 88 | return progress >= 1; 89 | }); 90 | }); 91 | /* eslint-enable no-console */ 92 | 93 | /* eslint-disable no-console */ 94 | it('works with long extraneous console output', function(done) { 95 | var plugin = new NyanProgressPlugin({ debounceInterval: 50 }); 96 | plugin.handler(0, 'started'); 97 | var i = 0; 98 | 99 | console.log('extraneous message on start'); 100 | 101 | loop(0.01, function(progress) { 102 | if (progress < 1) { 103 | plugin.handler(progress, 'progress: ' + progress); 104 | } else { 105 | plugin.handler(1, 'finished'); 106 | console.log('extraneous message on end'); 107 | done(); 108 | } 109 | if (i === 1000) { 110 | for (var j = 1; j <= 50; j++) { 111 | console.log('extraneous message on progress ' + j + ' of 50'); 112 | }; 113 | } 114 | i++; 115 | return progress >= 1; 116 | }); 117 | }); 118 | /* eslint-enable no-console */ 119 | 120 | /* eslint-disable no-console */ 121 | it('works with cursor position restoring', function(done) { 122 | var plugin = new NyanProgressPlugin({ debounceInterval: 50, restoreCursorPosition: true }); 123 | plugin.handler(0, 'started'); 124 | var i = 0; 125 | 126 | console.log('extraneous message on start'); 127 | 128 | loop(0.01, function(progress) { 129 | if (progress < 1) { 130 | plugin.handler(progress, 'progress: ' + progress); 131 | } else { 132 | plugin.handler(1, 'finished'); 133 | console.log('extraneous message on end'); 134 | done(); 135 | } 136 | if (i === 1000) { 137 | for (var j = 1; j <= 50; j++) { 138 | console.log('extraneous message on progress ' + j + ' of 50'); 139 | }; 140 | } 141 | i++; 142 | return progress >= 1; 143 | }); 144 | }); 145 | /* eslint-enable no-console */ 146 | 147 | it('works with custom message', function(done) { 148 | var plugin = new NyanProgressPlugin({ 149 | getProgressMessage: function(progress, message) { 150 | return 'Nyan cat says: "' + message + '"'; 151 | }, 152 | nyanCatSays: function(progress) { 153 | return progress === 1 ? 'My work here is done!' : (progress * 100).toFixed(1); 154 | } 155 | }); 156 | plugin.handler(0, 'started'); 157 | 158 | loop(0.01, function(progress) { 159 | if (progress < 1) { 160 | plugin.handler(progress, 'progress: ' + progress); 161 | } else { 162 | plugin.handler(1, 'finished'); 163 | done(); 164 | } 165 | 166 | return progress >= 1; 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var AnsiEscapes = require('ansi-escapes'); 3 | var AnsiStyles = require('ansi-styles'); 4 | var webpack = require('webpack'); 5 | 6 | require('object.assign').shim(); 7 | 8 | var bold = AnsiStyles.bold; 9 | var inverse = AnsiStyles.inverse; 10 | 11 | var red = AnsiStyles.red; 12 | var yellow = AnsiStyles.yellow; 13 | var green = AnsiStyles.green; 14 | var blue = AnsiStyles.blue; 15 | var magenta = AnsiStyles.magenta; 16 | 17 | var bgRed = AnsiStyles.bgRed; 18 | var bgYellow = AnsiStyles.bgYellow; 19 | var bgGreen = AnsiStyles.bgGreen; 20 | var bgBlue = AnsiStyles.bgBlue; 21 | 22 | var cursorUp = AnsiEscapes.cursorUp; 23 | var cursorDown = AnsiEscapes.cursorDown; 24 | var eraseEndLine = AnsiEscapes.eraseEndLine; 25 | var cursorSavePosition = AnsiEscapes.cursorSavePosition; 26 | var cursorRestorePosition = AnsiEscapes.cursorRestorePosition; 27 | 28 | var width = 50; 29 | var stdoutLineCount = 0; 30 | 31 | var nyanTemplate = { 32 | ascii: [ 33 | ' ,--------, ', 34 | ' │▗▝ ▞ ▝ ˄---˄ ', 35 | '~│ ▞ ▞ ❬.◕‿‿◕.❭', 36 | ' `w-w---- w w ' 37 | ], 38 | colors: [ 39 | ' ggggggggggg ', 40 | ' gMMMMMMMggggg ', 41 | 'ggMMMMMMgwwwwwwg', 42 | ' gggggggggggg ' 43 | ] 44 | }; 45 | 46 | var nyanSaysTemplate = { 47 | ascii: [ 48 | ' ,--------, ,(-)-.', 49 | ' │▗▝ ▞ ▝ ˄---˄ / (X) |', 50 | '~│ ▞ ▞ ❬.◕‿‿◕.❭--(-)-’', 51 | ' `w-w---- w w ' 52 | ], 53 | colors: [ 54 | ' ggggggggggg w(w)ww', 55 | ' gMMMMMMMggggg ww(w)ww', 56 | 'ggMMMMMMgwwwwwwgww(w)ww', 57 | ' gggggggggggg ' 58 | ] 59 | }; 60 | 61 | var templateColorMap = { 62 | g: function(t) { return t; }, 63 | M: function(t) { return wrap(bold, wrap(magenta, wrap(inverse, t))); }, 64 | w: function(t) { return wrap(bold, t); } 65 | } 66 | 67 | function wrap(color, text) { 68 | return color.open + text + color.close; 69 | } 70 | 71 | function prepareNyan(template, colorToAscii, text) { 72 | text = text && text.toString(); 73 | return template.ascii.map(function(row, idx) { 74 | return row.replace(/\((.)\)/, function(m, c) { 75 | return c === 'X' ? text : text.replace(/./g, c); 76 | }).split('').reduce(function (arr, chr, j) { 77 | var color = template.colors[idx][j]; 78 | var last = arr[arr.length - 1]; 79 | if (last && last.colorCode === color) { 80 | last.text += chr; 81 | return arr; 82 | } else { 83 | return arr.concat({ 84 | colorCode: color, 85 | color: colorToAscii[color] || function(t) { return t; }, 86 | text: chr 87 | }); 88 | } 89 | }, []); 90 | }); 91 | } 92 | 93 | var nyanDefault = prepareNyan(nyanTemplate, templateColorMap); 94 | 95 | var rainbow = [ 96 | [ 97 | function(t) { return wrap(red, t); }, 98 | function(t) { return t.replace(/./g, ' '); } 99 | ], 100 | [ 101 | function(t) { return wrap(bgRed, wrap(yellow, t)); }, 102 | function(t) { return wrap(bold, wrap(red, wrap(bgRed, t))); } 103 | ], 104 | [ 105 | function(t) { return wrap(bgYellow, wrap(green, t)); }, 106 | function(t) { return wrap(bold, wrap(yellow, wrap(bgYellow, t))); } 107 | ], 108 | [ 109 | function(t) { return wrap(bgGreen, wrap(blue, t)); }, 110 | function(t) { return wrap(bold, wrap(green, wrap(bgGreen, t))); } 111 | ], 112 | [ 113 | function(t) { return wrap(inverse, wrap(blue, t)); }, 114 | function(t) { return wrap(bold, wrap(blue, wrap(bgBlue, t))); } 115 | ] 116 | ]; 117 | 118 | function drawRainbow(colors, width, step) { 119 | var wave = ['\u2584', '\u2591']; 120 | var text = ''; 121 | var line = ''; 122 | var idx = step; 123 | for (var i = 0; i < width; i++) { 124 | text += wave[idx % 2]; 125 | if((step + i) % 4 === 0) { 126 | line += colors[idx % 2](text); 127 | text = ''; 128 | idx++; 129 | } 130 | } 131 | return text ? line + colors[idx % 2](text) : line; 132 | } 133 | 134 | function drawNyan(nyan, line, idx) { 135 | return nyan[idx].reduce(function(l, obj) { 136 | return l + obj.color(obj.text); 137 | }, line); 138 | } 139 | 140 | function onProgress(progress, messages, step, isInProgress, options) { 141 | var progressWidth = Math.ceil(progress * width); 142 | var nyanText = options.nyanCatSays(progress, messages); 143 | 144 | if (isInProgress) { 145 | if (options.restoreCursorPosition) { 146 | options.logger(cursorSavePosition + cursorUp(1)); 147 | } 148 | options.logger(cursorUp(rainbow.length + stdoutLineCount + 2)); 149 | } else { 150 | options.logger(''); 151 | } 152 | 153 | for (var i = 0; i < rainbow.length; i++) { 154 | var line = drawRainbow(rainbow[i], progressWidth, step); 155 | var nyanLine = i + ((step % 8 < 4) ? -1 : 0); 156 | if (nyanLine < 4 && nyanLine >= 0) { 157 | line = drawNyan( 158 | nyanText ? prepareNyan(nyanSaysTemplate, templateColorMap, nyanText) : nyanDefault, 159 | line, 160 | nyanLine 161 | ); 162 | } 163 | 164 | options.logger(line + eraseEndLine); 165 | } 166 | options.logger(options.getProgressMessage(progress, messages, AnsiStyles) + 167 | eraseEndLine + (!isInProgress ? cursorDown(1) : '') 168 | ); 169 | if (isInProgress) { 170 | if (options.restoreCursorPosition) { 171 | options.logger(cursorRestorePosition + cursorUp(1)); 172 | } else if (stdoutLineCount > 0) { 173 | options.logger(cursorDown(stdoutLineCount - 1)); 174 | } 175 | } 176 | } 177 | 178 | module.exports = function NyanProgressPlugin(options) { 179 | var timer = 0; 180 | var shift = 0; 181 | var originalStdoutWrite; 182 | var isPrintingProgress = false; 183 | var isStarted = false; 184 | var startTime = 0; 185 | 186 | options = Object.assign({ 187 | debounceInterval: 180, 188 | logger: console.log.bind(console), // eslint-disable-line no-console 189 | hookStdout: true, 190 | getProgressMessage: function(percentage, messages, styles) { 191 | return styles.cyan.open + messages[0] + styles.cyan.close + 192 | (messages[1] ? 193 | ' ' + styles.green.open + '(' + messages[1] + ')' + styles.green.close : 194 | '' 195 | ); 196 | }, 197 | nyanCatSays: function (progress) { return progress === 1 && 'Nyan!'; } 198 | }, options); 199 | 200 | if (options.hookStdout) { 201 | originalStdoutWrite = process.stdout.write; 202 | process.stdout.write = function(msg) { 203 | originalStdoutWrite.apply(process.stdout, arguments); 204 | if (isStarted && !isPrintingProgress) { 205 | stdoutLineCount += msg.split('\n').length - 1; 206 | } 207 | } 208 | } 209 | 210 | return new webpack.ProgressPlugin(function(progress, message) { 211 | var now = new Date().getTime(); 212 | if (!isStarted) { 213 | onProgress(progress, [message], shift++, false, options); 214 | startTime = now; 215 | isStarted = true; 216 | } else if (progress === 1) { 217 | isPrintingProgress = true; 218 | var endTimeMessage = 'build time: ' + (now - startTime) / 1000 + 's'; 219 | onProgress(progress, [message, endTimeMessage], shift++, true, options); 220 | isPrintingProgress = false; 221 | 222 | if (originalStdoutWrite) { 223 | process.stdout.write = originalStdoutWrite; 224 | } 225 | stdoutLineCount = 0; 226 | isStarted = false; 227 | } else if (now - timer > options.debounceInterval) { 228 | timer = now; 229 | isPrintingProgress = true; 230 | onProgress(progress, [message], shift++, true, options); 231 | isPrintingProgress = false; 232 | } 233 | }); 234 | }; 235 | --------------------------------------------------------------------------------