From dbad4b6ba89a05168be8eb702340f9887e1bf46a Mon Sep 17 00:00:00 2001 From: Patrik Henningsson Date: Mon, 25 Jul 2016 16:19:30 +0200 Subject: [PATCH] Use promises in API --- bin/cli.js | 107 ++++++++++++++++++++++-------------------- lib/graph.js | 8 ++-- lib/madge.js | 65 ++++++++++++++++--------- package.json | 1 + test/amd.js | 99 ++++++++++++++++++++++++--------------- test/cjs.js | 97 +++++++++++++++++++++++--------------- test/es6.js | 112 +++++++++++++++++++++++++++----------------- test/files/cjs/a.js | 2 + test/files/cjs/b.js | 1 + test/files/cjs/c.js | 0 test/madge.js | 71 ++++++++++++++++++++-------- 11 files changed, 346 insertions(+), 217 deletions(-) create mode 100644 test/files/cjs/a.js create mode 100644 test/files/cjs/b.js create mode 100644 test/files/cjs/c.js diff --git a/bin/cli.js b/bin/cli.js index 48a74ec8..9455b165 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -55,56 +55,61 @@ if (!program.color) { config.edgeColor = '#757575'; } -const res = madge(program.args[0], config); - -if (program.list || (!program.summary && !program.circular && !program.depends && !program.image && !program.dot && !program.json)) { - printResult.list(res.obj(), { - colors: program.color, - output: program.output - }); -} - -if (program.summary) { - printResult.summary(res.obj(), { - colors: program.color, - output: program.output - }); -} - -if (program.json) { - process.stdout.write(JSON.stringify(res.tree) + '\n'); -} - -if (program.circular) { - const circular = res.circular(); - - printResult.circular(circular, { - colors: program.color, - output: program.output - }); - - if (circular.length) { - process.exit(1); - } -} - -if (program.depends) { - printResult.depends(res.depends(program.depends), { - colors: program.color, - output: program.output - }); -} - -if (program.image) { - res.image((image) => { - fs.writeFile(program.image, image, (err) => { - if (err) { - throw err; +madge(program.args[0], config) + .then((res) => { + if (program.list || (!program.summary && !program.circular && !program.depends && !program.image && !program.dot && !program.json)) { + printResult.list(res.obj(), { + colors: program.color, + output: program.output + }); + } + + if (program.summary) { + printResult.summary(res.obj(), { + colors: program.color, + output: program.output + }); + } + + if (program.json) { + process.stdout.write(JSON.stringify(res.tree) + '\n'); + } + + if (program.circular) { + const circular = res.circular(); + + printResult.circular(circular, { + colors: program.color, + output: program.output + }); + + if (circular.length) { + process.exit(1); } - }); + } + + if (program.depends) { + printResult.depends(res.depends(program.depends), { + colors: program.color, + output: program.output + }); + } + + if (program.image) { + return res.image().then((image) => { + fs.writeFile(program.image, image, (err) => { + if (err) { + throw err; + } + }); + }); + } + + if (program.dot) { + process.stdout.write(res.dot()); + } + }) + .catch((err) => { + console.log(err); + process.exit(1); }); -} - -if (program.dot) { - process.stdout.write(res.dot()); -} diff --git a/lib/graph.js b/lib/graph.js index 58d76847..2c2b767c 100644 --- a/lib/graph.js +++ b/lib/graph.js @@ -55,9 +55,9 @@ function createGraphvizOptions(config) { * Creates a PNG image from the module dependency graph. * @param {Object} modules * @param {Object} config - * @param {Function} callback + * @return {Promise} */ -module.exports.image = function (modules, config, callback) { +module.exports.image = function (modules, config) { const g = graphviz.digraph('G'); const nodes = {}; @@ -85,7 +85,9 @@ module.exports.image = function (modules, config, callback) { }); }); - g.output(createGraphvizOptions(config), callback); + return new Promise((resolve) => { + g.output(createGraphvizOptions(config), resolve); + }); }; /** diff --git a/lib/madge.js b/lib/madge.js index b3ef3aff..ee1330cb 100644 --- a/lib/madge.js +++ b/lib/madge.js @@ -1,7 +1,7 @@ 'use strict'; -const fs = require('fs'); const path = require('path'); +const fs = require('mz/fs'); const dependencyTree = require('dependency-tree'); const cyclic = require('./cyclic'); const graph = require('./graph'); @@ -27,7 +27,7 @@ class Madge { * Class constructor. * @constructor * @api public - * @param {String|Object} filename + * @param {String} filename * @param {Object} config */ constructor(filename, config) { @@ -38,26 +38,43 @@ class Madge { this.config = Object.assign({}, defaultConfig, config); debug('using config', this.config); + this.filename = filename; this.excludeRegex = this.config.exclude ? new RegExp(this.config.exclude) : false; this.rootDirectory = this.config.directory ? path.resolve(this.config.directory) : path.dirname(filename); + } - if (typeof filename === 'object') { - this.tree = filename; - } else { - if (!fs.statSync(filename).isFile()) { // eslint-disable-line no-sync - throw new Error('Directory not supported'); - } - - this.tree = this.convertDependencyTree(dependencyTree({ - filename: filename, - directory: this.rootDirectory, - requireConfig: this.config.requireConfig, - webpackConfig: this.config.webpackConfig, - filter: this.pathFilter.bind(this) - })); - } - - this.sortDependencies(); + /** + * Will start parsing filename and compute dependencies. + * @return {Promise} + */ + parse() { + return fs + .exists(this.filename) + .then((exists) => { + if (!exists) { + throw new Error('Filename ' + this.filename + ' does not exists'); + } + + return fs.stat(this.filename); + }) + .then((stats) => { + if (!stats.isFile()) { + throw new Error('Filename ' + this.filename + ' is not a file'); + } + }) + .then(() => { + this.tree = this.convertDependencyTree(dependencyTree({ + filename: this.filename, + directory: this.rootDirectory, + requireConfig: this.config.requireConfig, + webpackConfig: this.config.webpackConfig, + filter: this.pathFilter.bind(this) + })); + + this.sortDependencies(); + + return this; + }); } /** @@ -176,11 +193,13 @@ class Madge { /** * Return the module dependency graph as a PNG image. * @api public - * @param {Function} callback + * @return {Promise} */ - image(callback) { - graph.image(this.tree, this.config, callback); + image() { + return graph.image(this.tree, this.config); } } -module.exports = (filename, config) => new Madge(filename, config); +module.exports = (filename, config) => { + return new Madge(filename, config).parse(); +}; diff --git a/package.json b/package.json index 9caba727..db8d378d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "debug": "^2.2.0", "dependency-tree": "^5.4.1", "graphviz": "^0.0.8", + "mz": "^2.4.0", "rc": "^1.1.6" }, "devDependencies": { diff --git a/test/amd.js b/test/amd.js index a51cb3ae..cb4a2a4a 100644 --- a/test/amd.js +++ b/test/amd.js @@ -7,65 +7,88 @@ require('should'); describe('AMD', () => { const dir = __dirname + '/files/amd'; - it('should find recursive dependencies', () => { - madge(dir + '/ok/a.js').obj().should.eql({ - 'a': ['sub/b'], - 'sub/b': ['sub/c'], - 'sub/c': ['d'], - 'd': [] - }); + it('should find recursive dependencies', (done) => { + madge(dir + '/ok/a.js').then((res) => { + res.obj().should.eql({ + 'a': ['sub/b'], + 'sub/b': ['sub/c'], + 'sub/c': ['d'], + 'd': [] + }); + done(); + }).catch(done); }); - it('should ignore plugins', () => { - madge(dir + '/plugin.js').obj().should.eql({ - 'plugin': ['ok/d'], - 'ok/d': [] - }); + it('should ignore plugins', (done) => { + madge(dir + '/plugin.js').then((res) => { + res.obj().should.eql({ + 'plugin': ['ok/d'], + 'ok/d': [] + }); + done(); + }).catch(done); }); - it('should find nested dependencies', () => { - madge(dir + '/nested/main.js').obj().should.eql({ - 'a': [], - 'b': [], - 'main': [ - 'a', - 'b' - ] - }); + it('should find nested dependencies', (done) => { + madge(dir + '/nested/main.js').then((res) => { + res.obj().should.eql({ + 'a': [], + 'b': [], + 'main': [ + 'a', + 'b' + ] + }); + done(); + }).catch(done); }); - it('should find circular dependencies', () => { - madge(dir + '/circular/main.js').circular().should.eql([ - ['a', 'c'], - ['f', 'g', 'h'] - ]); + it('should find circular dependencies', (done) => { + madge(dir + '/circular/main.js').then((res) => { + res.circular().should.eql([ + ['a', 'c'], + ['f', 'g', 'h'] + ]); + done(); + }).catch(done); }); - it('should find circular dependencies with relative paths', () => { - madge(dir + '/circularRelative/a.js').circular().should.eql([['a', 'foo/b']]); + it('should find circular dependencies with relative paths', (done) => { + madge(dir + '/circularRelative/a.js').then((res) => { + res.circular().should.eql([['a', 'foo/b']]); + done(); + }).catch(done); }); - it('should find circular dependencies with alias', () => { + it('should find circular dependencies with alias', (done) => { madge(dir + '/circularAlias/dos.js', { requireConfig: dir + '/circularAlias/config.js' - }).circular().should.eql([['dos', 'x86']]); + }).then((res) => { + res.circular().should.eql([['dos', 'x86']]); + done(); + }).catch(done); }); - it('should work for files with ES6 code inside', () => { - madge(dir + '/amdes6.js') - .obj().should.eql({ + it('should work for files with ES6 code inside', (done) => { + madge(dir + '/amdes6.js').then((res) => { + res.obj().should.eql({ 'amdes6': ['ok/d'], 'ok/d': [] }); + done(); + }).catch(done); }); - it('should use paths found in RequireJS config', () => { + it('should use paths found in RequireJS config', (done) => { madge(dir + '/requirejs/a.js', { requireConfig: dir + '/requirejs/config.js' - }).obj().should.eql({ - 'a': ['vendor/jquery-2.0.3'], - 'vendor/jquery-2.0.3': [] - }); + }).then((res) => { + res.obj().should.eql({ + 'a': ['vendor/jquery-2.0.3'], + 'vendor/jquery-2.0.3': [] + }); + done(); + }).catch(done); }); it.skip('should compile coffeescript on-the-fly', () => { diff --git a/test/cjs.js b/test/cjs.js index 3467c316..0ce6d71a 100644 --- a/test/cjs.js +++ b/test/cjs.js @@ -7,60 +7,81 @@ require('should'); describe('CommonJS', () => { const dir = __dirname + '/files/cjs'; - it('should find recursive dependencies', () => { - madge(dir + '/normal/a.js').obj().should.eql({ - 'a': ['sub/b'], - 'd': [], - 'sub/b': ['sub/c'], - 'sub/c': ['d'] - }); + it('should find recursive dependencies', (done) => { + madge(dir + '/normal/a.js').then((res) => { + res.obj().should.eql({ + 'a': ['sub/b'], + 'd': [], + 'sub/b': ['sub/c'], + 'sub/c': ['d'] + }); + done(); + }).catch(done); }); - it('should handle paths outside directory', () => { - madge(dir + '/normal/sub/c.js').obj().should.eql({ - '../d': [], - 'c': ['../d'] - }); + it('should handle paths outside directory', (done) => { + madge(dir + '/normal/sub/c.js').then((res) => { + res.obj().should.eql({ + '../d': [], + 'c': ['../d'] + }); + done(); + }).catch(done); }); - it('should find circular dependencies', () => { - madge(dir + '/circular/a.js').circular().should.eql([ - ['a', 'b', 'c'] - ]); + it('should find circular dependencies', (done) => { + madge(dir + '/circular/a.js').then((res) => { + res.circular().should.eql([ + ['a', 'b', 'c'] + ]); + done(); + }).catch(done); }); - it('should exclude core modules by default', () => { - madge(dir + '/core.js').obj().should.eql({ - 'core': [] - }); + it('should exclude core modules by default', (done) => { + madge(dir + '/core.js').then((res) => { + res.obj().should.eql({ + 'core': [] + }); + done(); + }).catch(done); }); - it('should exclude NPM modules by default', () => { - madge(dir + '/npm.js').obj().should.eql({ - 'normal/d': [], - 'npm': ['normal/d'] - }); + it('should exclude NPM modules by default', (done) => { + madge(dir + '/npm.js').then((res) => { + res.obj().should.eql({ + 'normal/d': [], + 'npm': ['normal/d'] + }); + done(); + }).catch(done); }); - it('should be able to include NPM modules', () => { + it('should be able to include NPM modules', (done) => { madge(dir + '/npm.js', { includeNpm: true - }).obj().should.eql({ - 'node_modules/a': [], - 'normal/d': [], - 'npm': ['node_modules/a', 'normal/d'] - }); + }).then((res) => { + res.obj().should.eql({ + 'node_modules/a': [], + 'normal/d': [], + 'npm': ['node_modules/a', 'normal/d'] + }); + done(); + }).catch(done); }); - it('should be able to show file extensions', () => { + it('should be able to show file extensions', (done) => { madge(dir + '/normal/a.js', { showFileExtension: true - }).obj().should.eql({ - 'a.js': ['sub/b.js'], - 'd.js': [], - 'sub/b.js': ['sub/c.js'], - 'sub/c.js': ['d.js'] - }); + }).then((res) => { + res.obj().should.eql({ + 'a.js': ['sub/b.js'], + 'd.js': [], + 'sub/b.js': ['sub/c.js'], + 'sub/c.js': ['d.js'] + }); + done(); + }).catch(done); }); }); diff --git a/test/es6.js b/test/es6.js index 314224ad..bd031377 100644 --- a/test/es6.js +++ b/test/es6.js @@ -7,65 +7,89 @@ require('should'); describe('ES6', () => { const dir = __dirname + '/files/es6'; - it('should find circular dependencies', () => { - madge(dir + '/circular/a.js').circular().should.eql([ - ['a', 'b', 'c'] - ]); + it('should find circular dependencies', (done) => { + madge(dir + '/circular/a.js').then((res) => { + res.circular().should.eql([ + ['a', 'b', 'c'] + ]); + done(); + }).catch(done); }); - it('should tackle errors in files', () => { - madge(dir + '/error.js').obj().should.eql({ - 'error': [] - }); + it('should tackle errors in files', (done) => { + madge(dir + '/error.js').then((res) => { + res.obj().should.eql({ + 'error': [] + }); + done(); + }).catch(done); }); - it('should find absolute imports from the root', () => { - madge(dir + '/absolute.js').obj().should.eql({ - 'absolute': ['absolute/a'], - 'absolute/a': [] - }); + it('should find absolute imports from the root', (done) => { + madge(dir + '/absolute.js').then((res) => { + res.obj().should.eql({ + 'absolute': ['absolute/a'], + 'absolute/a': [] + }); + done(); + }).catch(done); }); - it('should find imports on files with ES7', () => { - madge(dir + '/async.js').obj().should.eql({ - 'absolute/b': [], - 'async': ['absolute/b'] - }); + it('should find imports on files with ES7', (done) => { + madge(dir + '/async.js').then((res) => { + res.obj().should.eql({ + 'absolute/b': [], + 'async': ['absolute/b'] + }); + done(); + }).catch(done); }); - it('should support export x from "./file"', () => { - madge(dir + '/re-export/c.js').obj().should.eql({ - 'a': [], - 'b-default': ['a'], - 'b-named': ['a'], - 'b-star': ['a'], - 'c': [ - 'b-default', - 'b-named', - 'b-star' - ] - }); + it('should support export x from "./file"', (done) => { + madge(dir + '/re-export/c.js').then((res) => { + res.obj().should.eql({ + 'a': [], + 'b-default': ['a'], + 'b-named': ['a'], + 'b-star': ['a'], + 'c': [ + 'b-default', + 'b-named', + 'b-star' + ] + }); + done(); + }).catch(done); }); - it('should find imports on files with JSX content', () => { - madge(dir + '/jsx.js').obj().should.eql({ - 'jsx': ['absolute/b'], - 'absolute/b': [] - }); + it('should find imports on files with JSX content', (done) => { + madge(dir + '/jsx.js').then((res) => { + res.obj().should.eql({ + 'jsx': ['absolute/b'], + 'absolute/b': [] + }); + done(); + }).catch(done); }); - it('should find import in JSX files', () => { - madge(dir + '/jsx/basic.jsx').obj().should.eql({ - 'basic': ['other'], - 'other': [] - }); + it('should find import in JSX files', (done) => { + madge(dir + '/jsx/basic.jsx').then((res) => { + res.obj().should.eql({ + 'basic': ['other'], + 'other': [] + }); + done(); + }).catch(done); }); - it('should be able to exclude modules', () => { + it('should be able to exclude modules', (done) => { madge(dir + '/normal/a.js', { exclude: '.*\/sub' - }).obj().should.eql({ - 'a': [] - }); + }).then((res) => { + res.obj().should.eql({ + 'a': [] + }); + done(); + }).catch(done); }); }); diff --git a/test/files/cjs/a.js b/test/files/cjs/a.js new file mode 100644 index 00000000..92fdfb39 --- /dev/null +++ b/test/files/cjs/a.js @@ -0,0 +1,2 @@ + require('./b'); + require('./c'); \ No newline at end of file diff --git a/test/files/cjs/b.js b/test/files/cjs/b.js new file mode 100644 index 00000000..426ce71f --- /dev/null +++ b/test/files/cjs/b.js @@ -0,0 +1 @@ +require('./c'); \ No newline at end of file diff --git a/test/files/cjs/c.js b/test/files/cjs/c.js new file mode 100644 index 00000000..e69de29b diff --git a/test/madge.js b/test/madge.js index 26fb8323..75c43954 100644 --- a/test/madge.js +++ b/test/madge.js @@ -5,36 +5,67 @@ const madge = require('../lib/madge'); require('should'); describe('Madge', () => { - describe('#constructor', () => { - it('should throw error on missing filename argument', () => { - (() => { - madge(); - }).should.throw('Filename argument is missing'); - }); + it('should throw error on missing filename argument', () => { + (() => { + madge(); + }).should.throw('Filename argument is missing'); + }); + + it('should return a Promise', () => { + madge(__dirname + '/files/cjs/a.js').should.be.Promise(); // eslint-disable-line new-cap }); + + it('should throw error if filename argument is not a file', (done) => { + madge(__dirname + '/files').catch((err) => { + err.message.should.match(/is not a file/); + done(); + }).catch(done); + }); + + it('should throw error if file does not exists', (done) => { + madge(__dirname + '/missing.js').catch((err) => { + err.message.should.match(/does not exists/); + done(); + }).catch(done); + }); + describe('#obj', () => { - it('should return dependency object', () => { - madge({a: ['b', 'c']}).obj().should.eql({a: ['b', 'c']}); + it('should return dependency object', (done) => { + madge(__dirname + '/files/cjs/a.js').then((res) => { + res.obj().should.eql({ + a: ['b', 'c'], + b: ['c'], + c: [] + }); + done(); + }).catch(done); }); }); describe('#dot', () => { - it('should be able to output graphviz DOT format', () => { - madge({ - a: ['b', 'c'], - b: ['c'], - c: [] - }).dot().should.eql('digraph G {\n "a";\n "b";\n "c";\n "a" -> "b";\n "a" -> "c";\n "b" -> "c";\n}\n'); + it('should be able to output graphviz DOT format', (done) => { + madge(__dirname + '/files/cjs/b.js').then((res) => { + res.dot().should.eql('digraph G {\n "b";\n "c";\n "b" -> "c";\n}\n'); + done(); + }).catch(done); }); }); describe('#depends', () => { - it('should return modules that depends on another', () => { - madge({ - a: ['b', 'c'], - b: ['c'], - c: [] - }).depends('c').should.eql(['a', 'b']); + it('should return modules that depends on another', (done) => { + madge(__dirname + '/files/cjs/a.js').then((res) => { + res.depends('c').should.eql(['a', 'b']); + done(); + }).catch(done); + }); + }); + + describe('#image', () => { + it('should return a Promise', (done) => { + madge(__dirname + '/files/cjs/a.js').then((res) => { + res.image('c').should.be.Promise(); // eslint-disable-line new-cap + done(); + }).catch(done); }); }); });