What is broccoli-persistent-filter?
broccoli-persistent-filter is a Broccoli plugin that provides a base class for creating filters that can cache their results to improve build performance. It is particularly useful for tasks that involve transforming files, such as transpiling, minifying, or linting.
What are broccoli-persistent-filter's main functionalities?
Basic Filtering
This feature allows you to create a custom filter by extending the `Filter` class and implementing the `processString` method. The `processString` method is where you define the transformation logic for the file contents.
const Filter = require('broccoli-persistent-filter');
class MyFilter extends Filter {
constructor(inputNode, options) {
super(inputNode, options);
}
processString(contents, relativePath) {
// Perform some transformation on the file contents
return contents.toUpperCase();
}
}
module.exports = MyFilter;
Caching
This feature demonstrates how to implement caching in your custom filter. By overriding the `cacheKeyProcessString` method, you can generate a cache key based on the file contents, which helps in reusing the cached results for unchanged files.
const Filter = require('broccoli-persistent-filter');
class MyFilter extends Filter {
constructor(inputNode, options) {
super(inputNode, options);
this.extensions = ['js'];
this.targetExtension = 'js';
}
processString(contents, relativePath) {
// Perform some transformation on the file contents
return contents.toUpperCase();
}
cacheKeyProcessString(contents, relativePath) {
// Generate a cache key based on the file contents
return contents;
}
}
module.exports = MyFilter;
Handling Multiple File Types
This feature shows how to handle multiple file types by setting the `extensions` property. The `targetExtension` property specifies the extension of the output files.
const Filter = require('broccoli-persistent-filter');
class MyFilter extends Filter {
constructor(inputNode, options) {
super(inputNode, options);
this.extensions = ['js', 'css'];
this.targetExtension = 'txt';
}
processString(contents, relativePath) {
// Perform some transformation on the file contents
return contents.toUpperCase();
}
}
module.exports = MyFilter;
Other packages similar to broccoli-persistent-filter
broccoli-filter
broccoli-filter is a base class for Broccoli plugins that process files. It is similar to broccoli-persistent-filter but does not include built-in caching mechanisms. Developers need to implement their own caching if required.
broccoli-plugin
broccoli-plugin is a lower-level base class for creating Broccoli plugins. It provides more flexibility but requires more boilerplate code compared to broccoli-persistent-filter. It does not include built-in filtering or caching functionalities.
broccoli-caching-writer
broccoli-caching-writer is another base class for Broccoli plugins that provides caching capabilities. It is similar to broccoli-persistent-filter but focuses more on writing files to the output directory efficiently.
broccoli-persistent-filter
Helper base class for Broccoli plugins that map input files into output files. Except with a persistent cache to fast restarts.
one-to-one.
API
class Filter {
constructor(inputNode: BroccoliNode, options: FilterOptions): Filter;
abstract processString(contents: string, relativePath: string): {string | object };
virtual getDestFilePath(relativePath: string): string;
postProcess(results: object, relativePath: string): object
}
Options
extensions
: An array of file extensions to process, e.g. ['md', 'markdown']
.targetExtension
: The file extension of the corresponding output files, e.g.
'html'
.inputEncoding
: The character encoding used for reading input files to be
processed (default: 'utf8'
). For binary files, pass null
to receive a
Buffer
object in processString
.outputEncoding
: The character encoding used for writing output files after
processing (default: 'utf8'
). For binary files, pass null
and return a
Buffer
object from processString
.name
, annotation
: Same as
broccoli-plugin;
see there.
All options except name
and annotation
can also be set on the prototype
instead of being passed into the constructor.
Example Usage
var Filter = require('broccoli-filter');
Awk.prototype = Object.create(Filter.prototype);
Awk.prototype.constructor = Awk;
function Awk(inputNode, search, replace, options) {
options = options || {};
Filter.call(this, inputNode, {
annotation: options.annotation
});
this.search = search;
this.replace = replace;
}
Awk.prototype.extensions = ['txt'];
Awk.prototype.targetExtension = 'txt';
Awk.prototype.processString = function(content, relativePath) {
return content.replace(this.search, this.replace);
};
In Brocfile.js
, use your new Awk
plugin like so:
var node = new Awk('docs', 'ES6', 'ECMAScript 2015');
module.exports = node;
Persistent Cache
Adding persist flag allows a subclass to persist state across restarts. This exists to mitigate the upfront cost of some more expensive transforms on warm boot. It does not aim to improve incremental build performance, if it does, it should indicate something is wrong with the filter or input filter in question.
How does it work?
It does so but establishing a 2 layer file cache. The first layer, is the entire bucket.
The second, cacheKeyProcessString
is a per file cache key.
Together, these two layers should provide the right balance of speed and sensibility.
The bucket level cacheKey must be stable but also never become stale. If the key is not
stable, state between restarts will be lost and performance will suffer. On the flip-side,
if the cacheKey becomes stale changes may not be correctly reflected.
It is configured by subclassing and refining cacheKey
method. A good key here, is
likely the name of the plugin, its version and the actual versions of its dependencies.
Subclass.prototype.cacheKey = function() {
return md5(Filter.prototype.call(this) + inputOptionsChecksum + dependencyVersionChecksum);
}
The second key, represents the contents of the file. Typically the base-class's functionality
is sufficient, as it merely generates a checksum of the file contents. If for some reason this
is not sufficient, it can be re-configured via subclassing.
Subclass.prototype.cacheKeyProcessString = function(string, relativePath) {
return superAwesomeDigest(string);
}
It is recommended that persistent re-builds is opt-in by the consuming plugin author, as if no reasonable cache key can be created it should not be used.
var myTree = new SomePlugin('lib', { persist: true });
Warning
By using the persistent cache, a lot of small files will be created on the disk without being deleted.
This might use all the inodes of your disk.
You need to make sure to clean regularly the old files or configure your system to do so.
On OSX, files that aren't accessed in three days are deleted from /tmp
.
On systems using systemd, systemd-tmpfiles should already be present and regularly clean up the /tmp
directory.
On Debian-like systems, you can use tmpreaper.
On RedHad-like systems, you can use tmpwatch.
By default, the files are stored in the operatin system's default directory for temporary files,
but you can change this location by setting the BROCCOLI_PERSISTENT_FILTER_CACHE_ROOT
environment variable to the path of another folder.
FAQ
Upgrading from 0.1.x to 1.x
You must now call the base class constructor. For example:
function MyPlugin(inputTree) {
this.inputTree = inputTree;
}
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.