qjs
Version:
Use the await keyword with Q promises to tame your async code
129 lines (111 loc) • 4.21 kB
JavaScript
var falafel = require('falafel');
var streamline = require('streamline/lib/callbacks/transform');
var streamlineRT = require('streamline/lib/callbacks/runtime');
var Q = require('q');
var comp = require('./compile');
var secretPrefix = '_qjsgeneratedcode_';
function rethrow(err, stack){
var str = stack.input;
var filename = stack.filename;
var lineno = stack.lineno;
var lines = str.split('\n')
, start = Math.max(lineno - 3, 0)
, end = Math.min(lines.length, lineno + 3);
// Error context
var context = lines.slice(start, end).map(function(line, i){
var curr = i + start + 1;
return (curr == lineno ? ' >> ' : ' ')
+ curr
+ '| '
+ line;
}).join('\n');
// Alter exception message
err.path = filename;
err.message = (filename || 'Unknown File') + ':'
+ lineno + '\n'
+ context + '\n\n'
+ err.message;
throw err;
}
function addStackTrace(module, output) {
var rethrow = 'catch (err) {' + secretPrefix + 'rethrow(err, ' + secretPrefix +'stack);}';
var statement = /Statement$/g;
output = falafel(output, function (node) {
if (node.type === 'BlockStatement' && /Function/g.test(node.parent.type)) {
node.update([
'{try ',
node.source(),
rethrow,
'}'
].join("\n"));
} else if (statement.test(node.type)) {
if (node.parent.type === 'BlockStatement') {
node.update(secretPrefix + 'stack.lineno = ' + node.loc.start.line + ';' + node.source());
}
}
});
// Adds the fancy stack trace meta info
return str = [
'try {',
output,
'} ', rethrow
].join("\n");
}
function async(fn, n) {
return function async() {
var args = Array.prototype.slice.call(arguments);
while (args.length < n) {
args.push(null);
}
return Q.nbind(fn).apply(this, args);
}
}
function await(promise, cb) {
if (Q.isFulfilled(promise)) {
cb(null, Q.nearer(promise));
} else {
Q.when(promise, function (val) {
cb(null, val);
}, cb).end();
}
}
function transformNode(node) {
if (node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && node.callee.name === 'await') {
if (node.arguments.length < 1) throw new Error('You must provide a promise to await.');
if (node.arguments.length > 1) throw new Error("You can't await more than one promise.");
node.update(node.source().replace(/\)$/g, ', _)'));
var parent = node.parent;
while (parent && parent.type !== 'FunctionDeclaration' && parent.type !== 'FunctionExpression') {
parent = parent.parent;
}
if (!parent) throw new Error('You must put your await expression inside a function.');
parent.asyncWrappNeeded = true;
} else if (node.asyncWrappNeeded) {
var prefix = '';
if (node.type === 'FunctionDeclaration') prefix = 'var ' + node.id.name + ' = ';
node.update(prefix + secretPrefix + 'async(' + node.source().replace(/^(function *\w*\([\w\,\ ]*)\)/g,
'$1' + (node.params.length > 0?', ':'') + '_)') + ', ' + node.params.length + ');');
}
}
function compiler( fn) {
var split = (/^(function *\w*\([\w\,\ ]*\) *\{)([\w\W]*)(\})$/g).exec(fn);
var source = split[2];
source = addStackTrace(module, source);
var output = source;
output = falafel(output, transformNode).toString();
//comp.debug(output);
output = streamline.transform(output, {sourceName: module.filename});
output = output.replace("require('streamline/lib/callbacks/runtime')", secretPrefix + 'rt');
return split[1] + output + split[3];
}
function compile(module, fn) {
var locals = {};
locals.await = await;
locals.Q = Q;
locals[secretPrefix + 'rt'] = streamlineRT;
locals[secretPrefix + 'stack'] = { lineno: 1, input: fn.toString(), filename: module.filename};
locals[secretPrefix + 'rethrow'] = rethrow;
locals[secretPrefix + 'async'] = async;
comp(module, fn, compiler, locals)();
}
module.exports = compile;