diff --git a/.thought/partials/usage.md.hbs b/.thought/partials/usage.md.hbs index 0922a9b..82530f8 100644 --- a/.thought/partials/usage.md.hbs +++ b/.thought/partials/usage.md.hbs @@ -3,4 +3,4 @@ Run `analyze-module-size` in your project directory. The output will be something like this: (Note that the displayed sizes are accumulated from the each module an its dependencies): -{{exec 'node src/index.js'}} \ No newline at end of file +{{exec 'node bin/analyze-module-size.js'}} \ No newline at end of file diff --git a/README.md b/README.md index a6f7e8c..7d650aa 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,59 @@ Run `analyze-module-size` in your project directory. The output will be somethin (Note that the displayed sizes are accumulated from the each module an its dependencies): ``` - +size: 64k... with-dependencies: 1724k +├── bluebird@3.5.0, 716k, 0 deps +├─┬ globby@6.1.0, 484k, 20 deps +│ ├─┬ glob@7.1.1, 340k, 13 deps +│ │ ├─┬ minimatch@3.0.3, 132k, 3 deps +│ │ │ └─┬ brace-expansion@1.1.7, 84k, 2 deps +│ │ │ ├── concat-map@0.0.1, 40k, 0 deps +│ │ │ └── balanced-match@0.4.2, 24k, 0 deps +│ │ ├─┬ inflight@1.0.6, 60k, 3 deps +│ │ │ ├─┬ once@1.4.0, 40k, 1 deps +│ │ │ │ └── wrappy@1.0.2, 20k, 0 deps +│ │ │ └── wrappy@1.0.2, 20k, 0 deps +│ │ ├─┬ once@1.4.0, 40k, 1 deps +│ │ │ └── wrappy@1.0.2, 20k, 0 deps +│ │ ├── fs.realpath@1.0.0, 32k, 0 deps +│ │ ├── inherits@2.0.3, 24k, 0 deps +│ │ └── path-is-absolute@1.0.1, 20k, 0 deps +│ ├─┬ pinkie-promise@2.0.1, 44k, 1 deps +│ │ └── pinkie@2.0.4, 24k, 0 deps +│ ├─┬ array-union@1.0.2, 40k, 1 deps +│ │ └── array-uniq@1.0.3, 20k, 0 deps +│ ├── object-assign@4.1.1, 20k, 0 deps +│ └── pify@2.3.0, 20k, 0 deps +├─┬ glob@7.1.1, 340k, 13 deps +│ ├─┬ minimatch@3.0.3, 132k, 3 deps +│ │ └─┬ brace-expansion@1.1.7, 84k, 2 deps +│ │ ├── concat-map@0.0.1, 40k, 0 deps +│ │ └── balanced-match@0.4.2, 24k, 0 deps +│ ├─┬ inflight@1.0.6, 60k, 3 deps +│ │ ├─┬ once@1.4.0, 40k, 1 deps +│ │ │ └── wrappy@1.0.2, 20k, 0 deps +│ │ └── wrappy@1.0.2, 20k, 0 deps +│ ├─┬ once@1.4.0, 40k, 1 deps +│ │ └── wrappy@1.0.2, 20k, 0 deps +│ ├── fs.realpath@1.0.0, 32k, 0 deps +│ ├── inherits@2.0.3, 24k, 0 deps +│ └── path-is-absolute@1.0.1, 20k, 0 deps +├─┬ chalk@1.1.3, 144k, 7 deps +│ ├─┬ has-ansi@2.0.0, 40k, 1 deps +│ │ └── ansi-regex@2.1.1, 20k, 0 deps +│ ├─┬ strip-ansi@3.0.1, 40k, 1 deps +│ │ └── ansi-regex@2.1.1, 20k, 0 deps +│ ├── ansi-styles@2.2.1, 20k, 0 deps +│ ├── escape-string-regexp@1.0.5, 20k, 0 deps +│ └── supports-color@2.0.0, 20k, 0 deps +├─┬ debug@2.6.6, 128k, 1 deps +│ └── ms@0.7.3, 20k, 0 deps +├── archy@1.0.0, 52k, 0 deps +├── graceful-fs@4.1.11, 48k, 0 deps +├─┬ deep-aplus@1.0.4, 44k, 1 deps +│ └── lodash.isplainobject@4.0.6, 20k, 0 deps +├── progress@2.0.0, 44k, 0 deps +└── pify@2.3.0, 20k, 0 deps ``` diff --git a/bin/analyze-module-size.js b/bin/analyze-module-size.js index ae086cd..8fdf2a4 100755 --- a/bin/analyze-module-size.js +++ b/bin/analyze-module-size.js @@ -4,28 +4,14 @@ var {analyze} = require('../src/index') var realFs = require('fs') var gracefulFs = require('graceful-fs') -var tsProgress = require('ts-progress') +var {ProgressHandler} = require('../src/progress') gracefulFs.gracefulify(realFs) -const {promise, progress} = analyze(process.cwd(), {depth: process.argv[2]}) - -var progressBar = null - -progress - .on('dependencies', dep => { - progressBar = tsProgress.create({ - title: 'Loading package info', - total: dep * 2 - }) - }) - .on('dep-start-load', pkg => progressBar.update()) - .on('dep-end-load', pkg => progressBar.update()) - -promise.then( - (output) => { - progressBar.done() - return process.stdout.write(output) - }, - (err) => process.stderr.write(err.stack + '\n') -) +analyze(process.cwd(), {progress: new ProgressHandler(process.stderr), depth: process.argv[2]}) + .then( + (output) => { + return process.stdout.write(output) + }, + (err) => process.stderr.write(err.stack + '\n') + ) diff --git a/package.json b/package.json index 30ba504..51f0719 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,12 @@ "globby": "^6.1.0", "graceful-fs": "^4.1.11", "pify": "^2.3.0", - "ts-progress": "^0.1.3" + "progress": "^2.0.0" }, "devDependencies": { "chai": "^3.5.0", "chai-as-promised": "^6.0.0", + "concat-stream": "^1.6.0", "dirty-chai": "^1.2.2", "eslint": "^3.17.1", "eslint-config-standard": "^7.0.1", diff --git a/src/DependencyTree.js b/src/DependencyTree.js index 65ff376..05d6888 100644 --- a/src/DependencyTree.js +++ b/src/DependencyTree.js @@ -1,11 +1,9 @@ const path = require('path') -const pify = require('pify') -const glob = pify(require('glob')) +const {findPackages} = require('./find-packages') var {Package} = require('./Package') -var debug = require('debug')('analyze-module-size:DependencyTree') -var EventEmitter = require('events') var Promise = require('bluebird') const deep = require('deep-aplus')(Promise) +const {NullProgressHandler} = require('./progress') class DependencyTree { /** @@ -34,50 +32,44 @@ class DependencyTree { this.all = all } - static loadFrom (packageJsonPath) { + static loadFrom (packageJsonPath, progressHandler = new NullProgressHandler()) { // Compute "this" package var rootPackage = Package.loadFrom(packageJsonPath) - const progress = new EventEmitter() // Compute dependencies var cwd = path.dirname(packageJsonPath) - var dependencies = glob('**/node_modules/*/package.json', {cwd}) - .then((dependencies) => { - debug('dependencies resolved') - progress.emit('unfiltered-dependencies', dependencies.length) - // Only allow paths liks node_modules/pkg-name/node_modules/pkg-name/node_modules/pkg-name/package.json - const filteredDependencies = dependencies - .filter((packageJson) => { - progress.emit('dep-filtered', dependencies.length) - return packageJson.match(/^(node_modules\/[^/]*\/)*package.json/) - }) - debug('dependencies filtered') - progress.emit('dependencies', filteredDependencies.length) + var dependencies = findPackages(cwd, progressHandler) + .then((dependencies) => { + progressHandler.allDependenciesFound(dependencies.length) // Load all dependency with a concurrency level of 4 // Empiric tests show that it is not really slower than running fully parallel, // but the progress bar behaves much more consistent. return Promise.map( - filteredDependencies, + dependencies, (packageJson) => { - progress.emit('dep-start-load', packageJson) return Package.loadFrom(path.join(cwd, packageJson)) .then(pkg => { - progress.emit('dep-end-load', packageJson) + progressHandler.dependencyLoaded(pkg) return pkg }) }, {concurrency: 4} ) }) - return { - progress: progress, - promise: deep({rootPackage, dependencies}).then(function ({rootPackage, dependencies}) { - var {prod, dev, manual} = Package.connectAll(rootPackage, dependencies) - return new DependencyTree(rootPackage, prod.dependencies, dev.dependencies, manual.dependencies, dependencies) - }) - } + return deep({rootPackage, dependencies}).then(function ({rootPackage, dependencies}) { + progressHandler.connectAll() + var {prod, dev, manual} = Package.connectAll(rootPackage, dependencies) + progressHandler.done() + return new DependencyTree( + rootPackage, + prod.dependencies, + dev.dependencies, + manual.dependencies, + dependencies + ) + }) } } diff --git a/src/find-packages.js b/src/find-packages.js new file mode 100644 index 0000000..2cb6cf2 --- /dev/null +++ b/src/find-packages.js @@ -0,0 +1,27 @@ +var Glob = require('glob').Glob + +/** + * @param {string} cwd + * @param {ProgressHandler} progressHandler + */ +function findPackages (cwd, progressHandler) { + return new Promise((resolve, reject) => { + const matcher = new Glob( + '**/node_modules/*/package.json', + {cwd}, + (err, results) => err ? /* istanbul ignore next */ reject(err) : resolve(results) + ) + matcher.on('match', function (file) { + progressHandler.dependencyFound(file) + }) + }) + .then(dependencies => { + // Only allow paths liks node_modules/pkg-name/node_modules/pkg-name/node_modules/pkg-name/package.json + return dependencies.filter((file) => { + const valid = file.match(/^(node_modules\/[^/]*\/)*package.json/) + return valid + }) + }) +} + +module.exports = {findPackages} diff --git a/src/index.js b/src/index.js index 64231a8..555417c 100755 --- a/src/index.js +++ b/src/index.js @@ -9,24 +9,23 @@ const sortby = require('lodash.sortby') const archy = require('archy') const chalk = require('chalk') const path = require('path') +const {NullProgressHandler} = require('./progress') /** * * @param cwd * @param options * @param {number} options.depth The number of levels to display in the tree-view + * @param {ProgressHandler} options.progress */ -function analyze (cwd, options) { - const {promise, progress} = DependencyTree.loadFrom(path.join(cwd, 'package.json')) - return { - progress, - promise: promise.then(function (tree) { +function analyze (cwd, options = {}) { + return DependencyTree.loadFrom(path.join(cwd, 'package.json'), options.progress || new NullProgressHandler()) + .then(function (tree) { return archy({ label: `size: ${tree.rootPackage.stats.totalBlockSize() / 1024}k... with-dependencies: ${tree.rootPackage.totalStats().totalBlockSize() / 1024}k`, - nodes: toArchy(tree.prod, options && options.depth) + nodes: toArchy(tree.prod, options.depth) }) }) - } } /** diff --git a/src/progress.js b/src/progress.js new file mode 100644 index 0000000..b767fcc --- /dev/null +++ b/src/progress.js @@ -0,0 +1,69 @@ +const debug = require('debug')('analyze-module-size:progress') +const ProgressBar = require('progress') + +class ProgressHandler { + constructor (stream) { + this.stream = stream + this.foundDepsProgress = new ProgressBar('dependencies found: :current', {total: 1000, stream: this.stream}) + this.loadedDepsProgress = null + } + + /** + * Notify the user that a dependency (package.json in node_modules) has been found + * @param {string} file the path to the package.json file + */ + dependencyFound (file) { + this.foundDepsProgress.tick() + } + + allDependenciesFound (dep) { + this.stream.write(' ...done\n') + this.loadedDepsProgress = new ProgressBar('Loading dependency details: :bar :locationxxxxxxxxxxxxxxxxxx', { + total: dep, + stream: this.stream + }) + } + + /** + * Update the progressbar after a dependency has been loaded. + * (Increase the tick-count) + */ + dependencyLoaded (pkg) { + this.loadedDepsProgress.tick({locationxxxxxxxxxxxxxxxxxx: pkg.location().substr(0, 'locationxxxxxxxxxxxxxxxxxx'.length)}) + } + + connectAll () { + this.stream.write('Connecting dependency graph\n') + } + + done () { + this.stream.write('done\n') + } +} + +/** + * No actual output (only if DEBUG=anaylze-module-size:progress) + */ +class NullProgressHandler { + dependencyFound (file) { + debug('dependencyFound', file) + } + + allDependenciesFound (count) { + debug('allDependenciesFound', count) + } + + dependencyLoaded (pkg) { + debug('dependencyLoaded', pkg.location()) + } + + connectAll () { + debug('connectAll') + } + + done () { + debug('done') + } +} + +module.exports = {ProgressHandler, NullProgressHandler} diff --git a/test/DependencyTree-spec.js b/test/DependencyTree-spec.js index b5d16af..a942793 100644 --- a/test/DependencyTree-spec.js +++ b/test/DependencyTree-spec.js @@ -11,7 +11,6 @@ describe('The DependencyTree-class:', function () { it('should create a DependencyTree with production dependencies', function () { return DependencyTree.loadFrom('test/fixtures/moduleWithDeps/package.json') - .promise .then((tree) => expect(visit(tree.prod)).to.deep.equal([ { '_id': 'dep1@1.0.0', @@ -40,7 +39,6 @@ describe('The DependencyTree-class:', function () { it('should create a DependencyTree with dev dependencies', function () { return DependencyTree.loadFrom('test/fixtures/moduleWithDeps/package.json') - .promise .then((tree) => expect(visit(tree.dev)).to.deep.equal([ { '_id': 'devdep1@1.0.0', @@ -58,7 +56,6 @@ describe('The DependencyTree-class:', function () { it('should create a DependencyTree with manually installed dependencies', function () { return DependencyTree.loadFrom('test/fixtures/moduleWithDeps/package.json') - .promise .then((tree) => expect(visit(tree.manual)).to.deep.equal([ { '_id': 'manualdep1@1.0.0', diff --git a/test/index-spec.js b/test/index-spec.js index 8632e2a..f06d6c6 100644 --- a/test/index-spec.js +++ b/test/index-spec.js @@ -14,7 +14,6 @@ require('chalk').enabled = false describe('The index-function (module main function):', function () { it('should return an archy-tree for the dependencies of the referenced package directory', function () { return analyze('test/fixtures/moduleWithDeps') - .promise .then((result) => { expect(result).to.equal(fs.readFileSync('test/fixtures/moduleWithDeps.txt', 'utf-8')) }) @@ -22,7 +21,6 @@ describe('The index-function (module main function):', function () { it('should cut the display at a specific depth if specified', function () { return analyze('test/fixtures/moduleWithDeps', {depth: 1}) - .promise .then((result) => { expect(result).to.equal(fs.readFileSync('test/fixtures/moduleWithDeps-depth1.txt', 'utf-8')) }) diff --git a/test/progress-spec.js b/test/progress-spec.js new file mode 100644 index 0000000..3d43c4c --- /dev/null +++ b/test/progress-spec.js @@ -0,0 +1,78 @@ +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const expect = chai.expect +const {ProgressHandler} = require('../src/progress') + +const concat = require('concat-stream') + +describe('progress:', function () { + describe('the ProgressHandler', function (done) { + it('should log the progress', function () { + var stream = null + + const check = new Promise((resolve, reject) => { + stream = concat(function (contents) { + try { + expect(contents).to.equal(` +dependencies found: 1 ...done + +Loading dependency details: =--- a +Loading dependency details: ==-- b +Loading dependency details: ===- c +Loading dependency details: ==== d +Connecting dependency graph +done +--end--`) + } catch (e) { + return reject(e) + } + resolve() + }) + stream.isTTY = true + stream.columns = 80 + stream.rows = 25 + stream.cursorTo = function () { + this.write('\n') + } + stream.clearLine = function (n) { + } + }) + + var progress = new ProgressHandler(stream) + + var calls = Promise.resolve() + .then(() => progress.dependencyFound('abc')) + .then(() => delay()) + .then(() => progress.allDependenciesFound(4)) + .then(() => delay()) + .then(() => progress.dependencyLoaded(mockPkg('a'))) + .then(() => delay()) + .then(() => progress.dependencyLoaded(mockPkg('b'))) + .then(() => delay()) + .then(() => progress.dependencyLoaded(mockPkg('c'))) + .then(() => delay()) + .then(() => progress.dependencyLoaded(mockPkg('d'))) + .then(() => delay()) + .then(() => progress.connectAll()) + .then(() => delay()) + .then(() => progress.done()) + .then(() => delay()) + .then(() => stream.end('--end--')) + + return Promise.all([calls, check]) + }) + }) +}) + +function delay (ms) { + return new Promise((resolve, reject) => setTimeout(resolve, ms || 20)) +} + +function mockPkg (location) { + return { + location: () => location + } +}