qjs
Version:
Use the await keyword with Q promises to tame your async code
190 lines (177 loc) • 7.72 kB
JavaScript
var falafel = require('falafel');
var Q = require('q');
var comp = require('./compile');
var qjs = require('../');
var secretPrefix = '_qjsgeneratedcode_';
var id = 0;
var statementTransformers = {};
function transformAwait(node) {
if (node.arguments.length !== 1) throw new Error('Await must have exactly one argument');
var awaitValue = node.arguments[0].source();
var awaitID = secretPrefix + 'await_result_' + (id++);
if (node.callee.range[0] < node.range[0]) node.callee.update('');
node.update(awaitID);
node.parent.mustTransform = true;
if (!node.parent.awaitRequired) {
node.parent.awaitRequired = {source: awaitValue, key: awaitID};
} else {
throw new Error('Await already required');
}
}
function transformVariableDeclarator(node) {
transformUsingParent(node);
var fn = node.parent;
while (!(fn.parent.type === 'FunctionExpression' || fn.parent.type === 'FunctionDeclaration') && fn.parent) {
fn = fn.parent;
}
if (!(fn.parent.type === 'FunctionExpression' || fn.parent.type === 'FunctionDeclaration')) {
return;
}
var vars = (fn.vars = fn.vars || []);
node.declarations.forEach(function (decl) {
vars.push(decl.id.name);
});
var addSemicolon = /\;$/g.test(node.source());
node.update('(' + node.source().replace(/^var/g, ' ').replace(/\;$/g, '') + ')' + (addSemicolon?';':''));
}
statementTransformers['ReturnStatement'] = function transformReturnStatement(node) {
if (node.awaitRequired) {
node.update('return Q.when(' + node.awaitRequired.source + ', function _qjs(' + node.awaitRequired.key + ') { ' + node.source() + '});');
}
//node.parent.mustTransform = true;
};
statementTransformers['AssignmentExpression'] = transformUsingParent;
statementTransformers['BinaryExpression'] = transformUsingParent;
statementTransformers['UpdateExpression'] = transformUsingParent;
statementTransformers['CallExpression'] = transformUsingParent;
statementTransformers['MemberExpression'] = function transformMemberExpression(node) {
if (node.parent.type === 'BlockStatement') transformExpressionStatement(node);
else transformUsingParent(node);
}
statementTransformers['ExpressionStatement'] = transformExpressionStatement;
function transformExpressionStatement(node) {
var supportedBlockStatements = {};
supportedBlockStatements['FunctionDeclaration'] = true;
supportedBlockStatements['FunctionExpression'] = true;
supportedBlockStatements['WhileStatement'] = true;
supportedBlockStatements['ForStatement'] = true;
supportedBlockStatements['IfStatement'] = true;
if (node.awaitRequired) {
if (node.parent.type === 'BlockStatement' && supportedBlockStatements[node.parent.parent.type]) {
node.update('return Q.when(' + node.awaitRequired.source + ', function _qjs(' + node.awaitRequired.key + ') { ' + node.source());
node.parent.mustTransform = true;
node.parent.addEndings = node.parent.addEndings ? node.parent.addEndings + 1 : 1;
} else {
console.log(node);
console.log(node.parent.type);
console.log(node.parent.parent.type);
}
} else {
node.parent.mustTransform = node.parent.mustTransform || node.mustTransform;
}
};
function transformBlockStatement(node) {
node.parent.mustTransform = node.parent.mustTransform || node.mustTransform;
var src = node.source();
while (node.addEndings && (node.addEndings--)) {
src += ')}';
}
if (node.vars) {
src = src.replace(/^\{/g, '{var ' + node.vars.join(', ') + ';');
}
node.update(src);
};
statementTransformers['WhileStatement'] = function transformWhileStatement(node) {
var test = node.test.source();
if (node.awaitRequired) {
test = 'Q.when(' + node.awaitRequired.source + ', function _qjs(' + node.awaitRequired.key + ') {return ' + test + '});';
}
node.update('return ' + secretPrefix + 'qjs' + '.while(function () { return ' + test + '; }).do(function () ' + node.body.source() + ')' +
'.continue().then(function () {');
node.parent.mustTransform = true;
node.parent.addEndings = node.parent.addEndings ? node.parent.addEndings + 1 : 1;
};
statementTransformers['ForStatement'] = function transformForStatement(node) {
node.update('return ' + secretPrefix + 'qjs' + '.for(' +
node.init.source() +
', function () { return ' + node.test.source() +
'; }, function () { return ' + node.update.source() + '; }).do(function () ' + node.body.source() + ')' +
'.continue().then(function () {');
node.parent.mustTransform = true;
node.parent.addEndings = node.parent.addEndings ? node.parent.addEndings + 1 : 1;
};
statementTransformers['IfStatement'] = function transformIfStatement(node) {
var result = 'return ' + secretPrefix + 'qjs' +
'.if(' + node.test.source() + ').then(function () { '
+ removeBraces(node.consequent.source()) + '})'
if (node.alternate) {
result += '.else(function () { ' + removeBraces(node.alternate.source()) + '})';
}
result += '.continue().then(function () {';
node.update(result);
node.parent.mustTransform = true;
node.parent.addEndings = node.parent.addEndings ? node.parent.addEndings + 1 : 1;
};
function transformUsingParent(node) {
if (!node.parent.awaitRequired) {
node.parent.awaitRequired = node.awaitRequired;
} else {
throw new Error('Await already required');
}
node.parent.mustTransform = node.parent.mustTransform || node.mustTransform;
}
function transformNode(node) {
if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
if (node.awaitRequired) {
console.log(node);
}
} else if (node.type === 'BlockStatement') {
transformBlockStatement(node);
} else if (node.type === 'VariableDeclaration') {
transformVariableDeclarator(node);
} else if (node.type === 'CallExpression' && node.callee && node.callee.name === 'await'){
return transformAwait(node);
} else if (node.mustTransform) {
if (statementTransformers[node.type]) {
return statementTransformers[node.type](node);
}
console.log(node);
console.log(node.awaitRequired);
} else if (node.type === 'WhileStatement') {
console.log(node);
}
}
function compiler( fn) {
var split = (/^(function *\w*\([\w\,\ ]*\) *\{)([\w\W]*)(\})$/g).exec(fn);
var source = split[2];
var output = source;
output = falafel(output, transformNode).toString();
//comp.debug(output);
return split[1] + output + split[3];
}
function compile(module, fn) {
if (arguments.length !== 2) throw new Error('Compile must be supplied with exactly 2 arguments: a module object and a function to compile.')
var locals = {};
locals.await = await;
locals.Q = Q;
locals[secretPrefix + 'stack'] = { lineno: 1, input: fn.toString(), filename: module.filename};
locals[secretPrefix + 'rethrow'] = rethrow;
locals[secretPrefix + 'qjs'] = qjs;
var compiled = comp(module, fn, compiler, locals);
//comp.debug(compiled);
compiled();
}
module.exports = compile;
function await(val) {
return val;
}
function rethrow(err) {
throw err;
}
function removeBraces(str) {
if (/^\{/g.test(str) && /\}$/g.test(str)) {
return str.substr(1, str.length - 2);
} else {
return str;
}
}