diff --git a/tools/broccoli/broccoli-merge-trees.spec.ts b/tools/broccoli/broccoli-merge-trees.spec.ts
new file mode 100644
index 0000000000..f420a77fff
--- /dev/null
+++ b/tools/broccoli/broccoli-merge-trees.spec.ts
@@ -0,0 +1,47 @@
+///
+///
+
+let mockfs = require('mock-fs');
+import fs = require('fs');
+import {TreeDiffer} from './tree-differ';
+import {MergeTrees} from './broccoli-merge-trees';
+
+describe('MergeTrees', () => {
+ afterEach(() => mockfs.restore());
+
+ function mergeTrees(inputPaths, cachePath, options) {
+ return new MergeTrees(inputPaths, cachePath, options);
+ }
+
+ function MakeTreeDiffers(rootDirs) {
+ let treeDiffers = rootDirs.map((rootDir) => new TreeDiffer('MergeTrees', rootDir));
+ treeDiffers.diffTrees = () => { return treeDiffers.map(tree => tree.diffTree()); };
+ return treeDiffers;
+ }
+
+ function read(path) { return fs.readFileSync(path, "utf-8"); }
+
+ it('should copy the file from the right-most inputTree', () => {
+ let testDir: any = {
+ 'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
+ 'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})},
+ 'tree3': {'foo.js': mockfs.file({content: 'tree3/foo.js content', mtime: new Date(1000)})}
+ };
+ mockfs(testDir);
+ let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
+ let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {});
+ treeMerger.rebuild(treeDiffer.diffTrees());
+ expect(read('dest/foo.js')).toBe('tree3/foo.js content');
+
+ delete testDir.tree2['foo.js'];
+ delete testDir.tree3['foo.js'];
+ mockfs(testDir);
+ treeMerger.rebuild(treeDiffer.diffTrees());
+ expect(read('dest/foo.js')).toBe('tree1/foo.js content');
+
+ testDir.tree2['foo.js'] = mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)});
+ mockfs(testDir);
+ treeMerger.rebuild(treeDiffer.diffTrees());
+ expect(read('dest/foo.js')).toBe('tree2/foo.js content');
+ });
+});
diff --git a/tools/broccoli/broccoli-merge-trees.ts b/tools/broccoli/broccoli-merge-trees.ts
new file mode 100644
index 0000000000..e5d9bb67ef
--- /dev/null
+++ b/tools/broccoli/broccoli-merge-trees.ts
@@ -0,0 +1,86 @@
+import fs = require('fs');
+import fse = require('fs-extra');
+import path = require('path');
+var symlinkOrCopySync = require('symlink-or-copy').sync;
+import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
+
+function pathExists(filePath) {
+ try {
+ if (fs.statSync(filePath)) {
+ return true;
+ }
+ } catch (e) {
+ if (e.code !== "ENOENT") {
+ throw e;
+ }
+ }
+ return false;
+}
+
+function outputFileSync(sourcePath, destPath) {
+ let dirname = path.dirname(destPath);
+ fse.mkdirsSync(dirname, {fs: fs});
+ fse.removeSync(destPath);
+ symlinkOrCopySync(sourcePath, destPath);
+}
+
+export class MergeTrees implements DiffingBroccoliPlugin {
+ private mergedPaths: {[key: string]: number} = Object.create(null);
+
+ constructor(public inputPaths: string[], public cachePath: string, public options) {}
+
+ rebuild(treeDiffs: DiffResult[]) {
+ treeDiffs.forEach((treeDiff: DiffResult, index) => {
+ let inputPath = this.inputPaths[index];
+ let existsLater = (relativePath) => {
+ for (let i = treeDiffs.length - 1; i > index; --i) {
+ if (pathExists(path.join(this.inputPaths[i], relativePath))) {
+ return true;
+ }
+ }
+ return false;
+ };
+ let existsSooner = (relativePath) => {
+ for (let i = index - 1; i >= 0; --i) {
+ if (pathExists(path.join(this.inputPaths[i], relativePath))) {
+ return i;
+ }
+ }
+ return -1;
+ };
+ treeDiff.changedPaths.forEach((changedPath) => {
+ let inputTreeIndex = this.mergedPaths[changedPath];
+ if (inputTreeIndex !== index && !existsLater(changedPath)) {
+ inputTreeIndex = this.mergedPaths[changedPath] = index;
+ let sourcePath = path.join(inputPath, changedPath);
+ let destPath = path.join(this.cachePath, changedPath);
+ outputFileSync(sourcePath, destPath);
+ }
+ });
+
+ treeDiff.removedPaths.forEach((removedPath) => {
+ let inputTreeIndex = this.mergedPaths[removedPath];
+
+ // if inputTreeIndex !== index, this same file was handled during
+ // changedPaths handling
+ if (inputTreeIndex !== index) return;
+
+ let destPath = path.join(this.cachePath, removedPath);
+ fse.removeSync(destPath);
+ let newInputTreeIndex = existsSooner(removedPath);
+
+ // Update cached value (to either newInputTreeIndex value or undefined)
+ this.mergedPaths[removedPath] = newInputTreeIndex;
+
+ if (newInputTreeIndex >= 0) {
+ // Copy the file from the newInputTreeIndex inputPath if necessary.
+ let newInputPath = this.inputPaths[newInputTreeIndex];
+ let sourcePath = path.join(newInputPath, removedPath);
+ outputFileSync(sourcePath, destPath);
+ }
+ });
+ });
+ }
+}
+
+export default wrapDiffingPlugin(MergeTrees);
diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts
index 3df98400cf..e3abe11d23 100644
--- a/tools/broccoli/trees/browser_tree.ts
+++ b/tools/broccoli/trees/browser_tree.ts
@@ -3,7 +3,7 @@
var Funnel = require('broccoli-funnel');
var flatten = require('broccoli-flatten');
var htmlReplace = require('../html-replace');
-var mergeTrees = require('broccoli-merge-trees');
+import mergeTrees from '../broccoli-merge-trees';
var path = require('path');
var replace = require('broccoli-replace');
var stew = require('broccoli-stew');
diff --git a/tools/broccoli/trees/dart_tree.ts b/tools/broccoli/trees/dart_tree.ts
index b6848e8afc..b2c9b263e6 100644
--- a/tools/broccoli/trees/dart_tree.ts
+++ b/tools/broccoli/trees/dart_tree.ts
@@ -6,7 +6,7 @@ import {MultiCopy} from './../multi_copy';
import destCopy from '../broccoli-dest-copy';
var Funnel = require('broccoli-funnel');
var glob = require('glob');
-var mergeTrees = require('broccoli-merge-trees');
+import mergeTrees from '../broccoli-merge-trees';
var path = require('path');
var renderLodashTemplate = require('broccoli-lodash');
var replace = require('broccoli-replace');
diff --git a/tools/broccoli/trees/node_tree.ts b/tools/broccoli/trees/node_tree.ts
index 575197cf9f..d2f7a660ea 100644
--- a/tools/broccoli/trees/node_tree.ts
+++ b/tools/broccoli/trees/node_tree.ts
@@ -4,7 +4,7 @@ import destCopy from '../broccoli-dest-copy';
import compileWithTypescript from '../broccoli-typescript';
import transpileWithTraceur from '../traceur/index';
var Funnel = require('broccoli-funnel');
-var mergeTrees = require('broccoli-merge-trees');
+import mergeTrees from '../broccoli-merge-trees';
var path = require('path');
var renderLodashTemplate = require('broccoli-lodash');
var replace = require('broccoli-replace');