UNPKG

6.21 kBJavaScriptView Raw
1'use strict';
2
3const os = require('os');
4const path = require('path');
5const pify = require('pify');
6const commondir = require('commondir');
7const walk = require('walkdir');
8const dependencyTree = require('dependency-tree');
9const log = require('./log');
10
11const stat = pify(require('fs').stat);
12
13/**
14 * Check if running on Windows.
15 * @type {Boolean}
16 */
17const isWin = (os.platform() === 'win32');
18
19class Tree {
20 /**
21 * Class constructor.
22 * @constructor
23 * @api public
24 * @param {Array} srcPaths
25 * @param {Object} config
26 * @return {Promise}
27 */
28 constructor(srcPaths, config) {
29 this.srcPaths = srcPaths.map((s) => path.resolve(s));
30 log('using src paths %o', this.srcPaths);
31
32 this.config = config;
33 log('using config %o', this.config);
34
35 return this.getDirs()
36 .then(this.setBaseDir.bind(this))
37 .then(this.getFiles.bind(this))
38 .then(this.generateTree.bind(this));
39 }
40
41 /**
42 * Set the base directory (compute the common one if multiple).
43 * @param {Array} dirs
44 */
45 setBaseDir(dirs) {
46 if (this.config.baseDir) {
47 this.baseDir = path.resolve(this.config.baseDir);
48 } else {
49 this.baseDir = commondir(dirs);
50 }
51
52 log('using base directory %s', this.baseDir);
53 }
54
55 /**
56 * Get directories from the source paths
57 * @return {Promise} resolved with an array of directories
58 */
59 getDirs() {
60 return Promise
61 .all(this.srcPaths.map((srcPath) => {
62 return stat(srcPath)
63 .then((stats) => stats.isDirectory() ? srcPath : path.dirname(path.resolve(srcPath)));
64 }));
65 }
66
67 /**
68 * Get all files found from the source paths
69 * @return {Promise} resolved with an array of files
70 */
71 getFiles() {
72 const files = [];
73
74 return Promise
75 .all(this.srcPaths.map((srcPath) => {
76 return stat(srcPath)
77 .then((stats) => {
78 if (stats.isFile()) {
79 if (this.isGitPath(srcPath)) {
80 return;
81 }
82
83 files.push(path.resolve(srcPath));
84
85 return;
86 }
87
88 walk.sync(srcPath, (filePath, stat) => {
89 if (this.isGitPath(filePath) || this.isNpmPath(filePath) || !stat.isFile()) {
90 return;
91 }
92
93 const ext = path.extname(filePath).replace('.', '');
94
95 if (files.indexOf(filePath) < 0 && this.config.fileExtensions.indexOf(ext) >= 0) {
96 files.push(filePath);
97 }
98 });
99 });
100 }))
101 .then(() => files);
102 }
103
104 /**
105 * Generate the tree from the given files
106 * @param {Array} files
107 * @return {Object}
108 */
109 generateTree(files) {
110 const depTree = {};
111 const visited = {};
112 const nonExistent = [];
113 const npmPaths = {};
114 const pathCache = {};
115
116 files.forEach((file) => {
117 if (visited[file]) {
118 return;
119 }
120
121 Object.assign(depTree, dependencyTree({
122 filename: file,
123 directory: this.baseDir,
124 requireConfig: this.config.requireConfig,
125 webpackConfig: this.config.webpackConfig,
126 tsConfig: this.config.tsConfig,
127 visited: visited,
128 filter: (dependencyFilePath, traversedFilePath) => {
129 let dependencyFilterRes = true;
130 const isNpmPath = this.isNpmPath(dependencyFilePath);
131
132 if (this.isGitPath(dependencyFilePath)) {
133 return false;
134 }
135
136 if (this.config.dependencyFilter) {
137 dependencyFilterRes = this.config.dependencyFilter(dependencyFilePath, traversedFilePath, this.baseDir);
138 }
139
140 if (this.config.includeNpm && isNpmPath) {
141 (npmPaths[traversedFilePath] = npmPaths[traversedFilePath] || []).push(dependencyFilePath);
142 }
143
144 return !isNpmPath && (dependencyFilterRes || dependencyFilterRes === undefined);
145 },
146 detective: this.config.detectiveOptions,
147 nonExistent: nonExistent
148 }));
149 });
150
151 let tree = this.convertTree(depTree, {}, pathCache, npmPaths);
152
153 for (const npmKey in npmPaths) {
154 const id = this.processPath(npmKey, pathCache);
155
156 npmPaths[npmKey].forEach((npmPath) => {
157 tree[id].push(this.processPath(npmPath, pathCache));
158 });
159 }
160
161 if (this.config.excludeRegExp) {
162 tree = this.exclude(tree, this.config.excludeRegExp);
163 }
164
165 return {
166 tree: this.sort(tree),
167 skipped: nonExistent
168 };
169 }
170
171 /**
172 * Convert deep tree produced by dependency-tree to a
173 * shallow (one level deep) tree used by madge.
174 * @param {Object} depTree
175 * @param {Object} tree
176 * @param {Object} pathCache
177 * @return {Object}
178 */
179 convertTree(depTree, tree, pathCache) {
180 for (const key in depTree) {
181 const id = this.processPath(key, pathCache);
182
183 if (!tree[id]) {
184 tree[id] = [];
185
186 for (const dep in depTree[key]) {
187 tree[id].push(this.processPath(dep, pathCache));
188 }
189
190 this.convertTree(depTree[key], tree, pathCache);
191 }
192 }
193
194 return tree;
195 }
196
197 /**
198 * Process absolute path and return a shorter one.
199 * @param {String} absPath
200 * @param {Object} cache
201 * @return {String}
202 */
203 processPath(absPath, cache) {
204 if (cache[absPath]) {
205 return cache[absPath];
206 }
207
208 let relPath = path.relative(this.baseDir, absPath);
209
210 if (isWin) {
211 relPath = relPath.replace(/\\/g, '/');
212 }
213
214 cache[absPath] = relPath;
215
216 return relPath;
217 }
218
219 /**
220 * Check if path is from NPM folder
221 * @param {String} path
222 * @return {Boolean}
223 */
224 isNpmPath(path) {
225 return path.indexOf('node_modules') >= 0;
226 }
227
228 /**
229 * Check if path is from .git folder
230 * @param {String} filePath
231 * @return {Boolean}
232 */
233 isGitPath(filePath) {
234 return filePath.split(path.sep).indexOf('.git') !== -1;
235 }
236
237 /**
238 * Exclude modules from tree using RegExp.
239 * @param {Object} tree
240 * @param {Array} excludeRegExp
241 * @return {Object}
242 */
243 exclude(tree, excludeRegExp) {
244 const regExpList = excludeRegExp.map((re) => new RegExp(re));
245
246 function regExpFilter(id) {
247 return regExpList.findIndex((regexp) => regexp.test(id)) < 0;
248 }
249
250 return Object
251 .keys(tree)
252 .filter(regExpFilter)
253 .reduce((acc, id) => {
254 acc[id] = tree[id].filter(regExpFilter);
255 return acc;
256 }, {});
257 }
258
259 /**
260 * Sort tree.
261 * @param {Object} tree
262 * @return {Object}
263 */
264 sort(tree) {
265 return Object
266 .keys(tree)
267 .sort()
268 .reduce((acc, id) => {
269 acc[id] = tree[id].sort();
270 return acc;
271 }, {});
272 }
273}
274
275/**
276 * Expose API.
277 * @param {Array} srcPaths
278 * @param {Object} config
279 * @return {Promise}
280 */
281module.exports = (srcPaths, config) => new Tree(srcPaths, config);