Skip to content

Commit

Permalink
Merge pull request emberjs#89 from babel/rwjblue-patch-1
Browse files Browse the repository at this point in the history
Allow plugins to bust the cache.
  • Loading branch information
rwjblue authored Aug 11, 2016
2 parents adde9b9 + 3f493c6 commit cd9aec9
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 5 deletions.
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Each one of them builds on top of the previous example so you can progess from b
* [es6-fruits](https://github.com/givanse/broccoli-babel-examples/tree/master/es6-fruits) - Execute a single ES6 script.
* [es6-website](https://github.com/givanse/broccoli-babel-examples/tree/master/es6-website) - Build a simple website.
* [es6-modules](https://github.com/givanse/broccoli-babel-examples/tree/master/es6-modules) - Handle modules and unit tests.

## About source map

Currently this plugin only supports inline source map. If you need
Expand Down Expand Up @@ -64,3 +64,40 @@ You don't always need this, review which features need the polyfill here: [ES6 F
var esTranspiler = require('broccoli-babel-transpiler');
var scriptTree = esTranspiler(inputTree, { browserPolyfill: true });
```

## Plugins

Use of custom plugins works similarly to `babel` itself. You would pass a `plugins` array in `options`:

```js
var esTranspiler = require('broccoli-babel-transpiler');
var applyFeatureFlags = require('babel-plugin-feature-flags');

var featureFlagPlugin = applyFeatureFlags({
import: { module: 'ember-metal/features' },
features: {
'ember-metal-blah': true
}
});

var scriptTree = esTranspiler(inputTree, {
plugins: [
featureFlagPlugin
]
});
```

### Caching

broccoli-babel-transpiler uses a persistent cache to enable rebuilds to be significantly faster (by avoiding transpilation for files that have not changed).
However, since a plugin can do many things to affect the transpiled output it must also influence the cache key to ensure transpiled files are rebuilt
if the plugin changes (or the plugins configuration).

In order to aid plugin developers in this process, broccoli-babel-transpiler will invoke two methods on a plugin so that it can augment the cache key:

* `cacheKey` - This method is used to describe any runtime information that may want to invalidate the cached result of each file transpilation. This is
generally only needed when the configuration provided to the plugin is used to modify the AST output by a plugin like `babel-plugin-filter-imports` (module
exports to strip from a build), `babel-plugin-feature-flags` (configured features and current status to strip or embed in a final build), or
`babel-plugin-htmlbars-inline-precompile` (uses `ember-template-compiler.js` to compile inlined templates).
* `baseDir` - This method is expected to return the plugins base dir. The provided `baseDir` is used to ensure the cache is invalidated if any of the
plugin's files change (including its deps). Each plugin should implement `baseDir` as: `Plugin.prototype.baseDir = function() { return \_\_dirname; };`.
5 changes: 5 additions & 0 deletions fixtures/plugin-a/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";

module.exports = function() {
return 'plugin-a goes here but this isnt in plugin-b';
};
5 changes: 5 additions & 0 deletions fixtures/plugin-a/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "plugin-a",
"version": "1.0.0",
"main": "index.js"
}
5 changes: 5 additions & 0 deletions fixtures/plugin-b/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";

module.exports = function() {
return 'plugin-b';
};
5 changes: 5 additions & 0 deletions fixtures/plugin-b/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "plugin-b",
"version": "1.0.0",
"main": "index.js"
}
65 changes: 65 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var stringify = require('json-stable-stringify');
var mergeTrees = require('broccoli-merge-trees');
var funnel = require('broccoli-funnel');
var crypto = require('crypto');
var hashForDep = require('hash-for-dep');

function getExtensionsRegex(extensions) {
return extensions.map(function(extension) {
Expand All @@ -34,6 +35,10 @@ function Babel(inputTree, _options) {
Filter.call(this, inputTree, options);

delete options.persist;

this.console = options.console || console;
delete options.console;

this.options = options;
this.moduleMetadata = {};
this.extensions = this.options.filterExtensions || ['js'];
Expand Down Expand Up @@ -111,6 +116,66 @@ Babel.prototype.optionsHash = function() {
hash[key] = (typeof value === 'function') ? (value + '') : value;
}

if (options.plugins) {
hash.plugins = [];

var cacheableItems = options.plugins.slice();

for (var i = 0; i < cacheableItems.length; i++) {
var item = cacheableItems[i];

var type = typeof item;
var augmentsCacheKey = false;
var providesBaseDir = false;
var requiresBaseDir = true;

if (type === 'function') {
augmentsCacheKey = typeof item.cacheKey === 'function';
providesBaseDir = typeof item.baseDir === 'function';

if (augmentsCacheKey) {
hash.plugins.push(item.cacheKey());
}

if (providesBaseDir) {
var depHash = hashForDep(item.baseDir());

hash.plugins.push(depHash);
}

if (!providesBaseDir && requiresBaseDir){
// prevent caching completely if the plugin doesn't provide baseDir
// we cannot ensure that we aren't causing invalid caching pain...
this.console.warn('broccoli-babel-transpiler is opting out of caching due to a plugin that does not provide a caching strategy: `' + item + '`.');
hash.plugins.push((new Date).getTime() + '|' + Math.random());
break;
}
} else if (Array.isArray(item)) {
item.forEach(function(part) {
cacheableItems.push(part);
});

continue;
} else if (type !== 'object' || item === null) {
// handle native strings, numbers, or null (which can JSON.stringify properly)
hash.plugins.push(item);
continue;
} else if (type === 'object') {
// itereate all keys in the item and push them into the cache
var keys = Object.keys(item);
keys.forEach(function(key) {
cacheableItems.push(key);
cacheableItems.push(item[key]);
});
continue;
} else {
this.console.warn('broccoli-babel-transpiler is opting out of caching due to an non-cacheable item: `' + item + '` (' + type + ').');
hash.plugins.push((new Date).getTime() + '|' + Math.random());
break;
}
}
}

this._optionsHash = crypto.createHash('md5').update(stringify(hash), 'utf8').digest('hex');
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@
"homepage": "https://github.com/babel/broccoli-babel-transpiler",
"dependencies": {
"babel-core": "^6.0.0",
"broccoli-persistent-filter": "^1.0.1",
"broccoli-funnel": "^1.0.0",
"broccoli-merge-trees": "^1.0.0",
"broccoli-persistent-filter": "^1.0.1",
"clone": "^1.0.2",
"hash-for-dep": "^1.0.2",
"json-stable-stringify": "^1.0.0"
},
"devDependencies": {
Expand Down
152 changes: 149 additions & 3 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -416,12 +416,19 @@ describe('consume broccoli-babel-transpiler options', function() {
});

describe('when options change', function() {
var originalHash, options;
var originalHash, options, fakeConsole, consoleMessages;

beforeEach(function() {
fakeConsole = {
warn: function(message) { consoleMessages.push(message); }
};
consoleMessages = [];

options = {
bar: 1,
baz: function() {}
baz: function() {},
console: fakeConsole,
plugins: []
};

var babel = new Babel('foo', options);
Expand All @@ -431,29 +438,168 @@ describe('when options change', function() {

it('clears cache for added properties', function() {
options.foo = 1;
options.console = fakeConsole;
var babelNew = new Babel('foo', options);

expect(babelNew.optionsHash()).to.not.eql(originalHash);
});

it('includes object plugins cacheKey result in hash', function() {
options.plugins = [
{ cacheKey: function() { return 'hi!'; }}
];
options.console = fakeConsole;
var babelNew = new Babel('foo', options);

expect(babelNew.optionsHash()).to.not.eql(originalHash);
});

it('includes function plugins cacheKey result in hash', function() {
function fakePlugin() {}
fakePlugin.cacheKey = function() { return 'Hi!'; };

options.plugins = [
fakePlugin
];
options.console = fakeConsole;
var babelNew = new Babel('foo', options);

expect(babelNew.optionsHash()).to.not.eql(originalHash);
});

it('includes string plugins in hash calculation', function() {
options.plugins = [
'foo'
];
options.console = fakeConsole;
var babelNew = new Babel('foo', options);

expect(babelNew.optionsHash()).to.not.eql(originalHash);
});

it('includes plugins specified with options in hash calculation when cacheable', function() {
var pluginOptions = { foo: 'bar' };
options.plugins = [
['foo', pluginOptions]
];
options.console = fakeConsole;
var first = new Babel('foo', options);
var firstOptions = first.optionsHash();

options.console = fakeConsole;
var second = new Babel('foo', options);
var secondOptions = second.optionsHash();
expect(firstOptions).to.eql(secondOptions);

pluginOptions.qux = 'huzzah';
options.console = fakeConsole;
var third = new Babel('foo', options);
var thirdOptions = third.optionsHash();

expect(firstOptions).to.not.eql(thirdOptions);
});

it('invalidates plugins specified with options when not-cacheable', function() {
function thing() { }
var pluginOptions = { foo: 'bar', thing: thing };
options.plugins = [
['foo', pluginOptions]
];
options.console = fakeConsole;
var first = new Babel('foo', options);
var firstOptions = first.optionsHash();

options.console = fakeConsole;
var second = new Babel('foo', options);
var secondOptions = second.optionsHash();
expect(firstOptions).to.not.eql(secondOptions);
});

it('plugins specified with options can have functions with `baseDir`', function() {
var dir = path.join(inputPath, 'plugin-a');
function thing() { }
thing.baseDir = function() { return dir; };
var pluginOptions = { foo: 'bar', thing: thing };
options.plugins = [
['foo', pluginOptions]
];

options.console = fakeConsole;
var first = new Babel('foo', options);
var firstOptions = first.optionsHash();

options.console = fakeConsole;
var second = new Babel('foo', options);
var secondOptions = second.optionsHash();
expect(firstOptions).to.eql(secondOptions);

dir = path.join(inputPath, 'plugin-b');
options.console = fakeConsole;
var third = new Babel('foo', options);
var thirdOptions = third.optionsHash();

expect(firstOptions).to.not.eql(thirdOptions);
});

it('a plugins `baseDir` method is used for hash generation', function() {
var dir = path.join(inputPath, 'plugin-a');

function plugin() {}
plugin.baseDir = function() {
return dir;
};
options.plugins = [ plugin ];

options.console = fakeConsole;
var first = new Babel('foo', options);
var firstOptions = first.optionsHash();

dir = path.join(inputPath, 'plugin-b');
options.console = fakeConsole;
var second = new Babel('foo', options);
var secondOptions = second.optionsHash();

expect(firstOptions).to.not.eql(secondOptions);
});

it('a plugin without a baseDir invalidates the cache every time', function() {
function plugin() {}
plugin.toString = function() { return '<derp plugin>'; };
options.plugins = [ plugin ];

options.console = fakeConsole;
var babel1 = new Babel('foo', options);
options.console = fakeConsole;
var babel2 = new Babel('foo', options);

expect(babel1.optionsHash()).to.not.eql(babel2.optionsHash());
expect(consoleMessages).to.eql([
'broccoli-babel-transpiler is opting out of caching due to a plugin that does not provide a caching strategy: `<derp plugin>`.',
'broccoli-babel-transpiler is opting out of caching due to a plugin that does not provide a caching strategy: `<derp plugin>`.'
]);
});

it('clears cache for updated properties', function() {
options.bar = 2;
options.console = fakeConsole;
var babelNew = new Babel('foo', options);

expect(babelNew.optionsHash()).to.not.eql(originalHash);
});

it('clears cache for added methods', function() {
options.foo = function() {};
options.console = fakeConsole;
var babelNew = new Babel('foo', options);

expect(babelNew.optionsHash()).to.not.eql(originalHash);
});

it('clears cache for updated methods', function() {
options.baz = function() { return 1; };
options.console = fakeConsole;
var babelNew = new Babel('foo', options);

expect(babelNew.optionsHash()).to.not.eql(originalHash);
});
});
});

0 comments on commit cd9aec9

Please sign in to comment.