Skip to content

Commit

Permalink
Move tree generation to tree.js and add support folders
Browse files Browse the repository at this point in the history
  • Loading branch information
pahen committed Aug 17, 2016
1 parent 77d0a75 commit 07a36ed
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 202 deletions.
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ So I decided it was high time for version 1.0 to be released and take the opport
* Renamed many of the settings in the [config](README.md#configuration)
* Option `--json` should now be used instead of `--output json`
* The generation of the dependency tree is now delegated to the external module [dependency-tree](https://github.com/mrjoelkemp/node-dependency-tree)
* A single file will now be used as an entry instead of scanning entire folder(s)
* Dependencies will now be extracted recursively from the single file
* Dependencies will now be extracted recursively
* NPM installed dependencies are now excluded by default
* Node.js core modules are now excluded
* The [API](README.md#api) is now using promises
Expand All @@ -33,7 +32,6 @@ So I decided it was high time for version 1.0 to be released and take the opport
* Option `--read`
* Option `--find-nested-dependencies`
* Option `--paths`
* Option `--extensions`
* Option `--config`
* Option `--output`
* Option `--break-on-error`
Expand Down
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* Works for JavaScript (AMD, CommonJS, ES6 modules) and CSS preprocessors (Sass, Stylus)
* NPM installed dependencies are excluded by default (can be enabled in config)
* All core Node.js modules (assert, path, fs, etc) are excluded
* Get default file to scan from package.json (bin or main)

Read the [changelog](CHANGELOG.md) for latest changes.

Expand Down Expand Up @@ -58,11 +57,13 @@ $ apt-get install graphviz

# API

## madge(filePath: string, config: object)
## madge(path: string|array, config: object)

> `path` is a single file or directory to read (or an array of files/directories).
> `config` is optional and should be [configuration](#configuration) to be used.
Returns a `Promise` resolved with the Madge instance object.
> Returns a `Promise` resolved with the Madge instance object.
## Functions

Expand Down Expand Up @@ -134,6 +135,7 @@ Property | Type | Default | Description
--- | --- | --- | ---
`baseDir` | String | null | Base directory to use when resolving paths (defaults to `filePath` directory)
`includeNpm` | Boolean | false | If node_modules should be included
`fileExtensions` | Array | ['js'] | Valid file extensions used when scanning a folder for files
`showFileExtension` | Boolean | false | If file extensions should be included in module name
`excludeRegExp` | Array | false | An array of RegExp to use for excluding modules from the graph
`requireConfig` | String | null | RequireJS config for resolving aliased modules
Expand Down Expand Up @@ -161,12 +163,36 @@ Property | Type | Default | Description

## Examples

> List all module dependencies
> List dependencies from a single file
```sh
$ madge path/src/app.js
```

> List dependencies from multiple files
```sh
$ madge path/src/foo.js path/src/bar.js
```

> List dependencies from all *.js files found in a directory
```sh
$ madge path/src
```

> List dependencies from multiple directories
```sh
$ madge path/src/foo path/src/bar
```

> List dependencies from all *.js and *.jsx files found in a directory
```sh
$ madge --extensions js,jsx path/src
```

> Finding circular dependencies
```sh
Expand Down
55 changes: 18 additions & 37 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
'use strict';

const process = require('process');
const path = require('path');
const fs = require('mz/fs');
const program = require('commander');
const rc = require('rc')('madge');
const readPackage = require('read-package-json');
const version = require('../package.json').version;

program
.version(version)
.usage('[options] [file|dir]')
.usage('[options] <src...>')
.option('--basedir <path>', 'base directory for resolving paths')
.option('--list', 'show dependency list (default)')
.option('--summary', 'show dependency count summary')
Expand All @@ -22,12 +19,19 @@ program
.option('--image <file>', 'write graph to file as an image')
.option('--layout <name>', 'layout engine to use for graph (dot/neato/fdp/sfdp/twopi/circo)')
.option('--dot', 'show graph using the DOT language')
.option('--extensions <list>', 'comma separated string of valid file extensions')
.option('--show-extension', 'include file extension in module name', false)
.option('--require-config <file>', 'path to RequireJS config')
.option('--webpack-config <file>', 'path to webpack config')
.option('--no-color', 'disable color in output and image', false)
.option('--debug', 'turn on debug output', false)
.parse(process.argv);

if (!program.args.length) {
console.log(program.helpInformation());
process.exit(1);
}

if (program.debug) {
process.env.DEBUG = '*';
}
Expand All @@ -39,15 +43,14 @@ if (!program.color) {
const log = require('../lib/log');
const output = require('../lib/output');
const madge = require('../lib/api');
const target = program.args[0] || process.cwd();
const config = Object.assign({}, rc);

delete config._;
delete config.config;
delete config.configs;

if (rc.config) {
log('using runtime configuration from %s', rc.config);
log('using runtime config %s', rc.config);
}

['layout', 'requireConfig', 'webpackConfig'].forEach((option) => {
Expand All @@ -64,6 +67,14 @@ if (program.exclude) {
config.excludeRegExp = [program.exclude];
}

if (program.extensions) {
config.fileExtensions = program.extensions.split(',').map((s) => s.trim());
}

if (program.showExtension) {
config.showFileExtension = true;
}

if (!program.color) {
config.backgroundColor = '#ffffff';
config.nodeColor = '#00000';
Expand All @@ -72,37 +83,7 @@ if (!program.color) {
config.edgeColor = '#757575';
}

fs
.stat(target)
.then((stats) => {
if (stats.isFile()) {
return program.args[0];
}

const pkgPath = path.join(target, 'package.json');

return new Promise((resolve, reject) => {
readPackage(pkgPath, (err, pkg) => {
if (err) {
reject('Could not read ' + pkgPath + '. Choose another directory or specify file.');
return;
}

config.baseDir = target;

if (pkg.bin) {
log('extracted file path from "bin" entry in ' + pkgPath);
resolve(path.join(target, pkg.bin[Object.keys(pkg.bin)[0]]));
} else if (pkg.main) {
log('extracted file path from "main" entry in ' + pkgPath);
resolve(path.join(target, pkg.main));
} else {
reject('Could not get file to scan by reading package.json. Update package.json or specify a file.');
}
});
});
})
.then((filePath) => madge(filePath, config))
madge(program.args, config)
.then((res) => {
if (program.summary) {
return output.summary(res.obj(), {
Expand Down
158 changes: 16 additions & 142 deletions lib/api.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
'use strict';

const path = require('path');
const fs = require('mz/fs');
const log = require('./log');
const dependencyTree = require('dependency-tree');
const tree = require('./tree');
const cyclic = require('./cyclic');
const graph = require('./graph');

const defaultConfig = {
baseDir: null,
excludeRegExp: false,
extensions: ['js'],
showFileExtension: false,
includeNpm: false,
requireConfig: null,
Expand All @@ -25,152 +23,30 @@ const defaultConfig = {
graphVizPath: false
};

/**
* Exclude modules from tree using RegExp.
* @param {Object} tree
* @param {Array} excludeRegExp
* @return {Object}
*/
function excludeFromTreeByRegExp(tree, excludeRegExp) {
const regExpList = excludeRegExp.map((re) => new RegExp(re));

function regExpFilter(id) {
return regExpList.findIndex((regexp) => regexp.test(id)) < 0;
}

return Object
.keys(tree)
.filter(regExpFilter)
.reduce((acc, id) => {
acc[id] = tree[id].filter(regExpFilter);
return acc;
}, {});
}

/**
* Sort given tree.
* @param {Object} tree
* @return {Object}
*/
function sortDependencies(tree) {
return Object
.keys(tree)
.sort()
.reduce((acc, id) => {
acc[id] = tree[id].sort();
return acc;
}, {});
}

class Madge {
/**
* Class constructor.
* @constructor
* @api public
* @param {String} filePath
* @param {String|Array} path
* @param {Object} config
*/
constructor(filePath, config) {
if (!filePath) {
throw new Error('Filename argument is missing');
}

this.config = Object.assign({}, defaultConfig, config);
log('using config %o', this.config);

this.filePath = path.resolve(filePath);
log('using file path: %s', this.filePath);

this.baseDir = path.resolve(this.config.baseDir || path.dirname(filePath));
log('using base directory: %s', this.baseDir);
}

/**
* Will start parsing filename and compute dependencies.
* @return {Promise}
*/
parse() {
return fs
.exists(this.filePath)
.then((exists) => {
if (!exists) {
throw new Error('Filename ' + this.filePath + ' does not exists');
}

return fs.stat(this.filePath);
})
.then((stats) => {
if (!stats.isFile()) {
throw new Error('Filename ' + this.filePath + ' is not a file');
}
})
.then(() => {
this.tree = this.convertDependencyTree(dependencyTree({
filename: this.filePath,
directory: this.baseDir,
requireConfig: this.config.requireConfig,
webpackConfig: this.config.webpackConfig,
filter: this.pathFilter.bind(this)
}));

if (this.config.excludeRegExp) {
this.tree = excludeFromTreeByRegExp(this.tree, this.config.excludeRegExp);
}

this.tree = sortDependencies(this.tree);

return this;
});
}

/**
* Convert deep tree produced by `dependency-tree` to internal format used by Madge.
* @param {Object} tree
* @param {Object} [graph]
* @return {Object}
*/
convertDependencyTree(tree, graph) {
graph = graph || {};

Object
.keys(tree)
.forEach((key) => {
const id = this.processPath(key);

if (!graph[id]) {
graph[id] = Object
.keys(tree[key])
.map((dep) => this.processPath(dep));
}

this.convertDependencyTree(tree[key], graph);
});

return graph;
}

/**
* Process path.
* @param {String} absPath
* @return {String}
*/
processPath(absPath) {
absPath = path.relative(this.baseDir, absPath);
constructor(path, config) {
if (!path) {
throw new Error('path argument not provided');
}

if (!this.config.showFileExtension) {
absPath = absPath.replace(/\.\w+$/, '');
if (typeof path === 'string') {
path = [path];
}

return absPath;
}
this.config = Object.assign({}, defaultConfig, config);

/**
* Function used to determine if a module (and its subtree) should be included in the dependency tree.
* @param {String} path
* @return {Boolean}
*/
pathFilter(path) {
return this.config.includeNpm || path.indexOf('node_modules') < 0;
return tree(path, this.config).then((tree) => {
this.tree = tree;
return this;
});
}

/**
Expand Down Expand Up @@ -229,10 +105,8 @@ class Madge {

/**
* Expose API.
* @param {String} filePath
* @param {String|Array} path
* @param {Object} config
* @return {Promise}
*/
module.exports = (filePath, config) => {
return new Madge(filePath, config).parse();
};
module.exports = (path, config) => new Madge(path, config);
Loading

0 comments on commit 07a36ed

Please sign in to comment.