Skip to content

Instantly share code, notes, and snippets.

@wearhere
Last active July 28, 2017 03:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wearhere/0e1b6dff58191c8e2c0a545ff5477a76 to your computer and use it in GitHub Desktop.
Save wearhere/0e1b6dff58191c8e2c0a545ff5477a76 to your computer and use it in GitHub Desktop.
Draft of module for adding custom helpers as well as a bind operator that caches. Solution for https://github.com/tc39/proposal-bind-operator/issues/46.
var f = ctx::ns.obj.func;
var g = ::ns.obj.func;
var h = new X::y;
import template from 'babel-template';
const programCache = new WeakMap();
/**
* Adds a custom helper function to the program.
*
* @param {Path} path - The AST path at which you want to use the helper.
* @param {String} name - The name of the helper. This is primarily useful for
* reading the transpiled output. It does not need to be globally unique
* --this function will create a new unique identifier from this name.
* @param {String} body - The body of the helper. Put "HELPER" wherever you
* reference the function by name.
* @return {Identifier} The identifier of the helper. This is unique per
* <program, name, body> tuple.
*
* @throws {Error} If a function with a different body is already registered for `name`.
*/
export default function(path, name, body) {
if (!body.includes('HELPER')) {
throw new Error('Cannot register helper ("HELPER" token is missing).');
}
const program = path.scope.getProgramParent();
let names = programCache.get(program);
if (!names) programCache.set(program, names = new Map());
let helpers = names.get(name);
if (!helpers) names.set(name, helpers = new Map());
let id = helpers.get(body);
if (!id) {
id = program.path.scope.generateUidIdentifier(name);
const fn = template(body)({ HELPER: id });
program.path.unshiftContainer('body', fn);
helpers.set(body, id);
}
return id;
}
import addCustomHelper from './add-custom-helper';
export default function({ types: t }) {
function getTempId(scope) {
let id = scope.path.getData('functionBind');
if (id) return id;
id = scope.generateDeclaredUidIdentifier('context');
return scope.path.setData('functionBind', id);
}
function getStaticContext(bind, scope) {
const object = bind.object || bind.callee.object;
return scope.isStatic(object) && object;
}
function inferBindContext(bind, scope) {
const staticContext = getStaticContext(bind, scope);
if (staticContext) return staticContext;
const tempId = getTempId(scope);
if (bind.object) {
bind.callee = t.sequenceExpression([
t.assignmentExpression('=', tempId, bind.object),
bind.callee
]);
} else {
bind.callee.object = t.assignmentExpression('=', tempId, bind.callee.object);
}
return tempId;
}
const helperTemplate = `
const HELPER = (function() {
const cache = new WeakMap();
return function(fn, obj) {
let fns = cache.get(obj);
if (!fns) cache.set(obj, fns = new WeakMap());
let bound = fns.get(fn);
if (!bound) fns.set(fn, bound = fn.bind(obj));
return bound;
}
})();
`;
return {
inherits: require('babel-plugin-syntax-function-bind'),
visitor: {
CallExpression({ node, scope }) {
const bind = node.callee;
if (!t.isBindExpression(bind)) return;
const context = inferBindContext(bind, scope);
node.callee = t.memberExpression(bind.callee, t.identifier('call'));
node.arguments.unshift(context);
},
BindExpression(path) {
const { node, scope } = path;
const context = inferBindContext(node, scope);
const helperId = addCustomHelper(path, 'bindCache', helperTemplate);
path.replaceWith(t.callExpression(helperId, [node.callee, context]));
}
}
};
}
const lib = {
foo() {}
};
assert.equal(::lib.foo, ::lib.foo);
var _context;
const _bindCache = function () {
const cache = new WeakMap();
return function (fn, obj) {
let fns = cache.get(obj);
if (!fns) cache.set(obj, fns = new WeakMap());
let bound = fns.get(fn);
if (!bound) fns.set(fn, bound = fn.bind(obj));
return bound;
};
}();
var f = _bindCache((_context = ctx, ns.obj.func), _context);
var g = _bindCache((_context = ns.obj).func, _context);
var h = _bindCache((_context = new X(), y), _context);
@wearhere
Copy link
Author

wearhere commented Jul 28, 2017

add-custom-helper.js is modelled after https://github.com/github/babel-plugin-transform-custom-element-classes/blob/9216cb84612e575bb3fea20209eb9e3ea50c1b65/lib/index.js#L11. I've had difficulty finding documentation for Babel's AST APIs (https://github.com/thejameskyle/babel-handbook shows examples, but only of a subset of APIs, and doesn't really document parameters anyhow) so I'm not sure if I'm using the right APIs. Seems there might be a few ways to do things too 🙂.

babel-plugin-transform-bind-cache.js is forked from https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-function-bind, with my cache implementation roughly modelled after tc39/proposal-bind-operator#46 (comment).

It successfully transpiles actual.js into expected.js and passes the assert in exec.js, as well as passes all the tests from https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-function-bind (updated for the new implementation), so as far as I'm concerned this is done, and am totally glad to put this up in a PR against that module or actually publish this, just wanted to get some feedback first—it's my first Babel plugin. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment