diff --git a/README.md b/README.md index 30b8f880..4c1f64be 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,7 @@ Property | Type | Default | Description `requireConfig` | String | null | RequireJS config for resolving aliased modules `webpackConfig` | String | null | Webpack config for resolving aliased modules `tsConfig` | String\|Object | null | TypeScript config for resolving aliased modules - Either a path to a tsconfig file or an object containing the config +`depth` | Number | null | Maximum dependency depth from source files to display `layout` | String | dot | Layout to use in the graph `rankdir` | String | LR | Sets the [direction](https://graphviz.gitlab.io/_pages/doc/info/attrs.html#d:rankdir) of the graph layout `fontName` | String | Arial | Font name to use in the graph diff --git a/bin/cli.js b/bin/cli.js index 99b53370..c57e1db3 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -32,6 +32,7 @@ program .option('--require-config ', 'path to RequireJS config') .option('--webpack-config ', 'path to webpack config') .option('--ts-config ', 'path to typescript config') + .option('--depth ', 'maximum depth from source files to draw') .option('--include-npm', 'include shallow NPM modules', false) .option('--no-color', 'disable color in output and image', false) .option('--no-spinner', 'disable progress spinner', false) @@ -113,6 +114,17 @@ if (program.tsConfig) { config.tsConfig = program.tsConfig; } +if (program.depth) { + config.depth = Number(program.depth); +} + +if (config.depth) { + if (!Number.isInteger(config.depth) || config.depth < 0) { + console.log('%s %s', chalk.red('✖'), 'Invalid depth'); + process.exit(1); + } +} + if (program.includeNpm) { config.includeNpm = program.includeNpm; } diff --git a/lib/api.js b/lib/api.js index 26773624..c7e5c826 100644 --- a/lib/api.js +++ b/lib/api.js @@ -14,6 +14,7 @@ const defaultConfig = { requireConfig: null, webpackConfig: null, tsConfig: null, + depth: null, rankdir: 'LR', layout: 'dot', fontName: 'Arial', diff --git a/lib/tree.js b/lib/tree.js index 2db6de2a..0298460c 100644 --- a/lib/tree.js +++ b/lib/tree.js @@ -114,10 +114,6 @@ class Tree { const pathCache = {}; files.forEach((file) => { - if (visited[file]) { - return; - } - Object.assign(depTree, dependencyTree({ filename: file, directory: this.baseDir, @@ -148,7 +144,7 @@ class Tree { })); }); - let tree = this.convertTree(depTree, {}, pathCache, npmPaths); + let tree = this.convertTree(visited, depTree, this.config.depth); for (const npmKey in npmPaths) { const id = this.processPath(npmKey, pathCache); @@ -171,27 +167,56 @@ class Tree { /** * Convert deep tree produced by dependency-tree to a * shallow (one level deep) tree used by madge. - * @param {Object} depTree + * @param {Object} modules * @param {Object} tree - * @param {Object} pathCache + * @param {number} [depthLimit] * @return {Object} */ - convertTree(depTree, tree, pathCache) { - for (const key in depTree) { - const id = this.processPath(key, pathCache); - - if (!tree[id]) { - tree[id] = []; - - for (const dep in depTree[key]) { - tree[id].push(this.processPath(dep, pathCache)); + convertTree(modules, tree, depthLimit) { + const self = this; + const depths = {}; + const deepDependencies = {}; + + function calculateDepths(tree, depth) { + if (depth <= depthLimit) { + for (const dependency in tree) { + depths[dependency] = true; + calculateDepths(modules[dependency], depth + 1); } + } + } + + function getDeepDependencies(dependency) { + if (deepDependencies[dependency] === null) { + return []; + } - this.convertTree(depTree[key], tree, pathCache); + if (!(dependency in deepDependencies)) { + deepDependencies[dependency] = null; + deepDependencies[dependency] = [...new Set(Object.keys(modules[dependency]).flatMap( + (dependency) => dependency in depths ? [dependency] : getDeepDependencies(dependency) + ))]; } + + return deepDependencies[dependency]; + } + + const pathCache = {}; + const result = {}; + + if (!Number.isInteger(depthLimit)) { + Object.entries(modules).forEach(([module, dependencies]) => { + result[self.processPath(module, pathCache)] = Object.keys(dependencies).map((dependency) => self.processPath(dependency, pathCache)); + }); + } else { + calculateDepths(tree, 0); + + Object.keys(depths).forEach((module) => { + result[self.processPath(module, pathCache)] = getDeepDependencies(module).map((dependency) => self.processPath(dependency, pathCache)); + }); } - return tree; + return result; } /** diff --git a/test/api.js b/test/api.js index 2afd3f74..77da00d7 100644 --- a/test/api.js +++ b/test/api.js @@ -336,4 +336,52 @@ describe('API', () => { .catch(done); }); }); + + describe('output depth limit', () => { + const baseDir = path.join(__dirname, 'deep-dependencies'); + const entryFile = path.join(baseDir, 'a.js'); + + it('limits the output depth to 0', async () => { + const res = await madge(entryFile, {depth: 0}); + + res.obj().should.eql({ + 'a.js': [] + }); + }); + + it('limits the output depth to 1', async () => { + const res = await madge(entryFile, {depth: 1}); + + res.obj().should.eql({ + 'a.js': ['b.js', 'c.js'], + 'b.js': [], + 'c.js': ['b.js', 'c.js'] + }); + }); + + it('limits the output depth to 2', async () => { + const res = await madge(entryFile, {depth: 2}); + + res.obj().should.eql({ + 'a.js': ['b.js', 'c.js'], + 'b.js': [], + 'c.js': ['d.js', 'e.js'], + 'd.js': [], + 'e.js': ['b.js', 'c.js'] + }); + }); + + it('limits the output depth to 3', async () => { + const res = await madge(entryFile, {depth: 3}); + + res.obj().should.eql({ + 'a.js': ['b.js', 'c.js'], + 'b.js': [], + 'c.js': ['d.js', 'e.js'], + 'd.js': [], + 'e.js': ['f.js'], + 'f.js': ['b.js', 'c.js'] + }); + }); + }); }); diff --git a/test/deep-dependencies/a.js b/test/deep-dependencies/a.js new file mode 100644 index 00000000..ebd87797 --- /dev/null +++ b/test/deep-dependencies/a.js @@ -0,0 +1,2 @@ +import 'b.js'; +import 'c.js'; diff --git a/test/deep-dependencies/b.js b/test/deep-dependencies/b.js new file mode 100644 index 00000000..e69de29b diff --git a/test/deep-dependencies/c.js b/test/deep-dependencies/c.js new file mode 100644 index 00000000..bf5025aa --- /dev/null +++ b/test/deep-dependencies/c.js @@ -0,0 +1,2 @@ +import 'd.js'; +import 'e.js'; diff --git a/test/deep-dependencies/d.js b/test/deep-dependencies/d.js new file mode 100644 index 00000000..e69de29b diff --git a/test/deep-dependencies/e.js b/test/deep-dependencies/e.js new file mode 100644 index 00000000..67494ad9 --- /dev/null +++ b/test/deep-dependencies/e.js @@ -0,0 +1 @@ +import 'f.js'; diff --git a/test/deep-dependencies/f.js b/test/deep-dependencies/f.js new file mode 100644 index 00000000..0e4f1086 --- /dev/null +++ b/test/deep-dependencies/f.js @@ -0,0 +1,2 @@ +import 'c.js'; +import 'g.js'; diff --git a/test/deep-dependencies/g.js b/test/deep-dependencies/g.js new file mode 100644 index 00000000..6d3f9dfd --- /dev/null +++ b/test/deep-dependencies/g.js @@ -0,0 +1 @@ +import 'b.js';