├── .gitignore ├── .travis.yml ├── bin ├── exec.js └── jsize ├── test.js ├── LICENSE ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tmp/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | -------------------------------------------------------------------------------- /bin/exec.js: -------------------------------------------------------------------------------- 1 | // Used to run size calculations in a separate process. 2 | require('../index')(process.argv.slice(2)) 3 | .then(stats => process.stdout.write(JSON.stringify(stats))) 4 | .catch(error => process.stderr.write(error.message)) 5 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const util = require('util') 5 | const jsize = require('./index') 6 | const execFile = util.promisify(require('child_process').execFile) 7 | 8 | test('programmatic usage', async t => { 9 | const size = await jsize('react@15.6.2') 10 | t.deepEqual(size, { 11 | gzipped: 7502, 12 | initial: 117471, 13 | minified: 22339 14 | }) 15 | }) 16 | 17 | test('programmatic usage +', async t => { 18 | const size = await jsize(['react@15.6.2', 'react-dom@15.6.2']) 19 | t.deepEqual(size, { 20 | gzipped: 44404, 21 | initial: 668109, 22 | minified: 146936 23 | }) 24 | }) 25 | 26 | test('cli usage', async t => { 27 | const {stdout} = await execFile('bin/jsize', ['react@15.6.2']) 28 | t.regex(stdout, /react@15\.6\.2 *= *7\.5 kB \(gzipped\)/) 29 | }) 30 | 31 | test('cli usage +', async t => { 32 | const {stdout} = await execFile('bin/jsize', ['react@15.6.2+react-dom@15.6.2']) 33 | t.regex(stdout, /react@15\.6\.2 \+ react-dom@15\.6\.2 *= *44\.4 kB \(gzipped\)/) 34 | }) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anton Medvedev 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": "jsize", 3 | "version": "6.0.1", 4 | "description": "Find out minified and gziped package size", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "ava", 8 | "lint": "standard --verbose | snazzy", 9 | "prepublish": "npm run lint" 10 | }, 11 | "keywords": [ 12 | "size", 13 | "gzip", 14 | "minify" 15 | ], 16 | "author": "Anton Medvedev ", 17 | "license": "MIT", 18 | "repository": "antonmedv/jsize", 19 | "dependencies": { 20 | "babel-minify": "^0.2.0", 21 | "chalk": "^2.3.0", 22 | "commander": "^2.12.2", 23 | "enhanced-resolve": "^3.4.1", 24 | "execa": "^0.8.0", 25 | "gzip-size": "^4.1.0", 26 | "log-update": "^2.3.0", 27 | "memory-fs": "^0.4.1", 28 | "npm": "^5.5.1", 29 | "ora": "^1.3.0", 30 | "parse-package-name": "^0.1.0", 31 | "pretty-bytes": "^4.0.2", 32 | "text-table": "^0.2.0", 33 | "update-notifier": "^2.3.0", 34 | "webpack": "^3.9.1" 35 | }, 36 | "bin": { 37 | "jsize": "./bin/jsize" 38 | }, 39 | "devDependencies": { 40 | "ava": "^0.24.0", 41 | "snazzy": "^7.0.0", 42 | "standard": "^10.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | Find out minified and gzipped npm package size. 8 | ``` 9 | npm install -g jsize 10 | ``` 11 | 12 | ## Features 13 | * [Scoped packages](https://docs.npmjs.com/misc/scope) 14 | * Individual files within packages 15 | * Multiple packages at once 16 | * Easy CLI and programmatic usage 17 | 18 | ## CLI Usage 19 | 20 | ``` 21 | $ jsize react + react-dom angular vue 22 | 23 | react + react-dom = 44.2 kB (gzipped) 24 | angular = 61.5 kB (gzipped) 25 | vue = 20.9 kB (gzipped) 26 | 27 | ``` 28 | 29 | ``` 30 | $ jsize @cycle/dom + @cycle/run 31 | 32 | @cycle/dom + @cycle/run = 16.7 kB (gzipped) 33 | 34 | ``` 35 | 36 | ``` 37 | $ jsize lodash/map + lodash/filter redux 38 | 39 | lodash/map + lodash/filter = 5.88 kB (gzipped) 40 | redux = 2.77 kB (gzipped) 41 | 42 | ``` 43 | 44 | ``` 45 | $ jsize --verbose jquery 46 | 47 | Package Initial Minified Gzipped 48 | 49 | jquery = 271 kB 88.6 kB 30.8 kB 50 | 51 | ``` 52 | 53 | ### Options 54 | 55 | * #### `-v, --verbose` 56 | 57 | Display initial size, minified size and gzip size. 58 | 59 | ``` 60 | $ jsize jquery -v 61 | 62 | Package Initial Minified Gzipped 63 | 64 | jquery = 271 kB 87.3 kB 30.6 kB 65 | 66 | ``` 67 | 68 | ## Programmatic Usage 69 | 70 | ```js 71 | import jsize from 'jsize' 72 | 73 | jsize('lodash').then(({ initial, minified, gzipped }) => { 74 | // Work with values (all in bytes). 75 | }) 76 | 77 | // Also supports multiple entries. 78 | jsize(['lodash/map', 'lodash/filter']) 79 | ``` 80 | 81 | ## Total size of multiple entries 82 | 83 | You can add up multiple entries by using `+` between entry names. 84 | This is useful because in some cases like in lodash there is a runtime which is a one time cost. 85 | 86 | ```js 87 | $ jsize lodash/map + lodash/assign + lodash/filter 88 | 89 | lodash/map + lodash/assign + lodash/filter = 6.63 kB (gzipped) 90 | 91 | ``` 92 | 93 | Sizes look much larger when comparing individually because it doesn't account for the shared runtime. 94 | 95 | ```js 96 | $ jsize lodash/map lodash/assign lodash/filter 97 | 98 | lodash/map = 5.89 kB (gzipped) 99 | lodash/assign = 2.78 kB (gzipped) 100 | lodash/filter = 5.85 kB (gzipped) 101 | 102 | ``` 103 | 104 | ## Peer Dependencies 105 | 106 | When a package has `peerDependencies` they are automatically excluded from the bundle size. 107 | To have a better idea of the total size of all dependencies you must add up all peers as well. 108 | 109 | ``` 110 | $ jsize react 111 | 112 | react = 7.23 kB (gzipped) 113 | 114 | $ jsize react+react-dom 115 | 116 | react + react-dom = 43.6 kB (gzipped) 117 | 118 | ``` 119 | 120 | ## License 121 | 122 | Licensed under the [MIT license](https://github.com/antonmedv/jsize/blob/master/LICENSE). 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const os = require('os') 5 | const execFile = require('execa') 6 | const webpack = require('webpack') 7 | const minify = require('babel-minify') 8 | const gzipSize = require('gzip-size') 9 | const MemoryFs = require('memory-fs') 10 | const Buffer = require('buffer').Buffer 11 | const parsePackageName = require('parse-package-name') 12 | const enhancedResolve = require('enhanced-resolve') 13 | const tmp = path.join(os.tmpdir(), 'jsize-' + Math.random().toString(36).substring(7)) 14 | const npmBin = path.join(require.resolve('npm/package.json'), '../../.bin/npm') 15 | const resolver = enhancedResolve.ResolverFactory.createResolver({ 16 | fileSystem: new enhancedResolve.NodeJsInputFileSystem(), 17 | mainFields: ['browser', 'module', 'main'] 18 | }) 19 | require('fs').mkdirSync(tmp) 20 | 21 | /** 22 | * Calculates the sizes (initial, minified and gziped) for a given package. 23 | * 24 | * @param {string|string[]} pkgs - the package(s) to check the size of. 25 | * @return {Promise} 26 | */ 27 | module.exports = function jsize (pkgs) { 28 | // Parse all package details. (allows for single or multiple packages) 29 | pkgs = [].concat(pkgs).map(parsePackageName) 30 | // Get unique package ids. 31 | const ids = pkgs 32 | .map(it => it.name + '@' + (it.version || 'latest')) 33 | .filter((id, i, all) => all.indexOf(id) === i) 34 | 35 | // Install modules. 36 | return install(ids) 37 | // Lookup install paths for each module. 38 | .then(() => Promise.all(pkgs.map(loadPaths))) 39 | // Extract entry and external files, then build with webpack. 40 | .then(paths => build({ 41 | entry: paths.map(path => path.entry), 42 | externals: paths.reduce((externals, path) => { 43 | const peers = require(path.package).peerDependencies 44 | if (!peers) return externals 45 | return externals.concat(Object.keys(peers)) 46 | }, []) 47 | })) 48 | // Calculate sizes. 49 | .then(script => { 50 | const minified = minify(script).code 51 | return gzipSize(minified).then(gzipped => { 52 | return { 53 | initial: Buffer.byteLength(script, 'utf8'), 54 | minified: Buffer.byteLength(minified), 55 | gzipped: gzipped 56 | } 57 | }) 58 | }) 59 | } 60 | 61 | /** 62 | * Installs packages with npm to the temp directory. 63 | * 64 | * @param {string[]} ids - the list of packages to install. 65 | */ 66 | function install (ids) { 67 | return execFile(npmBin, ['i', '--no-save', '--prefix', tmp].concat(ids)) 68 | } 69 | 70 | /** 71 | * Uses webpack to build a file in memory and return the bundle. 72 | * 73 | * @param {object} config - webpack config options. 74 | * @return {Promise} 75 | */ 76 | function build (config) { 77 | return new Promise((resolve, reject) => { 78 | const compiler = webpack(Object.assign(config, { 79 | output: { filename: 'file' }, 80 | plugins: [ 81 | new webpack.DefinePlugin({ 82 | 'process.env.NODE_ENV': '"production"', 83 | 'process.browser': true 84 | }) 85 | ] 86 | }), (err, stats) => { 87 | if (err || stats.hasErrors()) reject(err || new Error(stats.toString('errors-only'))) 88 | const compilation = stats.compilation 89 | const compiler = compilation.compiler 90 | const memoryFs = compiler.outputFileSystem 91 | const outputFile = compilation.assets.file.existsAt 92 | resolve(memoryFs.readFileSync(outputFile, 'utf8')) 93 | }) 94 | compiler.outputFileSystem = new MemoryFs() 95 | }) 96 | } 97 | 98 | /** 99 | * Given package details loads resolved package and entry files. 100 | * 101 | * @param {object} pkg - the parsed package details. 102 | * @return {Promise} 103 | */ 104 | function loadPaths (pkg) { 105 | const name = pkg.name 106 | const file = pkg.path 107 | return resolveFile(tmp, path.join(name, file)).then(entry => ({ 108 | entry: entry, 109 | package: path.join(tmp, 'node_modules', name, 'package.json') 110 | })) 111 | } 112 | 113 | /** 114 | * Async resolve a files path using nodes module resolution. 115 | * 116 | * @param {string} dir - the directory to look in. 117 | * @param {string} file - the file to find. 118 | * @return {Promise} 119 | */ 120 | function resolveFile (dir, file) { 121 | return new Promise((resolve, reject) => { 122 | resolver.resolve({}, dir, file, (err, result) => { 123 | if (err) reject(err) 124 | else resolve(result) 125 | }) 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /bin/jsize: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const updateNotifier = require('update-notifier') 5 | const pkg = require('../package.json') 6 | const chalk = require('chalk') 7 | const spinner = require('ora')() 8 | const execFile = require('execa') 9 | const program = require('commander') 10 | const logUpdate = require('log-update') 11 | const prettyBytes = require('pretty-bytes') 12 | const stringWidth = require('string-width') 13 | const createTable = require('text-table') 14 | const execPath = require.resolve('./exec') 15 | 16 | // Check for cli updates. 17 | updateNotifier({pkg: pkg}).notify() 18 | 19 | // Parse cli options. 20 | program 21 | .usage('[options] ') 22 | .option('-v, --verbose', 'display initial, minified and gzipped size') 23 | .parse(process.argv) 24 | 25 | const bundles = parse(program.args).map(packages => ({ 26 | packages, 27 | name: packages.join(chalk.dim(' + ')), 28 | stats: null, 29 | error: null 30 | })) 31 | 32 | const headings = [ 33 | 'Package', 34 | '', 35 | 'Initial', 36 | 'Minified', 37 | 'Gzipped' 38 | ].map(heading => chalk.dim(heading)) 39 | 40 | if (program.args.length > 0) { 41 | // Start cli render loop. 42 | const interval = setInterval(render, 100) 43 | 44 | // Build and analyze packages sequentially. 45 | Promise.all(bundles.map(bundle => { 46 | // Runs jsize in a child process to avoid lag in the loading animation. 47 | return execFile('node', [execPath].concat(bundle.packages)) 48 | .then(result => { 49 | if (result.stderr) throw new Error(result.stderr) 50 | bundle.stats = JSON.parse(result.stdout) 51 | }) 52 | .catch(err => { bundle.error = err.message }) 53 | })).then(() => { 54 | clearInterval(interval) 55 | render() 56 | 57 | const err = bundles 58 | .map(bundle => bundle.error) 59 | .filter(Boolean) 60 | .join('\n') 61 | 62 | if (err) { 63 | console.error(chalk.red(err)) 64 | process.exit(1) 65 | } 66 | }).catch(err => { 67 | console.error(err) 68 | process.exit(1) 69 | }) 70 | } else { 71 | program.help() 72 | } 73 | 74 | /** 75 | * Parses program args to list of bundles, each bundle is list of packages. 76 | * Example: "a + c b" -> [ ['a', 'b'], ['c'] ] 77 | * 78 | * @param {array} args 79 | * @returns {array} 80 | */ 81 | function parse (args) { 82 | // Tokenize args to standard form: ['a+c', '+', 'b'] -> ['a', '+', 'c', '+', 'b'] 83 | const tokens = args 84 | .join(' ') 85 | .replace(/\+/g, ' + ') 86 | .split(' ') 87 | .filter(s => s !== '') 88 | 89 | const next = () => tokens.shift() 90 | const pack = () => { 91 | lookahead && x[x.length - 1].push(lookahead) 92 | lookahead = next() 93 | } 94 | 95 | let x = [] 96 | let lookahead = next() 97 | 98 | do { 99 | x.push([]) 100 | pack() 101 | while (lookahead === '+') { 102 | lookahead = next() 103 | pack() 104 | } 105 | } while (lookahead || tokens.length > 0) 106 | 107 | return x 108 | } 109 | 110 | /** 111 | * Renders a table with the current results to the terminal. 112 | */ 113 | function render () { 114 | const loading = spinner.frame() 115 | const table = bundles.map(bundle => { 116 | const stats = bundle.stats 117 | const row = [ 118 | bundle.name, 119 | chalk.dim('=') 120 | ] 121 | 122 | if (stats) { 123 | if (program.verbose) { 124 | row.push( 125 | prettyBytes(stats.initial), 126 | prettyBytes(stats.minified), 127 | colorBytes(stats.gzipped) 128 | ) 129 | } else { 130 | row.push(colorBytes(stats.gzipped) + chalk.dim(' (gzipped)')) 131 | } 132 | } else { 133 | const fill = bundle.error ? chalk.red('X') : loading 134 | row.push(fill) 135 | 136 | if (program.verbose) { 137 | row.push(fill, fill) 138 | } 139 | } 140 | 141 | return row 142 | }) 143 | 144 | if (program.verbose) { 145 | table.unshift(headings, ['']) 146 | } 147 | 148 | logUpdate( 149 | '\n' + 150 | createTable(table, {stringLength: stringWidth}) 151 | .split('\n') 152 | .map(s => ' ' + s) 153 | .join('\n') + 154 | '\n' 155 | ) 156 | } 157 | 158 | /** 159 | * Converts bytes into human readable, color coated format. 160 | * 161 | * @param {number} n - the number of bytes. 162 | * @return {string} 163 | */ 164 | function colorBytes (n) { 165 | const str = prettyBytes(n) 166 | if (n < 1000) { 167 | return chalk.underline.green(str) 168 | } else if (n < 5000) { 169 | return chalk.green(str) 170 | } else if (n < 50000) { 171 | return chalk.yellow(str) 172 | } else { 173 | return chalk.red(str) 174 | } 175 | } 176 | --------------------------------------------------------------------------------