ember-legacy-class-transform
Version:
The default blueprint for ember-cli addons.
338 lines (294 loc) • 11.3 kB
JavaScript
'use strict'
var fs = require('fs')
var rimraf = require('rimraf');
var symlinkOrCopySync = require('symlink-or-copy').sync
var loggerGen = require('heimdalljs-logger');
var FSTree = require('fs-tree-diff');
var Entry = require('./entry');
var heimdall = require('heimdalljs');
var canSymlink = require('can-symlink')();
var defaultIsEqual = FSTree.defaultIsEqual;
function ApplyPatchesSchema() {
this.mkdir = 0;
this.rmdir = 0;
this.unlink = 0;
this.change = 0;
this.create = 0;
this.other = 0;
this.processed = 0;
this.linked = 0;
}
function unlinkOrRmrfSync(path) {
if (canSymlink) {
fs.unlinkSync(path);
} else {
rimraf.sync(path);
}
}
class MergeTrees {
constructor(inputPaths, outputPath, options) {
options = options || {}
var name = 'merge-trees:' + (options.annotation || '')
if (!Array.isArray(inputPaths)) {
throw new TypeError(name + ': Expected array, got: [' + inputPaths +']')
}
this._logger = loggerGen(name);
this.inputPaths = inputPaths
this.outputPath = outputPath
this.options = options
this._currentTree = FSTree.fromPaths([]);
}
merge() {
this._logger.debug('deriving patches');
var instrumentation = heimdall.start('derivePatches');
var fileInfos = this._mergeRelativePath('');
var entries = fileInfos.map(function(fileInfo) {
return fileInfo.entry;
});
var newTree = FSTree.fromEntries(entries);
var patches = this._currentTree.calculatePatch(newTree, isEqual);
instrumentation.stats.patches = patches.length;
instrumentation.stats.entries = entries.length;
instrumentation.stop();
this._currentTree = newTree;
instrumentation = heimdall.start('applyPatches', ApplyPatchesSchema);
try {
this._logger.debug('applying patches');
this._applyPatch(patches, instrumentation.stats);
} catch(e) {
this._logger.warn('patch application failed, starting from scratch');
// Whatever the failure, start again and do a complete build next time
this._currentTree = FSTree.fromPaths([]);
rimraf.sync(this.outputPath);
throw e;
}
instrumentation.stop();
}
_applyPatch(patch, instrumentation) {
patch.forEach(function(patch) {
var operation = patch[0];
var relativePath = patch[1];
var entry = patch[2];
var outputFilePath = this.outputPath + '/' + relativePath;
var inputFilePath = entry && entry.basePath + '/' + relativePath;
switch(operation) {
case 'mkdir': {
instrumentation.mkdir++;
return this._applyMkdir(entry, inputFilePath, outputFilePath);
}
case 'rmdir': {
instrumentation.rmdir++;
return this._applyRmdir(entry, inputFilePath, outputFilePath);
}
case 'unlink': {
instrumentation.unlink++;
return fs.unlinkSync(outputFilePath);
}
case 'create': {
instrumentation.create++;
return symlinkOrCopySync(inputFilePath, outputFilePath);
}
case 'change': {
instrumentation.change++;
return this._applyChange(entry, inputFilePath, outputFilePath);
}
}
}, this);
}
_applyMkdir(entry, inputFilePath, outputFilePath) {
if (entry.linkDir) {
return symlinkOrCopySync(inputFilePath, outputFilePath);
} else {
return fs.mkdirSync(outputFilePath);
}
}
_applyRmdir(entry, inputFilePath, outputFilePath) {
if (entry.linkDir) {
return unlinkOrRmrfSync(outputFilePath);
} else {
return fs.rmdirSync(outputFilePath);
}
}
_applyChange(entry, inputFilePath, outputFilePath) {
if (entry.isDirectory()) {
if (entry.linkDir) {
// directory copied -> link
fs.rmdirSync(outputFilePath);
return symlinkOrCopySync(inputFilePath, outputFilePath);
} else {
// directory link -> copied
//
// we don't check for `canSymlink` here because that is handled in
// `isLinkStateEqual`. If symlinking is not supported we will not get
// directory change operations
fs.unlinkSync(outputFilePath);
fs.mkdirSync(outputFilePath);
return
}
} else {
// file changed
fs.unlinkSync(outputFilePath);
return symlinkOrCopySync(inputFilePath, outputFilePath);
}
}
_mergeRelativePath(baseDir, possibleIndices) {
var inputPaths = this.inputPaths;
var overwrite = this.options.overwrite;
var result = [];
var isBaseCase = (possibleIndices === undefined);
// baseDir has a trailing path.sep if non-empty
var i, j, fileName, fullPath, subEntries;
// Array of readdir arrays
var names = inputPaths.map(function (inputPath, i) {
if (possibleIndices == null || possibleIndices.indexOf(i) !== -1) {
return fs.readdirSync(inputPath + '/' + baseDir).sort()
} else {
return []
}
})
// Guard against conflicting capitalizations
var lowerCaseNames = {}
for (i = 0; i < this.inputPaths.length; i++) {
for (j = 0; j < names[i].length; j++) {
fileName = names[i][j]
var lowerCaseName = fileName.toLowerCase()
// Note: We are using .toLowerCase to approximate the case
// insensitivity behavior of HFS+ and NTFS. While .toLowerCase is at
// least Unicode aware, there are probably better-suited functions.
if (lowerCaseNames[lowerCaseName] === undefined) {
lowerCaseNames[lowerCaseName] = {
index: i,
originalName: fileName
}
} else {
var originalIndex = lowerCaseNames[lowerCaseName].index
var originalName = lowerCaseNames[lowerCaseName].originalName
if (originalName !== fileName) {
throw new Error('Merge error: conflicting capitalizations:\n'
+ baseDir + originalName + ' in ' + this.inputPaths[originalIndex] + '\n'
+ baseDir + fileName + ' in ' + this.inputPaths[i] + '\n'
+ 'Remove one of the files and re-add it with matching capitalization.\n'
+ 'We are strict about this to avoid divergent behavior '
+ 'between case-insensitive Mac/Windows and case-sensitive Linux.'
)
}
}
}
}
// From here on out, no files and directories exist with conflicting
// capitalizations, which means we can use `===` without .toLowerCase
// normalization.
// Accumulate fileInfo hashes of { isDirectory, indices }.
// Also guard against conflicting file types and overwriting.
var fileInfo = {}
var inputPath;
var infoHash;
for (i = 0; i < inputPaths.length; i++) {
inputPath = inputPaths[i];
for (j = 0; j < names[i].length; j++) {
fileName = names[i][j]
// TODO: walk backwards to skip stating files we will just drop anyways
var entry = buildEntry(baseDir + fileName, inputPath);
var isDirectory = entry.isDirectory();
if (fileInfo[fileName] == null) {
fileInfo[fileName] = {
entry: entry,
isDirectory: isDirectory,
indices: [i] // indices into inputPaths in which this file exists
};
} else {
fileInfo[fileName].entry = entry;
fileInfo[fileName].indices.push(i)
// Guard against conflicting file types
var originallyDirectory = fileInfo[fileName].isDirectory
if (originallyDirectory !== isDirectory) {
throw new Error('Merge error: conflicting file types: ' + baseDir + fileName
+ ' is a ' + (originallyDirectory ? 'directory' : 'file')
+ ' in ' + this.inputPaths[fileInfo[fileName].indices[0]]
+ ' but a ' + (isDirectory ? 'directory' : 'file')
+ ' in ' + this.inputPaths[i] + '\n'
+ 'Remove or rename either of those.'
)
}
// Guard against overwriting when disabled
if (!isDirectory && !overwrite) {
throw new Error('Merge error: '
+ 'file ' + baseDir + fileName + ' exists in '
+ this.inputPaths[fileInfo[fileName].indices[0]] + ' and ' + this.inputPaths[i] + '\n'
+ 'Pass option { overwrite: true } to mergeTrees in order '
+ 'to have the latter file win.'
)
}
}
}
}
// Done guarding against all error conditions. Actually merge now.
for (i = 0; i < this.inputPaths.length; i++) {
for (j = 0; j < names[i].length; j++) {
fileName = names[i][j]
fullPath = this.inputPaths[i] + '/' + baseDir + fileName
infoHash = fileInfo[fileName]
if (infoHash.isDirectory) {
if (infoHash.indices.length === 1 && canSymlink) {
// This directory appears in only one tree: we can symlink it without
// reading the full tree
infoHash.entry.linkDir = true;
result.push(infoHash);
} else {
if (infoHash.indices[0] === i) { // avoid duplicate recursion
subEntries = this._mergeRelativePath(baseDir + fileName + '/', infoHash.indices);
// FSTreeDiff requires intermediate directory entries, so push
// `infoHash` (this dir) as well as sub entries.
result.push(infoHash);
result.push.apply(result, subEntries);
}
}
} else { // isFile
if (infoHash.indices[infoHash.indices.length-1] === i) {
result.push(infoHash);
} else {
// This file exists in a later inputPath. Do nothing here to have the
// later file win out and thus "overwrite" the earlier file.
}
}
}
}
if (isBaseCase) {
// FSTreeDiff requires entries to be sorted by `relativePath`.
return result.sort(function (a, b) {
var pathA = a.entry.relativePath;
var pathB = b.entry.relativePath;
if (pathA === pathB) {
return 0;
} else if (pathA < pathB) {
return -1;
} else {
return 1;
}
});
} else {
return result;
}
}
}
function isLinkStateEqual(entryA, entryB) {
// We don't symlink files, only directories
if (!(entryA.isDirectory() && entryB.isDirectory())) {
return true;
}
// We only symlink on systems that support it
if (!canSymlink) {
return true;
}
// This can change between rebuilds if a dir goes from existing in multiple
// input sources to exactly one input source, or vice versa
return entryA.linkDir === entryB.linkDir;
}
function isEqual(entryA, entryB) {
return defaultIsEqual(entryA, entryB) && isLinkStateEqual(entryA, entryB);
}
function buildEntry(relativePath, basePath) {
var stat = fs.statSync(basePath + '/' + relativePath);
return new Entry(relativePath, basePath, stat.mode, stat.size, stat.mtime);
}
module.exports = MergeTrees