diff --git a/index.js b/index.js index c92a36f..e014924 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,9 @@ var copyDereferenceSync = require('copy-dereference').sync; var Cache = require('./lib/cache'); var debugGenerator = require('debug'); var keyForFile = require('./lib/key-for-file'); +var PersistentCache = require('async-disk-cache'); +var hashForDep = require('hash-for-dep'); +var md5Hex = require('md5-hex'); module.exports = Filter; @@ -43,6 +46,16 @@ function Filter(inputTree, options) { this.inputEncoding = options.inputEncoding; if (options.outputEncoding != null) this.outputEncoding = options.outputEncoding; + if (options.persist) { + if (/^win/.test(process.platform)) { + console.log('Unfortunately persistent cache is currently not available on windows based systems.'); + } else { + this.persistent = options.persist; + this._peristentCache = new PersistentCache(this.cacheKey(), { + compression: 'deflate' + }); + } + } } this._cache = new Cache(); @@ -75,6 +88,36 @@ Filter.prototype.build = function build() { }); }; +/* + * @private + * + * + * @method cachKey + * @return {String} this filters top-level cache key + */ +Filter.prototype.cacheKey = function() { + return hashForDep(this.baseDir()); +}; + +/* @public + * + * @method baseDir + * @returns {String} absolute path to the root of the filter... + */ +Filter.prototype.baseDir = function() { + throw Error('Filter must implement prototype.baseDir'); +}; + +/* + * @public + * + * @method cacheKeyProcessString + * @return {String} this filters top-level cache key + */ +Filter.prototype.cacheKeyProcessString = function(string) { + return md5Hex(string); +}; + Filter.prototype.canProcessFile = function canProcessFile(relativePath) { return !!this.getDestFilePath(relativePath); @@ -160,19 +203,32 @@ Filter.prototype.processFile = if (outputEncoding === void 0) outputEncoding = 'utf8'; var contents = fs.readFileSync( srcDir + '/' + relativePath, { encoding: inputEncoding }); + var promise; - 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 - }); - }); + if (this.persistent) { + var key = this.cacheKeyProcessString(contents, relativePath); + promise = this._peristentCache.get(key).then(function(entry) { + return entry.isCached ? entry.value : self.processString(contents, relativePath); + }); + } else { + promise = Promise.resolve(this.processString(contents, relativePath)); + } + + return promise.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 + }); + + if (self.persistent) { + return self._peristentCache.set(key, outputString); + } + }); }; Filter.prototype.processString = diff --git a/package.json b/package.json index 2bbedf0..114049c 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,13 @@ "cache" ], "dependencies": { + "async-disk-cache": "^1.0.0", "broccoli-kitchen-sink-helpers": "^0.2.7", "broccoli-plugin": "^1.0.0", "copy-dereference": "^1.0.0", "debug": "^2.2.0", + "hash-for-dep": "0.0.3", + "md5-hex": "^1.0.2", "mkdirp": "^0.5.1", "promise-map-series": "^0.2.1", "rsvp": "^3.0.18", diff --git a/test/fixtures/package.json b/test/fixtures/package.json new file mode 100644 index 0000000..bde99de --- /dev/null +++ b/test/fixtures/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/test/test.js b/test/test.js index 61217ec..e6785a7 100644 --- a/test/test.js +++ b/test/test.js @@ -45,6 +45,10 @@ ReplaceFilter.prototype.processString = function(contents, relativePath) { return result; }; +ReplaceFilter.prototype.baseDir = function() { + return '../'; +}; + function IncompleteFilter(inputTree, options) { if (!this) return new IncompleteFilter(inputTree, options); Filter.call(this, inputTree, options); @@ -290,6 +294,58 @@ describe('Filter', function() { to.equal('utf8'); }); + if (!/^win/.test(process.platform)) { + describe('persistent cache', function() { + var f; + function F(inputTree, options) { Filter.call(this, inputTree, options); } + inherits(F, Filter); + F.prototype.baseDir = function() { + return '../'; + }; + + beforeEach(function() { + f = new F(fixturePath, { persist: true }); + }); + + it('cache is initialized', function() { + expect(f._peristentCache).to.be.ok; + }); + + it('default `baseDir` implementation throws an Unimplemented Exception', function() { + function F(inputTree, options) { Filter.call(this, inputTree, options); } + inherits(F, Filter); + expect(function() { + new F(fixturePath, { persist: true }); + }).to.throw(/Filter must implement prototype.baseDir/); + }); + + it('`cacheKeyProcessString` return correct first level file cache', function() { + expect(f.cacheKeyProcessString('foo-bar-baz', 'relative-path')).to.eql('4c43793687f9a7170a9149ad391cbf70'); + }); + + it('filter properly reads file tree', function() { + var builder = makeBuilder(ReplaceFilter, fixturePath, function(awk) { + return awk; + }); + + return builder('dir', { + persist: true, + glob: '**/*.md', + search: 'dogs', + replace: 'cats' + }).then(function(results) { + expect(results.files).to.deep.eql([ + 'a/', + 'a/README.md', + 'a/bar/', + 'a/bar/bar.js', + 'a/foo.js' + ]); + }); + }); + }); + } + describe('processFile', function() { beforeEach(function() { sinon.spy(fs, 'mkdirSync');