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');