diff --git a/.gitignore b/.gitignore index c2f87f8d..5ed6a505 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ -gen.sh -config.json node_modules -!test/files/cjs/node_modules -/.project -/.settings +.madgerc \ No newline at end of file diff --git a/.npmignore b/.npmignore index 0c30401e..30d74d25 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1 @@ -test -examples \ No newline at end of file +test \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 3ac3a5f3..190e37b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,15 @@ language: node_js node_js: - 4 +- 6 -before_install: -- sudo apt-get update -- sudo apt-get install graphviz \ No newline at end of file +sudo: false + +addons: + apt: + packages: + - graphviz + +script: +- npm test +- npm run debug \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ccb28c5c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,142 @@ +# CHANGELOG + +## v1.0.0 (Aug 19, 2016) + +After 4 years of adding features/fixes it started to be hard to maintain the project and fix some outstanding issues due to how madge was designed to work. + +So I decided it was high time for version 1.0 to be released and take the opportunity to do a major rewrite and reduce the size and responsibility of the project and delegate some work to [external libraries](https://github.com/mrjoelkemp/node-dependency-tree). This introduced many breaking changes. Here's the most important ones. + +**Added:** + +* Automatic module type detection thanks to [precinct](https://github.com/mrjoelkemp/node-precinct) +* Determine image format based on file extension (SVG support) +* Reading [config](README.md#configuration) from `.madgerc` (replaces `--config`) +* Option `--webpack-config` for supporting aliased module paths +* Option `--debug` for turning on debug output + +**Changed:** + +* 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) +* Recurse into child dependencies to get a complete dependency tree of a file +* NPM installed dependencies are now excluded by default +* Node.js core modules are now excluded +* The [API](README.md#api) is now using promises + +**Removed:** + +* Option `--format` since the format is now detected automatically from the file content +* Option `--optimized` and `--main-require-module` since we no longer support RequireJS builds (r.js) +* Option `--read` +* Option `--find-nested-dependencies` +* Option `--paths` +* Option `--config` +* Option `--output` +* Option `--break-on-error` +* CoffeeScript support +* Event callbacks `onParseFile` and `onAddModule` +* NPM shrinkwrap no longer used + +## v0.6.0 (July 06, 2016) +* Refactored Madge to use ES6 and now requires Node.js 4 to run. + +## v0.5.5 (July 03, 2016) +* Add note about Graphviz and Windows in README. +* Fix matching absolute path in Windows (Thanks to nadejdashed). +* Support for ES6 re-export syntax (Thanks to Oli Lalonde). +* Support files with ES6 (Thanks to Joel Kemp). +* Improve readme circular return object (Thanks to Way Of The Future). + +## v0.5.4 (June 13, 2016) +* Improved JSX and ES7 support (Thanks to Joel Kemp). + +## v0.5.3 (November 25, 2015) +* Correct regex on CommonJS parser to detect a core module (Thanks to Guillaume Gomez). + +## v0.5.2 (October 16, 2015) +* Updated dependency resolve to latest version. + +## v0.5.1 (October 15, 2015) +* Updated dependencies to newer versions (Thanks to Martin Kapp). + +## v0.5.0 (April 2, 2015) +* Added support for ES6 modules (Thanks to Marc Laval). +* Added support for setting custom file extension name (Thanks to Marc Laval). + +## v0.4.1 (December 19, 2014) +* Fixed issues with absolute paths for modules IDs in Windows (all tests should now pass on Windows too). + +## v0.4.0 (December 19, 2014) +* Add support for JSX (React) and additional module paths (Thanks to Ben Lowery). +* Fix for detecting presence of AMD or CommonJS modules (Thanks to Aaron Russ). +* Now resolves the module IDs from the RequireJS paths-config properly (Thanks to russaa). +* Added support for option findNestedDependencies to find nested dependencies in AMD modules. + +## v0.3.5 (Septemper 22, 2014) +* Fix issue with number of graph node lines increased with each render (Thanks to Colin H. Fredericks). + +## v0.3.4 (Septemper 04, 2014) +* Correctly detect circular dependencies when using path aliases in RequireJS config (Thanks to Nicolas Ramz). + +## v0.3.3 (July 11, 2014) +* Fixed bug with relative paths in AMD not handled properly when checking for cyclic dependencies. + +## v0.3.2 (June 25, 2014) +* Handle anonymous require() as entry in the RequireJS optimized file (Thanks to Benjamin Horsleben). + +## v0.3.1 (June 03, 2014) +* Apply exclude to RequireJS shim dependencies (Thanks to Michael White). + +## v0.3.0 (May 25, 2014) +* Added support for onParseFile and onAddModule options (Thanks to Brandon Selway). +* Added JSON output option (Thanks to Drew Foehn). +* Fix for optimized files including dependency information for excluded modules (Thanks to Drew Foehn). Fixes [issue](https://github.com/pahen/madge/issues/26). + +## v0.2.0 (April 17, 2014) +* Added support for including shim dependencies found in RequiredJS config (specify with option -R). + +## v0.1.9 (February 17, 2014) +* Ensure forward slashes are used in modules paths (Windows). + +## v0.1.8 (January 27, 2014) +* Added support for reading AMD dependencies from a r.js optimized file by using option -O. + +## v0.1.7 (September 20, 2013) +* Added missing fontsize option when generating images. + +## v0.1.6 (September 04, 2013) +* AMD plugins are now ignored as dependencies. Fixes [issue](https://github.com/pahen/grunt-madge/issues/1). + +## v0.1.5 (September 04, 2013) +* Fixed Windows [issue](https://github.com/pahen/madge/issues/17) when reading from standard input with --read. + +## v0.1.4 (January 10, 2013) +* Switched library for walking directory tree which should solve issues on [Windows](https://github.com/pahen/madge/issues/8). + +## v0.1.3 (December 28, 2012) +* Added proper exit code when running "madge --circular" so it can be used in build scripts. + +## v0.1.2 (November 15, 2012) +* Relative AMD module identifiers (if the first term is "." or "..") are now resolved. + +## v0.1.1 (September 3, 2012) +* Tweaked circular dependency path output. + +## v0.1.0 (September 3, 2012) +* Complete path in circular dependencies is now printed (and marked as red in image graphs). + +## v0.0.5 (August 8, 2012) +* Added support for CoffeeScript. Files with extension .coffee will automatically be compiled on-the-fly. + +## v0.0.4 (August 17, 2012) +* Fixed dependency issues with Node.js v0.8. + +## v0.0.3 (July 01, 2012) +* Added support for Node.js v0.8 and dropped support for lower versions. + +## v0.0.2 (May 21, 2012) +* Added ability to read config file and customize colors. + +## v0.0.1 (May 20, 2012) +* Initial release. \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 98011770..00000000 --- a/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -MIT License - -Copyright (c) 2012 Patrik Henningsson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a64fec2d..18c3d8a8 100644 --- a/README.md +++ b/README.md @@ -7,222 +7,238 @@ [![NPM Status](http://img.shields.io/npm/dm/madge.svg?style=flat-square)](https://www.npmjs.org/package/madge) [![Donate](https://img.shields.io/badge/donate-paypal-blue.svg?style=flat-square)](https://paypal.me/pahen) -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. +**Madge** is a developer tool for generating a visual graph of your module dependencies, finding circular dependencies, and give you other useful info. Joel Kemp's awesome [dependency-tree](https://github.com/mrjoelkemp/node-dependency-tree) is used for extracting the dependency tree. -## Examples -Here's a very simple example of a generated image. - -![](https://github.com/pahen/node-madge/raw/master/examples/small.png) +* 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 +* Recurse into child dependencies to get a complete dependency tree of a file - - blue = has dependencies - - green = has no dependencies - - red = has circular dependencies +Read the [changelog](CHANGELOG.md) for latest changes. -Here's an example generated from the [Express](https://github.com/visionmedia/express) project. +## Examples -![](https://github.com/pahen/node-madge/raw/master/examples/express.png) +> Graph generated from the madge source code. -And some terminal usage. + + + -![](https://github.com/pahen/node-madge/raw/master/examples/terminal.png) +> A graph with circular dependencies. Blue has dependencies, green has no dependencies, and red has circular dependencies. -# Installation + + + -To install as a library: +## See it in action - $ npm install madge + + + -To install the CLI: +# Installation - $ npm -g install madge +```sh +$ npm -g install madge +``` ## Graphviz (optional) -Only required if you want to generate the visual graphs using [Graphviz](http://www.graphviz.org/). +> Only required if you want to generate the visual graphs using [Graphviz](http://www.graphviz.org/). ### Mac OS X - $ port install graphviz - -OR - - $ brew install graphviz +```sh +$ brew install graphviz || port install graphviz +``` ### Ubuntu - $ apt-get install graphviz +```sh +$ apt-get install graphviz +``` # API - var madge = require('madge'); - var dependencyObject = madge('./'); - console.log(dependencyObject.tree); - -## madge(src, opts) +## madge(path: string|array, config: object) -{Object|Array|String} **src** (required) +> `path` is a single file or directory to read (or an array of files/directories). -- Object - a dependency tree. -- Array - an Array of directories to scan. -- String - a directory to scan. +> `config` is optional and should be [configuration](#configuration) to be used. -{Object} **opts** (optional) +> Returns a `Promise` resolved with the Madge instance object. -- {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. -- {Boolean} **findNestedDependencies**. True if nested dependencies should be found in AMD modules. Defaults to false. -- {String} **mainRequireModule**. Name of the module if parsing an optimized file (r.js), where the main file used `require()` instead of `define`. Defaults to `''`. -- {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']`. +## Functions -## dependency object (returned from madge) - -#### .opts - -Options object passed used in the constructor. - -#### .tree - -Dependency tree object. Can be overwritten with an object in the format: +#### .obj() - { - 'module1': ['dep1a', 'dep1b'], - 'module2': ['dep2a'] - } +> Returns an `Object` with all dependencies. -#### .obj() +```javascript +const madge = require('madge'); -Alias to the tree property. +madge('path/to/app.js').then((res) => { + console.log(res.obj()); +}); +``` #### .circular() -Circular dependencies object, returns: +> Returns an `Array` with all modules that has circular dependencies. - { - 'getArray': function, /** @param {} array */ - 'isCyclic': function /** @param {id} boolean */ - } +```javascript +const madge = require('madge'); -#### .depends() +madge('path/to/app.js').then((res) => { + console.log(res.circular()); +}); +``` -Returns a list of modules that depends on a given module. +#### .depends() -#### .dot() +> Returns an `Array` with all modules that depends on a given module. -Get a DOT representation of the module dependency graph. +```javascript +const madge = require('madge'); -#### .image(opts, callback) +madge('path/to/app.js').then((res) => { + console.log(res.depends()); +}); +``` -Get an image representation of the module dependency graph. +#### .dot() -- {Object} **opts** (required). - - {String} **layout**. The layout to use. Defaults to 'DOT'. - - {String} **fontFace**. The font face to use. Defaults to 'Times-Roman'. - - {Object} **imageColors**. Object with color information (all colors are strings containing hex values). - - {String} **bgcolor**. The backgound color. - - {String} **edge**. The edge color. - - {String} **dependencies**. The color for dependencies and for text if fontColor is not present. - - {String} **fontColor**. The color for text. -- {Function} **callback** (required). Receives the rendered image as the first argument. +> Returns a `Promise` resolved with a DOT representation of the module dependency graph. + +```javascript +const madge = require('madge'); + +madge('path/to/app.js') + .then((res) => res.dot()) + .then((output) => { + console.log(output); + }); +``` + +#### .image(imagePath: string) + +> Write the graph as an image to the given image path. The [image format](http://www.graphviz.org/content/output-formats) to use is determined from the file extension. Returns a `Promise` resolved with a full path to the written image. + +```javascript +const madge = require('madge'); + +madge('path/to/app.js') + .then((res) => res.image('path/to/image.svg')) +}); +``` + +# Configuration + +Property | Type | Default | Description +--- | --- | --- | --- +`baseDir` | String | null | Base directory to use instead of the default +`includeNpm` | Boolean | false | If node_modules should be included +`fileExtensions` | Array | ['js'] | Valid file extensions used to find files in directories +`showFileExtension` | Boolean | false | If file extension should be included in module name +`excludeRegExp` | Array | false | An array of RegExp for excluding modules +`requireConfig` | String | null | RequireJS config for resolving aliased modules +`webpackConfig` | String | null | Webpack config for resolving aliased modules +`layout` | String | dot | Layout to use in the graph +`fontName` | String | Arial | Font name to use in the graph +`fontSize` | String | 14px | Font size to use in the graph +`backgroundColor` | String | #000000 | Background color for the graph +`nodeColor` | String | #c6c5fe | Default node color to use in the graph +`noDependencyColor` | String | #cfffac | Color to use for nodes with no dependencies +`cyclicNodeColor` | String | #ff6c60 | Color to use for circular dependencies +`edgeColor` | String | #757575 | Edge color to use in the graph +`graphVizPath` | String | null | Custom GraphViz path + +> Note that when running the CLI it's possible to use a runtime configuration file. The config should placed in `.madgerc` in your project or home folder. Look [here](https://github.com/dominictarr/rc#standards) for alternative locations for the file. Here's an example: + +```json +{ + "showFileExtension": true, + "fontSize": "10px" +} +``` # CLI - Usage: madge [options] - - Options: +## Examples - -h, --help output usage information - -V, --version output the version number - -f, --format format to parse (amd/cjs/es6) - -s, --summary show summary of all dependencies - -L, --list show list of all dependencies - -c, --circular show circular dependencies - -d, --depends show modules that depends on the given id - -x, --exclude a regular expression for excluding modules - -t, --dot output graph in the DOT language - -i, --image write graph to file as a PNG image - -l, --layout layout engine to use for image graph (dot/neato/fdp/sfdp/twopi/circo) - -b, --break-on-error break on parse errors & missing modules - -n, --no-colors skip colors in output and images - -r, --read skip scanning folders and read JSON from stdin - -C, --config provide a config file - -R, --require-config include shim dependencies and path aliases found in RequireJS config file - -O, --optimized if given file is optimized with r.js - -M --main-require-module name of the primary RequireJS module, if it's included with `require()` - -j --json output dependency tree in json +> List dependencies from a single file +```sh +$ madge path/src/app.js +``` -## Examples: +> List dependencies from multiple files -### List all module dependencies (CommonJS) +```sh +$ madge path/src/foo.js path/src/bar.js +``` - $ madge /path/src +> List dependencies from all *.js files found in a directory -### List all module dependencies (AMD) +```sh +$ madge path/src +``` - $ madge --format amd /path/src +> List dependencies from multiple directories -### List all module dependencies (ES6) +```sh +$ madge path/src/foo path/src/bar +``` - $ madge --format es6 /path/src +> List dependencies from all *.js and *.jsx files found in a directory -### Finding circular dependencies +```sh +$ madge --extensions js,jsx path/src +``` - $ madge --circular /path/src +> Finding circular dependencies -### Show modules that depends on a given module +```sh +$ madge --circular path/src/app.js +``` - $ madge --depends 'wheels' /path/src +> Show modules that depends on a given module -### Excluding modules +```sh +$ madge --depends 'wheels' path/src/app.js +``` - $ madge --exclude '^foo$|^bar$|^tests' /path/src +> Excluding modules -### Save graph as a PNG image (graphviz required) +```sh +$ madge --exclude '^(foo|bar)$' path/src/app.js +``` - $ madge --image graph.png /path/src +> Save graph as a SVG image (graphviz required) -### Save graph as a [DOT](http://en.wikipedia.org/wiki/DOT_language) file for further processing (graphviz required) +```sh +$ madge --image graph.svg path/src/app.js +``` - $ madge --dot /path/src > graph.gv +> Save graph as a [DOT](http://en.wikipedia.org/wiki/DOT_language) file for further processing (graphviz required) -### Run on optimized file by r.js (RequireJS optimizer) - $ r.js -o app-build.js - $ madge --format amd --optimized app-build.js +```sh +$ madge --dot path/src/app.js > graph.gv +``` -### Include shim dependencies found in RequireJS config - $ madge --format amd --require-config path/config.js path/src +# Debugging -### Pipe predefined results (the example image was produced with the following command) +> To enable debugging output if you encounter problems, run madge with the `--debug` option then throw the result in a gist when creating issues on GitHub. - $ cat << EOF | madge --read --image example.png - { - "a": ["b", "c", "d"], - "b": ["c"], - "c": [], - "d": ["a"] - } - EOF +```sh +$ madge --debug path/src/app.js +``` -## Config (use with --config) +# Running tests - { - "format": "amd", - "image": "dependencyMap.png", - "fontFace": "Arial", - "fontSize": "14px", - "imageColors": { - "noDependencies" : "#0000ff", - "dependencies" : "#00ff00", - "circular" : "#bada55", - "edge" : "#666666", - "bgcolor": "#ffffff" - } - } +```sh +$ npm test +``` # FAQ @@ -247,136 +263,6 @@ minimize a global energy function, which is equivalent to statistical multi-dime * **circo** circular layout, after Six and Tollis 99, Kauffman and Wiese 02. This is suitable for certain diagrams of multiple cyclic structures, such as certain telecommunications networks. -# Running tests - - $ npm test - -# Release Notes - -## v0.6.0 (July 06, 2016) -* Refactored Madge to use ES6 and now requires Node.js 4 to run. - -## v0.5.5 (July 03, 2016) -* Add note about Graphviz and Windows in README. -* Fix matching absolute path in Windows (Thanks to nadejdashed). -* Support for ES6 re-export syntax (Thanks to Oli Lalonde). -* Support files with ES6 (Thanks to Joel Kemp). -* Improve readme circular return object (Thanks to Way Of The Future). - -## v0.5.4 (June 13, 2016) -* Improved JSX and ES7 support (Thanks to Joel Kemp). - -## v0.5.3 (November 25, 2015) -* Correct regex on CommonJS parser to detect a core module (Thanks to Guillaume Gomez). - -## v0.5.2 (October 16, 2015) -* Updated dependency resolve to latest version. - -## v0.5.1 (October 15, 2015) -* Updated dependencies to newer versions (Thanks to Martin Kapp). - -## v0.5.0 (April 2, 2015) -* Added support for ES6 modules (Thanks to Marc Laval). -* Added support for setting custom file extension name (Thanks to Marc Laval). - -## v0.4.1 (December 19, 2014) -* Fixed issues with absolute paths for modules IDs in Windows (all tests should now pass on Windows too). - -## v0.4.0 (December 19, 2014) -* Add support for JSX (React) and additional module paths (Thanks to Ben Lowery). -* Fix for detecting presence of AMD or CommonJS modules (Thanks to Aaron Russ). -* Now resolves the module IDs from the RequireJS paths-config properly (Thanks to russaa). -* Added support for option findNestedDependencies to find nested dependencies in AMD modules. - -## v0.3.5 (Septemper 22, 2014) -* Fix issue with number of graph node lines increased with each render (Thanks to Colin H. Fredericks). - -## v0.3.4 (Septemper 04, 2014) -* Correctly detect circular dependencies when using path aliases in RequireJS config (Thanks to Nicolas Ramz). - -## v0.3.3 (July 11, 2014) -* Fixed bug with relative paths in AMD not handled properly when checking for cyclic dependencies. - -## v0.3.2 (June 25, 2014) -* Handle anonymous require() as entry in the RequireJS optimized file (Thanks to Benjamin Horsleben). - -## v0.3.1 (June 03, 2014) -* Apply exclude to RequireJS shim dependencies (Thanks to Michael White). - -## v0.3.0 (May 25, 2014) -* Added support for onParseFile and onAddModule options (Thanks to Brandon Selway). -* Added JSON output option (Thanks to Drew Foehn). -* Fix for optimized files including dependency information for excluded modules (Thanks to Drew Foehn). Fixes [issue](https://github.com/pahen/madge/issues/26). - -## v0.2.0 (April 17, 2014) -* Added support for including shim dependencies found in RequiredJS config (specify with option -R). - -## v0.1.9 (February 17, 2014) -* Ensure forward slashes are used in modules paths (Windows). - -## v0.1.8 (January 27, 2014) -* Added support for reading AMD dependencies from a r.js optimized file by using option -O. - -## v0.1.7 (September 20, 2013) -* Added missing fontsize option when generating images. - -## v0.1.6 (September 04, 2013) -* AMD plugins are now ignored as dependencies. Fixes [issue](https://github.com/pahen/grunt-madge/issues/1). - -## v0.1.5 (September 04, 2013) -* Fixed Windows [issue](https://github.com/pahen/node-madge/issues/17) when reading from standard input with --read. - -## v0.1.4 (January 10, 2013) -* Switched library for walking directory tree which should solve issues on [Windows](https://github.com/pahen/node-madge/issues/8). - -## v0.1.3 (December 28, 2012) -* Added proper exit code when running "madge --circular" so it can be used in build scripts. - -## v0.1.2 (November 15, 2012) -* Relative AMD module identifiers (if the first term is "." or "..") are now resolved. - -## v0.1.1 (September 3, 2012) -* Tweaked circular dependency path output. - -## v0.1.0 (September 3, 2012) -* Complete path in circular dependencies is now printed (and marked as red in image graphs). - -## v0.0.5 (August 8, 2012) -* Added support for CoffeeScript. Files with extension .coffee will automatically be compiled on-the-fly. - -## v0.0.4 (August 17, 2012) -* Fixed dependency issues with Node.js v0.8. - -## v0.0.3 (July 01, 2012) -* Added support for Node.js v0.8 and dropped support for lower versions. - -## v0.0.2 (May 21, 2012) -* Added ability to read config file and customize colors. - -## v0.0.1 (May 20, 2012) -* Initial release. - # License -(The MIT License) - -Copyright (c) 2012 Patrik Henningsson <patrik.henningsson@gmail.com> - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MIT License \ No newline at end of file diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 00000000..9f317a68 --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,133 @@ +#!/usr/bin/env node +'use strict'; + +const process = require('process'); +const program = require('commander'); +const rc = require('rc')('madge'); +const version = require('../package.json').version; + +program + .version(version) + .usage('[options] ') + .option('-b --basedir ', 'base directory for resolving paths') + .option('-s, --summary', 'show dependency count summary') + .option('-c, --circular', 'show circular dependencies') + .option('-d, --depends ', 'show module dependents') + .option('-x, --exclude ', 'exclude modules using RegExp') + .option('-j, --json', 'output as JSON') + .option('-i, --image ', 'write graph to file as an image') + .option('-l, --layout ', 'layout engine to use for graph (dot/neato/fdp/sfdp/twopi/circo)') + .option('--dot', 'show graph using the DOT language') + .option('--extensions ', 'comma separated string of valid file extensions') + .option('--show-extension', 'include file extension in module name', false) + .option('--require-config ', 'path to RequireJS config') + .option('--webpack-config ', '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 = '*'; +} + +if (!program.color) { + process.env.DEBUG_COLORS = false; +} + +const log = require('../lib/log'); +const output = require('../lib/output'); +const madge = require('../lib/api'); +const config = Object.assign({}, rc); + +delete config._; +delete config.config; +delete config.configs; + +if (rc.config) { + log('using runtime config %s', rc.config); +} + +['layout', 'requireConfig', 'webpackConfig'].forEach((option) => { + if (program[option]) { + config[option] = program[option]; + } +}); + +if (program.basedir) { + config.baseDir = program.basedir; +} + +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'; + config.noDependencyColor = '#00000'; + config.cyclicNodeColor = '#000000'; + config.edgeColor = '#757575'; +} + +madge(program.args, config) + .then((res) => { + if (program.summary) { + return output.summary(res.obj(), { + json: program.json + }); + } + + if (program.depends) { + return output.depends(res.depends(program.depends), { + json: program.json + }); + } + + if (program.circular) { + const circular = res.circular(); + + output.circular(circular, { + json: program.json + }); + + if (circular.length) { + process.exit(1); + } + + return; + } + + if (program.image) { + return res.image(program.image).then((imagePath) => { + console.log('Image created at %s', imagePath); + }); + } + + if (program.dot) { + return res.dot().then((output) => { + process.stdout.write(output); + }); + } + + return output.list(res.obj(), { + json: program.json + }); + }) + .catch((err) => { + output.error(err); + + process.exit(1); + }); diff --git a/bin/madge b/bin/madge deleted file mode 100755 index 8f482bb0..00000000 --- a/bin/madge +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -/** - * Module dependencies - */ -const fs = require('fs'); -const version = require('../package.json').version; -const program = require('commander'); -const printResult = require('../lib/print'); -const madge = require('../lib/madge'); - -program - .version(version) - .usage('[options] ') - .option('-f, --format ', 'format to parse (amd/cjs/es6)', 'cjs') - .option('-L, --list', 'show list of all dependencies (default)') - .option('-s, --summary', 'show summary of all dependencies') - .option('-c, --circular', 'show circular dependencies') - .option('-d, --depends ', 'show modules that depends on the given id') - .option('-x, --exclude ', 'a regular expression for excluding modules') - .option('-t, --dot', 'output graph in the DOT language') - .option('-i, --image ', 'write graph to file as a PNG image') - .option('-l, --layout ', 'layout engine to use for image graph (dot/neato/fdp/sfdp/twopi/circo)', 'dot') - .option('-b, --break-on-error', 'break on parse errors & missing modules', false) - .option('-n, --no-colors', 'skip colors in output and images', false) - .option('-r, --read', 'skip scanning folders and read JSON from stdin') - .option('-C, --config ', 'provide a config file') - .option('-R, --require-config ', 'include shim dependencies and paths found in RequireJS config file') - .option('-O, --optimized', 'if given file is optimized with r.js', false) - .option('-N, --find-nested-dependencies', 'find nested dependencies in AMD modules', false) - .option('-M, --main-require-module ', 'name of the primary RequireJS module, if it\'s included with `require()`', '') - .option('-j --json', 'output dependency tree in json') - .option('-p --paths ', 'additional comma separated paths to search for dependencies (CJS only)', '') - .option('-e --extensions ', 'comma separated string of valid file extensions', 'js,coffee') - .parse(process.argv); - -if (!program.args.length && !program.read && !program.requireConfig) { - console.log(program.helpInformation()); - process.exit(1); -} - -let src = program.args; - -// Check config file -if (program.config && fs.existsSync(program.config)) { // eslint-disable-line no-sync - const configOptions = JSON.parse(fs.readFileSync(program.config, 'utf8')); // eslint-disable-line no-sync - // Duck punch the program with the new options - // Config file take precedence - for (const k in configOptions) { - if (configOptions.hasOwnProperty(k)) { - program[k] = configOptions[k]; - } - } -} - -// Read from standard input -if (program.read) { - let buffer = ''; - process.stdin.resume(); - process.stdin.setEncoding('utf8'); - process.stdin.on('data', (chunk) => { - buffer += chunk; - }); - process.stdin.on('end', () => { - src = JSON.parse(buffer); - run(); - }); -} else { - run(); -} - -function run() { - // Start parsing - const res = madge(src, { - format: program.format, - breakOnError: program.breakOnError, - exclude: program.exclude, - optimized: program.optimized, - requireConfig: program.requireConfig, - mainRequireModule: program.mainRequireModule, - paths: program.paths ? program.paths.split(',') : undefined, - extensions: program.extensions.split(',').map((str) => '.' + str), - findNestedDependencies: program.findNestedDependencies - }); - - // Ouput summary - if (program.summary) { - printResult.summary(res.obj(), { - colors: program.colors, - output: program.output - }); - } - - // Output circular dependencies - if (program.circular) { - printResult.circular(res.circular(), { - colors: program.colors, - output: program.output - }); - } - - // Output module dependencies - if (program.depends) { - printResult.depends(res.depends(program.depends), { - colors: program.colors, - output: program.output - }); - } - - // Write image - if (program.image) { - res.image({ - colors: program.colors, - layout: program.layout, - fontFace: program.font, - fontSize: program.fontSize, - imageColors: program.imageColors - }, (image) => { - fs.writeFile(program.image, image, (err) => { - if (err) { - throw err; - } - }); - }); - } - - // Output DOT - if (program.dot) { - process.stdout.write(res.dot()); - } - - // Output JSON - if (program.json) { - process.stdout.write(JSON.stringify(res.tree) + '\n'); - } - - // Output text (default) - if (program.list || (!program.summary && !program.circular && !program.depends && !program.image && !program.dot && !program.json)) { - printResult.list(res.obj(), { - colors: program.colors, - output: program.output - }); - } -} diff --git a/examples/express.png b/examples/express.png deleted file mode 100644 index dd47c522..00000000 Binary files a/examples/express.png and /dev/null differ diff --git a/examples/small.png b/examples/small.png deleted file mode 100644 index a3ee2a13..00000000 Binary files a/examples/small.png and /dev/null differ diff --git a/examples/terminal.png b/examples/terminal.png deleted file mode 100644 index 87956eec..00000000 Binary files a/examples/terminal.png and /dev/null differ diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 00000000..68ffdf48 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,112 @@ +'use strict'; + +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, + webpackConfig: null, + layout: 'dot', + fontName: 'Arial', + fontSize: '14px', + backgroundColor: '#000000', + nodeColor: '#c6c5fe', + noDependencyColor: '#cfffac', + cyclicNodeColor: '#ff6c60', + edgeColor: '#757575', + graphVizPath: false +}; + +class Madge { + /** + * Class constructor. + * @constructor + * @api public + * @param {String|Array} path + * @param {Object} config + * @return {Promise} + */ + constructor(path, config) { + if (!path) { + throw new Error('path argument not provided'); + } + + if (typeof path === 'string') { + path = [path]; + } + + this.config = Object.assign({}, defaultConfig, config); + + return tree(path, this.config).then((tree) => { + this.tree = tree; + return this; + }); + } + + /** + * Return the module dependency graph as an object. + * @api public + * @return {Object} + */ + obj() { + return this.tree; + } + + /** + * Return the modules that has circular dependencies. + * @api public + * @return {Object} + */ + circular() { + return cyclic(this.tree); + } + + /** + * Return a list of modules that depends on the given module. + * @api public + * @param {String} id + * @return {Array} + */ + depends(id) { + return Object + .keys(this.tree) + .filter((module) => this.tree[module].indexOf(id) >= 0); + } + + /** + * Return the module dependency graph as DOT output. + * @api public + * @return {Promise} + */ + dot() { + return graph.dot(this.tree, this.config); + } + + /** + * Write dependency graph to image. + * @api public + * @param {String} imagePath + * @return {Promise} + */ + image(imagePath) { + if (!imagePath) { + return Promise.reject(new Error('imagePath not provided')); + } + + return graph.image(this.tree, imagePath, this.config); + } +} + +/** + * Expose API. + * @param {String|Array} path + * @param {Object} config + * @return {Promise} + */ +module.exports = (path, config) => new Madge(path, config); diff --git a/lib/cyclic.js b/lib/cyclic.js index a4acfcdf..5fcc376d 100644 --- a/lib/cyclic.js +++ b/lib/cyclic.js @@ -48,7 +48,7 @@ function resolver(id, modules, circular, resolved, unresolved) { /** * Finds all circular dependencies for the given modules. * @param {Object} modules - * @return {Object} + * @return {Array} */ module.exports = function (modules) { const circular = []; @@ -59,28 +59,5 @@ module.exports = function (modules) { resolver(id, modules, circular, resolved, unresolved); }); - return { - /** - * Expose the circular dependency array. - * @return {Array} - */ - getArray() { - return circular; - }, - - /** - * Check if the given module is part of a circular dependency. - * @param {String} id - * @return {Boolean} - */ - isCyclic(id) { - let cyclic = false; - circular.forEach((path) => { - if (path.indexOf(id) >= 0) { - cyclic = true; - } - }); - return cyclic; - } - }; + return circular; }; diff --git a/lib/graph.js b/lib/graph.js index 26a295a4..0b22cd28 100644 --- a/lib/graph.js +++ b/lib/graph.js @@ -1,6 +1,8 @@ 'use strict'; -const exec = require('child_process').exec; +const path = require('path'); +const fs = require('mz/fs'); +const exec = require('mz/child_process').exec; const cyclic = require('./cyclic'); const graphviz = require('graphviz'); @@ -9,125 +11,140 @@ const graphviz = require('graphviz'); * @param {Object} node * @param {String} color */ -function nodeColor(node, color) { +function setNodeColor(node, color) { node.set('color', color); node.set('fontcolor', color); } -/** - * Set color for nodes without dependencies. - * @param {Object} node - * @param {String} [color] - */ -function noDependencyNode(node, color) { - nodeColor(node, color || '#cfffac'); -} - /** * Check if Graphviz is installed on the system. - * @throws Error + * @param {Object} config + * @return {Promise} */ -function checkGraphvizInstalled() { - exec('gvpr -V', (error, stdout, stderr) => { - if (error !== null) { +function checkGraphvizInstalled(config) { + if (config.graphVizPath) { + const cmd = path.join(config.graphVizPath, 'gvpr -V'); + return exec(cmd) + .catch(() => { + throw new Error('Could not execute ' + cmd); + }); + } + + return exec('gvpr -V') + .catch((error) => { throw new Error('Graphviz could not be found. Ensure that "gvpr" is in your $PATH.\n' + error); - } - }); + }); } /** * Return options to use with graphviz digraph. - * @param {Object} opts + * @param {Object} config * @return {Object} */ -function createGraphvizOptions(opts) { - // Valid attributes: http://www.graphviz.org/doc/info/attrs.html - const G = { - layout: opts.layout || 'dot', - overlap: false, - bgcolor: '#ffffff' - }; - - const N = { - fontname: opts.fontFace || 'Times-Roman', - fontsize: opts.fontSize || 14 - }; - - const E = {}; - - if (opts.colors) { - G.bgcolor = opts.imageColors.bgcolor || '#000000'; - E.color = opts.imageColors.edge || '#757575'; - N.color = opts.imageColors.dependencies || '#c6c5fe'; - N.fontcolor = opts.imageColors.fontColor || opts.imageColors.dependencies || '#c6c5fe'; - } - +function createGraphvizOptions(config) { return { - 'type': 'png', - 'G': G, - 'E': E, - 'N': N + G: { + overlap: false, + pad: 0.111, + layout: config.layout, + bgcolor: config.backgroundColor + }, + E: { + color: config.edgeColor + }, + N: { + fontname: config.fontName, + fontsize: config.fontSize, + color: config.nodeColor, + fontcolor: config.nodeColor + } }; } /** - * Creates a PNG image from the module dependency graph. - * @param {Object} modules - * @param {Object} opts - * @param {Function} callback + * Creates the graphviz graph. + * @param {Object} modules + * @param {Object} config + * @param {Object} options + * @return {Promise} */ -module.exports.image = function (modules, opts, callback) { +function createGraph(modules, config, options) { const g = graphviz.digraph('G'); const nodes = {}; + const cyclicModules = cyclic(modules).reduce((a, b) => a.concat(b), []); - checkGraphvizInstalled(); - - opts.imageColors = opts.imageColors || {}; - - const cyclicResults = cyclic(modules); + if (config.graphVizPath) { + g.setGraphVizPath(config.graphVizPath); + } Object.keys(modules).forEach((id) => { - nodes[id] = nodes[id] || g.addNode(id); - if (opts.colors && modules[id]) { - if (!modules[id].length) { - noDependencyNode(nodes[id], opts.imageColors.noDependencies); - } else if (cyclicResults.isCyclic(id)) { - nodeColor(nodes[id], (opts.imageColors.circular || '#ff6c60')); - } + + if (!modules[id].length) { + setNodeColor(nodes[id], config.noDependencyColor); + } else if (cyclicModules.indexOf(id) >= 0) { + setNodeColor(nodes[id], config.cyclicNodeColor); } modules[id].forEach((depId) => { nodes[depId] = nodes[depId] || g.addNode(depId); - if (opts.colors && !modules[depId]) { - noDependencyNode(nodes[depId], opts.imageColors.noDependencies); + + if (!modules[depId]) { + setNodeColor(nodes[depId], config.noDependencyColor); } + g.addEdge(nodes[id], nodes[depId]); }); }); - g.output(createGraphvizOptions(opts), callback); + return new Promise((resolve, reject) => { + g.output(options, resolve, (code, out, err) => { + reject(new Error(err)); + }); + }); +} + +/** + * Creates an image from the module dependency graph. + * @param {Object} modules + * @param {String} imagePath + * @param {Object} config + * @return {Promise} + */ +module.exports.image = function (modules, imagePath, config) { + const options = createGraphvizOptions(config); + + options.type = path.extname(imagePath).replace('.', '') || 'png'; + + return checkGraphvizInstalled(config) + .then(() => { + return createGraph(modules, config, options) + .then((image) => fs.writeFile(imagePath, image)) + .then(() => path.resolve(imagePath)); + }); }; /** * Return the module dependency graph as DOT output. * @param {Object} modules - * @return {String} + * @param {Object} config + * @return {Promise} */ -module.exports.dot = function (modules) { +module.exports.dot = function (modules, config) { const nodes = {}; const g = graphviz.digraph('G'); - checkGraphvizInstalled(); + return checkGraphvizInstalled(config) + .then(() => { + Object.keys(modules).forEach((id) => { + nodes[id] = nodes[id] || g.addNode(id); - Object.keys(modules).forEach((id) => { - nodes[id] = nodes[id] || g.addNode(id); + modules[id].forEach((depId) => { + nodes[depId] = nodes[depId] || g.addNode(depId); + g.addEdge(nodes[id], nodes[depId]); + }); + }); - modules[id].forEach((depId) => { - nodes[depId] = nodes[depId] || g.addNode(depId); - g.addEdge(nodes[id], nodes[depId]); + return g.to_dot(); }); - }); - - return g.to_dot(); }; diff --git a/lib/log.js b/lib/log.js new file mode 100644 index 00000000..3e3a0fb0 --- /dev/null +++ b/lib/log.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('debug')('madge'); diff --git a/lib/madge.js b/lib/madge.js deleted file mode 100644 index 7c4d1e1d..00000000 --- a/lib/madge.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -const cyclic = require('./cyclic'); -const CJS = require('./parse/cjs'); -const AMD = require('./parse/amd'); -const ES6 = require('./parse/es6'); -const graph = require('./graph'); - -class Madge { - /** - * Class constructor. - * @constructor - * @api public - * @param {String|Array|Object} src - * @param {Object} opts - */ - constructor(src, opts) { - let tree = []; - - this.opts = opts || {}; - this.opts.format = String(this.opts.format || 'cjs').toLowerCase(); - - if (typeof src === 'object' && !Array.isArray(src)) { - this.tree = src; - return; - } - - if (typeof src === 'string') { - src = [src]; - } - - if (src && src.length) { - tree = this.parse(src); - } - - this.tree = tree; - } - - /** - * Parse the given source folder(s). - * @param {Array|Object} src - * @return {Object} - */ - parse(src) { - if (this.opts.format === 'cjs') { - return new CJS(src, this.opts, this).tree; - } else if (this.opts.format === 'amd') { - return new AMD(src, this.opts, this).tree; - } else if (this.opts.format === 'es6') { - return new ES6(src, this.opts, this).tree; - } else { - throw new Error('invalid module format "' + this.opts.format + '"'); - } - } - - /** - * Return the module dependency graph as an object. - * @api public - * @return {Object} - */ - obj() { - return this.tree; - } - - /** - * Return the modules that has circular dependencies. - * @api public - * @return {Object} - */ - circular() { - return cyclic(this.tree); - } - - /** - * Return a list of modules that depends on the given module. - * @api public - * @param {String} id - * @return {Array|Object} - */ - depends(id) { - return Object.keys(this.tree).filter((module) => { - if (this.tree[module]) { - return this.tree[module].reduce((acc, dependency) => { - if (dependency === id) { - acc = module; - } - return acc; - }, false); - } - }); - } - - /** - * Return the module dependency graph as DOT output. - * @api public - * @return {String} - */ - dot() { - return graph.dot(this.tree); - } - - /** - * Return the module dependency graph as a PNG image. - * @api public - * @param {Object} opts - * @param {Function} callback - */ - image(opts, callback) { - graph.image(this.tree, opts, callback); - } -} - -module.exports = (src, opts) => new Madge(src, opts); diff --git a/lib/output.js b/lib/output.js new file mode 100644 index 00000000..81d2a514 --- /dev/null +++ b/lib/output.js @@ -0,0 +1,116 @@ +'use strict'; + +const chalk = require('chalk'); + +const red = chalk.red; +const cyan = chalk.cyan; +const grey = chalk.grey; +const green = chalk.green; + +/** + * Print given object as JSON. + * @param {Object} obj + * @return {String} + */ +function printJSON(obj) { + return console.log(JSON.stringify(obj, null, ' ')); +} + +/** + * Print module dependency graph as indented text (or JSON). + * @param {Object} modules + * @param {Object} opts + * @return {undefined} + */ +module.exports.list = function (modules, opts) { + opts = opts || {}; + + if (opts.json) { + return printJSON(modules); + } + + Object.keys(modules).forEach((id) => { + console.log(cyan(id)); + modules[id].forEach((depId) => { + console.log(grey(' ' + depId)); + }); + }); +}; + +/** + * Print a summary of module dependencies. + * @param {Object} modules + * @param {Object} opts + * @return {undefined} + */ +module.exports.summary = function (modules, opts) { + const o = {}; + + opts = opts || {}; + + Object.keys(modules).sort((a, b) => { + return modules[b].length - modules[a].length; + }).forEach((id) => { + if (opts.json) { + o[id] = modules[id].length; + } else { + console.log(grey(id + ': ') + cyan(modules[id].length)); + } + }); + + if (opts.json) { + return printJSON(o); + } +}; + +/** + * Print the result from Madge.circular(). + * @param {Array} circular + * @param {Object} opts + * @return {undefined} + */ +module.exports.circular = function (circular, opts) { + if (opts.json) { + return printJSON(circular); + } + + if (!circular.length) { + console.log(green('No circular dependencies found!')); + } else { + circular.forEach((path, idx) => { + path.forEach((module, idx) => { + if (idx) { + process.stdout.write(cyan(' -> ')); + } + process.stdout.write(red(module)); + }); + process.stdout.write('\n'); + }); + } +}; + +/** + * Print the result from Madge.depends(). + * @param {Array} depends + * @param {Object} opts + * @return {undefined} + */ +module.exports.depends = function (depends, opts) { + if (opts.json) { + return printJSON(depends); + } + + depends.forEach((id) => { + console.log(grey(id)); + }); +}; + +/** + * Print error to the console. + * @param {Object} err + * @param {Object} opts + * @return {undefined} + */ +module.exports.error = function (err) { + console.log(red(err.stack ? err.stack : err)); +}; diff --git a/lib/parse/amd.js b/lib/parse/amd.js deleted file mode 100644 index 5821691c..00000000 --- a/lib/parse/amd.js +++ /dev/null @@ -1,263 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const amdetective = require('amdetective'); -const parse = require('./parse'); -const Base = require('./base'); - -/** - * Merge the two given trees. - * @param {Object} a - * @param {Object} b - */ -function mergeTrees(a, b) { - Object.keys(b).forEach((id) => { - if (!a[id]) { - a[id] = []; - } - - b[id].forEach((dep) => { - if (a[id].indexOf(dep) < 0) { - a[id].push(dep); - } - }); - }); -} - -/** - * Helper for re-mapping path-refs to id-refs that are specified in RequireJS' path config. - * @param {Object} deps (dependency-list) - * @param {Object} pathDefs (path-definitions from requirejs-config) - * @param {String} baseDir (base directory of source files) - */ -function convertPathsToIds(deps, pathDefs, baseDir) { - let path, pathDeps, i1, len1, i2, len2; - - if (baseDir) { - baseDir += '/'; - } else { - baseDir = ''; - } - - Object.keys(pathDefs).forEach((id) => { - path = pathDefs[id]; - - // if path does not start with / or a protocol: prepend with baseDir - if (!/^[^\/]+:\/\/|^\//m.test(path)) { - path = baseDir + path; - } - - if (path !== id && deps[path]) { - if (deps[id] && deps[id].length > 0) { - pathDeps = deps[path].slice(0, deps[path].length - 1); - - // remove entries from , if already contained in - for (i1 = 0, len1 = pathDeps.length; i1 < len1; ++i1) { - for (i2 = 0, len2 = deps[id].length; i2 < len2; ++i2) { - if (pathDeps[i1] === deps[id][i2]) { - pathDeps.splice(i1--, 1); - break; - } - } - } - deps[id] = deps[id].concat(pathDeps); - } else { - deps[id] = deps[path]; - } - - delete deps[path]; - } else if (!deps[id]) { - deps[id] = []; - } - - // normalize entries within deps-arrays (i.e. replace path-refs with id-refs) - Object.keys(pathDefs).forEach((id) => { - path = baseDir + pathDefs[id]; - if (deps[id]) { - for (i1 = 0, len1 = deps[id].length; i1 < len1; ++i1) { - // replace path-ref with id-ref (if necessary) - if (deps[id][i1] === path) { - deps[id][i1] = id; - } - } - } - }); - }); -} - -/** - * Read shim dependencies from RequireJS config. - * @param {String} filename - * @param {String} [exclude] - * @return {Object} - */ -function getShimDepsFromConfig(filename, exclude) { - const deps = {}; - const config = parse.findConfig(filename, fs.readFileSync(filename, 'utf8')); // eslint-disable-line no-sync - const excludeRegex = exclude ? new RegExp(exclude) : false; - const isIncluded = function (key) { - return !(excludeRegex && key.match(excludeRegex)); - }; - - if (config.shim) { - Object.keys(config.shim).filter(isIncluded).forEach((key) => { - if (config.shim[key].deps) { - deps[key] = config.shim[key].deps.filter(isIncluded); - } else { - deps[key] = []; - } - }); - } - - return deps; -} - -/** -* Read path definitions from RequireJS config. -* @param {String} filename -* @param {String} [exclude] -* @return {Object} -*/ -function getPathsFromConfig(filename, exclude) { - const paths = {}; - const config = parse.findConfig(filename, fs.readFileSync(filename, 'utf8')); // eslint-disable-line no-sync - const excludeRegex = exclude ? new RegExp(exclude) : false; - const isIncluded = function (key) { - return !(excludeRegex && key.match(excludeRegex)); - }; - - if (config.paths) { - Object.keys(config.paths).filter(isIncluded).forEach((key) => { - paths[key] = config.paths[key]; - }); - } - - return paths; -} - -/** - * Read baseUrl from RequireJS config. - * @param {String} filename - * @param {String} srcBaseDir - * @return {String} - */ -function getBaseUrlFromConfig(filename, srcBaseDir) { - const config = parse.findConfig(filename, fs.readFileSync(filename, 'utf8')); // eslint-disable-line no-sync - return config.baseUrl ? path.relative(srcBaseDir, config.baseUrl) : ''; -} - -class AMD extends Base { - /** - * @constructor - * @param {Array} src - * @param {Object} opts - * @param {Object} parent - */ - constructor(src, opts, parent) { - super(src, opts, parent); - - if (opts.requireConfig) { - let baseDir = src.length ? src[0].replace(/\\/g, '/') : ''; - baseDir = getBaseUrlFromConfig(opts.requireConfig, baseDir); - convertPathsToIds(this.tree, getPathsFromConfig(opts.requireConfig, opts.exclude), baseDir); - mergeTrees(this.tree, getShimDepsFromConfig(opts.requireConfig, opts.exclude)); - } - } - - /** - * Normalize a module file path and return a proper identificator. - * @param {String} filename - * @return {String} - */ - normalize(filename) { - return this.replaceBackslashInPath(path.relative(this.baseDir, filename).replace(this.extRegEx, '')); - } - - /** - * Parse the given file and return all found dependencies. - * @param {String} filename - * @return {Array} - */ - parseFile(filename) { - const dependencies = []; - const src = this.getFileSource(filename); - - this.emit('parseFile', { - filename: filename, - src: src - }); - - if (/define|require\s*\(/m.test(src)) { - amdetective(src, {findNestedDependencies: this.opts.findNestedDependencies}).map((obj) => { - return typeof obj === 'string' ? [obj] : obj.deps; - }).filter((deps) => { - deps.filter((id) => { - // Ignore RequireJS IDs and plugins - return id !== 'require' && id !== 'exports' && id !== 'module' && !id.match(/\.?\w\!/); - }).map((id) => { - // Only resolve relative module identifiers (if the first term is "." or "..") - if (id.charAt(0) !== '.') { - return id; - } - - const depFilename = path.resolve(path.dirname(filename), id); - - if (depFilename) { - return this.normalize(depFilename); - } - }).forEach((id) => { - if (!this.isExcluded(id) && dependencies.indexOf(id) < 0) { - dependencies.push(id); - } - }); - }); - } - - return dependencies; - } - - /** - * Get module dependencies from optimize file (r.js). - * @param {String} filename - */ - addOptimizedModules(filename) { - const anonymousRequire = []; - - amdetective(this.getFileSource(filename)) - .filter((obj) => { - const id = obj.name || obj; - return id !== 'require' && id !== 'exports' && id !== 'module' && !id.match(/\.?\w\!/) && !this.isExcluded(id); - }) - .forEach((obj) => { - if (typeof obj === 'string') { - anonymousRequire.push(obj); - return; - } - - if (!this.isExcluded(obj.name)) { - this.tree[obj.name] = obj.deps.filter((id) => { - return id !== 'require' && id !== 'exports' && id !== 'module' && !id.match(/\.?\w\!/) && !this.isExcluded(id); - }); - } - }); - - if (anonymousRequire.length > 0) { - this.tree[this.opts.mainRequireModule || ''] = anonymousRequire; - } - } - - /** - * Parse the given `filename` and add it to the module tree. - * @param {String} filename - */ - addModule(filename) { - if (this.opts.optimized) { - this.addOptimizedModules(filename); - } else { - Base.prototype.addModule.call(this, filename); - } - } -} - -module.exports = AMD; diff --git a/lib/parse/base.js b/lib/parse/base.js deleted file mode 100644 index 8aaf842d..00000000 --- a/lib/parse/base.js +++ /dev/null @@ -1,188 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const resolve = require('resolve'); -const EventEmitter = require('events').EventEmitter; -const commondir = require('commondir'); -const finder = require('walkdir'); -const coffee = require('coffee-script'); - -class Base extends EventEmitter { - /** - * Traversing `src` and fetches all dependencies. - * @constructor - * @param {Array} src - * @param {Object} opts - * @param {Object} parent - */ - constructor(src, opts, parent) { - super(); - - if (opts.onParseFile) { - this.on('parseFile', opts.onParseFile.bind(parent)); - } - - if (opts.onAddModule) { - this.on('addModule', opts.onAddModule.bind(parent)); - } - - this.opts = opts; - - if (typeof this.opts.extensions === 'undefined') { - this.opts.extensions = ['.js']; - } - - this.tree = {}; - this.extRegEx = new RegExp('\\.(coffee|jsx|' + this.opts.extensions.map((str) => { - return str.substring(1); - }).join('|') + ')$', 'g'); - this.coffeeExtRegEx = /\.coffee$/; - src = this.resolveTargets(src); - this.excludeRegex = opts.exclude ? new RegExp(opts.exclude) : false; - this.baseDir = this.getBaseDir(src); - this.readFiles(src); - this.sortDependencies(); - } - - /** - * Resolve the given `id` to a filename. - * @param {String} dir - * @param {String} id - * @return {String} - */ - resolve(dir, id) { - try { - return resolve.sync(id, { - basedir: dir, - paths: this.opts.paths, - extensions: this.opts.extensions - }); - } catch (e) { - if (this.opts.breakOnError) { - console.log(String('\nError while resolving module from: ' + id).red); - throw e; - } - return id; - } - } - - /** - * Get the most common dir from the `src`. - * @param {Array} src - * @return {String} - */ - getBaseDir(src) { - const dir = commondir(src); - - if (!fs.statSync(dir).isDirectory()) { // eslint-disable-line no-sync - return path.dirname(dir); - } - - return dir; - } - - /** - * Resolves all paths in `sources` and ensure we have a absolute path. - * @param {Array} sources - * @return {Array} - */ - resolveTargets(sources) { - return sources.map((src) => path.resolve(src)); - } - - /** - * Normalize a module file path and return a proper identificator. - * @param {String} filename - * @return {String} - */ - normalize(filename) { - return this.replaceBackslashInPath(path.relative(this.baseDir, filename).replace(this.extRegEx, '')); - } - - /** - * Check if module should be excluded. - * @param {String} id - * @return {Boolean} - */ - isExcluded(id) { - return this.excludeRegex && id.match(this.excludeRegex); - } - - /** - * Parse the given `filename` and add it to the module tree. - * @param {String} filename - */ - addModule(filename) { - const id = this.normalize(filename); - - if (!this.isExcluded(id) && fs.existsSync(filename)) { // eslint-disable-line no-sync - try { - this.tree[id] = this.parseFile(filename); - this.emit('addModule', {id: id, dependencies: this.tree[id]}); - } catch (e) { - if (this.opts.breakOnError) { - console.log(String('\nError while parsing file: ' + filename).red); - throw e; - } - } - } - } - - /** - * Traverse `sources` and parse files found. - * @param {Array} sources - */ - readFiles(sources) { - sources.forEach((src) => { - if (fs.statSync(src).isDirectory()) { // eslint-disable-line no-sync - finder.sync(src).filter((filename) => { - return filename.match(this.extRegEx); - }).forEach((filename) => { - this.addModule(filename); - }); - } else { - this.addModule(src); - } - }); - } - - /** - * Read the given filename and compile it if necessary and return the content. - * @param {String} filename - * @return {String} - */ - getFileSource(filename) { - const src = fs.readFileSync(filename, 'utf8'); // eslint-disable-line no-sync - - if (filename.match(this.coffeeExtRegEx)) { - return coffee.compile(src, { - header: false, - bare: true - }); - } - - return src; - } - - /** - * Sort dependencies by name. - */ - sortDependencies() { - this.tree = Object.keys(this.tree).sort().reduce((acc, id) => { - (acc[id] = this.tree[id]).sort(); - return acc; - }, {}); - } - - /** - * Replace back slashes in path (Windows) with forward slashes (*nix). - * @param {String} path - * @return {String} - */ - replaceBackslashInPath(path) { - return path.replace(/\\/g, '/'); - } -} - -module.exports = Base; diff --git a/lib/parse/cjs.js b/lib/parse/cjs.js deleted file mode 100644 index 4635f095..00000000 --- a/lib/parse/cjs.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const path = require('path'); -const detective = require('detective'); -const Base = require('./base'); - -class CJS extends Base { - /** - * Normalize a module file path and return a proper identificator. - * @param {String} filename - * @return {String} - */ - normalize(filename) { - filename = this.replaceBackslashInPath(filename); - if (filename.charAt(0) !== '/' && !filename.match(/^[A-Za-z:]+\//i)) { - // a core module (not mapped to a file) - return filename; - } - return super.normalize(filename); - } - - /** - * Parse the given file and return all found dependencies. - * @param {String} filename - * @return {Array} - */ - parseFile(filename) { - const dependencies = []; - const src = this.getFileSource(filename); - - this.emit('parseFile', { - filename: filename, - src: src - }); - - if (/require\s*\(/m.test(src)) { - detective(src).map((id) => { - const depFilename = this.resolve(path.dirname(filename), id); - if (depFilename) { - return this.normalize(depFilename); - } - }).filter((id) => { - if (!this.isExcluded(id) && dependencies.indexOf(id) < 0) { - dependencies.push(id); - } - }); - } - - return dependencies; - } -} - -module.exports = CJS; diff --git a/lib/parse/es6.js b/lib/parse/es6.js deleted file mode 100644 index bb6e27cf..00000000 --- a/lib/parse/es6.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -const path = require('path'); -const detective = require('detective-es6'); -const Base = require('./base'); - -class ES6 extends Base { - /** - * Parse the given file and return all found dependencies. - * @param {String} filename - * @return {Array} - */ - parseFile(filename) { - const dependencies = []; - const src = this.getFileSource(filename); - - this.emit('parseFile', { - filename: filename, - src: src - }); - - if (/import.*from/m.test(src) || /export.*from/m.test(src)) { - detective(src).map((id) => { - const depFilename = this.resolve(path.dirname(filename), id); - - if (depFilename) { - return this.normalize(depFilename); - } - }).filter((id) => { - if (!this.isExcluded(id) && dependencies.indexOf(id) < 0) { - dependencies.push(id); - } - }); - } - - return dependencies; - } -} - -module.exports = ES6; diff --git a/lib/parse/parse.js b/lib/parse/parse.js deleted file mode 100644 index 0201b40b..00000000 --- a/lib/parse/parse.js +++ /dev/null @@ -1,851 +0,0 @@ -'use strict'; - -/** - * Copied from https://github.com/jrburke/r.js/blob/master/build/jslib/parse.js with a couple - * of changes to make it run in node. - */ - -/** - * @license Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. - * Available via the MIT or new BSD license. - * see: http://github.com/jrburke/requirejs for details - */ - -/* eslint-disable */ - -var uglify = require('uglify-js'), - parser = uglify.parser, - processor = uglify.uglify, - ostring = Object.prototype.toString, - isArray; - -if (Array.isArray) { - isArray = Array.isArray; -} else { - isArray = function (it) { - return ostring.call(it) === "[object Array]"; - }; -} - -/** - * Determines if the AST node is an array literal - */ -function isArrayLiteral(node) { - return node[0] === 'array'; -} - -/** - * Determines if the AST node is an object literal - */ -function isObjectLiteral(node) { - return node[0] === 'object'; -} - -/** - * Converts a regular JS array of strings to an AST node that - * represents that array. - * @param {Array} ary - * @param {Node} an AST node that represents an array of strings. - */ -function toAstArray(ary) { - var output = [ - 'array', - [] - ], - i, item; - - for (i = 0; (item = ary[i]); i++) { - output[1].push([ - 'string', - item - ]); - } - - return output; -} - -/** - * Validates a node as being an object literal (like for i18n bundles) - * or an array literal with just string members. If an array literal, - * only return array members that are full strings. So the caller of - * this function should use the return value as the new value for the - * node. - * - * This function does not need to worry about comments, they are not - * present in this AST. - * - * @param {Node} node an AST node. - * - * @returns {Node} an AST node to use for the valid dependencies. - * If null is returned, then it means the input node was not a valid - * dependency. - */ -function validateDeps(node) { - var newDeps = ['array', []], - arrayArgs, i, dep; - - if (!node) { - return null; - } - - if (isObjectLiteral(node) || node[0] === 'function') { - return node; - } - - //Dependencies can be an object literal or an array. - if (!isArrayLiteral(node)) { - return null; - } - - arrayArgs = node[1]; - - for (i = 0; i < arrayArgs.length; i++) { - dep = arrayArgs[i]; - if (dep[0] === 'string') { - newDeps[1].push(dep); - } - } - return newDeps[1].length ? newDeps : null; -} - -/** - * Gets dependencies from a node, but only if it is an array literal, - * and only if the dependency is a string literal. - * - * This function does not need to worry about comments, they are not - * present in this AST. - * - * @param {Node} node an AST node. - * - * @returns {Array} of valid dependencies. - * If null is returned, then it means the input node was not a valid - * array literal, or did not have any string literals.. - */ -function getValidDeps(node) { - var newDeps = [], - arrayArgs, i, dep; - - if (!node) { - return null; - } - - if (isObjectLiteral(node) || node[0] === 'function') { - return null; - } - - //Dependencies can be an object literal or an array. - if (!isArrayLiteral(node)) { - return null; - } - - arrayArgs = node[1]; - - for (i = 0; i < arrayArgs.length; i++) { - dep = arrayArgs[i]; - if (dep[0] === 'string') { - newDeps.push(dep[1]); - } - } - return newDeps.length ? newDeps : null; -} - -/** - * Main parse function. Returns a string of any valid require or define/require.def - * calls as part of one JavaScript source string. - * @param {String} moduleName the module name that represents this file. - * It is used to create a default define if there is not one already for the file. - * This allows properly tracing dependencies for builds. Otherwise, if - * the file just has a require() call, the file dependencies will not be - * properly reflected: the file will come before its dependencies. - * @param {String} moduleName - * @param {String} fileName - * @param {String} fileContents - * @param {Object} options optional options. insertNeedsDefine: true will - * add calls to require.needsDefine() if appropriate. - * @returns {String} JS source string or null, if no require or define/require.def - * calls are found. - */ -function parse(moduleName, fileName, fileContents, options) { - options = options || {}; - - //Set up source input - var moduleDeps = [], - result = '', - moduleList = [], - needsDefine = true, - astRoot = parser.parse(fileContents), - i, moduleCall, depString; - - parse.recurse(astRoot, function (callName, config, name, deps) { - //If name is an array, it means it is an anonymous module, - //so adjust args appropriately. An anonymous module could - //have a FUNCTION as the name type, but just ignore those - //since we just want to find dependencies. - if (name && isArrayLiteral(name)) { - deps = name; - name = null; - } - - if (!(deps = getValidDeps(deps))) { - deps = []; - } - - //Get the name as a string literal, if it is available. - if (name && name[0] === 'string') { - name = name[1]; - } else { - name = null; - } - - if (callName === 'define' && (!name || name === moduleName)) { - needsDefine = false; - } - - if (!name) { - //If there is no module name, the dependencies are for - //this file/default module name. - moduleDeps = moduleDeps.concat(deps); - } else { - moduleList.push({ - name: name, - deps: deps - }); - } - - //If define was found, no need to dive deeper, unless - //the config explicitly wants to dig deeper. - return !options.findNestedDependencies; - }, options); - - if (options.insertNeedsDefine && needsDefine) { - result += 'require.needsDefine("' + moduleName + '");'; - } - - if (moduleDeps.length || moduleList.length) { - for (i = 0; (moduleCall = moduleList[i]); i++) { - if (result) { - result += '\n'; - } - - //If this is the main module for this file, combine any - //"anonymous" dependencies (could come from a nested require - //call) with this module. - if (moduleCall.name === moduleName) { - moduleCall.deps = moduleCall.deps.concat(moduleDeps); - moduleDeps = []; - } - - depString = moduleCall.deps.length ? '["' + moduleCall.deps.join('","') + '"]' : '[]'; - result += 'define("' + moduleCall.name + '",' + depString + ');'; - } - if (moduleDeps.length) { - if (result) { - result += '\n'; - } - depString = moduleDeps.length ? '["' + moduleDeps.join('","') + '"]' : '[]'; - result += 'define("' + moduleName + '",' + depString + ');'; - } - } - - return result ? result : null; -} - -//Add some private methods to object for use in derived objects. -parse.isArray = isArray; -parse.isObjectLiteral = isObjectLiteral; -parse.isArrayLiteral = isArrayLiteral; - -/** - * Handles parsing a file recursively for require calls. - * @param {Array} parentNode the AST node to start with. - * @param {Function} onMatch function to call on a parse match. - * @param {Object} [options] This is normally the build config options if - * it is passed. - * @param {Function} [recurseCallback] function to call on each valid - * node, defaults to parse.parseNode. - */ -parse.recurse = function (parentNode, onMatch, options, recurseCallback) { - var hasHas = options && options.has, - i, node; - - recurseCallback = recurseCallback || this.parseNode; - - if (isArray(parentNode)) { - for (i = 0; i < parentNode.length; i++) { - node = parentNode[i]; - if (isArray(node)) { - //If has config is in play, if calls have been converted - //by this point to be true/false values. So, if - //options has a 'has' value, skip if branches that have - //literal false values. - - //uglify returns if constructs in an array: - //[0]: 'if' - //[1]: the condition, ['name', true | false] for the has replaced case. - //[2]: the block to process if true - //[3]: the block to process if false - //For if/else if/else, the else if is in the [3], - //so only ever have to deal with this structure. - if (hasHas && node[0] === 'if' && node[1] && node[1][0] === 'name' && - (node[1][1] === 'true' || node[1][1] === 'false')) { - if (node[1][1] === 'true') { - this.recurse([node[2]], onMatch, options, recurseCallback); - } else { - this.recurse([node[3]], onMatch, options, recurseCallback); - } - } else { - if (recurseCallback(node, onMatch)) { - //The onMatch indicated parsing should - //stop for children of this node. - continue; - } - this.recurse(node, onMatch, options, recurseCallback); - } - } - } - } -}; - -/** - * Determines if the file defines require(). - * @param {String} fileName - * @param {String} fileContents - * @returns {Boolean} - */ -parse.definesRequire = function (fileName, fileContents) { - var astRoot = parser.parse(fileContents); - return this.nodeHasRequire(astRoot); -}; - -/** - * Finds require("") calls inside a CommonJS anonymous module wrapped in a - * define(function(require, exports, module){}) wrapper. These dependencies - * will be added to a modified define() call that lists the dependencies - * on the outside of the function. - * @param {String} fileName - * @param {String} fileContents - * @returns {Array} an array of module names that are dependencies. Always - * returns an array, but could be of length zero. - */ -parse.getAnonDeps = function (fileName, fileContents) { - var astRoot = parser.parse(fileContents), - defFunc = this.findAnonDefineFactory(astRoot); - - return parse.getAnonDepsFromNode(defFunc); -}; - -/** - * Finds require("") calls inside a CommonJS anonymous module wrapped - * in a define function, given an AST node for the definition function. - * @param {Node} node the AST node for the definition function. - * @returns {Array} and array of dependency names. Can be of zero length. - */ -parse.getAnonDepsFromNode = function (node) { - var deps = [], - funcArgLength; - - if (node) { - this.findRequireDepNames(node, deps); - - //If no deps, still add the standard CommonJS require, exports, module, - //in that order, to the deps, but only if specified as function args. - //In particular, if exports is used, it is favored over the return - //value of the function, so only add it if asked. - funcArgLength = node[2] && node[2].length; - if (funcArgLength) { - deps = (funcArgLength > 1 ? ["require", "exports", "module"] : - ["require"]).concat(deps); - } - } - return deps; -}; - -/** - * Finds the function in define(function (require, exports, module){}); - * @param {Array} node - * @returns {Boolean} - */ -parse.findAnonDefineFactory = function (node) { - var callback, i, n, call, args; - - if (isArray(node)) { - if (node[0] === 'call') { - call = node[1]; - args = node[2]; - if ((call[0] === 'name' && call[1] === 'define') || - (call[0] === 'dot' && call[1][1] === 'require' && call[2] === 'def')) { - - //There should only be one argument and it should be a function, - //or a named module with function as second arg - if (args.length === 1 && args[0][0] === 'function') { - return args[0]; - } else if (args.length === 2 && args[0][0] === 'string' && - args[1][0] === 'function') { - return args[1]; - } - } - } - - //Check child nodes - for (i = 0; i < node.length; i++) { - n = node[i]; - if ((callback = this.findAnonDefineFactory(n))) { - return callback; - } - } - } - - return null; -}; - -/** - * Finds any config that is passed to requirejs. - * @param {String} fileName - * @param {String} fileContents - * - * @returns {Object} a config object. Will be null if no config. - * Can throw an error if the config in the file cannot be evaluated in - * a build context to valid JavaScript. - */ -parse.findConfig = function (fileName, fileContents) { - /*jslint evil: true */ - //This is a litle bit inefficient, it ends up with two uglifyjs parser - //calls. Can revisit later, but trying to build out larger functional - //pieces first. - var foundConfig = null, - astRoot = parser.parse(fileContents); - - parse.recurse(astRoot, function (configNode) { - var jsConfig; - - if (!foundConfig && configNode) { - jsConfig = parse.nodeToString(configNode); - foundConfig = eval('(' + jsConfig + ')'); - return foundConfig; - } - return undefined; - }, null, parse.parseConfigNode); - - return foundConfig; -}; - -/** - * Finds all dependencies specified in dependency arrays and inside - * simplified commonjs wrappers. - * @param {String} fileName - * @param {String} fileContents - * - * @returns {Array} an array of dependency strings. The dependencies - * have not been normalized, they may be relative IDs. - */ -parse.findDependencies = function (fileName, fileContents, options) { - //This is a litle bit inefficient, it ends up with two uglifyjs parser - //calls. Can revisit later, but trying to build out larger functional - //pieces first. - var dependencies = [], - astRoot = parser.parse(fileContents); - - parse.recurse(astRoot, function (callName, config, name, deps) { - //Normalize the input args. - if (name && isArrayLiteral(name)) { - deps = name; - name = null; - } - - if ((deps = getValidDeps(deps))) { - dependencies = dependencies.concat(deps); - } - }, options); - - return dependencies; -}; - -/** - * Finds only CJS dependencies, ones that are the form require('stringLiteral') - */ -parse.findCjsDependencies = function (fileName, fileContents, options) { - //This is a litle bit inefficient, it ends up with two uglifyjs parser - //calls. Can revisit later, but trying to build out larger functional - //pieces first. - var dependencies = [], - astRoot = parser.parse(fileContents); - - parse.recurse(astRoot, function (dep) { - dependencies.push(dep); - }, options, function (node, onMatch) { - - var call, args; - - if (!isArray(node)) { - return false; - } - - if (node[0] === 'call') { - call = node[1]; - args = node[2]; - - if (call) { - //A require('') use. - if (call[0] === 'name' && call[1] === 'require' && - args[0][0] === 'string') { - return onMatch(args[0][1]); - } - } - } - - return false; - - }); - - return dependencies; -}; - -/** - * Determines if define(), require({}|[]) or requirejs was called in the - * file. Also finds out if define() is declared and if define.amd is called. - */ -parse.usesAmdOrRequireJs = function (fileName, fileContents, options) { - var astRoot = parser.parse(fileContents), - uses; - - parse.recurse(astRoot, function (prop) { - if (!uses) { - uses = {}; - } - uses[prop] = true; - }, options, parse.findAmdOrRequireJsNode); - - return uses; -}; - -/** - * Determines if require(''), exports.x =, module.exports =, - * __dirname, __filename are used. So, not strictly traditional CommonJS, - * also checks for Node variants. - */ -parse.usesCommonJs = function (fileName, fileContents, options) { - var uses = null, - assignsExports = false, - astRoot = parser.parse(fileContents); - - parse.recurse(astRoot, function (prop) { - if (prop === 'varExports') { - assignsExports = true; - } else if (prop !== 'exports' || !assignsExports) { - if (!uses) { - uses = {}; - } - uses[prop] = true; - } - }, options, function (node, onMatch) { - - var call, args; - - if (!isArray(node)) { - return false; - } - - if (node[0] === 'name' && (node[1] === '__dirname' || node[1] === '__filename')) { - return onMatch(node[1].substring(2)); - } else if (node[0] === 'var' && node[1] && node[1][0] && node[1][0][0] === 'exports') { - //Hmm, a variable assignment for exports, so does not use cjs exports. - return onMatch('varExports'); - } else if (node[0] === 'assign' && node[2] && node[2][0] === 'dot') { - args = node[2][1]; - - if (args) { - //An exports or module.exports assignment. - if (args[0] === 'name' && args[1] === 'module' && - node[2][2] === 'exports') { - return onMatch('moduleExports'); - } else if (args[0] === 'name' && args[1] === 'exports') { - return onMatch('exports'); - } - } - } else if (node[0] === 'call') { - call = node[1]; - args = node[2]; - - if (call) { - //A require('') use. - if (call[0] === 'name' && call[1] === 'require' && - args[0][0] === 'string') { - return onMatch('require'); - } - } - } - - return false; - - }); - - return uses; -}; - - -parse.findRequireDepNames = function (node, deps) { - var moduleName, i, n, call, args; - - if (isArray(node)) { - if (node[0] === 'call') { - call = node[1]; - args = node[2]; - - if (call && call[0] === 'name' && call[1] === 'require') { - moduleName = args[0]; - if (moduleName[0] === 'string') { - deps.push(moduleName[1]); - } - } - - - } - - //Check child nodes - for (i = 0; i < node.length; i++) { - n = node[i]; - this.findRequireDepNames(n, deps); - } - } -}; - -/** - * Determines if a given node contains a require() definition. - * @param {Array} node - * @returns {Boolean} - */ -parse.nodeHasRequire = function (node) { - if (this.isDefineNode(node)) { - return true; - } - - if (isArray(node)) { - for (var i = 0, n; i < node.length; i++) { - n = node[i]; - if (this.nodeHasRequire(n)) { - return true; - } - } - } - - return false; -}; - -/** - * Is the given node the actual definition of define(). Actually uses - * the definition of define.amd to find require. - * @param {Array} node - * @returns {Boolean} - */ -parse.isDefineNode = function (node) { - //Actually look for the define.amd = assignment, since - //that is more indicative of RequireJS vs a plain require definition. - var assign; - if (!node) { - return null; - } - - if (node[0] === 'assign' && node[1] === true) { - assign = node[2]; - if (assign[0] === 'dot' && assign[1][0] === 'name' && - assign[1][1] === 'define' && assign[2] === 'amd') { - return true; - } - } - return false; -}; - -/** - * Determines if a specific node is a valid require or define/require.def call. - * @param {Array} node - * @param {Function} onMatch a function to call when a match is found. - * It is passed the match name, and the config, name, deps possible args. - * The config, name and deps args are not normalized. - * - * @returns {String} a JS source string with the valid require/define call. - * Otherwise null. - */ -parse.parseNode = function (node, onMatch) { - var call, name, config, deps, args, cjsDeps; - - if (!isArray(node)) { - return false; - } - - if (node[0] === 'call') { - call = node[1]; - args = node[2]; - - if (call) { - if (call[0] === 'name' && - (call[1] === 'require' || call[1] === 'requirejs')) { - - //It is a plain require() call. - config = args[0]; - deps = args[1]; - if (isArrayLiteral(config)) { - deps = config; - config = null; - } - - if (!(deps = validateDeps(deps))) { - return null; - } - - return onMatch("require", null, null, deps); - - } else if (call[0] === 'name' && call[1] === 'define') { - - //A define call - name = args[0]; - deps = args[1]; - //Only allow define calls that match what is expected - //in an AMD call: - //* first arg should be string, array, function or object - //* second arg optional, or array, function or object. - //This helps weed out calls to a non-AMD define, but it is - //not completely robust. Someone could create a define - //function that still matches this shape, but this is the - //best that is possible, and at least allows UglifyJS, - //which does create its own internal define in one file, - //to be inlined. - if (((name[0] === 'string' || isArrayLiteral(name) || - name[0] === 'function' || isObjectLiteral(name))) && - (!deps || isArrayLiteral(deps) || - deps[0] === 'function' || isObjectLiteral(deps) || - // allow define(['dep'], factory) pattern - (isArrayLiteral(name) && deps[0] === 'name' && args.length === 2))) { - - //If first arg is a function, could be a commonjs wrapper, - //look inside for commonjs dependencies. - //Also, if deps is a function look for commonjs deps. - if (name && name[0] === 'function') { - cjsDeps = parse.getAnonDepsFromNode(name); - if (cjsDeps.length) { - name = toAstArray(cjsDeps); - } - } else if (deps && deps[0] === 'function') { - cjsDeps = parse.getAnonDepsFromNode(deps); - if (cjsDeps.length) { - deps = toAstArray(cjsDeps); - } - } - - return onMatch("define", null, name, deps); - } - } - } - } - - return false; -}; - -/** - * Looks for define(), require({} || []), requirejs({} || []) calls. - */ -parse.findAmdOrRequireJsNode = function (node, onMatch) { - var call, args, configNode, type; - - if (!isArray(node)) { - return false; - } - - if (node[0] === 'defun' && node[1] === 'define') { - type = 'declaresDefine'; - } else if (node[0] === 'assign' && node[2] && node[2][2] === 'amd' && - node[2][1] && node[2][1][0] === 'name' && - node[2][1][1] === 'define') { - type = 'defineAmd'; - } else if (node[0] === 'call') { - call = node[1]; - args = node[2]; - - if (call) { - if ((call[0] === 'dot' && - (call[1] && call[1][0] === 'name' && - (call[1][1] === 'require' || call[1][1] === 'requirejs')) && - call[2] === 'config')) { - //A require.config() or requirejs.config() call. - type = call[1][1] + 'Config'; - } else if (call[0] === 'name' && - (call[1] === 'require' || call[1] === 'requirejs')) { - //A require() or requirejs() config call. - //Only want ones that start with an object or an array. - configNode = args[0]; - if (configNode[0] === 'object' || configNode[0] === 'array') { - type = call[1]; - } - } else if (call[0] === 'name' && call[1] === 'define') { - //A define call. - type = 'define'; - } - } - } - - if (type) { - return onMatch(type); - } - - return false; -}; - -/** - * Determines if a specific node is a valid require/requirejs config - * call. That includes calls to require/requirejs.config(). - * @param {Array} node - * @param {Function} onMatch a function to call when a match is found. - * It is passed the match name, and the config, name, deps possible args. - * The config, name and deps args are not normalized. - * - * @returns {String} a JS source string with the valid require/define call. - * Otherwise null. - */ -parse.parseConfigNode = function (node, onMatch) { - var call, configNode, args; - - if (!isArray(node)) { - return false; - } - - if (node[0] === 'call') { - call = node[1]; - args = node[2]; - - if (call) { - //A require.config() or requirejs.config() call. - if ((call[0] === 'dot' && - (call[1] && call[1][0] === 'name' && - (call[1][1] === 'require' || call[1][1] === 'requirejs')) && - call[2] === 'config') || - //A require() or requirejs() config call. - - (call[0] === 'name' && - (call[1] === 'require' || call[1] === 'requirejs')) - ) { - //It is a plain require() call. - configNode = args[0]; - - if (configNode[0] !== 'object') { - return null; - } - - return onMatch(configNode); - - } - } - } - - return false; -}; - -/** - * Converts an AST node into a JS source string. Does not maintain formatting - * or even comments from original source, just returns valid JS source. - * @param {Array} node - * @returns {String} a JS source string. - */ -parse.nodeToString = function (node) { - return processor.gen_code(node, true); -}; - -module.exports = parse; \ No newline at end of file diff --git a/lib/print.js b/lib/print.js deleted file mode 100644 index 1b7d3f69..00000000 --- a/lib/print.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -require('colors'); - -/** - * Return colored string (or not). - * @param {String} str - * @param {String} name - * @param {Boolean} use - * @return {String} - */ -function c(str, name, use) { - return use ? String(str)[name] : str; -} - -/** - * Return the given object as JSON. - * @param {Object} obj - * @return {String} - */ -function toJSON(obj) { - return JSON.stringify(obj, null, ' ') + '\n'; -} - -/** - * Print module dependency graph as indented text (or JSON). - * @param {Object} modules - * @param {Object} opts - * @return {undefined} - */ -module.exports.list = function (modules, opts) { - opts = opts || {}; - - if (opts.output === 'json') { - return process.stdout.write(toJSON(modules)); - } - - Object.keys(modules).forEach((id) => { - console.log(c(id, 'cyan', opts.colors)); - modules[id].forEach((depId) => { - console.log(c(' ' + depId, 'grey', opts.colors)); - }); - }); -}; - -/** - * Print a summary of module dependencies. - * @param {Object} modules - * @param {Object} opts - * @return {undefined} - */ -module.exports.summary = function (modules, opts) { - const o = {}; - - opts = opts || {}; - - Object.keys(modules).sort((a, b) => { - return modules[b].length - modules[a].length; - }).forEach((id) => { - if (opts.output === 'json') { - o[id] = modules[id].length; - } else { - console.log(c(id + ': ', 'grey', opts.colors) + c(modules[id].length, 'cyan', opts.colors)); - } - }); - - if (opts.output === 'json') { - return process.stdout.write(toJSON(o)); - } -}; - -/** - * Print the result from Madge.circular(). - * @param {Object} circular - * @param {Object} opts - * @return {undefined} - */ -module.exports.circular = function (circular, opts) { - const arr = circular.getArray(); - - if (opts.output === 'json') { - return process.stdout.write(toJSON(arr)); - } - - if (!arr.length) { - console.log(c('No circular dependencies found!', 'green', opts.colors)); - } else { - arr.forEach((path, idx) => { - path.forEach((module, idx) => { - if (idx) { - process.stdout.write(c(' -> ', 'cyan', opts.colors)); - } - process.stdout.write(c(module, 'red', opts.colors)); - }); - process.stdout.write('\n'); - }); - process.exit(1); - } -}; - -/** - * Print the result from Madge.depends(). - * @param {Object} modules - * @param {Object} opts - * @return {undefined} - */ -module.exports.depends = function (modules, opts) { - if (opts.output === 'json') { - return process.stdout.write(toJSON(modules)); - } - - modules.forEach((id) => { - console.log(c(id, 'grey', opts.colors)); - }); -}; diff --git a/lib/tree.js b/lib/tree.js new file mode 100644 index 00000000..c8683158 --- /dev/null +++ b/lib/tree.js @@ -0,0 +1,222 @@ +'use strict'; + +const fs = require('mz/fs'); +const path = require('path'); +const commondir = require('commondir'); +const walk = require('walkdir'); +const dependencyTree = require('dependency-tree'); +const log = require('./log'); + +class Tree { + /** + * Class constructor. + * @constructor + * @api public + * @param {Array} srcPaths + * @param {Object} config + * @return {Promise} + */ + constructor(srcPaths, config) { + this.srcPaths = srcPaths.map((s) => path.resolve(s)); + log('using src paths %o', this.srcPaths); + + this.config = config; + log('using config %o', this.config); + + return this.getDirs() + .then(this.setBaseDir.bind(this)) + .then(this.getFiles.bind(this)) + .then(this.generateTree.bind(this)); + } + + /** + * Generate the tree from the given files + * @param {Array} files + * @return {Object} + */ + generateTree(files) { + const depTree = {}; + const visited = {}; + + files.forEach((file) => { + if (visited[file]) { + return; + } + + Object.assign(depTree, dependencyTree({ + filename: file, + directory: this.baseDir, + requireConfig: this.config.requireConfig, + webpackConfig: this.config.webpackConfig, + visited: visited, + filter: this.filterPath.bind(this) + })); + }); + + let tree = this.flatten(depTree); + + if (this.config.excludeRegExp) { + tree = this.exclude(tree, this.config.excludeRegExp); + } + + tree = this.sort(tree); + + return tree; + } + + /** + * Filter out some paths from found files + * @param {String} path + * @return {Boolean} + */ + filterPath(path) { + return this.config.includeNpm || path.indexOf('node_modules') < 0; + } + + /** + * Get directories from the source paths + * @return {Promise} resolved with an array of directories + */ + getDirs() { + return Promise + .all(this.srcPaths.map((srcPath) => { + return fs + .stat(srcPath) + .then((stats) => stats.isDirectory() ? srcPath : path.dirname(path.resolve(srcPath))); + })); + } + + /** + * Get all files found from the source paths + * @return {Promise} resolved with an array of files + */ + getFiles() { + const files = []; + + return Promise + .all(this.srcPaths.map((srcPath) => { + return fs + .stat(srcPath) + .then((stats) => { + if (stats.isFile()) { + files.push(path.resolve(srcPath)); + return; + } + + walk.sync(srcPath, (filePath, stat) => { + if (!this.filterPath(filePath) || !stat.isFile()) { + return; + } + + const ext = path.extname(filePath).replace('.', ''); + + if (files.indexOf(filePath) < 0 && this.config.extensions.indexOf(ext) >= 0) { + files.push(filePath); + } + }); + }); + })) + .then(() => files); + } + + /** + * Set the base directory (compute the common one if multiple). + * @param {Array} dirs + */ + setBaseDir(dirs) { + if (this.config.baseDir) { + this.baseDir = path.resolve(this.config.baseDir); + } else { + this.baseDir = commondir(dirs); + } + + log('using base directory %s', this.baseDir); + } + + + /** + * Flatten deep tree produced by `dependency-tree`. + * @param {Object} deepTree + * @param {Object} [tree] + * @return {Object} + */ + flatten(deepTree, tree) { + tree = tree || {}; + + Object + .keys(deepTree) + .forEach((key) => { + const id = this.processPath(key); + + if (!tree[id]) { + tree[id] = Object + .keys(deepTree[key]) + .map((dep) => this.processPath(dep)); + } + + this.flatten(deepTree[key], tree); + }); + + return tree; + } + + /** + * Process path. + * @param {String} absPath + * @return {String} + */ + processPath(absPath) { + absPath = path.relative(this.baseDir, absPath); + + if (!this.config.showFileExtension) { + absPath = absPath.replace(/\.\w+$/, ''); + } + + return absPath; + } + + /** + * Exclude modules from tree using RegExp. + * @param {Object} tree + * @param {Array} excludeRegExp + * @return {Object} + */ + exclude(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 tree. + * @param {Object} tree + * @return {Object} + */ + sort(tree) { + return Object + .keys(tree) + .sort() + .reduce((acc, id) => { + acc[id] = tree[id].sort(); + return acc; + }, {}); + } +} + + /** + * Expose API. + * @param {Array} srcPaths + * @param {Object} config + * @return {Promise} + */ +module.exports = (srcPaths, config) => new Tree(srcPaths, config); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json deleted file mode 100644 index 200738e5..00000000 --- a/npm-shrinkwrap.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "name": "madge", - "version": "0.5.4", - "dependencies": { - "amdetective": { - "version": "0.2.1", - "from": "amdetective@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/amdetective/-/amdetective-0.2.1.tgz", - "dependencies": { - "esprima": { - "version": "2.7.2", - "from": "esprima@>=2.7.0 <2.8.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz" - } - } - }, - "coffee-script": { - "version": "1.10.0", - "from": "coffee-script@1.10.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz" - }, - "colors": { - "version": "1.1.2", - "from": "colors@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz" - }, - "commander": { - "version": "2.9.0", - "from": "commander@>=2.9.0 <3.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "dependencies": { - "graceful-readlink": { - "version": "1.0.1", - "from": "graceful-readlink@>=1.0.0", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" - } - } - }, - "commondir": { - "version": "1.0.1", - "from": "commondir@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" - }, - "detective": { - "version": "4.3.1", - "from": "detective@>=4.3.1 <5.0.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-4.3.1.tgz", - "dependencies": { - "acorn": { - "version": "1.2.2", - "from": "acorn@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz" - }, - "defined": { - "version": "1.0.0", - "from": "defined@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" - } - } - }, - "detective-es6": { - "version": "1.1.5", - "from": "detective-es6@1.1.5", - "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-1.1.5.tgz", - "dependencies": { - "node-source-walk": { - "version": "3.0.0", - "from": "node-source-walk@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-3.0.0.tgz", - "dependencies": { - "babylon": { - "version": "6.8.2", - "from": "babylon@>=6.8.1 <6.9.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.8.2.tgz", - "dependencies": { - "babel-runtime": { - "version": "6.9.2", - "from": "babel-runtime@>=6.0.0 <7.0.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.9.2.tgz", - "dependencies": { - "core-js": { - "version": "2.4.0", - "from": "core-js@>=2.4.0 <3.0.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.0.tgz" - }, - "regenerator-runtime": { - "version": "0.9.5", - "from": "regenerator-runtime@>=0.9.5 <0.10.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz" - } - } - } - } - }, - "object-assign": { - "version": "4.1.0", - "from": "object-assign@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz" - } - } - } - } - }, - "graphviz": { - "version": "0.0.8", - "from": "graphviz@0.0.8", - "resolved": "https://registry.npmjs.org/graphviz/-/graphviz-0.0.8.tgz", - "dependencies": { - "temp": { - "version": "0.4.0", - "from": "temp@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.4.0.tgz" - } - } - }, - "resolve": { - "version": "1.1.7", - "from": "resolve@>=1.1.7 <2.0.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz" - }, - "uglify-js": { - "version": "1.3.5", - "from": "uglify-js@>=1.3.5 <2.0.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.5.tgz" - }, - "walkdir": { - "version": "0.0.11", - "from": "walkdir@0.0.11", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz" - } - } -} diff --git a/package.json b/package.json index 368a39ec..a0cc0fb7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "repository": "git://github.com/pahen/madge", "homepage": "https://github.com/pahen/madge", "license": "MIT", - "description": "Create graphs from your CommonJS, AMD or ES6 module dependencies.", + "description": "Create graphs from module dependencies.", "keywords": [ "ES6", "ES7", @@ -22,37 +22,35 @@ "engines": { "node": ">=4.x.x" }, + "main": "./lib/api", + "bin": { + "madge": "./bin/cli.js" + }, "scripts": { - "test": "npm run lint && npm run mocha && npm run madge", + "test": "npm run lint && npm run mocha", "mocha": "mocha test/*.js", - "lint": "eslint bin/madge lib test/*.js", - "madge": "bin/madge -c -L ./lib", - "release": "npm test && release-it -n -i patch", - "release:minor": "npm run test && release-it -n -i minor", - "release:major": "npm run test && release-it -n -i major" + "watch": "mocha --watch --growl test/*.js", + "lint": "eslint bin/cli.js lib test/*.js", + "debug": "bin/cli.js --debug bin lib", + "generate": "npm run generate:small && npm run generate:madge", + "generate:small": "bin/cli.js --image /tmp/simple.svg test/files/cjs/circular/a.js", + "generate:madge": "bin/cli.js --image /tmp/madge.svg bin lib" }, "dependencies": { - "amdetective": "~0.2.1", - "coffee-script": "1.10.0", - "colors": "^1.1.2", + "chalk": "^1.1.3", "commander": "^2.9.0", "commondir": "^1.0.1", - "detective": "^4.3.1", - "detective-es6": "1.1.5", - "graphviz": "0.0.8", - "resolve": "^1.1.7", - "uglify-js": "^1.3.5", + "debug": "^2.2.0", + "dependency-tree": "^5.5.0", + "graphviz": "^0.0.8", + "mz": "^2.4.0", + "rc": "^1.1.6", "walkdir": "0.0.11" }, "devDependencies": { "@aptoma/eslint-config": "^4.0.0", "eslint": "^3.0.0", "mocha": "^2.3.3", - "should": "*", - "release-it": "^2.4.0" - }, - "main": "./lib/madge", - "bin": { - "madge": "./bin/madge" + "should": "^9.0.2" } } diff --git a/test/amd.js b/test/amd.js index 965b1476..a08f2f85 100644 --- a/test/amd.js +++ b/test/amd.js @@ -1,130 +1,93 @@ /* eslint-env mocha */ 'use strict'; +const madge = require('../lib/api'); require('should'); -const madge = require('../lib/madge'); -describe('module format (AMD)', () => { - - it('should behave as expected on ok files', () => { - madge([__dirname + '/files/amd/ok'], { - format: 'amd' - }).obj().should.eql({'a': ['sub/b'], 'd': [], 'e': ['sub/c'], 'sub/b': ['sub/c'], 'sub/c': ['d']}); - }); - - it('should handle optimized files', () => { - madge([__dirname + '/files/amd/a-built.js'], { - format: 'amd', - optimized: true - }).obj().should.eql({'a': ['sub/b'], 'd': [], 'sub/b': ['sub/c'], 'sub/c': ['d']}); - }); - - it('should handle optimized files originating with a `require` call', () => { - madge([__dirname + '/files/amd/b-built.js'], { - format: 'amd', - optimized: true - }).obj().should.eql({'': ['sub/b'], 'a': [], 'd': [], 'sub/b': ['sub/c'], 'sub/c': ['d']}); - }); - - it('should handle optimized files originating with a `require` call and a designated main module', () => { - madge([__dirname + '/files/amd/b-built.js'], { - format: 'amd', - optimized: true, - mainRequireModule: 'a' - }).obj().should.eql({'a': ['sub/b'], 'd': [], 'sub/b': ['sub/c'], 'sub/c': ['d']}); - }); - - it('should merge in shim dependencies found in RequireJS config', () => { - madge([__dirname + '/files/amd/requirejs/a.js'], { - format: 'amd', - requireConfig: __dirname + '/files/amd/requirejs/config.js' - }).obj().should.eql({'a': ['jquery'], 'jquery': [], 'jquery.foo': ['jquery'], 'jquery.bar': ['jquery'], 'baz': ['quux'], 'quux': []}); - }); - - it('should be able to exclude modules', () => { - madge([__dirname + '/files/amd/ok'], { - format: 'amd', - exclude: '^sub' - }).obj().should.eql({'a': [], 'd': [], 'e': []}); - - madge([__dirname + '/files/amd/ok'], { - format: 'amd', - exclude: '.*\/c$' - }).obj().should.eql({'a': ['sub/b'], 'd': [], 'e': [], 'sub/b': []}); - - madge([__dirname + '/files/amd/requirejs/a.js'], { - format: 'amd', - requireConfig: __dirname + '/files/amd/requirejs/config.js', - exclude: '^jquery.foo|quux$' - }).obj().should.eql({a: ['jquery'], 'jquery': [], 'jquery.bar': ['jquery'], 'baz': []}); - }); - - it('should tackle errors in files', () => { - madge([__dirname + '/files/amd/error.js'], { - format: 'amd' - }).obj().should.eql({error: []}); - }); - - it('should handle named modules', () => { - madge([__dirname + '/files/amd/namedWrapped/car.js'], { - format: 'amd' - }).obj().should.eql({'car': ['engine', 'wheels']}); - }); - - it('should find circular dependencies', () => { - madge([__dirname + '/files/amd/circular'], { - format: 'amd' - }).circular().getArray().should.eql([['a', 'c'], ['f', 'g', 'h']]); - }); - - it('should find circular dependencies with relative paths', () => { - madge([__dirname + '/files/amd/circularRelative'], { - format: 'amd' - }).circular().getArray().should.eql([['a', 'foo/b']]); - }); - - it('should find circular dependencies with alias', () => { - madge([__dirname + '/files/amd/circularAlias'], { - format: 'amd', - requireConfig: __dirname + '/files/amd/circularAlias/config.js' - }).circular().getArray().should.eql([['cpu', 'jsdos']]); - }); - - it('should find modules that depends on another', () => { - madge([__dirname + '/files/amd/ok'], { - format: 'amd' - }).depends('sub/c').should.eql(['e', 'sub/b']); - }); - - it('should compile coffeescript on-the-fly', () => { - madge([__dirname + '/files/amd/coffeescript'], { - format: 'amd' - }).obj().should.eql({'a': ['b'], 'b': []}); - }); - - it('should resolve relative module indentifiers', () => { - madge([__dirname + '/files/amd/relative'], { - format: 'amd' - }).obj().should.eql({'a': [], 'b': ['a'], 'foo/bar/d': ['a'], 'foo/c': ['a']}); - }); - - it('should ignore plugins', () => { - madge([__dirname + '/files/amd/plugin.js'], { - format: 'amd', - breakOnError: true - }).obj().should.eql({plugin: ['ok/a']}); - }); - - it('should find nested dependencies', () => { - madge([__dirname + '/files/amd/nested/main.js'], { - format: 'amd', - findNestedDependencies: true - }).obj().should.eql({'main': ['a', 'b']}); - }); - - it('should work for amd files with es6 code inside', () => { - madge([__dirname + '/files/amd/amdes6.js'], { - format: 'amd' - }).obj().should.eql({'amdes6': ['ok/a']}); +describe('AMD', () => { + const dir = __dirname + '/files/amd'; + + it('finds 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('ignores plugins', (done) => { + madge(dir + '/plugin.js').then((res) => { + res.obj().should.eql({ + 'plugin': ['ok/d'], + 'ok/d': [] + }); + done(); + }).catch(done); + }); + + it('finds nested dependencies', (done) => { + madge(dir + '/nested/main.js').then((res) => { + res.obj().should.eql({ + 'a': [], + 'b': [], + 'main': [ + 'a', + 'b' + ] + }); + done(); + }).catch(done); + }); + + it('finds circular dependencies', (done) => { + madge(dir + '/circular/main.js').then((res) => { + res.circular().should.eql([ + ['a', 'c'], + ['f', 'g', 'h'] + ]); + done(); + }).catch(done); + }); + + it('finds circular dependencies with relative paths', (done) => { + madge(dir + '/circularRelative/a.js').then((res) => { + res.circular().should.eql([['a', 'foo/b']]); + done(); + }).catch(done); + }); + + it('finds circular dependencies with alias', (done) => { + madge(dir + '/circularAlias/dos.js', { + requireConfig: dir + '/circularAlias/config.js' + }).then((res) => { + res.circular().should.eql([['dos', 'x86']]); + done(); + }).catch(done); + }); + + it('works 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('uses paths found in RequireJS config', (done) => { + madge(dir + '/requirejs/a.js', { + requireConfig: dir + '/requirejs/config.js' + }).then((res) => { + res.obj().should.eql({ + 'a': ['vendor/jquery-2.0.3'], + 'vendor/jquery-2.0.3': [] + }); + done(); + }).catch(done); }); }); diff --git a/test/api.js b/test/api.js new file mode 100644 index 00000000..af3ae414 --- /dev/null +++ b/test/api.js @@ -0,0 +1,176 @@ +/* eslint-env mocha */ +'use strict'; + +const os = require('os'); +const path = require('path'); +const fs = require('mz/fs'); +const madge = require('../lib/api'); + +require('should'); + +describe('Madge', () => { + it('throws error on missing path argument', () => { + (() => { + madge(); + }).should.throw('path argument not provided'); + }); + + it('returns a Promise', () => { + madge(__dirname + '/files/cjs/a.js').should.be.Promise(); // eslint-disable-line new-cap + }); + + it('throws error if file or directory does not exists', (done) => { + madge(__dirname + '/missing.js').catch((err) => { + err.message.should.match(/no such file or directory/); + done(); + }).catch(done); + }); + + it('takes single file as path', (done) => { + madge(__dirname + '/files/cjs/a.js').then((res) => { + res.obj().should.eql({ + 'a': ['b', 'c'], + 'b': ['c'], + 'c': [] + }); + done(); + }).catch(done); + }); + + it('takes an array of files as path and combines the result', (done) => { + madge([__dirname + '/files/cjs/a.js', __dirname + '/files/cjs/normal/d.js']).then((res) => { + res.obj().should.eql({ + 'a': ['b', 'c'], + 'b': ['c'], + 'c': [], + 'normal/d': [] + }); + done(); + }).catch(done); + }); + + it('take a single directory as path and find files in it', (done) => { + madge(__dirname + '/files/cjs/normal').then((res) => { + res.obj().should.eql({ + 'a': ['sub/b'], + 'd': [], + 'sub/b': ['sub/c'], + 'sub/c': ['d'] + }); + done(); + }).catch(done); + }); + + it('takes an array of directories as path and compute the basedir correctly', (done) => { + madge([__dirname + '/files/cjs/multibase/1', __dirname + '/files/cjs/multibase/2']).then((res) => { + res.obj().should.eql({ + '1/a': [], + '2/b': [] + }); + done(); + }).catch(done); + }); + + it('can exclude modules using RegExp', (done) => { + madge(__dirname + '/files/cjs/a.js', { + excludeRegExp: ['^b$'] + }).then((res) => { + res.obj().should.eql({ + a: ['c'], + c: [] + }); + done(); + }).catch(done); + }); + + describe('#obj', () => { + it('returns 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('returns a promise resolved with graphviz DOT output', (done) => { + madge(__dirname + '/files/cjs/b.js') + .then((res) => res.dot()) + .then((output) => { + output.should.eql('digraph G {\n "b";\n "c";\n "b" -> "c";\n}\n'); + done(); + }) + .catch(done); + }); + }); + + describe('#depends', () => { + it('returns 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', () => { + let imagePath; + + beforeEach(() => { + imagePath = path.join(os.tmpdir(), 'madge_' + Date.now() + '_image.png'); + }); + + afterEach(() => { + fs.unlink(imagePath); + }); + + it('rejects if a filename is not supplied', (done) => { + madge(__dirname + '/files/cjs/a.js') + .then((res) => res.image()) + .catch((err) => { + err.message.should.eql('imagePath not provided'); + done(); + }); + }); + + it('rejects on unsupported image format', (done) => { + madge(__dirname + '/files/cjs/a.js') + .then((res) => res.image('image.zyx')) + .catch((err) => { + err.message.should.match(/Format: "zyx" not recognized/); + done(); + }); + }); + + it('rejects if graphviz is not installed', (done) => { + madge(__dirname + '/files/cjs/a.js', {graphVizPath: '/invalid/path'}) + .then((res) => res.image('image.png')) + .catch((err) => { + err.message.should.eql('Could not execute /invalid/path/gvpr -V'); + done(); + }); + }); + + it('writes image to file', (done) => { + madge(__dirname + '/files/cjs/a.js') + .then((res) => res.image(imagePath)) + .then((writtenImagePath) => { + writtenImagePath.should.eql(imagePath); + + return fs + .exists(imagePath) + .then((exists) => { + if (!exists) { + throw new Error(imagePath + ' not created'); + } + done(); + }); + }) + .catch(done); + }); + }); +}); diff --git a/test/cjs.js b/test/cjs.js index cbbc716f..d280f132 100644 --- a/test/cjs.js +++ b/test/cjs.js @@ -1,61 +1,87 @@ /* eslint-env mocha */ 'use strict'; -const madge = require('../lib/madge'); +const madge = require('../lib/api'); require('should'); -describe('module format (CommonJS)', () => { +describe('CommonJS', () => { + const dir = __dirname + '/files/cjs'; - it('should behave as expected on ok files', () => { - madge([__dirname + '/files/cjs/normal']) - .obj().should.eql({'a': ['sub/b'], 'fancy-main/not-index': [], 'd': [], 'sub/b': ['sub/c'], 'sub/c': ['d']}); + it('finds 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 expressions in require call', () => { - madge([__dirname + '/files/cjs/both.js']) - .obj().should.eql({'both': ['node_modules/a', 'node_modules/b']}); + it('handles path outside directory', (done) => { + madge(dir + '/normal/sub/c.js').then((res) => { + res.obj().should.eql({ + '../d': [], + 'c': ['../d'] + }); + done(); + }).catch(done); }); - it('should handle require call and chained functions', () => { - madge([__dirname + '/files/cjs/chained.js']) - .obj().should.eql({'chained': ['node_modules/a', 'node_modules/b', 'node_modules/c']}); + it('finds circular dependencies', (done) => { + madge(dir + '/circular/a.js').then((res) => { + res.circular().should.eql([ + ['a', 'd'] + ]); + done(); + }).catch(done); }); - it('should handle nested require call', () => { - madge([__dirname + '/files/cjs/nested.js']) - .obj().should.eql({'nested': ['node_modules/a', 'node_modules/b', 'node_modules/c']}); + it('excludes core modules by default', (done) => { + madge(dir + '/core.js').then((res) => { + res.obj().should.eql({ + 'core': [] + }); + done(); + }).catch(done); }); - it('should handle strings in require call', () => { - madge([__dirname + '/files/cjs/strings.js']) - .obj().should.eql({strings: [ - 'events', 'node_modules/a', 'node_modules/b', 'node_modules/c', - 'node_modules/doom', 'node_modules/events2', 'node_modules/y' - ]}); + it('excludes 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 tackle errors in files', () => { - madge([__dirname + '/files/cjs/error.js']) - .obj().should.eql({'error': []}); + it('can include NPM modules', (done) => { + madge(dir + '/npm.js', { + includeNpm: true + }).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 exclude modules', () => { - madge([__dirname + '/files/cjs/normal'], { - exclude: '^sub' - }).obj().should.eql({'a': [], 'd': [], 'fancy-main/not-index': []}); - - madge([__dirname + '/files/cjs/normal'], { - exclude: '.*\/c$' - }).obj().should.eql({'a': ['sub/b'], 'd': [], 'sub/b': [], 'fancy-main/not-index': []}); - }); - - it('should find circular dependencies', () => { - madge([__dirname + '/files/cjs/circular']) - .circular().getArray().should.eql([['a', 'b', 'c']]); + it('can show file extensions', (done) => { + madge(dir + '/normal/a.js', { + showFileExtension: true + }).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); }); - it('should compile coffeescript on-the-fly', () => { - madge([__dirname + '/files/cjs/coffeescript']) - .obj().should.eql({'a': ['./b'], 'b': []}); - }); }); diff --git a/test/es6.js b/test/es6.js index ca22c89a..1892ffc0 100644 --- a/test/es6.js +++ b/test/es6.js @@ -1,71 +1,84 @@ /* eslint-env mocha */ 'use strict'; -const madge = require('../lib/madge'); +const madge = require('../lib/api'); require('should'); -describe('module format (ES6)', () => { +describe('ES6', () => { + const dir = __dirname + '/files/es6'; - it('should behave as expected on ok files', () => { - 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('finds 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([__dirname + '/files/es6/error.js'], { - format: 'es6' - }).obj().should.eql({'error': []}); + it('tackles error in files', (done) => { + madge(dir + '/error.js').then((res) => { + res.obj().should.eql({ + 'error': [] + }); + done(); + }).catch(done); }); - it('should be able to exclude modules', () => { - 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', () => { - madge([__dirname + '/files/es6/circular'], { - format: 'es6' - }).circular().getArray().should.eql([['a', 'b', 'c']]); - }); - - it('should find absolute imports from the root', () => { - madge([__dirname + '/files/es6/absolute.js', __dirname + '/files/es6/absolute'], { - format: 'es6' - }).obj().should.eql({'absolute': ['absolute/a'], 'absolute/a': ['absolute/b'], 'absolute/b': []}); + it('finds 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 jsx', () => { - madge([__dirname + '/files/es6/jsx.js'], { - format: 'es6' - }).obj().should.eql({'jsx': ['absolute/b']}); + it('finds 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 find imports on files with ES7', () => { - madge([__dirname + '/files/es6/async.js'], { - format: 'es6' - }).obj().should.eql({'async': ['absolute/b']}); + it('supports 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 support export x from "./file"', () => { - madge([__dirname + '/files/es6/re-export'], { - format: 'es6' - }).obj().should.eql({'a': [], 'b-default': ['a'], 'b-named': ['a'], 'b-star': ['a'], 'c': ['b-default', 'b-named', 'b-star']}); + it('finds 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('can detect imports in JSX files', () => { - madge([__dirname + '/files/es6/jsx/basic.jsx'], { - format: 'es6' - }).obj().should.eql({basic: [ - '../../../../other', - '../../../../react' - ]}); + it('finds import in JSX files', (done) => { + madge(dir + '/jsx/basic.jsx').then((res) => { + res.obj().should.eql({ + 'basic': ['other'], + 'other': [] + }); + done(); + }).catch(done); }); }); diff --git a/test/files/amd/amdes6.js b/test/files/amd/amdes6.js index d981c2c1..40c07164 100644 --- a/test/files/amd/amdes6.js +++ b/test/files/amd/amdes6.js @@ -1,5 +1,5 @@ define([ - 'ok/a' + 'ok/d' ], function(a) { 'use strict'; diff --git a/test/files/amd/circular/main.js b/test/files/amd/circular/main.js new file mode 100644 index 00000000..928b1077 --- /dev/null +++ b/test/files/amd/circular/main.js @@ -0,0 +1,3 @@ +define(['a', 'f'], function () { + return 'MAIN'; +}); \ No newline at end of file diff --git a/test/files/amd/circularRelative/a.coffee b/test/files/amd/circularRelative/a.coffee deleted file mode 100644 index 70388131..00000000 --- a/test/files/amd/circularRelative/a.coffee +++ /dev/null @@ -1 +0,0 @@ -define 'a', ['./foo/b'], -> \ No newline at end of file diff --git a/test/files/amd/circularRelative/a.js b/test/files/amd/circularRelative/a.js new file mode 100644 index 00000000..2af72e2b --- /dev/null +++ b/test/files/amd/circularRelative/a.js @@ -0,0 +1,3 @@ +define('a', ['./foo/b'], function () { + return 'A'; +}); \ No newline at end of file diff --git a/test/files/amd/circularRelative/foo/b.coffee b/test/files/amd/circularRelative/foo/b.coffee deleted file mode 100644 index a6e3778e..00000000 --- a/test/files/amd/circularRelative/foo/b.coffee +++ /dev/null @@ -1 +0,0 @@ -define 'foo/b', ['a'], -> \ No newline at end of file diff --git a/test/files/amd/circularRelative/foo/b.js b/test/files/amd/circularRelative/foo/b.js new file mode 100644 index 00000000..1ca79c69 --- /dev/null +++ b/test/files/amd/circularRelative/foo/b.js @@ -0,0 +1,3 @@ +define('foo/b', ['../a'], function () { + return 'B'; +}); \ No newline at end of file diff --git a/test/files/amd/plugin.js b/test/files/amd/plugin.js index 8d557c96..86e6fdf7 100644 --- a/test/files/amd/plugin.js +++ b/test/files/amd/plugin.js @@ -1,3 +1,3 @@ -define(['ok/a', './locale!locales/not/exists'], function (D) { +define(['ok/d', './locale!locales/not/exists'], function (D) { return 'p'; }); \ No newline at end of file diff --git a/test/files/amd/relative/a.js b/test/files/amd/relative/a.js deleted file mode 100644 index 471690b0..00000000 --- a/test/files/amd/relative/a.js +++ /dev/null @@ -1,3 +0,0 @@ -define(function () { - return 'A'; -}); \ No newline at end of file diff --git a/test/files/amd/relative/b.js b/test/files/amd/relative/b.js deleted file mode 100644 index dd71ff83..00000000 --- a/test/files/amd/relative/b.js +++ /dev/null @@ -1,3 +0,0 @@ -define(['./a'], function (A) { - return 'B'; -}); \ No newline at end of file diff --git a/test/files/amd/relative/foo/bar/d.js b/test/files/amd/relative/foo/bar/d.js deleted file mode 100644 index ea8cd93c..00000000 --- a/test/files/amd/relative/foo/bar/d.js +++ /dev/null @@ -1,3 +0,0 @@ -define(['../../a'], function (A) { - return 'D'; -}); \ No newline at end of file diff --git a/test/files/amd/relative/foo/c.js b/test/files/amd/relative/foo/c.js deleted file mode 100644 index 9c651c76..00000000 --- a/test/files/amd/relative/foo/c.js +++ /dev/null @@ -1,3 +0,0 @@ -define(['../a'], function (A) { - return 'C'; -}); \ No newline at end of file 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/files/cjs/circular/a.js b/test/files/cjs/circular/a.js index f8a21a13..84c3d5e4 100644 --- a/test/files/cjs/circular/a.js +++ b/test/files/cjs/circular/a.js @@ -1 +1,3 @@ -var b = require('./b'); \ No newline at end of file +var b = require('./b'); +var c = require('./c'); +var d = require('./d'); \ No newline at end of file diff --git a/test/files/cjs/circular/c.js b/test/files/cjs/circular/c.js index 63443cd1..e69de29b 100644 --- a/test/files/cjs/circular/c.js +++ b/test/files/cjs/circular/c.js @@ -1 +0,0 @@ -var a = require('./a'); \ No newline at end of file diff --git a/test/files/cjs/circular/d.js b/test/files/cjs/circular/d.js new file mode 100644 index 00000000..63443cd1 --- /dev/null +++ b/test/files/cjs/circular/d.js @@ -0,0 +1 @@ +var a = require('./a'); \ No newline at end of file diff --git a/test/files/cjs/core.js b/test/files/cjs/core.js new file mode 100644 index 00000000..48f6d7b9 --- /dev/null +++ b/test/files/cjs/core.js @@ -0,0 +1,2 @@ +var fs = require('fs'); +var a = require('a'); \ No newline at end of file diff --git a/test/files/cjs/normal/fancy-main/not-index.js b/test/files/cjs/normal/fancy-main/not-index.js deleted file mode 100644 index 406504df..00000000 --- a/test/files/cjs/normal/fancy-main/not-index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = new Date(); \ No newline at end of file diff --git a/test/files/cjs/normal/fancy-main/package.json b/test/files/cjs/normal/fancy-main/package.json deleted file mode 100644 index 817387e7..00000000 --- a/test/files/cjs/normal/fancy-main/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "main": "not-index.js" -} \ No newline at end of file diff --git a/test/files/cjs/npm.js b/test/files/cjs/npm.js new file mode 100644 index 00000000..4a6d2286 --- /dev/null +++ b/test/files/cjs/npm.js @@ -0,0 +1,2 @@ +var a = require('a'); +var d = require('./normal/d'); \ No newline at end of file diff --git a/test/files/es6/absolute.js b/test/files/es6/absolute.js index 317bafc6..f1a71933 100644 --- a/test/files/es6/absolute.js +++ b/test/files/es6/absolute.js @@ -1 +1 @@ -import {A} from 'test/files/es6/absolute/a'; \ No newline at end of file +import {A} from 'absolute/a'; \ No newline at end of file diff --git a/test/files/es6/async.js b/test/files/es6/async.js index 4b22eaeb..9f5637e0 100644 --- a/test/files/es6/async.js +++ b/test/files/es6/async.js @@ -1,2 +1,2 @@ -import {B} from 'test/files/es6/absolute/b'; +import {B} from 'absolute/b'; async function foo() {} diff --git a/test/files/es6/jsx.js b/test/files/es6/jsx.js index d5235b45..b2662a0e 100644 --- a/test/files/es6/jsx.js +++ b/test/files/es6/jsx.js @@ -1,2 +1,2 @@ -import {B} from 'test/files/es6/absolute/b'; +import {B} from 'absolute/b'; var templ = ; diff --git a/test/madge.js b/test/madge.js deleted file mode 100644 index 430b1f33..00000000 --- a/test/madge.js +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-env mocha */ -'use strict'; - -const madge = require('../lib/madge'); -require('should'); - -describe('Madge', () => { - describe('source argument', () => { - it('should handle a string as argument', () => { - madge('test/files/cjs/normal/a.js').obj().should.eql({a: ['sub/b']}); - }); - - it('should handle an array as argument', () => { - madge(['test/files/cjs/normal/a.js']).obj().should.eql({a: ['sub/b']}); - }); - - it('should handle a object as argument', () => { - madge({ - a: ['b', 'c'], - b: ['c'], - c: [] - }).obj().should.eql({a: ['b', 'c'], b: ['c'], c: []}); - }); - }); - - describe('paths', () => { - it('should be ok with relative paths', () => { - madge(['test/files/cjs/normal/a.js']).obj().should.eql({a: ['sub/b']}); - }); - }); - - describe('basedir', () => { - it('should use common dir when given multiple paths (cjs)', () => { - madge([__dirname + '/files/cjs/multibase/1', __dirname + '/files/cjs/multibase/2']).obj().should.eql({'1/a': [], '2/b': []}); - }); - - it('should use common dir when given multiple paths (amd)', () => { - madge([__dirname + '/files/amd/multibase/foo', __dirname + '/files/amd/multibase/bar'], { - format: 'amd' - }).obj().should.eql({'foo/a': [], 'bar/b': []}); - }); - }); - - describe('extensions', () => { - it('should be ok with custom extensions', () => { - madge(['test/files/cjs/extensions'], {extensions: ['.js', '.cjs']}).obj().should.eql({a: ['b'], b: []}); - }); - }); - - describe('.tree', () => { - it('should accessible as an object', () => { - madge({a: ['b', 'c']}).tree.should.be.an.Object; - }); - }); - - describe('#obj', () => { - it('should return tree as object with dependencies as arrays', () => { - madge({a: ['b', 'c']}).obj().should.eql({a: ['b', 'c']}); - }); - }); - - 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'); - }); - }); -}); diff --git a/test/pluggable.js b/test/pluggable.js deleted file mode 100644 index e2e794fd..00000000 --- a/test/pluggable.js +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-env mocha */ -'use strict'; - -const madge = require('../lib/madge'); -require('should'); - -describe('Madge', () => { - describe('pluggable', () => { - it('should serve parseFile and addModule events for cjs', () => { - let fileAdd = ''; - let idAdd = ''; - const opts = {}; - opts.onParseFile = (obj) => { - const arr = obj.filename.split('/'); - fileAdd += arr[arr.length - 1]; - }; - opts.onAddModule = (obj) => { - idAdd += obj.id; - }; - madge([__dirname + '/files/cjs/normal'], opts); - (fileAdd + idAdd).should.eql('a.jsd.jsnot-index.jsb.jsc.js' + 'adfancy-main/not-indexsub/bsub/c'); - }); - }); - - describe('pluggable - amd', () => { - it('should serve parseFile and addModule events for amd', () => { - let fileAdd = ''; - let idAdd = ''; - const opts = {}; - opts.onParseFile = (obj) => { - const arr = obj.filename.split('/'); - fileAdd += arr[arr.length - 1]; - }; - opts.onAddModule = (obj) => { - idAdd += obj.id; - }; - opts.format = 'amd'; - madge([__dirname + '/files/amd/ok'], opts); - (idAdd + fileAdd).should.eql('adesub/bsub/c' + 'a.jsd.jse.jsb.jsc.js'); - }); - }); - - describe('pluggable - scope', () => { - it('should add idAdd property to the returned madger', () => { - const opts = {}; - opts.onAddModule = function (obj) { - if (this.idAdd) { - this.idAdd += obj.id; - } else { - this.idAdd = obj.id; - } - }; - const madger = madge([__dirname + '/files/cjs/normal'], opts); - madger.idAdd.should.eql('adfancy-main/not-indexsub/bsub/c'); - }); - }); -});