UNPKG

9.93 kBJavaScriptView Raw
1
2'use strict';
3
4let debug = require('debug')('mako-tree');
5let defaults = require('defaults');
6let File = require('./file');
7let Graph = require('graph.js/dist/graph.full.js');
8let path = require('path');
9let toposort = require('graph-toposort');
10
11const pwd = process.cwd();
12const relative = abs => path.relative(pwd, abs);
13
14
15/**
16 * Represents a dependency graph for the builder.
17 *
18 * @class
19 */
20class Tree {
21 /**
22 * Creates a new instance, particularly creating the Graph instance.
23 */
24 constructor() {
25 debug('initialize');
26 this.graph = new Graph();
27 }
28
29 /**
30 * Checks to see if the given `location` file exists in the graph.
31 *
32 * @param {String} location The absolute path to check for.
33 * @return {Boolean}
34 */
35 hasFile(location) {
36 return this.graph.hasVertex(location);
37 }
38
39 /**
40 * Adds a new file `location` to the graph. Specify `entry` if the file
41 * should be treated specially as an entry file. Returns the new `File`
42 * instance.
43 *
44 * @param {String} location The absolute path to the file.
45 * @param {Boolean} [entry] Whether or not this file is an entry.
46 * @return {File}
47 */
48 addFile(location, entry) {
49 if (!this.hasFile(location)) {
50 debug('adding node: %s (entry: %j)', relative(location), !!entry);
51 let file = new File(location, this, !!entry);
52 this.graph.addNewVertex(location, file);
53 }
54 return this.getFile(location);
55 }
56
57 /**
58 * Returns the `File` instance for the `location`.
59 *
60 * @param {String} location The absolute path to the file.
61 * @return {File}
62 */
63 getFile(location) {
64 return this.graph.vertexValue(location);
65 }
66
67 /**
68 * Retrieve a list of file paths based on the given criteria.
69 *
70 * Available `options`:
71 * - `topological` sort the files topologically
72 * - `objects` return the `File` instances
73 *
74 * @param {Object} [options] The filter criteria.
75 * @return {Array}
76 */
77 getFiles(options) {
78 let config = defaults(options, {
79 topological: false,
80 objects: false
81 });
82 debug('getting files: %j', config);
83
84 if (config.topological) {
85 let list = toposort(this.graph);
86 debug('%d files in tree', list.length);
87 return config.objects ? list.map(id => this.getFile(id)) : list;
88 }
89
90 debug('%d files in tree', this.size());
91 return Array.from(this.graph.vertices()).map(vertices(config.objects));
92 }
93
94 /**
95 * Remove the file at the given `location` from the graph.
96 *
97 * @param {String} location The absolute path to the file.
98 */
99 removeFile(location) {
100 debug('removing node %s', relative(location));
101 this.graph.removeVertex(location);
102 }
103
104 /**
105 * Tells us whether or not the file at `location` is an entry.
106 *
107 * @param {String} location The absolute path to the file.
108 * @return {Boolean}
109 */
110 isEntry(location) {
111 return !!this.getFile(location).isEntry();
112 }
113
114 /**
115 * Retrieve a list of entry file paths based on the given criteria.
116 *
117 * Available `options`:
118 * - `from` only pull entries that are reachable from this file
119 * - `objects` return the `File` instances
120 *
121 * @param {Object} [options] The filter criteria.
122 * @return {Array}
123 */
124 getEntries(options) {
125 let config = defaults(options, {
126 from: null,
127 objects: false
128 });
129 debug('getting entry files: %j', config);
130
131 let entries = Array.from(this.graph.sinks())
132 .filter(vertex => !!vertex[1].entry);
133
134 if (config.from) {
135 entries = entries.filter(vertex => {
136 let start = config.from;
137 let end = vertex[0];
138 return start === end || this.graph.hasPath(start, end);
139 });
140 }
141
142 debug('%d entries found', entries.length);
143 return entries.map(vertices(config.objects));
144 }
145
146 /**
147 * Checks to see if the given `parent` has a link to dependency `child`.
148 *
149 * @param {String} parent The absolute path for the parent file.
150 * @param {String} child The absolute path for the dependency file.
151 * @return {Boolean}
152 */
153 hasDependency(parent, child) {
154 return this.graph.hasEdge(child, parent);
155 }
156
157 /**
158 * Sets up the file `child` as a dependency of `parent`.
159 *
160 * @param {String} parent The absolute path for the parent file.
161 * @param {String} child The absolute path for the dependency file.
162 * @return {File}
163 */
164 addDependency(parent, child) {
165 debug('adding dependency %s -> %s', relative(child), relative(parent));
166 if (!this.hasFile(child)) this.addFile(child);
167 this.graph.addEdge(child, parent);
168 return this.getFile(child);
169 }
170
171 /**
172 * Removes the dependency `child` from the `parent` file.
173 *
174 * @param {String} parent The absolute path for the parent file.
175 * @param {String} child The absolute path for the dependency file.
176 */
177 removeDependency(parent, child) {
178 debug('removing dependency %s -> %s', relative(child), relative(parent));
179 this.graph.removeEdge(child, parent);
180 }
181
182 /**
183 * Removes all dependencies from the `parent` file.
184 *
185 * @param {String} parent The absolute path for the parent file.
186 */
187 removeDependencies(parent) {
188 debug('removing all dependencies from %s', relative(parent));
189 this.dependenciesOf(parent)
190 .forEach(child => this.removeDependency(parent, child));
191 }
192
193 /**
194 * Return a list of all files that the given `node` file depends on.
195 *
196 * Available `options`:
197 * - `recursive` when set, go recursively down the entire graph
198 * - `objects` return the `File` instances
199 *
200 * @param {String} node The absolute path to start from.
201 * @param {Object} [options] The search criteria.
202 * @return {Array}
203 */
204 dependenciesOf(node, options) {
205 let config = defaults(options, {
206 recursive: false,
207 objects: false
208 });
209 debug('getting dependencies of %s: %j', relative(node), config);
210
211 let deps = config.recursive
212 ? Array.from(this.graph.verticesWithPathTo(node))
213 : Array.from(this.graph.verticesTo(node));
214
215 debug('%d dependencies found', deps.length);
216 return deps.map(vertices(config.objects));
217 }
218
219 /**
220 * Checks to see if the given `child` has a link to dependant `parent`.
221 *
222 * @param {String} child The absolute path for the target file.
223 * @param {String} parent The absolute path for the dependant file.
224 * @return {Boolean}
225 */
226 hasDependant(child, parent) {
227 return this.graph.hasEdge(child, parent);
228 }
229
230 /**
231 * Sets up the given `parent` as a dependant of `child`. In other words,
232 * the reverse of addDependency()
233 *
234 * @param {String} child The absolute path for the target file.
235 * @param {String} parent The absolute path for the dependant file.
236 * @return {File}
237 */
238 addDependant(child, parent) {
239 debug('adding dependant %s <- %s', relative(parent), relative(child));
240 if (!this.hasFile(parent)) this.addFile(parent);
241 this.graph.addEdge(child, parent);
242 return this.getFile(parent);
243 }
244
245 /**
246 * Removes the dependant `parent` from the `child` file.
247 *
248 * @param {String} child The absolute path for the target file.
249 * @param {String} parent The absolute path for the dependant file.
250 */
251 removeDependant(child, parent) {
252 debug('removing dependant %s <- %s', relative(parent), relative(child));
253 this.graph.removeEdge(child, parent);
254 }
255
256 /**
257 * Removes all dependants from the `child` file.
258 *
259 * @param {String} child The absolute path for the target file.
260 */
261 removeDependants(child) {
262 debug('removing all dependants from %s', relative(child));
263 this.dependantsOf(child)
264 .forEach(parent => this.removeDependant(child, parent));
265 }
266
267 /**
268 * Return a list of all files that depend on the given `node` file.
269 *
270 * Available `options`:
271 * - `recursive` when set, go recursively down the entire graph
272 * - `objects` return the `File` instances
273 *
274 * @param {String} node The absolute path to start from.
275 * @param {Object} [options] The search criteria.
276 * @return {Array}
277 */
278 dependantsOf(node, options) {
279 let config = defaults(options, {
280 recursive: false,
281 objects: false
282 });
283 debug('getting dependants of %s: %j', relative(node), config);
284
285 let deps = config.recursive
286 ? Array.from(this.graph.verticesWithPathFrom(node))
287 : Array.from(this.graph.verticesFrom(node));
288
289 debug('%d dependants found', deps.length);
290 return deps.map(vertices(config.objects));
291 }
292
293 /**
294 * Tells us how large the underlying graph is.
295 *
296 * @return {Number}
297 */
298 size() {
299 return this.graph.vertexCount();
300 }
301
302 /**
303 * Returns a clone of the current `Tree` instance.
304 *
305 * @return {Tree}
306 */
307 clone() {
308 debug('cloning tree');
309 let tree = new Tree();
310 tree.graph = this.graph.clone(file => file.clone(tree), value => value);
311 debug('done cloning tree');
312 return tree;
313 }
314
315 /**
316 * Remove any files that cannot be reached from the given `entries`.
317 *
318 * @param {Array} [entries] A list of files to use as the search root.
319 */
320 prune(entries) {
321 debug('pruning orphaned files');
322 if (!entries) entries = this.getEntries();
323 let files = this.getFiles({ topological: true });
324
325 let deps = new Set(entries);
326 entries.forEach(entry => {
327 this.dependenciesOf(entry, { recursive: true }).forEach(file => {
328 deps.add(file);
329 });
330 });
331
332 files.forEach(file => {
333 if (!deps.has(file)) this.graph.destroyVertex(file);
334 });
335
336 debug('done pruning tree');
337 }
338}
339
340
341// single export
342module.exports = Tree;
343
344
345/**
346 * Helper for mapping a list of vertices. By default, the vertex key (absolute
347 * path) is returned, but if `value` is truthy, it will return the `File`
348 * object instead.
349 *
350 * @param {Boolean} value Whether to use the key or value.
351 * @return {Function}
352 */
353function vertices(value) {
354 return function (vertex) {
355 return value ? vertex[1] : vertex[0];
356 };
357}