DailyJS

Code Review: Burrito

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

code-review

Code Review: Burrito

Posted by Alex R. Young on .
Featured

code-review

Code Review: Burrito

Posted by Alex R. Young on .
*Code Review* is a series on DailyJS where I take a look at an open source project to see how it's built. Along the way we'll learn patterns and techniques by JavaScript masters. If you're looking for tips to write better apps, or just want to see how they're structured in established projects, then this is the tutorial series for you.

About Burrito

Burrito (npm: burrito) by James Halliday is a module that wraps useful functionality around an
Abstract Syntax Tree (AST) for JavaScript source. It's based on portions
of UglifyJS and uses UglifyJS to
generate the AST.

Usage

Burrito is a CommonJS module, so install it with npm then
require it:

var burrito = require('burrito');

burrito('var a = 1.0; a += Math.PI; Math.sin(a)', function(node) {
  console.log(node.name);
});

This walks over each node and logs node names. One of the author's
examples uses node.wrap to wrap each function call:

var burrito = require('burrito');

var src = burrito('f() && g(h())\nfoo()', function (node) {
  if (node.name === 'call') node.wrap('qqq(%s)');
});

console.log(src);

Now imagine the function qqq performs benchmarks, or code
coverage analysis; this is Burrito's niche.

Structure

The module is distributed with tests and examples. The library's source
comes in just one file. Most of the underlying work is performed by
UglifyJS and traverse. The
author has included developer dependencies in the package.json file, in
this case it's just Expresso.
This is always a good idea -- you don't want everyone to have to install
Expresso just to use your module.

In the main source file, burrito is defined and exported.
Most of the functions are relatively short, so the author has defined
things inline as they're needed rather than making a top heavy set of
require calls. Most of the code centres around
burrito.wrapNode.

This project makes good use of vm.runInNewContext(res,
context)
. The vm
module
,
provided by Node, includes several methods of executing JavaScript
without resorting to eval(). In this instance,
vm.runInNewContext is useful because it allows a context to
be created each time Burrito is used -- using eval would
execute the code in the local context giving access to local scope.

A local var for the module is exported as a function, which
is appropriate for this module, and some local functions are defined to
make calling uglify cleaner:

var deparse = function (ast, b) {
    return uglify.uglify.gen_code(ast, { beautify : b });
};

var parser = uglify.parser;
var parse = function (expr) {
    if (typeof expr !== 'string') throw 'expression should be a string';

    try {
        var ast = parser.parse.apply(null, arguments);

// ... snip
return ast;
};

var burrito = module.exports = function (code, cb) {
    var ast = parse(code.toString(), false, true);

    var ast_ = traverse(ast).map(function mapper () {
        wrapNode(this, cb);
    });

    return deparse(parse(deparse(ast_)), true);
};

The deparse and parse methods are mainly used
for convenience, but parse adds some additional exception
handling to make exceptions more readable.

As I mentioned, the real meat of the module is in wrapNode.
This creates a simple object that provides additional properties to make
working with the AST easier. It also defines the wrap
method exposed by the public API and used in the previous examples.
Wrapping each node is where the traverse module comes in
handy.

Tests

The tests are pretty solid. Most run through specific features of the
library, or presumably areas the author was concerned about. Here's an
example:

exports.binaryString = function () {
    var src = 'z(x + y)';
    var context = {
        x : 3,
        y : 4,
        z : function (n) { return n * 10 },
    };

    var res = burrito.microwave(src, context, function (node) {
        if (node.name === 'binary') {
            node.wrap('%a*2 - %b*2');
        }
    });

    assert.equal(res, 10 * (3*2 - 4*2));
};

Conclusion

Burrito is a nice little library, and I look forward to seeing what
people create with it. James seems to have a fascination with "meta"
JavaScript projects; I've looked at
node-browserify, js-traverse and a few others before (also his blog at substack.net is always
a lot of fun).

I initially felt like the main Burrito source file could have been
either split into separate files or possibly organised differently.
However, as I read through it I had no problem understanding and
navigating around the various functions. Once I got my head around
artefacts like deparse(parse(deparse(ast_)), true); I felt
confident that I could hack on it, at least a little bit.