UNPKG

ember-introjs

Version:
305 lines (259 loc) 8.81 kB
var RSVP = require('rsvp') var mapSeries = require('promise-map-series') var apiCompat = require('./api_compat') var heimdall = require('heimdalljs'); var SilentError = require('silent-error'); var BroccoliBuildError = require('./broccoli-build-error'); exports.Builder = Builder function Builder (tree) { this.tree = tree this.allTreesRead = [] // across all builds this._currentStep = undefined; this.canceled = false; this.isThrown = false; } function wrapStringErrors(reason) { var err if (typeof reason === 'string') { err = new Error(reason + ' [string exception]') } else { err = reason } throw err } function summarize(node) { return { directory: node.directory, graph: node, } } RSVP.EventTarget.mixin(Builder.prototype) Builder.prototype.build = function (willReadStringTree) { if (this.canceled) { return RSVP.Promise.reject(new Error('cannot build this builder, as it has been previously canceled')); } var builder = this var newTreesRead = [] var nodeCache = [] this.isThrown = false; this._currentStep = RSVP.Promise.resolve() .then(function () { // added for backwards compat. Can be safely removed for 1.0.0 builder.trigger('start'); return readAndReturnNodeFor(builder.tree) // call builder.tree.read() }) .then(summarize) .finally(appendNewTreesRead) .finally(unsetCurrentStep) .finally(function() { // added for backwards compat. Can be safely removed for 1.0.0 builder.trigger('end'); }) .catch(wrapStringErrors) return this._currentStep; function unsetCurrentStep() { builder._currentStep = null } function appendNewTreesRead() { for (var i = 0; i < newTreesRead.length; i++) { if (builder.allTreesRead.indexOf(newTreesRead[i]) === -1) { builder.allTreesRead.push(newTreesRead[i]) } } } // Read the `tree` and return its node, which in particular contains the // tree's output directory (node.directory) function readAndReturnNodeFor (tree) { if (builder.canceled) { var canceled = new SilentError('Build Canceled'); canceled.wasCanceled = true return RSVP.Promise.reject(canceled); } builder.warnIfNecessary(tree) tree = builder.wrapIfNecessary(tree) var index = newTreesRead.indexOf(tree) if (index !== -1) { // Return node from cache to deduplicate `.read` if (nodeCache[index].directory == null) { // node.directory gets set at the very end, so we have found an as-yet // incomplete node. This can happen if there is a cycle. throw new Error('Tree cycle detected') } var cachedNode = nodeCache[index]; heimdall.start({ name: getDescription(cachedNode.tree), broccoliNode: true, broccoliId: cachedNode.id, broccoliCachedNode: true, broccoliPluginName: getPluginName(cachedNode.tree) }).stop(); return RSVP.Promise.resolve(cachedNode) } var node = new Node(tree); var cookie = heimdall.start({ name: getDescription(tree), broccoliNode: true, broccoliId: node.id, broccoliCachedNode: false, broccoliPluginName: getPluginName(tree) }); node.__heimdall__ = heimdall.current; // better to use promises via heimdall.node(description, fn) // we don't actually support duplicate trees, as such we should likely tag them.. // and kill the parallel array structure newTreesRead.push(tree) nodeCache.push(node) var treeDirPromise if (typeof tree === 'string') { treeDirPromise = RSVP.Promise.resolve() .then(function () { if (willReadStringTree) willReadStringTree(tree) return tree }) } else if (!tree || (typeof tree.read !== 'function' && typeof tree.rebuild !== 'function')) { throw new Error('Invalid tree found. You must supply a path or an object with a `.read` (deprecated) or `.rebuild` function: ' + getDescription(tree)) } else { var readTreeRunning = false treeDirPromise = RSVP.Promise.resolve() .then(function () { return tree.read(function readTree (subtree) { if (readTreeRunning) { throw new Error('Parallel readTree call detected; read trees in sequence, e.g. using https://github.com/joliss/promise-map-series') } readTreeRunning = true return RSVP.Promise.resolve() .then(function () { return readAndReturnNodeFor(subtree) // recurse }) .then(function (childNode) { node.addChild(childNode) return childNode.directory }) .finally(function () { readTreeRunning = false }) }) }).catch(function(e) { if (typeof e === 'object' && e !== null && e.isSilentError) { throw e; } // because `readAndReturnNodeFor` is recursive // it does not make sense to throw multiple build errors, // we can just report a failure once. if (!builder.isThrown) { builder.isThrown = true; throw new BroccoliBuildError(e, node) } throw e }) .then(function (dir) { if (readTreeRunning) { throw new Error('.read returned before readTree finished') } return dir }) } return treeDirPromise .then(function (treeDir) { cookie.stop(); if (treeDir == null) throw new Error(tree + ': .read must return a directory') node.directory = treeDir return node }) } } function cleanupTree(tree) { if (typeof tree !== 'string') { return tree.cleanup() } } Builder.prototype._cancel = function () { this.canceled = true; return this._currentStep || RSVP.Promise.resolve(); }; Builder.prototype.cleanup = function () { var builder = this; return this._cancel().finally(function() { return mapSeries(builder.allTreesRead, cleanupTree) }).catch(function(e) { if (typeof e === 'object' && e !== null && e.wasCanceled) { // if the exception is that we canceled, then cancellation was // non-exceptional and we can safely recover } else { throw e; } }); } Builder.prototype.wrapIfNecessary = function (tree) { if (typeof tree.rebuild === 'function') { // Note: We wrap even if the plugin provides a `.read` function, so that // its new `.rebuild` function gets called. if (!tree.wrappedTree) { // memoize tree.wrappedTree = new apiCompat.NewStyleTreeWrapper(tree) } return tree.wrappedTree } else { return tree } } Builder.prototype.warnIfNecessary = function (tree) { if (process.env.BROCCOLI_WARN_READ_API && (typeof tree.read === 'function' || typeof tree.rebuild === 'function') && !tree.__broccoliFeatures__ && !tree.suppressDeprecationWarning) { if (!this.didPrintWarningIntro) { console.warn('[API] Warning: The .read and .rebuild APIs will stop working in the next Broccoli version') console.warn('[API] Warning: Use broccoli-plugin instead: https://github.com/broccolijs/broccoli-plugin') this.didPrintWarningIntro = true } console.warn('[API] Warning: Plugin uses .read/.rebuild API: ' + getDescription(tree)) tree.suppressDeprecationWarning = true } } var nodeId = 0 function Node(tree, heimdall) { this.id = nodeId++ this.subtrees = [] this.tree = tree this.parents = [] this.__heimdall__ = heimdall; // mimic information that `broccoliNodeInfo` returns this.info = { name: getPluginName(tree), annotation: getDescription(tree) } } Node.prototype.addChild = function Node$addChild(child) { this.subtrees.push(child) } Node.prototype.inspect = function() { return 'Node:' + this.id + ' subtrees: ' + this.subtrees.length } Node.prototype.toJSON = function() { var description = getDescription(this.tree) var subtrees = this.subtrees.map(function(node) { return node.id }) return { id: this.id, description: description, instantiationStack: this.tree._instantiationStack, subtrees: subtrees, } } exports.getPluginName = getPluginName function getPluginName(tree) { // string trees or plain POJO trees don't really have a plugin name if (!tree || tree.constructor === String || tree.constructor === Object) { return undefined; } return tree && tree.constructor && tree.constructor !== String && tree.constructor.name } exports.getDescription = getDescription function getDescription (tree) { return (tree && tree.annotation) || (tree && tree.description) || getPluginName(tree) || ('' + tree) }