Skip to content

Commit

Permalink
Add ES6 module support
Browse files Browse the repository at this point in the history
  • Loading branch information
marclaval committed Mar 17, 2015
1 parent 6d8332f commit d5b0b60
Show file tree
Hide file tree
Showing 26 changed files with 173 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"maxparams" : 5,
"maxdepth" : 5,
"maxstatements" : 25,
"maxcomplexity" : 10,
"maxcomplexity" : 11,
"predef" : [
"define",
"describe",
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Build Status](https://secure.travis-ci.org/pahen/madge.png)](http://travis-ci.org/pahen/madge)

Create graphs from your [CommonJS](http://nodejs.org/api/modules.html) or [AMD](https://github.com/amdjs/amdjs-api/wiki/AMD) module dependencies. Could also be useful for finding circular dependencies in your code. Tested on [Node.js](http://nodejs.org/) and [RequireJS](http://requirejs.org/) projects. Dependencies are calculated using static code analysis. CommonJS dependencies are found using James Halliday's [detective](https://github.com/substack/node-detective) and for AMD I'm using [amdetective](https://www.npmjs.org/package/amdetective). Modules written in [CoffeeScript](http://coffeescript.org/) with extension .coffee are supported and will automatically be compiled on-the-fly.
Create graphs from your [CommonJS](http://nodejs.org/api/modules.html), [AMD](https://github.com/amdjs/amdjs-api/wiki/AMD) or [ES6](https://people.mozilla.org/~jorendorff/es6-draft.html) module dependencies. Could also be useful for finding circular dependencies in your code. Tested on [Node.js](http://nodejs.org/) and [RequireJS](http://requirejs.org/) projects. Dependencies are calculated using static code analysis. CommonJS dependencies are found using James Halliday's [detective](https://github.com/substack/node-detective), for AMD I'm using [amdetective](https://www.npmjs.org/package/amdetective) and for ES6 [detective-es6](https://www.npmjs.com/package/detective-es6) is used. Modules written in [CoffeeScript](http://coffeescript.org/) with extension .coffee are supported and will automatically be compiled on-the-fly.

## Examples
Here's a very simple example of a generated image.
Expand Down Expand Up @@ -59,7 +59,7 @@ Only required if you want to generate the visual graphs using [Graphviz](http://

{Object} **opts** (optional)

- {String} **format**. The module format to expect, 'cjs' or 'amd'. Commonjs (cjs) is the default format.
- {String} **format**. The module format to expect, 'cjs', 'amd' or 'es6'. Commonjs (cjs) is the default format.
- {String} **exclude**. String from which a regex will be constructed for excluding files from the scan.
- {Boolean} **breakOnError**. True if the parser should stop on parse errors and when modules are missing, false otherwise. Defaults to false.
- {Boolean} **optimized**. True if the parser should read modules from a optimized file (r.js). Defaults to false.
Expand All @@ -68,6 +68,7 @@ Only required if you want to generate the visual graphs using [Graphviz](http://
- {String} **requireConfig**. Path to RequireJS config used to find shim dependencies and path aliases. Not used by default.
- {Function} **onParseFile**. Function to be called when parsing a file (argument will be an object with "filename" and "src" property set).
- {Function} **onAddModule** . Function to be called when adding a module to the module tree (argument will be an object with "id" and "dependencies" property set).
- {Array} **extensions**. List of file extensions which are considered. Defaults to `['.js']`.

## dependency object (returned from madge)

Expand Down Expand Up @@ -122,7 +123,7 @@ Get an image representation of the module dependency graph.

-h, --help output usage information
-V, --version output the version number
-f, --format <name> format to parse (amd/cjs)
-f, --format <name> format to parse (amd/cjs/es6)
-s, --summary show summary of all dependencies
-c, --circular show circular dependencies
-d, --depends <id> show modules that depends on the given id
Expand Down Expand Up @@ -150,6 +151,10 @@ Get an image representation of the module dependency graph.

$ madge --format amd /path/src

### List all module dependencies (ES6)

$ madge --format es6 /path/src

### Finding circular dependencies

$ madge --circular /path/src
Expand Down
3 changes: 3 additions & 0 deletions lib/madge.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var util = require('util'),
cyclic = require('./cyclic'),
CJS = require('./parse/cjs'),
AMD = require('./parse/amd'),
ES6 = require('./parse/es6'),
graph = require('./graph');

/**
Expand Down Expand Up @@ -128,6 +129,8 @@ function Madge(src, opts) {
tree = new CJS(src, this.opts, this).tree;
} else if (this.opts.format === 'amd') {
tree = new AMD(src, this.opts, this).tree;
} else if (this.opts.format === 'es6') {
tree = new ES6(src, this.opts, this).tree;
} else {
throw new Error('invalid module format "' + this.opts.format + '"');
}
Expand Down
9 changes: 5 additions & 4 deletions lib/parse/amd.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ AMD.prototype.parseFile = function (filename) {
var dependencies = [],
src = this.getFileSource(filename);

this.emit('parseFile', {filename: filename, src: src});
var fileData = {filename: filename, src: src};
this.emit('parseFile', fileData);

if (/define|require\s*\(/m.test(src)) {
amdetective(src, {findNestedDependencies: this.opts.findNestedDependencies}).map(function (obj) {
if (/define|require\s*\(/m.test(fileData.src)) {
amdetective(fileData.src, {findNestedDependencies: this.opts.findNestedDependencies}).map(function (obj) {
return typeof(obj) === 'string' ? [obj] : obj.deps;
}).filter(function (deps) {
deps.filter(function (id) {
Expand All @@ -58,7 +59,7 @@ AMD.prototype.parseFile = function (filename) {
return id;
}

var depFilename = path.resolve(path.dirname(filename), id);
var depFilename = path.resolve(path.dirname(fileData.filename), id);

if (depFilename) {
return this.normalize(depFilename);
Expand Down
8 changes: 6 additions & 2 deletions lib/parse/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ var Base = module.exports = function(src, opts, parent) {
}

this.opts = opts;
if (typeof this.opts.extensions === "undefined") {
this.opts.extensions = ['.js'];
}
this.tree = {};
this.extRegEx = /\.(js|coffee|jsx)$/;
this.extRegEx = new RegExp("\.(coffee|jsx|" + this.opts.extensions.map(function(el) {return el.substring(1);}).join("|") + ")$","g");
this.coffeeExtRegEx = /\.coffee$/;
this.jsxExtRegEx = /\.jsx$/;
src = this.resolveTargets(src);
Expand All @@ -53,7 +56,8 @@ Base.prototype.resolve = function (dir, id) {
try {
return resolve.sync(id, {
basedir: dir,
paths: this.opts.paths
paths: this.opts.paths,
extensions: this.opts.extensions
});
} catch (e) {
if (this.opts.breakOnError) {
Expand Down
9 changes: 5 additions & 4 deletions lib/parse/cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ CJS.prototype.parseFile = function (filename) {
var dependencies = [],
src = this.getFileSource(filename);

this.emit('parseFile', {filename: filename, src: src});
var fileData = {filename: filename, src: src};
this.emit('parseFile', fileData);

if (/require\s*\(/m.test(src)) {
detective(src).map(function (id) {
var depFilename = this.resolve(path.dirname(filename), id);
if (/require\s*\(/m.test(fileData.src)) {
detective(fileData.src).map(function (id) {
var depFilename = this.resolve(path.dirname(fileData.filename), id);
if (depFilename) {
return this.normalize(depFilename);
}
Expand Down
64 changes: 64 additions & 0 deletions lib/parse/es6.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

/**
* Module dependencies.
*/
var fs = require('fs'),
path = require('path'),
util = require('util'),
detective = require('detective-es6'),
colors = require('colors'),
Base = require('./base');

/**
* This class will parse the ES6 module format.
* @see http://nodejs.org/api/modules.html
* @constructor
*/
var ES6 = module.exports = function () {
Base.apply(this, arguments);
};

/**
* Inherit from `Base`.
*/
util.inherits(ES6, Base);

/**
* Parse the given file and return all found dependencies.
* @param {String} filename
* @return {Array}
*/
ES6.prototype.parseFile = function (filename) {
try {
if (fs.existsSync(filename)) {
var dependencies = [],
src = this.getFileSource(filename);

var fileData = {filename: filename, src: src};
this.emit('parseFile', fileData);

if (/import.*from/m.test(fileData.src)) {
detective(fileData.src).map(function (id) {
var depFilename = this.resolve(path.dirname(fileData.filename), id);
if (depFilename) {
return this.normalize(depFilename);
}
}, this).filter(function (id) {
if (!this.isExcluded(id) && dependencies.indexOf(id) < 0) {
dependencies.push(id);
}
}, this);

return dependencies;
}
}
} catch (e) {
if (this.opts.breakOnError) {
console.log(String('\nError while parsing file: ' + filename).red);
throw e;
}
}

return [];
};
4 changes: 4 additions & 0 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"commander": "1.0.0",
"commondir": "0.0.1",
"detective": "0.1.1",
"detective-es6": "1.1.0",
"graphviz": "0.0.7",
"react-tools": "0.12.1",
"resolve": "0.2.3",
Expand Down
41 changes: 41 additions & 0 deletions test/es6.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
var should = require('should'),
madge = require('../lib/madge');

describe('module format (ES6)', function () {

it('should behave as expected on ok files', function () {
madge([__dirname + '/files/es6/normal'], {
format: 'es6'
}).obj().should.eql({ 'a': [ 'sub/b' ], 'fancy-main/not-index': [], 'd': [], 'sub/b': [ 'sub/c' ], 'sub/c': [ 'd' ] });
});

it('should tackle errors in files', function () {
madge([__dirname + '/files/es6/error.js'], {
format: 'es6'
}).obj().should.eql({ 'error': [] });
});

it('should be able to exclude modules', function () {
madge([__dirname + '/files/es6/normal'], {
exclude: '^sub',
format: 'es6'
}).obj().should.eql({ 'a': [], 'd': [], 'fancy-main/not-index': [] });

madge([__dirname + '/files/es6/normal'], {
exclude: '.*\/c$',
format: 'es6'
}).obj().should.eql({ 'a': [ 'sub/b' ], 'd': [], 'sub/b': [], 'fancy-main/not-index': [], });
});

it('should find circular dependencies', function () {
madge([__dirname + '/files/es6/circular'], {
format: 'es6'
}).circular().getArray().should.eql([ ['a', 'b', 'c'] ]);
});

it('should find absolute imports from the root', function () {
madge([__dirname + '/files/es6/absolute.js', __dirname + '/files/es6/absolute'], {
format: 'es6'
}).obj().should.eql({ 'absolute': [ 'absolute/a' ], 'absolute/a': [ 'absolute/b' ], 'absolute/b': [] });
});
});
3 changes: 3 additions & 0 deletions test/files/cjs/extensions/a.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
var b = require('./b');

module.exports = 'A';
1 change: 1 addition & 0 deletions test/files/cjs/extensions/b.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'B';
1 change: 1 addition & 0 deletions test/files/es6/absolute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import {A} from 'test/files/es6/absolute/a';
3 changes: 3 additions & 0 deletions test/files/es6/absolute/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {B} from 'test/files/es6/absolute/b';

export const A = 'A';
1 change: 1 addition & 0 deletions test/files/es6/absolute/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const B = 'B';
1 change: 1 addition & 0 deletions test/files/es6/circular/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import * as B from './b';
1 change: 1 addition & 0 deletions test/files/es6/circular/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import * as C from './c';
1 change: 1 addition & 0 deletions test/files/es6/circular/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import * as A from './a';
3 changes: 3 additions & 0 deletions test/files/es6/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if (x) {
return;
}
3 changes: 3 additions & 0 deletions test/files/es6/normal/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {B} from './sub/b';

export const A = 'A';
1 change: 1 addition & 0 deletions test/files/es6/normal/d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const D = 'D';
1 change: 1 addition & 0 deletions test/files/es6/normal/fancy-main/not-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export constant Dt = new Date();
3 changes: 3 additions & 0 deletions test/files/es6/normal/fancy-main/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"main": "not-index.js"
}
3 changes: 3 additions & 0 deletions test/files/es6/normal/sub/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {C} from './c';

export const B = 'B';
3 changes: 3 additions & 0 deletions test/files/es6/normal/sub/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {D} from '../d';

export const C = 'C';
6 changes: 6 additions & 0 deletions test/madge.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ describe('Madge', function () {
});
});

describe('extensions', function () {
it('should be ok with custom extensions', function () {
madge(['test/files/cjs/extensions'], {extensions: ['.js', '.cjs']}).obj().should.eql({ a: [ 'b' ], b: [] });
});
});

describe('.tree', function () {
it('should accessible as an object', function () {
madge({a: ['b', 'c']}).tree.should.be.an.Object;
Expand Down

2 comments on commit d5b0b60

@alexeagle
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No support for export * from 'path', which we need to use this for Angular

@pahen
Copy link
Owner

@pahen pahen commented on d5b0b60 Oct 20, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.