From 270ac16f696f81a3d5496f5d2bf4e9b08a09f5e7 Mon Sep 17 00:00:00 2001 From: tkohlman Date: Wed, 18 Feb 2015 12:34:56 -0500 Subject: [PATCH] Return a tree object instead of a flat list and provide functions to produce pre-order and post-order traversals of the tree. The pre-order traversal yields the same output as the old behavior: a flat list of visited files without duplicates. --- README.md | 36 ++++++++-- bin/cli.js | 12 +++- index.js | 91 +++++++++++++++++++++++-- test/test.js | 185 +++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 283 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 1bda750..a6822eb 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,35 @@ ### dependency-tree [![npm](http://img.shields.io/npm/v/dependency-tree.svg)](https://npmjs.org/package/dependency-tree) [![npm](http://img.shields.io/npm/dm/dependency-tree.svg)](https://npmjs.org/package/dependency-tree) -> Get the dependency tree of a module (as a list) +> Get the dependency tree of a module `npm install dependency-tree` ### Usage ```js -var getTreeAsList = require('dependency-tree'); +var dependencyTree = require('dependency-tree'); -// Returns a list of filepaths for all visited dependencies -var tree = getTreeAsList('path/to/a/file', 'path/to/all/js/files'); +// Returns a dependency tree for the given file +var tree = dependencyTree('path/to/a/file', 'path/to/all/js/files'); + +// Returns a pre-order traversal of the tree with duplicate sub-trees pruned. +var preOrderList = dependencyTree.traversePreOrder(tree); + +// Returns a post-order traversal of the tree with duplicate sub-trees pruned. +// This is useful for bundling source files, because the list gives the +// concatenation order. +var postOrderList = dependencyTree.traversePostOrder(tree); ``` -Returns the entire dependency tree as a **flat** list of filepaths for a given module. -Basically, all files visited during traversal of the dependency-tree are returned. +Returns the entire dependency tree as an object containing the absolute path of the entry file (tree.root) and a mapping from each processed file to its direct dependencies (tree.nodes). For example, the following yields the direct dependencies (child, but not grand-child dependencies) of the root file: + +```js +var dependencyTree = require('dependency-tree'); + +var tree = dependencyTree('path/to/a/file', 'path/to/all/js/files'); + +var rootDependencies = tree.nodes[tree.root]; +``` * All core Node modules (assert, path, fs, etc) are removed from the dependency list by default * Works for AMD, CommonJS, ES6 modules and SASS files. @@ -31,10 +46,17 @@ used for avoiding redundant subtree generations. tree filename root ``` -Prints +Prints the pre-order and post-order traversals of the dependency tree ``` +Pre-Order: /a.js /b.js +/c.js + +Post-Order: +/b.js +/c.js +/a.js ``` diff --git a/bin/cli.js b/bin/cli.js index 9828277..4cc9427 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -2,12 +2,18 @@ 'use strict'; -var treeAsList = require('../'); +var dependencyTree = require('../'); var filename = process.argv[2]; var root = process.argv[3]; -var tree = treeAsList(filename, root); +var tree = dependencyTree(filename, root); -tree.forEach(function(node) { +console.log('Pre-Order:'); +dependencyTree.traversePreOrder(tree).forEach(function(node) { + console.log(node); +}); + +console.log('\nPost-Order:'); +dependencyTree.traversePostOrder(tree).forEach(function(node) { console.log(node); }); diff --git a/index.js b/index.js index c809fee..a73c1e2 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,9 @@ var path = require('path'); var fs = require('fs'); var resolveDependencyPath = require('resolve-dependency-path'); +var PRE_ORDER = 1; +var POST_ORDER = 2; + /** * Recursively find all dependencies (avoiding circular) traversing the entire dependency tree * and returns a flat list of all unique, visited nodes @@ -30,10 +33,89 @@ module.exports = function(filename, root, visited) { var results = traverse(filename, root, visited); results = removeDups(results); + var tree = { + root: filename, + nodes: visited + }; + return tree; +}; + +/** + * Executes a pre-order depth first search on the dependency tree and returns a + * list of absolute file paths. The order of files in the list will be the order + * in which the module processed files as it built the tree. The root (entry + * point) file will be first, followed by the root file's first dependency and + * the first dependency's dependencies. The list will not contain duplicates. + * + * @param {Object} tree - Tree object produced by this module. + */ +module.exports.traversePreOrder = function(tree) { + if (!tree) { throw new Error('tree not given'); } + if (!tree.root) { throw new Error('Tree object is missing root'); } + if (!tree.nodes) { throw new Error('Tree object is missing nodes'); } + + return traverseTree(tree.root, tree.nodes, {}, PRE_ORDER); +}; - return results; +/** + * Executes a post-order depth first search on the dependency tree and returns a + * list of absolute file paths. The order of files in the list will be the + * proper concatenation order for bundling. In other words, for any file in the + * list, all of that file's dependencies (direct or indirect) will appear at + * lower indeces in the list. The root (entry point) file will therefore appear + * last. The list will not contain duplicates. + * + * @param {Object} tree - Tree object produced by this module. + */ +module.exports.traversePostOrder = function(tree) { + if (!tree) { throw new Error('tree not given'); } + if (!tree.root) { throw new Error('Tree object is missing root'); } + if (!tree.nodes) { throw new Error('Tree object is missing nodes'); } + + return traverseTree(tree.root, tree.nodes, {}, POST_ORDER); }; +/** + * Executes an ordered depth first search on the dependency tree and returns a + * list of nodes. + * + * @param {String} root - The root or current node. + * @param {Object} nodes - Child map (node -> children[]) + * @param {Object} visited - Map of visited nodes (node -> true || false) + * @param {Integer} order - 1 = pre-order; 2 = post-order + */ +function traverseTree(root, nodes, visited, order) { + if ((order !== PRE_ORDER) && (order !== POST_ORDER)) { + throw new Error ('Traversal order not supported: ' + order); + } + + var list = []; + if (order === PRE_ORDER) { + list.push(root); + } + + // If the root has already been visited, it, and its dependencies, will + // already appear in the list. + if (visited[root]) { + return []; + } + + // Mark the node as visited + visited[root] = true; + + var children = nodes[root] || []; + + children.forEach(function(child) { + list = list.concat(traverseTree(child, nodes, visited, order)); + }); + + if (order === POST_ORDER) { + list.push(root); + } + + return list; +} + /** * Returns the list of dependencies for the given filename * Protected for testing @@ -52,7 +134,7 @@ module.exports._getDependencies = function(filename) { } return dependencies; -} +}; /** * @param {String} filename @@ -61,7 +143,7 @@ module.exports._getDependencies = function(filename) { * @return {String[]} */ function traverse(filename, root, visited) { - var tree = [filename]; + var tree = []; if (visited[filename]) { return visited[filename]; @@ -94,8 +176,9 @@ function traverse(filename, root, visited) { tree = removeDups(tree); visited[filename] = visited[filename].concat(tree); + tree.push(filename); return tree; -}; +} /** * Returns a list of unique items from the array diff --git a/test/test.js b/test/test.js index 550c529..c21566d 100644 --- a/test/test.js +++ b/test/test.js @@ -1,21 +1,24 @@ -var getTreeAsList = require('../'); +var dependencyTree = require('../'); var assert = require('assert'); var sinon = require('sinon'); -describe('getTreeAsList', function() { +describe('dependencyTree', function() { var root = __dirname + '/example/amd'; var filename = root + '/a.js'; function testTreesForFormat(format, ext) { ext = ext || '.js'; - it('returns a list form of the dependency tree for a file', function() { + it('returns an object form of the dependency tree for a file', function() { var root = __dirname + '/example/' + format; var filename = root + '/a' + ext; - var tree = getTreeAsList(filename, root) - assert(tree instanceof Array); - assert(tree.length === 3); + var tree = dependencyTree(filename, root); + assert(tree instanceof Object); + assert(tree.root); + assert(tree.nodes instanceof Object); + assert(tree.nodes[tree.root] instanceof Array); + assert(tree.nodes[tree.root].length === 2); }); } @@ -23,50 +26,52 @@ describe('getTreeAsList', function() { var root = __dirname + '/example/onlyRealDeps'; var filename = root + '/a.js'; - var tree = getTreeAsList(filename, root); - assert(tree.length === 1); - assert(tree[0].indexOf('a.js') !== -1); + var tree = dependencyTree(filename, root); + assert(tree.nodes[tree.root].length === 0); + assert(tree.root.indexOf('a.js') !== -1); }); it('does not choke on cyclic dependencies', function() { var root = __dirname + '/example/cyclic'; var filename = root + '/a.js'; - var spy = sinon.spy(getTreeAsList, '_getDependencies'); + var spy = sinon.spy(dependencyTree, '_getDependencies'); - var tree = getTreeAsList(filename, root); + var tree = dependencyTree(filename, root); assert(spy.callCount === 2); - assert(tree.length); - getTreeAsList._getDependencies.restore(); + assert(tree.nodes[tree.root].length); + dependencyTree._getDependencies.restore(); }); it('excludes Node core modules by default', function() { var root = __dirname + '/example/commonjs'; var filename = root + '/b.js'; - var tree = getTreeAsList(filename, root); - assert(tree.length === 1); - assert(tree[0].indexOf('b.js') !== -1); + var tree = dependencyTree(filename, root); + assert(tree.nodes[tree.root].length === 0); + assert(tree.root.indexOf('b.js') !== -1); }); it('returns a list of absolutely pathed files', function() { var root = __dirname + '/example/commonjs'; var filename = root + '/b.js'; - var tree = getTreeAsList(filename, root); - assert(tree[0].indexOf(process.cwd()) !== -1); + var tree = dependencyTree(filename, root); + for (var node in tree.nodes) { + assert(node.indexOf(process.cwd()) !== -1); + } }); describe('throws', function() { it('throws if the filename is missing', function() { assert.throws(function() { - getTreeAsList(undefined, root); + dependencyTree(undefined, root); }); }); it('throws if the root is missing', function() { assert.throws(function() { - getTreeAsList(filename); + dependencyTree(filename); }); }); }); @@ -74,12 +79,12 @@ describe('getTreeAsList', function() { describe('on file error', function() { it('does not throw', function() { assert.doesNotThrow(function() { - getTreeAsList('foo', root); + dependencyTree('foo', root); }); }); it('returns no dependencies', function() { - var tree = getTreeAsList('foo', root); + var tree = dependencyTree('foo', root); assert(!tree.length); }); }); @@ -88,11 +93,11 @@ describe('getTreeAsList', function() { var spy; beforeEach(function() { - spy = sinon.spy(getTreeAsList, '_getDependencies'); + spy = sinon.spy(dependencyTree, '_getDependencies'); }); afterEach(function() { - getTreeAsList._getDependencies.restore(); + dependencyTree._getDependencies.restore(); }); it('accepts an optional cache object for memoization (#2)', function() { @@ -105,8 +110,8 @@ describe('getTreeAsList', function() { __dirname + '/example/amd/c.js' ]; - var tree = getTreeAsList(filename, root, cache); - assert(tree.length === 3); + var tree = dependencyTree(filename, root, cache); + assert(tree.nodes[tree.root].length === 2); assert(spy.neverCalledWith(__dirname + '/example/amd/b.js')); }); @@ -118,7 +123,7 @@ describe('getTreeAsList', function() { // Shouldn't process the first file's tree cache[filename] = []; - var tree = getTreeAsList(filename, root, cache); + var tree = dependencyTree(filename, root, cache); assert(!tree.length); }); }); @@ -140,4 +145,130 @@ describe('getTreeAsList', function() { testTreesForFormat('sass', '.scss'); }); }); + + describe('traversePreOrder', function() { + var root = __dirname + '/example/amd'; + var filename = root + '/a.js'; + + function testTraversePreOrder(format, ext) { + ext = ext || '.js'; + + it('returns a pre-order list form of the dependency tree', function() { + var root = __dirname + '/example/' + format; + var filename = root + '/a' + ext; + + var tree = dependencyTree(filename, root); + var list = dependencyTree.traversePreOrder(tree); + assert(list instanceof Array); + assert(list.length === 3); + assert(list[0] === tree.root); + assert(list[1] === tree.nodes[tree.root][0]); + assert(list[2] === tree.nodes[tree.root][1]); + }); + } + + describe('throws', function() { + it('throws if tree is undefined', function() { + assert.throws(function() { + var tree; + dependencyTree.traversePostOrder(tree); + }, /tree not given/); + }); + + it('throws if tree.root is undefined', function() { + assert.throws(function() { + var tree = {}; + dependencyTree.traversePostOrder(tree); + }, /Tree object is missing root/); + }); + + it('throws if tree.nodes is undefined', function() { + assert.throws(function() { + var tree = {root: 'a.js'}; + dependencyTree.traversePostOrder(tree); + }, /Tree object is missing nodes/); + }); + }); + + describe('module formats', function() { + describe('amd', function() { + testTraversePreOrder('amd'); + }); + + describe('commonjs', function() { + testTraversePreOrder('commonjs'); + }); + + describe('es6', function() { + testTraversePreOrder('es6'); + }); + + describe('sass', function() { + testTraversePreOrder('sass', '.scss'); + }); + }); + }); + + describe('traversePostOrder', function() { + var root = __dirname + '/example/amd'; + var filename = root + '/a.js'; + + function testTraversePostOrder(format, ext) { + ext = ext || '.js'; + + it('returns a post-order list form of the dependency tree', function() { + var root = __dirname + '/example/' + format; + var filename = root + '/a' + ext; + + var tree = dependencyTree(filename, root); + var list = dependencyTree.traversePostOrder(tree); + assert(list instanceof Array); + assert(list.length === 3); + assert(list[0] === tree.nodes[tree.root][0]); + assert(list[1] === tree.nodes[tree.root][1]); + assert(list[2] === tree.root); + }); + } + + describe('throws', function() { + it('throws if tree is undefined', function() { + assert.throws(function() { + var tree; + dependencyTree.traversePostOrder(tree); + }, /tree not given/); + }); + + it('throws if tree.root is undefined', function() { + assert.throws(function() { + var tree = {}; + dependencyTree.traversePostOrder(tree); + }, /Tree object is missing root/); + }); + + it('throws if tree.nodes is undefined', function() { + assert.throws(function() { + var tree = {root: 'a.js'}; + dependencyTree.traversePostOrder(tree); + }, /Tree object is missing nodes/); + }); + }); + + describe('module formats', function() { + describe('amd', function() { + testTraversePostOrder('amd'); + }); + + describe('commonjs', function() { + testTraversePostOrder('commonjs'); + }); + + describe('es6', function() { + testTraversePostOrder('es6'); + }); + + describe('sass', function() { + testTraversePostOrder('sass', '.scss'); + }); + }); + }); });