UNPKG

18 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5const fs_1 = __importDefault(require("fs"));
6const tmp_1 = __importDefault(require("tmp"));
7const path_1 = __importDefault(require("path"));
8const broccoli_source_1 = require("broccoli-source");
9const transform_node_1 = __importDefault(require("./wrappers/transform-node"));
10const source_node_1 = __importDefault(require("./wrappers/source-node"));
11const builder_1 = __importDefault(require("./errors/builder"));
12const node_setup_1 = __importDefault(require("./errors/node-setup"));
13const build_1 = __importDefault(require("./errors/build"));
14const cancelation_request_1 = __importDefault(require("./cancelation-request"));
15const filter_map_1 = __importDefault(require("./utils/filter-map"));
16const events_1 = require("events");
17const node_1 = __importDefault(require("./wrappers/node"));
18const heimdall = require('heimdalljs');
19const underscoreString = require('underscore.string');
20const broccoliNodeInfo = require('broccoli-node-info');
21const logger = require('heimdalljs-logger')('broccoli:builder');
22// Clean up left-over temporary directories on uncaught exception.
23tmp_1.default.setGracefulCleanup();
24// For an explanation and reference of the API that we use to communicate with
25// nodes (__broccoliFeatures__ and __broccoliGetInfo__), see
26// https://github.com/broccolijs/broccoli/blob/master/docs/node-api.md
27// Build a graph of nodes, referenced by its final output node. Example:
28//
29// ```js
30// const builder = new Builder(outputNode)
31// try {
32// const { outputPath } = await builder.build()
33// } finally {
34// await builder.cleanup()
35// }
36// ```
37//
38// Note that the API of this Builder may change between minor Broccoli
39// versions. Backwards compatibility is only guaranteed for plugins, so any
40// plugin that works with Broccoli 1.0 will work with 1.x.
41class Builder extends events_1.EventEmitter {
42 constructor(outputNode, options = {}) {
43 super();
44 this.outputNode = outputNode;
45 this.tmpdir = options.tmpdir; // can be null
46 this.unwatchedPaths = [];
47 this.watchedPaths = [];
48 // nodeWrappers store additional bookkeeping information, such as paths.
49 // This array contains them in topological (build) order.
50 this._nodeWrappers = new Map();
51 // This populates this._nodeWrappers as a side effect
52 this.outputNodeWrapper = this.makeNodeWrapper(this.outputNode);
53 // Catching missing directories here helps prevent later errors when we set
54 // up the watcher.
55 this.checkInputPathsExist();
56 this.setupTmpDirs();
57 this.setupHeimdall();
58 this._cancelationRequest = undefined;
59 // Now that temporary directories are set up, we need to run the rest of the
60 // constructor in a try/catch block to clean them up if necessary.
61 try {
62 this.setupNodes();
63 this.outputPath = this.outputNodeWrapper.outputPath;
64 this.buildId = 0;
65 }
66 catch (e) {
67 this.cleanup();
68 throw e;
69 }
70 }
71 static get BuilderError() { return builder_1.default; }
72 static get InvalidNodeError() { return broccoliNodeInfo.InvalidNodeError; }
73 static get NodeSetupError() { return node_setup_1.default; }
74 static get BuildError() { return build_1.default; }
75 static get NodeWrapper() { return node_1.default; }
76 static get TransformNodeWrapper() { return transform_node_1.default; }
77 static get SourceNodeWrapper() { return source_node_1.default; }
78 // Trigger a (re)build.
79 //
80 // Returns a promise that resolves when the build has finished. If there is a
81 // build error, the promise is rejected with a Builder.BuildError instance.
82 // This method will never throw, and it will never be rejected with anything
83 // other than a BuildError.
84 async build() {
85 if (this._cancelationRequest) {
86 throw new builder_1.default('Cannot start a build if one is already running');
87 }
88 let pipeline = Promise.resolve();
89 this.buildId++;
90 for (const nw of this._nodeWrappers.values()) {
91 // Wipe all buildState objects at the beginning of the build
92 nw.buildState = {};
93 // the build is two passes, first we create a promise chain representing
94 // the complete build, then we pass that terminal promises which
95 // represents the build to the CancelationRequest, after which the build
96 // itself begins.
97 //
98 // 1. build up a promise chain, which represents the complete build
99 pipeline = pipeline.then(async () => {
100 // 3. begin next build step
101 this._cancelationRequest.throwIfRequested();
102 this.emit('beginNode', nw);
103 try {
104 await nw.build();
105 this.emit('endNode', nw);
106 }
107 catch (e) {
108 this.emit('endNode', nw);
109 // wrap the error which occurred from a node wrappers build with
110 // additional build information. This includes which build step
111 // caused the error, and where that build step was instantiated.
112 throw new build_1.default(e, nw);
113 }
114 });
115 }
116 // 2. Create CancelationRequest which waits on the complete build itself
117 // This allows us to initiate a cancellation, but wait until any
118 // un-cancelable work completes before canceling. This allows us to safely
119 // wait until cancelation is complete before performance actions such as
120 // cleanup, or restarting the build itself.
121 this._cancelationRequest = new cancelation_request_1.default(pipeline);
122 try {
123 await pipeline;
124 this.buildHeimdallTree(this.outputNodeWrapper);
125 }
126 finally {
127 let buildsSkipped = filter_map_1.default(this._nodeWrappers.values(), (nw) => nw.buildState.built === false).length;
128 logger.debug(`Total nodes skipped: ${buildsSkipped} out of ${this._nodeWrappers.size}`);
129 this._cancelationRequest = null;
130 }
131 }
132 async cancel() {
133 if (this._cancelationRequest) {
134 return this._cancelationRequest.cancel();
135 }
136 }
137 // Destructor-like method. Waits on current node to finish building, then cleans up temp directories
138 async cleanup() {
139 try {
140 await this.cancel();
141 }
142 finally {
143 await this.builderTmpDirCleanup();
144 }
145 }
146 // This method recursively traverses the node graph and returns a nodeWrapper.
147 // The nodeWrapper graph parallels the node graph 1:1.
148 makeNodeWrapper(node, _stack = []) {
149 let wrapper = this._nodeWrappers.get(node);
150 if (wrapper !== undefined) {
151 return wrapper;
152 }
153 // Turn string nodes into WatchedDir nodes
154 const originalNode = node; // keep original (possibly string) node around so we can later deduplicate
155 if (typeof node === 'string') {
156 node = new broccoli_source_1.WatchedDir(node, { annotation: 'string node' });
157 }
158 // Call node.__broccoliGetInfo__()
159 let nodeInfo;
160 try {
161 nodeInfo = broccoliNodeInfo.getNodeInfo(node);
162 }
163 catch (e) {
164 if (!(e instanceof broccoliNodeInfo.InvalidNodeError))
165 throw e;
166 // We don't have the instantiation stack of an invalid node, so to aid
167 // debugging, we instead report its parent node
168 const messageSuffix = _stack.length > 0
169 ? '\nused as input node to ' +
170 _stack[_stack.length - 1].label +
171 _stack[_stack.length - 1].formatInstantiationStackForTerminal()
172 : '\nused as output node';
173 throw new broccoliNodeInfo.InvalidNodeError(e.message + messageSuffix);
174 }
175 // Compute label, like "Funnel (test suite)"
176 let label = nodeInfo.name;
177 const labelExtras = [];
178 if (nodeInfo.nodeType === 'source')
179 labelExtras.push(nodeInfo.sourceDirectory);
180 if (nodeInfo.annotation != null)
181 labelExtras.push(nodeInfo.annotation);
182 if (labelExtras.length > 0)
183 label += ' (' + labelExtras.join('; ') + ')';
184 // We start constructing the nodeWrapper here because we'll need the partial
185 // nodeWrapper for the _stack. Later we'll add more properties.
186 const nodeWrapper = nodeInfo.nodeType === 'transform' ? new transform_node_1.default() : new source_node_1.default();
187 nodeWrapper.nodeInfo = nodeInfo;
188 nodeWrapper.originalNode = originalNode;
189 nodeWrapper.node = node;
190 nodeWrapper.label = label;
191 // Detect cycles
192 for (let i = 0; i < _stack.length; i++) {
193 if (_stack[i].node === originalNode) {
194 let cycleMessage = 'Cycle in node graph: ';
195 for (let j = i; j < _stack.length; j++) {
196 cycleMessage += _stack[j].label + ' -> ';
197 }
198 cycleMessage += nodeWrapper.label;
199 throw new builder_1.default(cycleMessage);
200 }
201 }
202 // For 'transform' nodes, recursively enter into the input nodes; for
203 // 'source' nodes, record paths.
204 let inputNodeWrappers = [];
205 if (nodeInfo.nodeType === 'transform') {
206 const newStack = _stack.concat([nodeWrapper]);
207 inputNodeWrappers = nodeInfo.inputNodes.map((inputNode) => {
208 return this.makeNodeWrapper(inputNode, newStack);
209 });
210 }
211 else {
212 // nodeType === 'source'
213 if (nodeInfo.watched) {
214 this.watchedPaths.push(nodeInfo.sourceDirectory);
215 }
216 else {
217 this.unwatchedPaths.push(nodeInfo.sourceDirectory);
218 }
219 }
220 // For convenience, all nodeWrappers get an `inputNodeWrappers` array; for
221 // 'source' nodes it's empty.
222 nodeWrapper.inputNodeWrappers = inputNodeWrappers;
223 nodeWrapper.id = this._nodeWrappers.size;
224 // this._nodeWrappers will contain all the node wrappers in topological
225 // order, i.e. each node comes after all its input nodes.
226 //
227 // It's unfortunate that we're mutating this._nodeWrappers as a side effect,
228 // but since we work backwards from the output node to discover all the
229 // input nodes, it's harder to do a side-effect-free topological sort.
230 this._nodeWrappers.set(nodeWrapper.originalNode, nodeWrapper);
231 return nodeWrapper;
232 }
233 get watchedSourceNodeWrappers() {
234 return filter_map_1.default(this._nodeWrappers.values(), (nw) => {
235 return nw.nodeInfo.nodeType === 'source' && nw.nodeInfo.watched;
236 });
237 }
238 checkInputPathsExist() {
239 // We might consider checking this.unwatchedPaths as well.
240 for (let i = 0; i < this.watchedPaths.length; i++) {
241 let isDirectory;
242 try {
243 isDirectory = fs_1.default.statSync(this.watchedPaths[i]).isDirectory();
244 }
245 catch (err) {
246 throw new builder_1.default('Directory not found: ' + this.watchedPaths[i]);
247 }
248 if (!isDirectory) {
249 throw new builder_1.default('Not a directory: ' + this.watchedPaths[i]);
250 }
251 }
252 }
253 setupTmpDirs() {
254 // Create temporary directories for each node:
255 //
256 // out-01-some-plugin/
257 // out-02-otherplugin/
258 // cache-01-some-plugin/
259 // cache-02-otherplugin/
260 //
261 // Here's an alternative directory structure we might consider (it's not
262 // clear which structure makes debugging easier):
263 //
264 // 01-some-plugin/
265 // out/
266 // cache/
267 // in-1 -> ... // symlink for convenience
268 // in-2 -> ...
269 // 02-otherplugin/
270 // ...
271 // @ts-ignore
272 const tmpObj = tmp_1.default.dirSync({
273 prefix: 'broccoli-',
274 unsafeCleanup: true,
275 dir: this.tmpdir || undefined,
276 });
277 this.builderTmpDir = tmpObj.name;
278 this.builderTmpDirCleanup = tmpObj.removeCallback;
279 for (let nodeWrapper of this._nodeWrappers.values()) {
280 if (nodeWrapper.nodeInfo.nodeType === 'transform') {
281 nodeWrapper.inputPaths = nodeWrapper.inputNodeWrappers.map((nw) => nw.outputPath);
282 nodeWrapper.outputPath = this.mkTmpDir(nodeWrapper, 'out');
283 if (nodeWrapper.nodeInfo.needsCache) {
284 nodeWrapper.cachePath = this.mkTmpDir(nodeWrapper, 'cache');
285 }
286 }
287 else {
288 // nodeType === 'source'
289 // We could name this .sourcePath, but with .outputPath the code is simpler.
290 nodeWrapper.outputPath = nodeWrapper.nodeInfo.sourceDirectory;
291 }
292 }
293 }
294 // Create temporary directory, like
295 // /tmp/broccoli-9rLfJh/out-067-merge_trees_vendor_packages
296 // type is 'out' or 'cache'
297 mkTmpDir(nodeWrapper, type) {
298 let nameAndAnnotation = nodeWrapper.nodeInfo.name + ' ' + (nodeWrapper.nodeInfo.annotation || '');
299 // slugify turns fooBar into foobar, so we call underscored first to
300 // preserve word boundaries
301 let suffix = underscoreString.underscored(nameAndAnnotation.substr(0, 60));
302 suffix = underscoreString.slugify(suffix).replace(/-/g, '_');
303 // 1 .. 147 -> '001' .. '147'
304 const paddedId = underscoreString.pad('' + nodeWrapper.id, ('' + this._nodeWrappers.size).length, '0');
305 const dirname = type + '-' + paddedId + '-' + suffix;
306 const tmpDir = path_1.default.join(this.builderTmpDir, dirname);
307 fs_1.default.mkdirSync(tmpDir);
308 return tmpDir;
309 }
310 // for compat
311 get nodeWrappers() {
312 return [...this._nodeWrappers.values()];
313 }
314 setupNodes() {
315 for (let nw of this._nodeWrappers.values()) {
316 try {
317 nw.setup(this.features);
318 }
319 catch (err) {
320 throw new node_setup_1.default(err, nw);
321 }
322 }
323 }
324 setupHeimdall() {
325 this.on('beginNode', node => {
326 let name;
327 if (node instanceof source_node_1.default) {
328 name = node.nodeInfo.sourceDirectory;
329 }
330 else {
331 name = node.nodeInfo.annotation || node.nodeInfo.name;
332 }
333 node.__heimdall_cookie__ = heimdall.start({
334 name,
335 label: node.label,
336 broccoliNode: true,
337 broccoliId: node.id,
338 // we should do this instead of reParentNodes
339 // broccoliInputIds: node.inputNodeWrappers.map(input => input.id),
340 broccoliCachedNode: false,
341 broccoliPluginName: node.nodeInfo.name,
342 });
343 node.__heimdall__ = heimdall.current;
344 });
345 this.on('endNode', node => {
346 if (node.__heimdall__) {
347 node.__heimdall_cookie__.stop();
348 }
349 });
350 }
351 buildHeimdallTree(outputNodeWrapper) {
352 if (!outputNodeWrapper.__heimdall__) {
353 return;
354 }
355 // Why?
356 reParentNodes(outputNodeWrapper);
357 // What uses this??
358 aggregateTime();
359 }
360 get features() {
361 return broccoliNodeInfo.features;
362 }
363}
364function reParentNodes(outputNodeWrapper) {
365 // re-parent heimdall nodes according to input nodes
366 const seen = new Set();
367 const queue = [outputNodeWrapper];
368 let node;
369 let parent;
370 let stack = [];
371 while ((node = queue.pop()) !== undefined) {
372 if (parent === node) {
373 parent = stack.pop();
374 }
375 else {
376 queue.push(node);
377 let heimdallNode = node.__heimdall__;
378 if (heimdallNode === undefined || seen.has(heimdallNode)) {
379 // make 0 time node
380 const cookie = heimdall.start(Object.assign({}, heimdallNode.id));
381 heimdallNode = heimdall.current;
382 heimdallNode.id.broccoliCachedNode = true;
383 cookie.stop();
384 heimdallNode.stats.time.self = 0;
385 }
386 else {
387 seen.add(heimdallNode);
388 // Only push children for non "cached inputs"
389 const inputNodeWrappers = node.inputNodeWrappers;
390 for (let i = inputNodeWrappers.length - 1; i >= 0; i--) {
391 queue.push(inputNodeWrappers[i]);
392 }
393 }
394 if (parent) {
395 heimdallNode.remove();
396 parent.__heimdall__.addChild(heimdallNode);
397 stack.push(parent);
398 }
399 parent = node;
400 }
401 }
402}
403function aggregateTime() {
404 let queue = [heimdall.current];
405 let stack = [];
406 let parent;
407 let node;
408 while ((node = queue.pop()) !== undefined) {
409 if (parent === node) {
410 parent = stack.pop();
411 if (parent !== undefined) {
412 parent.stats.time.total += node.stats.time.total;
413 }
414 }
415 else {
416 const children = node._children;
417 queue.push(node);
418 for (let i = children.length - 1; i >= 0; i--) {
419 queue.push(children[i]);
420 }
421 if (parent) {
422 stack.push(parent);
423 }
424 node.stats.time.total = node.stats.time.self;
425 parent = node;
426 }
427 }
428}
429module.exports = Builder;
430//# sourceMappingURL=builder.js.map
\No newline at end of file