From 055913be1b49adf3eac2a9c72b471436295eb9fe Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Fri, 15 Jan 2016 11:26:11 -0800 Subject: [PATCH 1/7] Updated for new API, removed caveats. --- README.md | 62 +++++++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 786ffd4..2f2cabe 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,31 @@ -# broccoli-filter - -[![Build Status](https://travis-ci.org/broccolijs/broccoli-filter.svg?branch=master)](https://travis-ci.org/broccolijs/broccoli-filter) -[![Build status](https://ci.appveyor.com/api/projects/status/hc68s0vbn9di4ehi/branch/master?svg=true)](https://ci.appveyor.com/project/joliss/broccoli-filter/branch/master) +# broccoli-multi-filter Helper base class for Broccoli plugins that map input files into output files -one-to-one. +one-to-many. This class is a drop-in replacement for [`broccoli-filter`](https://github.com/broccolijs/broccoli-filter). ## API ```js -class Filter { +class MultiFilter { /** * Abstract base-class for filtering purposes. * * Enforces that it is invoked on an instance of a class which prototypically * inherits from Filter, and which is not itself Filter. */ - constructor(inputNode: BroccoliNode, options: FilterOptions): Filter; + constructor(inputNode: BroccoliNode, options: FilterOptions): MultiFilter; /** * Abstract method `processString`: must be implemented on subclasses of * Filter. + * + * The `addOutputFile` callback accepts two arguments `(contents: string, outputRelativeFilename: string)` + * this file must be called to generate any side-effect files and make sure they are handled properly with + * the caching layer. * * The return value is written as the contents of the output file */ - abstract processString(contents: string, relativePath: string): string; + abstract processString(contents: string, relativePath: string, addOutputFile: Function): string; /** * Virtual method `getDestFilePath`: determine whether the source file should @@ -63,15 +64,16 @@ instead of being passed into the constructor. ### Example Usage ```js -var Filter = require('broccoli-filter'); +var MultiFilter = require('broccoli-multi-filter'); -Awk.prototype = Object.create(Filter.prototype); +Awk.prototype = Object.create(MultiFilter.prototype); Awk.prototype.constructor = Awk; function Awk(inputNode, search, replace, options) { options = options || {}; - Filter.call(this, inputNode, { + MultiFilter.call(this, inputNode, { annotation: options.annotation }); + this.keepOriginal = options.keepOriginal; this.search = search; this.replace = replace; } @@ -79,7 +81,12 @@ function Awk(inputNode, search, replace, options) { Awk.prototype.extensions = ['txt']; Awk.prototype.targetExtension = 'txt'; -Awk.prototype.processString = function(content, relativePath) { +Awk.prototype.processString = function(content, relativePath, addOutputFile) { + // Record the original content, but this could be a sourcemap file or any other side-effect. + // This can also be called multiple times -- once for each non-primary file. + if (this.keepOriginal) { + addOutputFile(content, relativePath + ".original"); + } return content.replace(this.search, this.replace); }; ``` @@ -91,34 +98,3 @@ var node = new Awk('docs', 'ES6', 'ECMAScript 2015'); module.exports = node; ``` - -## FAQ - -### Upgrading from 0.1.x to 1.x - -You must now call the base class constructor. For example: - -```js -// broccoli-filter 0.1.x: -function MyPlugin(inputTree) { - this.inputTree = inputTree; -} - -// broccoli-filter 1.x: -function MyPlugin(inputNode) { - Filter.call(this, inputNode); -} -``` - -Note that "node" is simply new terminology for "tree". - -### Source Maps - -**Can this help with compilers that are almost 1:1, like a minifier that takes -a `.js` and `.js.map` file and outputs a `.js` and `.js.map` file?** - -Not at the moment. I don't know yet how to implement this and still have the -API look beautiful. We also have to make sure that caching works correctly, as -we have to invalidate if either the `.js` or the `.js.map` file changes. My -plan is to write a source-map-aware uglifier plugin to understand this use -case better, and then extract common code back into this `Filter` base class. From b0a91a382fe2d4ae05189bb297c304178cc913f6 Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Fri, 15 Jan 2016 11:28:27 -0800 Subject: [PATCH 2/7] missed a couple references to `Filter` --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f2cabe..5dde345 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ class MultiFilter { /** * Abstract method `processString`: must be implemented on subclasses of - * Filter. + * MultiFilter. * * The `addOutputFile` callback accepts two arguments `(contents: string, outputRelativeFilename: string)` * this file must be called to generate any side-effect files and make sure they are handled properly with @@ -35,7 +35,7 @@ class MultiFilter { * `relativePath` to process the file with `processString`. Return a * different path to process the file with `processString` and rename it. * - * By default, if the options passed into the `Filter` constructor contain a + * By default, if the options passed into the `MultiFilter` constructor contain a * property `extensions`, and `targetExtension` is supplied, the first matching * extension in the list is replaced with the `targetExtension` option's value. */ From 99a1adf1e5712f8fe300f6965c8f739ad76c6228 Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Fri, 15 Jan 2016 11:33:19 -0800 Subject: [PATCH 3/7] add notes on how to convert from the original impl --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5dde345..c6bf146 100644 --- a/README.md +++ b/README.md @@ -98,3 +98,10 @@ var node = new Awk('docs', 'ES6', 'ECMAScript 2015'); module.exports = node; ``` + +## Converting from `broccoli-filter` + +1. Change your require to `broccoli-multi-filter`. +2. Make sure tests still pass. +3. Add the `addOutputFile` argument to your implementation of `processString`. +4. Call `addOutputFile` to generate any additional files you need to generate. From 5fbd55fc6f93d3c8fc903181d42fe1d7379f6fbd Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Fri, 15 Jan 2016 12:15:19 -0800 Subject: [PATCH 4/7] Update Filter to support one-to-many filtering. --- index.js | 57 ++++++++++++++++++++++++++++++++++------------------ package.json | 10 +++++---- test/test.js | 43 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/index.js b/index.js index 9799a05..37da421 100644 --- a/index.js +++ b/index.js @@ -117,9 +117,29 @@ Filter.prototype.processAndCacheFile = this._debug('cache prime: %s', relativePath); } + cacheEntry = { + hash: hash(srcDir, relativePath), + inputFile: relativePath, + results: [ + { + output: destDir + '/' + outputRelativeFile, + cache: self.cachePath + '/' + outputRelativeFile + } + ] + }; + + function addOutputFile(contents, outputRelativeFilename) { + var outputPath = path.join(destDir, outputRelativeFilename); + fs.writeFileSync(outputPath, contents, { encoding: self.outputEncoding }); + cacheEntry.results.push({ + output: destDir + '/' + outputRelativeFilename, + cache: self.cachePath + '/' + outputRelativeFilename + }); + } + return Promise.resolve(). then(function asyncProcessFile() { - return self.processFile(srcDir, destDir, relativePath); + return self.processFile(srcDir, destDir, relativePath, addOutputFile); }). then(copyToCache, // TODO(@caitp): error wrapper is for API compat, but is not particularly @@ -133,27 +153,22 @@ Filter.prototype.processAndCacheFile = }) 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)); - } + cacheEntry.results.forEach(function(result) { + if (fs.existsSync(result.cache)) { + fs.unlinkSync(result.cache); + } else { + mkdirp.sync(path.dirname(result.cache)); + } - copyDereferenceSync(entry.outputFile, entry.cacheFile); + copyDereferenceSync(result.output, result.cache); + }); - return self._cache.set(relativePath, entry); + return self._cache.set(relativePath, cacheEntry); } }; Filter.prototype.processFile = - function processFile(srcDir, destDir, relativePath) { + function processFile(srcDir, destDir, relativePath, addOutputFile) { var self = this; var inputEncoding = this.inputEncoding; var outputEncoding = this.outputEncoding; @@ -162,7 +177,7 @@ Filter.prototype.processFile = var contents = fs.readFileSync( srcDir + '/' + relativePath, { encoding: inputEncoding }); - return Promise.resolve(this.processString(contents, relativePath)). + return Promise.resolve(this.processString(contents, relativePath, addOutputFile)). then(function asyncOutputFilteredFile(outputString) { var outputPath = self.getDestFilePath(relativePath); if (outputPath == null) { @@ -177,7 +192,7 @@ Filter.prototype.processFile = }; Filter.prototype.processString = - function unimplementedProcessString(contents, relativePath) { + function unimplementedProcessString(contents, relativePath, addOutputFile) { throw new Error( 'When subclassing broccoli-filter you must implement the ' + '`processString()` method.'); @@ -199,7 +214,9 @@ function hash(src, filePath) { } function symlinkOrCopyFromCache(entry, dest, relativePath) { - mkdirp.sync(path.dirname(entry.outputFile)); + entry.results.forEach(function(result) { + mkdirp.sync(path.dirname(result.output)); - symlinkOrCopySync(entry.cacheFile, dest + '/' + relativePath); + symlinkOrCopySync(result.cache, dest + '/' + relativePath); + }); } diff --git a/package.json b/package.json index b276035..9ed0bcf 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { - "name": "broccoli-filter", + "name": "broccoli-multi-filter", "description": "Helper base class for Broccoli plugins that map input files into output files one-to-one", "version": "1.2.3", "author": [ "Jo Liss ", - "Caitlin Potter " + "Caitlin Potter ", + "Chris Eppstein " ], "main": "index.js", "scripts": { @@ -17,12 +18,13 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/broccolijs/broccoli-filter" + "url": "https://github.com/chriseppstein/broccoli-multi-filter" }, "keywords": [ "broccoli-helper", "filter", - "cache" + "cache", + "broccoli-filter" ], "dependencies": { "broccoli-kitchen-sink-helpers": "^0.2.7", diff --git a/test/test.js b/test/test.js index 9dfca91..c69ce03 100644 --- a/test/test.js +++ b/test/test.js @@ -52,6 +52,18 @@ function IncompleteFilter(inputTree, options) { inherits(IncompleteFilter, Filter); +function KeepOriginalFilter(inputTree, options) { + if (!this) return new KeepOriginalFilter(inputTree, options); + Filter.call(this, inputTree, options); +} +inherits(KeepOriginalFilter, Filter); + +KeepOriginalFilter.prototype.processString = function(contents, relativePath, addOutputFile) { + addOutputFile(contents, relativePath + ".original"); + return contents; +} +KeepOriginalFilter.extensions = ["js"]; + describe('Filter', function() { function makeBuilder(plugin, dir, prepSubject) { return makeTestHelper({ @@ -73,7 +85,7 @@ describe('Filter', function() { function write(relativePath, contents, encoding) { encoding = encoding === void 0 ? 'utf8' : encoding; mkdirp.sync(path.dirname(relativePath)); - fs.writeFileSync(relativePath, contents, { + fs.writKeepOriginalFiltereFileSync(relativePath, contents, { encoding: encoding }); } @@ -322,3 +334,32 @@ describe('Filter', function() { }); }); }); + +describe('MultiFilter', function() { + function read(relativePath, encoding) { + encoding = encoding === void 0 ? 'utf8' : encoding; + return fs.readFileSync(relativePath, encoding); + } + + function makeBuilder(plugin, dir, prepSubject) { + return makeTestHelper({ + subject: plugin, + fixturePath: dir, + prepSubject: prepSubject + }); + } + + afterEach(function() { + return cleanupBuilders(); + }); + + var noPrep = function(subject) { return subject; }; + it("should generate two files per input file", function() { + var builder = makeBuilder(KeepOriginalFilter, fixturePath, noPrep); + return builder('dir',{}).then(function(results) { + var kof = results.subject; + expect(read(results.directory + '/a/foo.js')). + to.equal(read(results.directory + '/a/foo.js.original')); + }); + }); +}); From fcbcd58c2f4ab5b982f69afe181f3e835b61fbda Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Mon, 18 Jan 2016 19:43:17 -0800 Subject: [PATCH 5/7] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ed0bcf..9aaedaf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "broccoli-multi-filter", "description": "Helper base class for Broccoli plugins that map input files into output files one-to-one", - "version": "1.2.3", + "version": "1.2.3-multi.0", "author": [ "Jo Liss ", "Caitlin Potter ", From f2f837e2c1fdbb713db05e0a754e1e184e410182 Mon Sep 17 00:00:00 2001 From: Kris Selden Date: Fri, 4 Mar 2016 12:02:22 -0800 Subject: [PATCH 6/7] add test to cover rebuild for multiple file output. --- index.js | 6 +++--- test/test.js | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 37da421..52f66e6 100644 --- a/index.js +++ b/index.js @@ -108,7 +108,7 @@ Filter.prototype.processAndCacheFile = if (cacheEntry.hash.hash === hashResult.hash) { this._debug('cache hit: %s', relativePath); - return symlinkOrCopyFromCache(cacheEntry, destDir, outputRelativeFile); + return symlinkOrCopyFromCache(cacheEntry); } else { this._debug('cache miss: %s \n - previous: %o \n - next: %o ', relativePath, cacheEntry.hash.key, hashResult.key); } @@ -213,10 +213,10 @@ function hash(src, filePath) { }; } -function symlinkOrCopyFromCache(entry, dest, relativePath) { +function symlinkOrCopyFromCache(entry) { entry.results.forEach(function(result) { mkdirp.sync(path.dirname(result.output)); - symlinkOrCopySync(result.cache, dest + '/' + relativePath); + symlinkOrCopySync(result.cache, result.output); }); } diff --git a/test/test.js b/test/test.js index c69ce03..8f492a4 100644 --- a/test/test.js +++ b/test/test.js @@ -356,10 +356,27 @@ describe('MultiFilter', function() { var noPrep = function(subject) { return subject; }; it("should generate two files per input file", function() { var builder = makeBuilder(KeepOriginalFilter, fixturePath, noPrep); + var lastDest; return builder('dir',{}).then(function(results) { - var kof = results.subject; expect(read(results.directory + '/a/foo.js')). - to.equal(read(results.directory + '/a/foo.js.original')); + to.equal('Nicest dogs in need of homes'); + expect(read(results.directory + '/a/foo.js.original')). + to.equal('Nicest dogs in need of homes'); + fs.writeFileSync(fixturePath + '/dir/a/foo.js', 'Nicest dogs in need of homes too'); + return results.builder(); + }).then(function (results) { + expect(read(results.directory + '/a/foo.js')). + to.equal('Nicest dogs in need of homes too'); + expect(read(results.directory + '/a/foo.js.original')). + to.equal('Nicest dogs in need of homes too'); + fs.writeFileSync(fixturePath + '/dir/a/foo.js', 'Nicest dogs in need of homes'); + return results.builder(); + }).then(function(results) { + expect(read(results.directory + '/a/foo.js')). + to.equal('Nicest dogs in need of homes'); + expect(read(results.directory + '/a/foo.js.original')). + to.equal('Nicest dogs in need of homes'); + return results.builder(); }); }); }); From 92e99f537c2deb549a59311a2dc6f184010bbd11 Mon Sep 17 00:00:00 2001 From: Huiyuan Yang Date: Mon, 15 Aug 2016 22:23:02 -0700 Subject: [PATCH 7/7] update to use broccoli-kitchen-sink-helpers 0.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9aaedaf..998d0ae 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "broccoli-filter" ], "dependencies": { - "broccoli-kitchen-sink-helpers": "^0.2.7", + "broccoli-kitchen-sink-helpers": "^0.3.1", "broccoli-plugin": "^1.0.0", "copy-dereference": "^1.0.0", "debug": "^2.2.0",