diff --git a/README.md b/README.md index 902e39a5..1619e9d2 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 86b67dd7..220fcb0e 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) @@ -112,6 +113,19 @@ if (program.tsConfig) { config.tsConfig = program.tsConfig; } +if ('depth' in program) { + config.depth = program.depth; +} + +if ('depth' in config) { + config.depth = Number(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..82fdc9bf 100644 --- a/lib/tree.js +++ b/lib/tree.js @@ -107,24 +107,19 @@ class Tree { * @return {Object} */ generateTree(files) { - const depTree = {}; - const visited = {}; + const modules = {}; const nonExistent = []; const npmPaths = {}; const pathCache = {}; files.forEach((file) => { - if (visited[file]) { - return; - } - - Object.assign(depTree, dependencyTree({ + dependencyTree({ filename: file, directory: this.baseDir, requireConfig: this.config.requireConfig, webpackConfig: this.config.webpackConfig, tsConfig: this.config.tsConfig, - visited: visited, + visited: modules, filter: (dependencyFilePath, traversedFilePath) => { let dependencyFilterRes = true; const isNpmPath = this.isNpmPath(dependencyFilePath); @@ -145,10 +140,10 @@ class Tree { }, detective: this.config.detectiveOptions, nonExistent: nonExistent - })); + }); }); - let tree = this.convertTree(depTree, {}, pathCache, npmPaths); + let tree = this.convertTree(modules, files, this.config.depth); for (const npmKey in npmPaths) { const id = this.processPath(npmKey, pathCache); @@ -171,27 +166,57 @@ 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 (let i = 0; i < tree.length; i++) { + const dependency = tree[i]; + depths[dependency] = true; + calculateDepths(Object.keys(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; } /**