Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 26 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
* 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
* 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
Expand All @@ -34,7 +35,7 @@ class Filter {
* `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.
*/
Expand Down Expand Up @@ -63,23 +64,29 @@ 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;
}

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);
};
```
Expand All @@ -92,33 +99,9 @@ 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?**
## Converting from `broccoli-filter`

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.
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.
61 changes: 39 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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.');
Expand All @@ -198,8 +213,10 @@ function hash(src, filePath) {
};
}

function symlinkOrCopyFromCache(entry, dest, relativePath) {
mkdirp.sync(path.dirname(entry.outputFile));
function symlinkOrCopyFromCache(entry) {
entry.results.forEach(function(result) {
mkdirp.sync(path.dirname(result.output));

symlinkOrCopySync(entry.cacheFile, dest + '/' + relativePath);
symlinkOrCopySync(result.cache, result.output);
});
}
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
"version": "1.2.3-multi.0",
"author": [
"Jo Liss <[email protected]>",
"Caitlin Potter <[email protected]>"
"Caitlin Potter <[email protected]>",
"Chris Eppstein <[email protected]>"
],
"main": "index.js",
"scripts": {
Expand All @@ -17,15 +18,16 @@
"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",
"broccoli-kitchen-sink-helpers": "^0.3.1",
"broccoli-plugin": "^1.0.0",
"copy-dereference": "^1.0.0",
"debug": "^2.2.0",
Expand Down
60 changes: 59 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what is going on here.

encoding: encoding
});
}
Expand Down Expand Up @@ -322,3 +334,49 @@ 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);
var lastDest;
return builder('dir',{}).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');
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();
});
});
});