ember-introjs
Version:
An Ember Component for intro.js
206 lines (174 loc) • 6.37 kB
JavaScript
'use strict';
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var Promise = require('rsvp').Promise;
var Plugin = require('broccoli-plugin');
var helpers = require('broccoli-kitchen-sink-helpers');
var walkSync = require('walk-sync');
var mapSeries = require('promise-map-series');
var symlinkOrCopySync = require('symlink-or-copy').sync;
var copyDereferenceSync = require('copy-dereference').sync;
var Cache = require('./lib/cache');
var debugGenerator = require('debug');
var keyForFile = require('./lib/key-for-file');
module.exports = Filter;
Filter.prototype = Object.create(Plugin.prototype);
Filter.prototype.constructor = Filter;
function Filter(inputTree, options) {
if (!this || !(this instanceof Filter) ||
Object.getPrototypeOf(this) === Filter.prototype) {
throw new TypeError('Filter is an abstract class and must be sub-classed');
}
var name = 'broccoli-filter:' + (this.constructor.name);
if (this.description) {
name += ' > [' + this.description + ']';
}
this._debug = debugGenerator(name);
Plugin.call(this, [inputTree]);
/* Destructuring assignment in node 0.12.2 would be really handy for this! */
if (options) {
if (options.extensions != null)
this.extensions = options.extensions;
if (options.targetExtension != null)
this.targetExtension = options.targetExtension;
if (options.inputEncoding != null)
this.inputEncoding = options.inputEncoding;
if (options.outputEncoding != null)
this.outputEncoding = options.outputEncoding;
}
this._cache = new Cache();
this._canProcessCache = Object.create(null);
this._destFilePathCache = Object.create(null);
}
Filter.prototype.build = function build() {
var self = this;
var srcDir = this.inputPaths[0];
var destDir = this.outputPath;
var paths = walkSync(srcDir);
this._cache.deleteExcept(paths).forEach(function(key) {
fs.unlinkSync(this.cachePath + '/' + key);
}, this);
return mapSeries(paths, function rebuildEntry(relativePath) {
var destPath = destDir + '/' + relativePath;
if (relativePath.slice(-1) === '/') {
mkdirp.sync(destPath);
} else {
if (self.canProcessFile(relativePath)) {
return self.processAndCacheFile(srcDir, destDir, relativePath);
} else {
var srcPath = srcDir + '/' + relativePath;
symlinkOrCopySync(srcPath, destPath);
}
}
});
};
Filter.prototype.canProcessFile =
function canProcessFile(relativePath) {
return !!this.getDestFilePath(relativePath);
};
Filter.prototype.getDestFilePath = function getDestFilePath(relativePath) {
if (this.extensions == null) return relativePath;
for (var i = 0, ii = this.extensions.length; i < ii; ++i) {
var ext = this.extensions[i];
if (relativePath.slice(-ext.length - 1) === '.' + ext) {
if (this.targetExtension != null) {
relativePath =
relativePath.slice(0, -ext.length) + this.targetExtension;
}
return relativePath;
}
}
return null;
}
Filter.prototype.processAndCacheFile =
function processAndCacheFile(srcDir, destDir, relativePath) {
var self = this;
var cacheEntry = this._cache.get(relativePath);
var outputRelativeFile = self.getDestFilePath(relativePath);
if (cacheEntry) {
var hashResult = hash(srcDir, cacheEntry.inputFile);
if (cacheEntry.hash.hash === hashResult.hash) {
this._debug('cache hit: %s', relativePath);
return symlinkOrCopyFromCache(cacheEntry, destDir, outputRelativeFile);
} else {
this._debug('cache miss: %s \n - previous: %o \n - next: %o ', relativePath, cacheEntry.hash.key, hashResult.key);
}
} else {
this._debug('cache prime: %s', relativePath);
}
return Promise.resolve().
then(function asyncProcessFile() {
return self.processFile(srcDir, destDir, relativePath);
}).
then(copyToCache,
// TODO(@caitp): error wrapper is for API compat, but is not particularly
// useful.
// istanbul ignore next
function asyncProcessFileErrorWrapper(e) {
if (typeof e !== 'object' || e === null) e = new Error('' + e);
e.file = relativePath;
e.treeDir = srcDir;
throw e;
})
function copyToCache() {
var entry = {
hash: hash(srcDir, relativePath),
inputFile: relativePath,
outputFile: destDir + '/' + outputRelativeFile,
cacheFile: self.cachePath + '/' + outputRelativeFile
};
if (fs.existsSync(entry.cacheFile)) {
fs.unlinkSync(entry.cacheFile);
} else {
mkdirp.sync(path.dirname(entry.cacheFile));
}
copyDereferenceSync(entry.outputFile, entry.cacheFile);
return self._cache.set(relativePath, entry);
}
};
Filter.prototype.processFile =
function processFile(srcDir, destDir, relativePath) {
var self = this;
var inputEncoding = this.inputEncoding;
var outputEncoding = this.outputEncoding;
if (inputEncoding === void 0) inputEncoding = 'utf8';
if (outputEncoding === void 0) outputEncoding = 'utf8';
var contents = fs.readFileSync(
srcDir + '/' + relativePath, { encoding: inputEncoding });
return Promise.resolve(this.processString(contents, relativePath)).
then(function asyncOutputFilteredFile(outputString) {
var outputPath = self.getDestFilePath(relativePath);
if (outputPath == null) {
throw new Error('canProcessFile("' + relativePath + '") is true, but getDestFilePath("' + relativePath + '") is null');
}
outputPath = destDir + '/' + outputPath;
mkdirp.sync(path.dirname(outputPath));
fs.writeFileSync(outputPath, outputString, {
encoding: outputEncoding
});
});
};
Filter.prototype.processString =
function unimplementedProcessString(contents, relativePath) {
throw new Error(
'When subclassing broccoli-filter you must implement the ' +
'`processString()` method.');
};
function hash(src, filePath) {
var path = src + '/' + filePath;
var key = keyForFile(path);
return {
key: key,
hash: helpers.hashStrings([
path,
key.size,
key.mode,
key.mtime
])
};
}
function symlinkOrCopyFromCache(entry, dest, relativePath) {
mkdirp.sync(path.dirname(entry.outputFile));
symlinkOrCopySync(entry.cacheFile, dest + '/' + relativePath);
}