/// /// /// import fs = require('fs'); import fse = require('fs-extra'); import path = require('path'); import {TreeDiffer, DiffResult} from './tree-differ'; import stabilizeTree from './broccoli-tree-stabilizer'; let symlinkOrCopy = require('symlink-or-copy'); export {DiffResult} from './tree-differ'; /** * Makes writing diffing plugins easy. * * Factory method that takes a class that implements the DiffingBroccoliPlugin interface and returns * an instance of BroccoliTree. * * @param pluginClass * @returns {DiffingPlugin} */ export function wrapDiffingPlugin(pluginClass): DiffingPluginWrapperFactory { return function() { return new DiffingPluginWrapper(pluginClass, arguments); }; } export interface DiffingBroccoliPlugin { rebuild(diff: (DiffResult | DiffResult[])): (Promise| DiffResult | void); cleanup ? () : void; } type DiffingPluginWrapperFactory = (inputTrees: (BroccoliTree | BroccoliTree[]), options?) => BroccoliTree; class DiffingPluginWrapper implements BroccoliTree { treeDiffer: TreeDiffer = null; treeDiffers: TreeDiffer[] = null; initialized = false; wrappedPlugin: DiffingBroccoliPlugin = null; inputTree = null; inputTrees = null; description = null; // props monkey-patched by broccoli builder: inputPath = null; inputPaths = null; cachePath = null; outputPath = null; private diffResult: DiffResult = null; constructor(private pluginClass, private wrappedPluginArguments) { if (Array.isArray(wrappedPluginArguments[0])) { this.inputTrees = this.stabilizeTrees(wrappedPluginArguments[0]); } else { this.inputTree = this.stabilizeTree(wrappedPluginArguments[0]); } this.description = this.pluginClass.name; } private getDiffResult(): (DiffResult | DiffResult[]) { let returnOrCalculateDiffResult = (tree, index) => { // returnOrCalculateDiffResult will do one of two things: // // If `this.diffResult` is null, calculate a DiffResult using TreeDiffer // for the input tree. // // Otherwise, `this.diffResult` was produced from the output of the // inputTree's rebuild() method, and can be used without being checked. // Set `this.diffResult` to null and return the previously stored value. let diffResult = tree.diffResult; if (diffResult) return diffResult; let differ = index === false ? this.treeDiffer : this.treeDiffers[index]; return differ.diffTree(); }; if (this.inputTrees) { return this.inputTrees.map(returnOrCalculateDiffResult); } else if (this.inputTree) { return returnOrCalculateDiffResult(this.inputTree, false); } else { throw new Error("Missing TreeDiffer"); } } private maybeStoreDiffResult(value: (DiffResult | void)) { if (!(value instanceof DiffResult)) value = null; this.diffResult = (value); } rebuild(): (Promise| void) { try { let firstRun = !this.initialized; this.init(); let diffResult = this.getDiffResult(); let result = this.wrappedPlugin.rebuild(diffResult); if (result) { let resultPromise = >(result); if (resultPromise.then) { // rebuild() -> Promise<> return resultPromise.then((result: (DiffResult | void)) => { this.maybeStoreDiffResult(result); this.relinkOutputAndCachePaths(); }); } } this.maybeStoreDiffResult(<(DiffResult | void)>(result)); this.relinkOutputAndCachePaths(); } catch (e) { e.message = `[${this.description}]: ${e.message}`; throw e; } } cleanup() { if (this.wrappedPlugin.cleanup) { this.wrappedPlugin.cleanup(); } } private relinkOutputAndCachePaths() { // just symlink the cache and output tree fs.rmdirSync(this.outputPath); symlinkOrCopy.sync(this.cachePath, this.outputPath); } private init() { if (!this.initialized) { let includeExtensions = this.pluginClass.includeExtensions || []; let excludeExtensions = this.pluginClass.excludeExtensions || []; let description = this.description; this.initialized = true; if (this.inputPaths) { this.treeDiffers = this.inputPaths.map((inputPath) => new TreeDiffer( description, inputPath, includeExtensions, excludeExtensions)); } else if (this.inputPath) { this.treeDiffer = new TreeDiffer(description, this.inputPath, includeExtensions, excludeExtensions); } this.wrappedPlugin = new this.pluginClass(this.inputPaths || this.inputPath, this.cachePath, this.wrappedPluginArguments[1]); } } private stabilizeTrees(trees: BroccoliTree[]) { // Prevent extensions to prevent array from being mutated from the outside. // For-loop used to avoid re-allocating a new array. for (let i = 0; i < trees.length; ++i) { trees[i] = this.stabilizeTree(trees[i]); } return Object.freeze(trees); } private stabilizeTree(tree: BroccoliTree) { // Ignore all DiffingPlugins as they are already stable, for others we don't know for sure // so we need to stabilize them. // Since it's not safe to use instanceof operator in node, we are checking the constructor.name. // // New-styler/rebuild trees should always be stable. let isNewStyleTree = !!(tree['newStyleTree'] || typeof tree.rebuild === 'function' || tree['isReadAPICompatTree'] || tree.constructor['name'] === 'Funnel'); return isNewStyleTree ? tree : stabilizeTree(tree); } }