1 |
|
2 | ;
|
3 |
|
4 | let debug = require('debug')('mako-tree');
|
5 | let defaults = require('defaults');
|
6 | let File = require('./file');
|
7 | let Graph = require('graph.js/dist/graph.full.js');
|
8 | let path = require('path');
|
9 | let toposort = require('graph-toposort');
|
10 |
|
11 | const pwd = process.cwd();
|
12 | const relative = abs => path.relative(pwd, abs);
|
13 |
|
14 |
|
15 | /**
|
16 | * Represents a dependency graph for the builder.
|
17 | *
|
18 | * @class
|
19 | */
|
20 | class 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
|
342 | module.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 | */
|
353 | function vertices(value) {
|
354 | return function (vertex) {
|
355 | return value ? vertex[1] : vertex[0];
|
356 | };
|
357 | }
|