From 23770f6d08b822adc4b78e956c42d16849f083e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Sun, 18 Apr 2021 15:46:58 +0200 Subject: [PATCH 1/8] Add support for displaying the brotli compressed size. --- index.js | 109 ++++++++++++++++++++++++++++++++++++--------------- package.json | 4 +- readme.md | 16 +++++++- 3 files changed, 95 insertions(+), 34 deletions(-) diff --git a/index.js b/index.js index b8c86cf..1ec1b60 100644 --- a/index.js +++ b/index.js @@ -6,22 +6,49 @@ const chalk = require('chalk'); const prettyBytes = require('pretty-bytes'); const StreamCounter = require('stream-counter'); const gzipSize = require('gzip-size'); +const brotliSize = require('brotli-size'); module.exports = options => { options = { pretty: true, showTotal: true, + uncompressed: !options || options.uncompressed || !(options.gzip || options.brotli), ...options }; - let totalSize = 0; let fileCount = 0; + const totalSize = {}; - function log(what, size) { + function log(what, sizes) { let {title} = options; title = title ? chalk.cyan(title) + ' ' : ''; - size = options.pretty ? prettyBytes(size) : (size + ' B'); - fancyLog(title + what + ' ' + chalk.magenta(size) + (options.gzip ? chalk.gray(' (gzipped)') : '')); + const desc = {uncompressed: '', gzip: ' (gzipped)', brotli: (' (brotli)')}; + const strings = Object.entries(sizes).map(([k, v]) => { + const size = options.pretty ? prettyBytes(v) : (v + ' B'); + return chalk.magenta(size) + chalk.gray(desc[k]); + }); + + fancyLog(title + what + ' ' + strings.join(chalk.magenta(', '))); + } + + function addPropWise(a, b) { + // eslint-disable-next-line guard-for-in + for (const k in b) { + a[k] = (a[k] + b[k]) || b[k]; + } + + return a; + } + + function hasSize(sizes) { + return Object.values(sizes).some(a => a > 0); + } + + function promisify(stream, property, event = 'end') { + return new Promise((resolve, reject) => { + stream.on(event, () => resolve(stream[property])); + stream.on('error', error => reject(error)); + }); } return through.obj((file, encoding, callback) => { @@ -36,9 +63,9 @@ module.exports = options => { return; } - totalSize += size; + addPropWise(totalSize, size); - if (options.showFiles === true && size > 0) { + if (options.showFiles === true && hasSize(size)) { log(chalk.blue(file.relative), size); } @@ -46,40 +73,58 @@ module.exports = options => { callback(null, file); }; + const calc = []; + const names = []; + if (file.isStream()) { + if (options.uncompressed) { + calc.push(promisify(file.contents.pipe(new StreamCounter()), 'bytes', 'finish')); + names.push('uncompressed'); + } + if (options.gzip) { - file.contents.pipe(gzipSize.stream()) - .on('error', finish) - .on('end', function () { - finish(null, this.gzipSize); - }); - } else { - file.contents.pipe(new StreamCounter()) - .on('error', finish) - .on('finish', function () { - finish(null, this.bytes); - }); + calc.push(promisify(file.contents.pipe(gzipSize.stream()), 'gzipSize')); + names.push('gzip'); } - return; + if (options.brotli) { + calc.push(promisify(file.contents.pipe(brotliSize.stream()), 'brotliSize')); + names.push('brotli'); + } } - if (options.gzip) { - (async () => { - try { - finish(null, await gzipSize(file.contents)); - } catch (error) { - finish(error); - } - })(); - } else { - finish(null, file.contents.length); + if (file.isBuffer()) { + if (options.uncompressed) { + // Shoehorning, because one size fits all + calc.push(new Promise(resolve => resolve(file.contents.length))); + names.push('uncompressed'); + } + + if (options.gzip) { + calc.push(gzipSize(file.contents)); + names.push('gzip'); + } + + if (options.brotli) { + calc.push(brotliSize.default(file.contents)); + names.push('brotli'); + } } - }, function (callback) { - this.size = totalSize; - this.prettySize = prettyBytes(totalSize); - if (!(fileCount === 1 && options.showFiles) && totalSize > 0 && fileCount > 0 && options.showTotal) { + (async () => { + try { + finish(null, await Promise.all(calc).then(res => { + // Name each result + return res.reduce((acc, cur, idx) => ({...acc, [names[idx]]: cur}), {}); + })); + } catch (error) { + finish(error); + } + })(); + }, function (callback) { + this.size = totalSize[Object.keys(totalSize)[0]]; + this.prettySize = prettyBytes(this.size); + if (!(fileCount === 1 && options.showFiles) && hasSize(totalSize) && fileCount > 0 && options.showTotal) { log(chalk.green('all files'), totalSize); } diff --git a/package.json b/package.json index 8646ca8..ec229ed 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ "measure", "inspect", "debug", - "gzip" + "gzip", + "brotli" ], "dependencies": { + "brotli-size": "^4.0.0", "chalk": "^2.3.0", "fancy-log": "^1.3.2", "gzip-size": "^5.1.1", diff --git a/readme.md b/readme.md index a774b3f..8659cca 100644 --- a/readme.md +++ b/readme.md @@ -48,7 +48,21 @@ Give it a title so it's possible to distinguish the output of multiple instances Type: `boolean`
Default: `false` -Displays the gzipped size instead. +Displays the gzipped size. + +##### brotli + +Type: `boolean`
+Default: `false` + +Displays the brotli compressed size. + +##### uncompressed + +Type: `boolean`
+Default: `false` if either of gzip or brotli is `true`, otherwise `true` + +Displays the uncompressed size. ##### pretty From 071a3f72e62a0bd3c21a14c20a43fd91ae7014bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Wed, 21 Apr 2021 21:34:59 +0200 Subject: [PATCH 2/8] Fix some comments --- index.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 1ec1b60..6de7078 100644 --- a/index.js +++ b/index.js @@ -8,11 +8,11 @@ const StreamCounter = require('stream-counter'); const gzipSize = require('gzip-size'); const brotliSize = require('brotli-size'); -module.exports = options => { +module.exports = (options = {}) => { options = { pretty: true, showTotal: true, - uncompressed: !options || options.uncompressed || !(options.gzip || options.brotli), + uncompressed: options.uncompressed || !(options.gzip || options.brotli), ...options }; @@ -95,8 +95,7 @@ module.exports = options => { if (file.isBuffer()) { if (options.uncompressed) { - // Shoehorning, because one size fits all - calc.push(new Promise(resolve => resolve(file.contents.length))); + calc.push(file.contents.length); names.push('uncompressed'); } @@ -113,10 +112,14 @@ module.exports = options => { (async () => { try { - finish(null, await Promise.all(calc).then(res => { - // Name each result - return res.reduce((acc, cur, idx) => ({...acc, [names[idx]]: cur}), {}); - })); + const res = await Promise.all(calc); + // Name each result + const namedResult = {}; + for (const [idx, size] of res.entries()) { + namedResult[names[idx]] = size; + } + + finish(null, namedResult); } catch (error) { finish(error); } From 7c77ba232e7668455b96b3d6d23c8767e55a700e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Wed, 21 Apr 2021 23:10:43 +0200 Subject: [PATCH 3/8] Fix some more comments --- index.js | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/index.js b/index.js index 6de7078..d44ee54 100644 --- a/index.js +++ b/index.js @@ -17,31 +17,22 @@ module.exports = (options = {}) => { }; let fileCount = 0; - const totalSize = {}; + const totalSize = new Map(); + const desc = {uncompressed: '', gzip: ' (gzipped)', brotli: (' (brotli)')}; function log(what, sizes) { let {title} = options; title = title ? chalk.cyan(title) + ' ' : ''; - const desc = {uncompressed: '', gzip: ' (gzipped)', brotli: (' (brotli)')}; - const strings = Object.entries(sizes).map(([k, v]) => { - const size = options.pretty ? prettyBytes(v) : (v + ' B'); - return chalk.magenta(size) + chalk.gray(desc[k]); + const sizeStrings = [...sizes].map(([key, size]) => { + size = options.pretty ? prettyBytes(size) : (size + ' B'); + return chalk.magenta(size) + chalk.gray(desc[key]); }); - fancyLog(title + what + ' ' + strings.join(chalk.magenta(', '))); - } - - function addPropWise(a, b) { - // eslint-disable-next-line guard-for-in - for (const k in b) { - a[k] = (a[k] + b[k]) || b[k]; - } - - return a; + fancyLog(title + what + ' ' + sizeStrings.join(chalk.magenta(', '))); } function hasSize(sizes) { - return Object.values(sizes).some(a => a > 0); + return [...sizes.values()].some(size => size > 0); } function promisify(stream, property, event = 'end') { @@ -57,16 +48,16 @@ module.exports = (options = {}) => { return; } - const finish = (error, size) => { + const finish = (error, sizes) => { if (error) { callback(new PluginError('gulp-size', error)); return; } - addPropWise(totalSize, size); + sizes.forEach((size, key) => totalSize.set(key, size + (totalSize.get(key) || 0))); - if (options.showFiles === true && hasSize(size)) { - log(chalk.blue(file.relative), size); + if (options.showFiles === true && hasSize(sizes)) { + log(chalk.blue(file.relative), sizes); } fileCount++; @@ -114,9 +105,9 @@ module.exports = (options = {}) => { try { const res = await Promise.all(calc); // Name each result - const namedResult = {}; + const namedResult = new Map(); for (const [idx, size] of res.entries()) { - namedResult[names[idx]] = size; + namedResult.set(names[idx], size); } finish(null, namedResult); @@ -125,7 +116,7 @@ module.exports = (options = {}) => { } })(); }, function (callback) { - this.size = totalSize[Object.keys(totalSize)[0]]; + this.size = totalSize.values().next().value; this.prettySize = prettyBytes(this.size); if (!(fileCount === 1 && options.showFiles) && hasSize(totalSize) && fileCount > 0 && options.showTotal) { log(chalk.green('all files'), totalSize); From bc50f5180eb4e14bf5eed863c4dae9316909b84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Thu, 22 Apr 2021 22:46:44 +0200 Subject: [PATCH 4/8] Fix more comments --- index.js | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index d44ee54..55ba3c0 100644 --- a/index.js +++ b/index.js @@ -18,14 +18,13 @@ module.exports = (options = {}) => { let fileCount = 0; const totalSize = new Map(); - const desc = {uncompressed: '', gzip: ' (gzipped)', brotli: (' (brotli)')}; function log(what, sizes) { let {title} = options; title = title ? chalk.cyan(title) + ' ' : ''; const sizeStrings = [...sizes].map(([key, size]) => { size = options.pretty ? prettyBytes(size) : (size + ' B'); - return chalk.magenta(size) + chalk.gray(desc[key]); + return chalk.magenta(size) + chalk.gray(key); }); fancyLog(title + what + ' ' + sizeStrings.join(chalk.magenta(', '))); @@ -54,7 +53,9 @@ module.exports = (options = {}) => { return; } - sizes.forEach((size, key) => totalSize.set(key, size + (totalSize.get(key) || 0))); + for (const [key, size] of sizes) { + totalSize.set(key, size + (totalSize.get(key) || 0)); + } if (options.showFiles === true && hasSize(sizes)) { log(chalk.blue(file.relative), sizes); @@ -64,53 +65,41 @@ module.exports = (options = {}) => { callback(null, file); }; - const calc = []; - const names = []; - + const selectedSizes = new Map(); if (file.isStream()) { if (options.uncompressed) { - calc.push(promisify(file.contents.pipe(new StreamCounter()), 'bytes', 'finish')); - names.push('uncompressed'); + selectedSizes.set('', promisify(file.contents.pipe(new StreamCounter()), 'bytes', 'finish')); } if (options.gzip) { - calc.push(promisify(file.contents.pipe(gzipSize.stream()), 'gzipSize')); - names.push('gzip'); + selectedSizes.set(' (gzipped)', promisify(file.contents.pipe(gzipSize.stream()), 'gzipSize')); } if (options.brotli) { - calc.push(promisify(file.contents.pipe(brotliSize.stream()), 'brotliSize')); - names.push('brotli'); + selectedSizes.set(' (brotli)', promisify(file.contents.pipe(brotliSize.stream()), 'brotliSize')); } } if (file.isBuffer()) { if (options.uncompressed) { - calc.push(file.contents.length); - names.push('uncompressed'); + selectedSizes.set('', file.contents.length); } if (options.gzip) { - calc.push(gzipSize(file.contents)); - names.push('gzip'); + selectedSizes.set(' (gzipped)', gzipSize(file.contents)); } if (options.brotli) { - calc.push(brotliSize.default(file.contents)); - names.push('brotli'); + selectedSizes.set(' (brotli)', brotliSize.default(file.contents)); } } (async () => { try { - const res = await Promise.all(calc); - // Name each result - const namedResult = new Map(); - for (const [idx, size] of res.entries()) { - namedResult.set(names[idx], size); - } - - finish(null, namedResult); + // We want to keep the names + const sizes = await Promise.all([...selectedSizes.entries()].map(async ([key, size]) => [key, await size])); + + finish(null, new Map(sizes)); } catch (error) { finish(error); } From 4c6a26874140a398e63d6b1530ff6b3194e41cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Fri, 23 Apr 2021 21:04:38 +0200 Subject: [PATCH 5/8] Remove superfluous .entries() Co-authored-by: Sindre Sorhus --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 55ba3c0..4083cdd 100644 --- a/index.js +++ b/index.js @@ -97,7 +97,7 @@ module.exports = (options = {}) => { (async () => { try { // We want to keep the names - const sizes = await Promise.all([...selectedSizes.entries()].map(async ([key, size]) => [key, await size])); + const sizes = await Promise.all([...selectedSizes].map(async ([key, size]) => [key, await size])); finish(null, new Map(sizes)); } catch (error) { From e7ccd0b7b51afb669258e4f8745ac1a6a5f0fb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Fri, 23 Apr 2021 21:30:22 +0200 Subject: [PATCH 6/8] Fix comments take 3 --- index.js | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 4083cdd..a663264 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,17 @@ const StreamCounter = require('stream-counter'); const gzipSize = require('gzip-size'); const brotliSize = require('brotli-size'); +function hasSize(sizes) { + return [...sizes.values()].some(size => size > 0); +} + +function promisify(stream, property, event = 'end') { + return new Promise((resolve, reject) => { + stream.on(event, () => resolve(stream[property])); + stream.on('error', error => reject(error)); + }); +} + module.exports = (options = {}) => { options = { pretty: true, @@ -19,28 +30,23 @@ module.exports = (options = {}) => { let fileCount = 0; const totalSize = new Map(); + const description = new Map([ + ['uncompressed', ''], + ['gzip', ' (gzipped)'], + ['brotli', ' (brotli)'] + ]); + function log(what, sizes) { let {title} = options; title = title ? chalk.cyan(title) + ' ' : ''; const sizeStrings = [...sizes].map(([key, size]) => { size = options.pretty ? prettyBytes(size) : (size + ' B'); - return chalk.magenta(size) + chalk.gray(key); + return chalk.magenta(size) + chalk.gray(description.get(key)); }); fancyLog(title + what + ' ' + sizeStrings.join(chalk.magenta(', '))); } - function hasSize(sizes) { - return [...sizes.values()].some(size => size > 0); - } - - function promisify(stream, property, event = 'end') { - return new Promise((resolve, reject) => { - stream.on(event, () => resolve(stream[property])); - stream.on('error', error => reject(error)); - }); - } - return through.obj((file, encoding, callback) => { if (file.isNull()) { callback(null, file); @@ -68,29 +74,29 @@ module.exports = (options = {}) => { const selectedSizes = new Map(); if (file.isStream()) { if (options.uncompressed) { - selectedSizes.set('', promisify(file.contents.pipe(new StreamCounter()), 'bytes', 'finish')); + selectedSizes.set('uncompressed', promisify(file.contents.pipe(new StreamCounter()), 'bytes', 'finish')); } if (options.gzip) { - selectedSizes.set(' (gzipped)', promisify(file.contents.pipe(gzipSize.stream()), 'gzipSize')); + selectedSizes.set('gzip', promisify(file.contents.pipe(gzipSize.stream()), 'gzipSize')); } if (options.brotli) { - selectedSizes.set(' (brotli)', promisify(file.contents.pipe(brotliSize.stream()), 'brotliSize')); + selectedSizes.set('brotli', promisify(file.contents.pipe(brotliSize.stream()), 'brotliSize')); } } if (file.isBuffer()) { if (options.uncompressed) { - selectedSizes.set('', file.contents.length); + selectedSizes.set('uncompressed', file.contents.length); } if (options.gzip) { - selectedSizes.set(' (gzipped)', gzipSize(file.contents)); + selectedSizes.set('gzip', gzipSize(file.contents)); } if (options.brotli) { - selectedSizes.set(' (brotli)', brotliSize.default(file.contents)); + selectedSizes.set('brotli', brotliSize.default(file.contents)); } } From fc13288936c691a7890b188e40aedf1bbe9461be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Wed, 28 Apr 2021 23:33:39 +0200 Subject: [PATCH 7/8] Add a few tests for the new options --- index.js | 22 +++++++++---------- test.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index a663264..09a7cd9 100644 --- a/index.js +++ b/index.js @@ -14,8 +14,8 @@ function hasSize(sizes) { function promisify(stream, property, event = 'end') { return new Promise((resolve, reject) => { - stream.on(event, () => resolve(stream[property])); - stream.on('error', error => reject(error)); + stream.on(event, () => resolve(stream[property])) + .on('error', error => reject(error)); }); } @@ -47,7 +47,7 @@ module.exports = (options = {}) => { fancyLog(title + what + ' ' + sizeStrings.join(chalk.magenta(', '))); } - return through.obj((file, encoding, callback) => { + return through.obj(async (file, encoding, callback) => { if (file.isNull()) { callback(null, file); return; @@ -100,16 +100,14 @@ module.exports = (options = {}) => { } } - (async () => { - try { - // We want to keep the names - const sizes = await Promise.all([...selectedSizes].map(async ([key, size]) => [key, await size])); + try { + // We want to keep the names + const sizes = await Promise.all([...selectedSizes].map(async ([key, size]) => [key, await size])); - finish(null, new Map(sizes)); - } catch (error) { - finish(error); - } - })(); + finish(null, new Map(sizes)); + } catch (error) { + finish(error); + } }, function (callback) { this.size = totalSize.values().next().value; this.prettySize = prettyBytes(this.size); diff --git a/test.js b/test.js index 7411641..9efa32b 100644 --- a/test.js +++ b/test.js @@ -92,6 +92,51 @@ it('should have `gzip` option', callback => { stream.end(); }); +it('should have `brotli` option', callback => { + const out = process.stdout.write.bind(process.stdout); + const stream = size({brotli: true}); + + process.stdout.write = string => { + out(string); + + if (/brotli/.test(string)) { + assert(true); + process.stdout.write = out; + callback(); + } + }; + + stream.write(new Vinyl({ + path: path.join(__dirname, 'fixture.js'), + contents: Buffer.from('unicorn world') + })); + + stream.end(); +}); + +it('should show `uncompressed`, `gzip` and `brotli` size', callback => { + const out = process.stdout.write.bind(process.stdout); + const stream = size({uncompressed: true, gzip: true, brotli: true}); + + process.stdout.write = string => { + out(string); + + // Name of compressions protocols and three numbers + if (/gzipped.*brotli/.test(string) && /(?:.*\b\d+){3}/.test(string)) { + assert(true); + process.stdout.write = out; + callback(); + } + }; + + stream.write(new Vinyl({ + path: path.join(__dirname, 'fixture.js'), + contents: Buffer.from('unicorn world') + })); + + stream.end(); +}); + it('should not show prettified size when `pretty` option is false', callback => { const out = process.stdout.write.bind(process.stdout); const stream = size({pretty: false}); @@ -170,3 +215,23 @@ it('should handle stream contents with `gzip` option', callback => { stream.end(); }); + +it('should handle stream contents with `brotli` option', callback => { + const contents = through(); + const stream = size({brotli: true}); + + stream.on('finish', () => { + assert.strictEqual(stream.size, 17); + assert.strictEqual(stream.prettySize, '17 B'); + callback(); + }); + + stream.write(new Vinyl({ + path: path.join(__dirname, 'fixture.js'), + contents + })); + + contents.end(Buffer.from('unicorn world')); + + stream.end(); +}); From 63bbf992ba9394029e4f508d447d16a287a34d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20Lind=C3=A9n?= Date: Thu, 29 Apr 2021 19:13:07 +0200 Subject: [PATCH 8/8] Revert making the whole callback function async --- index.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 09a7cd9..4d62e12 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ module.exports = (options = {}) => { fancyLog(title + what + ' ' + sizeStrings.join(chalk.magenta(', '))); } - return through.obj(async (file, encoding, callback) => { + return through.obj((file, encoding, callback) => { if (file.isNull()) { callback(null, file); return; @@ -100,14 +100,16 @@ module.exports = (options = {}) => { } } - try { - // We want to keep the names - const sizes = await Promise.all([...selectedSizes].map(async ([key, size]) => [key, await size])); + (async () => { + try { + // We want to keep the names + const sizes = await Promise.all([...selectedSizes].map(async ([key, size]) => [key, await size])); - finish(null, new Map(sizes)); - } catch (error) { - finish(error); - } + finish(null, new Map(sizes)); + } catch (error) { + finish(error); + } + })(); }, function (callback) { this.size = totalSize.values().next().value; this.prettySize = prettyBytes(this.size);