babel-plugin-async-to-promises
Version:
Transpile ES7 async/await to vanilla ES6 Promise chains
257 lines (224 loc) • 8.34 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
var _babelTypes = require('babel-types');
var _babelTemplate = require('babel-template');
var _babelTemplate2 = _interopRequireDefault(_babelTemplate);
var _jsExtend = require('js-extend');
var _utils = require('./utils');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
exports.default = {
LabeledStatement: {
// Babel seems to auto-remove labels from the AST if they don't make sense
// in a position. That makes it hard to keep track of if you're in a loop
// with label. So we move the label onto the node itself, and handle it
// manually (at least, if we're touching the loop, i.e. if it has an await
// somewhere inside).
enter: function enter(path) {
if ((0, _utils.containsAwait)(path)) {
path.node.body.loopLabel = path.node.label;
}
}
},
DoWhileStatement: function DoWhileStatement(path) {
// converts
//
// do {
// newBody;
// } while (node.test)
//
// into:
//
// await async function _recursive() {
// newBody;
// if (node.test) {
// return await _recursive();
// }
// }()
refactorLoop(path, false, this.addVarDecl, function (functionID) {
var continueBlock = (0, _babelTypes.blockStatement)([continueStatementEquiv(functionID)]);
path.node.body.body.push((0, _babelTypes.ifStatement)(path.node.test, continueBlock));
path.replaceWith(recursiveWrapFunction(functionID, path.node.body));
});
},
WhileStatement: function WhileStatement(path) {
// converts
//
// while (node.test) {
// newBody;
// }
//
// into:
//
// await async function _recursive() {
// if (node.test) {
// newBody;
// return await _recursive();
// }
// }()
refactorLoop(path, false, this.addVarDecl, function (functionID) {
path.node.body.body.push(continueStatementEquiv(functionID));
var body = (0, _babelTypes.blockStatement)([(0, _babelTypes.ifStatement)(path.node.test, path.node.body)]);
path.replaceWith(recursiveWrapFunction(functionID, body));
});
},
ForStatement: function ForStatement(path) {
// converts
//
// for(node.init, node.test, node.update) {
// newBody;
// }
//
// into:
//
// {
// node.init;
// await async function _recursive() {
// if (node.test) {
// newBody;
// node.update;
// return await _recursive();
// }
// }()
// }
ifShouldRefactorLoop(path, (0, _utils.containsAwait)(path.get('update')), function () {
path.node.body.body.push((0, _babelTypes.expressionStatement)(path.node.update));
path.replaceWithMultiple([(0, _babelTypes.expressionStatement)(path.node.init), (0, _babelTypes.whileStatement)(path.node.test, path.node.body)]);
});
},
ForInStatement: function ForInStatement(path) {
var _this = this;
// converts
// for (node.left in node.right) {
// newBody;
// }
//
// info:
//
// var _items = [];
// for (var _item in node.right) {
// _items.push(_item);
// }
// _items.reverse();
// await async function _recursive() {
// if (_items.length) {
// node.left = _items.pop();
// node.body;
// return await _recursive();
// }
// }
ifShouldRefactorLoop(path, false, function () {
var KEYS = (0, _babelTypes.identifier)(path.scope.generateUid('keys'));
var OBJECT = (0, _babelTypes.identifier)(path.scope.generateUid('object'));
_this.addVarDecl(KEYS);
_this.addVarDecl(OBJECT);
path.replaceWithMultiple(forInEquiv({
KEYS: KEYS, OBJECT: OBJECT,
KEY: (0, _babelTypes.identifier)(path.scope.generateUid('key')),
LEFT: path.node.left,
RIGHT: path.node.right,
BODY: path.node.body
}));
});
}
};
var forInEquiv = (0, _babelTemplate2.default)('\n OBJECT = RIGHT;\n KEYS = [];\n for (var KEY in OBJECT) {\n KEYS.push(KEY);\n }\n KEYS.reverse();\n while(KEYS.length) {\n LEFT = KEYS.pop();\n if (LEFT in OBJECT) {\n BODY;\n }\n }\n');
function recursiveWrapFunction(functionID, body) {
var func = (0, _utils.wrapFunction)(body);
func.callee.id = functionID;
return (0, _utils.awaitStatement)(func);
}
function insideAwaitContainingLabel(path) {
// walks the path tree to check if inside a label that also contains an await
// statement. (See also the LabeledStatement visitor.)
do {
if (path.node.loopLabel) {
return true;
}
} while (path = path.parentPath);
// no such label found
return false;
}
function ifShouldRefactorLoop(path, extraCheck, handler) {
// ensureBlock here is convenient, but has nothing to do with the method name
(0, _babelTypes.ensureBlock)(path.node);
if (extraCheck || insideAwaitContainingLabel(path) || loopContainsAwait(path.get('body'))) {
handler();
}
}
var NoSubLoopsVisitor = {
Loop: function Loop(path) {
path.skip();
}
};
// does the current loop (no subloops) contain an await statement?
var loopContainsAwait = (0, _utils.matcher)(['AwaitExpression'], (0, _jsExtend.extend)({}, _utils.NoSubFunctionsVisitor, NoSubLoopsVisitor));
function refactorLoop(path, extraCheck, addVarDecl, handler) {
ifShouldRefactorLoop(path, extraCheck, function () {
// gather info about the function & fix up its body (break + continue
// statements)
var label = path.node.loopLabel;
var functionID = label || (0, _babelTypes.identifier)(path.scope.generateUid('recursive'));
var info = { functionID: functionID };
path.get('body').traverse(BreakContinueReplacementVisitor, info);
// actual conversion
handler(functionID);
// if containing a return *or* a break statement that doesn't control the
// own loop (references a label of another loop), add:
//
// .then(function (_resp) {
// _temp = _resp;
// if (_temp !== _recursive) {
// return _temp;
// }
// });
if (info.addReturnHandler) {
var tmp = (0, _babelTypes.identifier)(path.scope.generateUid('temp'));
addVarDecl(tmp);
path.node.loopLabel = label;
path.replaceWithMultiple(loopReturnHandler({ TMP: tmp, BASE: path.node, FUNC: functionID }));
}
});
}
var loopReturnHandler = (0, _babelTemplate2.default)('\n TMP = BASE\n if (_temp !== FUNC) {\n return _temp;\n }\n');
var continueStatementEquiv = function continueStatementEquiv(funcID) {
// continue label; -> return await label();
var stmt = (0, _babelTypes.returnStatement)((0, _babelTypes.awaitExpression)((0, _babelTypes.callExpression)(funcID, [])));
// not a 'real' return
stmt.noHandlerRequired = true;
return stmt;
};
var BreakContinueReplacementVisitor = (0, _jsExtend.extend)({
ReturnStatement: function ReturnStatement(path) {
if (!path.node.noHandlerRequired && path.node.argument) {
// if a return statement added by the user - and actually returning
// something, we need to add a return handler later.
this.addReturnHandler = true;
}
},
// replace continue/break with their recursive equivalents
BreakStatement: function BreakStatement(path) {
// a break statement is replaced by returning the name of the loop function
// that should be broken. It's a convenient unique value.
//
// So: break; becomes return _recursive;
//
// and break myLabel; becomes return myLabel;
var label = getLabel(path, this.functionID);
var returnStmt = (0, _babelTypes.returnStatement)(getLabel(path, this.functionID));
if (label === this.functionID) {
// only if this controls the current loop, a return handler is unnecessary
returnStmt.noHandlerRequired = true;
}
path.replaceWith(returnStmt);
},
ContinueStatement: function ContinueStatement(path) {
// see break, with the difference that the function is called (and thus)
// executed next
path.replaceWith(continueStatementEquiv(getLabel(path, this.functionID)));
}
}, _utils.NoSubFunctionsVisitor, NoSubLoopsVisitor);
var getLabel = function getLabel(path, functionID) {
return path.node.label || functionID;
};
;