diff options
Diffstat (limited to 'babel-plugin-async-to-promises')
-rw-r--r-- | babel-plugin-async-to-promises/.babelrc | 3 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/.eslintrc.json | 9 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/.npmignore | 3 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/.travis.yml | 16 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/LICENSE.txt | 13 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/README.md | 133 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/lib/ifrefactor.js | 158 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/lib/index.js | 123 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/lib/looprefactor.js | 257 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/lib/promisechain.js | 167 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/lib/refactor.js | 285 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/lib/utils.js | 55 | ||||
-rw-r--r-- | babel-plugin-async-to-promises/package.json | 49 |
13 files changed, 1271 insertions, 0 deletions
diff --git a/babel-plugin-async-to-promises/.babelrc b/babel-plugin-async-to-promises/.babelrc new file mode 100644 index 0000000..c13c5f6 --- /dev/null +++ b/babel-plugin-async-to-promises/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/babel-plugin-async-to-promises/.eslintrc.json b/babel-plugin-async-to-promises/.eslintrc.json new file mode 100644 index 0000000..6427a45 --- /dev/null +++ b/babel-plugin-async-to-promises/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "extends": "eslint:recommended", + "parser": "babel-eslint", + "env": { + "node": true, + "es6": true + } +} + diff --git a/babel-plugin-async-to-promises/.npmignore b/babel-plugin-async-to-promises/.npmignore new file mode 100644 index 0000000..de9603e --- /dev/null +++ b/babel-plugin-async-to-promises/.npmignore @@ -0,0 +1,3 @@ +node_modules +src +coverage diff --git a/babel-plugin-async-to-promises/.travis.yml b/babel-plugin-async-to-promises/.travis.yml new file mode 100644 index 0000000..8f9e5df --- /dev/null +++ b/babel-plugin-async-to-promises/.travis.yml @@ -0,0 +1,16 @@ +language: node_js + +node_js: + - "0.10" + - "stable" + +script: + - npm run $COMMAND + +env: + - COMMAND=test:lint + - COMMAND=test:cov + +notifications: + email: false + diff --git a/babel-plugin-async-to-promises/LICENSE.txt b/babel-plugin-async-to-promises/LICENSE.txt new file mode 100644 index 0000000..0bc151c --- /dev/null +++ b/babel-plugin-async-to-promises/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright (c) 2016 Marten de Vries + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/babel-plugin-async-to-promises/README.md b/babel-plugin-async-to-promises/README.md new file mode 100644 index 0000000..63dab45 --- /dev/null +++ b/babel-plugin-async-to-promises/README.md @@ -0,0 +1,133 @@ +Kneden (babel-plugin-async-to-promises) +======================================= + +[![Build Status](https://travis-ci.org/marten-de-vries/kneden.svg?branch=master)](https://travis-ci.org/marten-de-vries/kneden) +[![Dependency Status](https://david-dm.org/marten-de-vries/kneden.svg)](https://david-dm.org/marten-de-vries/kneden) +[![devDependency Status](https://david-dm.org/marten-de-vries/kneden/dev-status.svg)](https://david-dm.org/marten-de-vries/kneden#info=devDependencies) + +> Transpile ES7 async/await to vanilla ES6 Promise chains + +**WARNING: Kneden +[is usable](https://github.com/pouchdb/pouchdb-plugin-helper/pull/9), but it's +also [not complete yet](https://github.com/marten-de-vries/kneden/issues/13).** + +Do you want an ES7 async/await transpiling [Babel](https://babeljs.io/) plugin, +that: + +- produces readable code - even when generator functions are not available? +- doesn't come with a runtime your users have to download? + +Then look no further! **Kneden (babel-plugin-async-to-promises)** can help you. + +## Example + +**In** + +```js +async function test() { + await db.destroy(); +} +``` + +**Out** + +```js +function test() { + return Promise.resolve().then(function () { + return db.destroy(); + }).then(function () {}); +} +``` + +(The last .then() might seem superfluous at first, but the first function +doesn't actually resolve to anything so it's necessary to make a valid +translation.) + +**Kneden** tries to translate ES7 async/await to promises in a manner similar to +how a human would do so. Loops are converted to recursive functions, and your +code is modified in such a way that a return won't just drop you in the next +part of the promise chain, but actually does what you expect it to do. + +For more examples, see the +[test/fixtures directory](https://github.com/marten-de-vries/kneden/tree/master/test/fixtures) +for both the input and output **Kneden** takes/produces. + +## Installation + +```sh +$ npm install babel-plugin-async-to-promises +``` + +## Usage + +Note: Kneden only supports transpiling ES5 with the addition of async/await. If +you're using other ES6 features (like arrow functions, let/const, classes, +etc.), make sure you transpile them down to valid ES5 code first using the +[babel es2015 preset](https://www.npmjs.com/package/babel-preset-es2015). See +[#19](https://github.com/marten-de-vries/kneden/issues/19) for more information. + +### Via `.babelrc` (Recommended) + +**.babelrc** + +```json +{ + "plugins": ["async-to-promises"] +} +``` + +### Via CLI + +```sh +$ babel --plugins async-to-promises script.js +``` + +### Via Node API + +```javascript +require("babel-core").transform("code", { + plugins: ["async-to-promises"] +}); +``` + +You can also use the plug-in in [Browserify](http://browserify.org/) using +[babelify](https://github.com/babel/babelify), in [Rollup](http://rollupjs.org/) +by using it in conjunction with +[rollup-plugin-babel](https://github.com/rollup/rollup-plugin-babel), and in +[Webpack](https://webpack.github.io/) using +[babel-loader](https://github.com/babel/babel-loader). + +Unsupported +----------- + +- Return statements aren't properly supported in switch and try/catch/finally + statements yet ([#13](https://github.com/marten-de-vries/kneden/issues/13)) +- No ``eval()``; but that's true for other Babel plugins/presets as well. + +Contributing +------------ + +There are a couple of ways to contribute, for example by: + +- Reporting test results with your code base +- Fixing bugs, for a nice starting task see the ones labeled '[good first bug](https://github.com/marten-de-vries/kneden/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+bug%22)'. + +Contributions are very welcome! Just open an issue or PR. + +What's up with the name? +------------------------ + +It's Dutch for 'to knead'/'to mold' - the program molds ES7 async/await +constructs into promises. It seemed applicable. [Pronounciation](https://upload.wikimedia.org/wikipedia/commons/0/0e/Nl-kneden.ogg). + +The npm package name is a more descriptive one as explained in +[issue #22](https://github.com/marten-de-vries/kneden/issues/22). + +License +------- + +ISC + +--- + +**Kneden** is a project by [Marten de Vries](https://ma.rtendevri.es/). diff --git a/babel-plugin-async-to-promises/lib/ifrefactor.js b/babel-plugin-async-to-promises/lib/ifrefactor.js new file mode 100644 index 0000000..904d77d --- /dev/null +++ b/babel-plugin-async-to-promises/lib/ifrefactor.js @@ -0,0 +1,158 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.SecondPassIfVisitor = exports.FirstPassIfVisitor = undefined; + +var _babelTypes = require('babel-types'); + +var _utils = require('./utils'); + +var _jsExtend = require('js-extend'); + +var FirstPassIfVisitor = exports.FirstPassIfVisitor = { + IfStatement: function IfStatement(path) { + var _this = this; + + var node = path.node; + + (0, _babelTypes.ensureBlock)(node, 'consequent'); + if (node.alternate) { + (0, _babelTypes.ensureBlock)(node, 'alternate'); + } + if (node.consequent.body.some(_babelTypes.isIfStatement) && containsReturnOrAwait(path)) { + (function () { + // flatten if statements. There are two ways to reach d() in the below. + // if a() && !b(), and if !a() && !b(). That's problematic during the + // promise conversion. + // + // if (a()) { + // if (b()) { + // return c(); + // } + // } + // return d(); + // + // this becomes instead: + // + // var _test = a(); + // if (_test && b()) { + // return c(); + // } + // return d(); + // + // which is better, but not quite the result we want yet. See for that + // the BlockStatement handler in the other IfRefactorVisitor below. + + var testID = (0, _babelTypes.identifier)(path.scope.generateUid('test')); + _this.addVarDecl(testID); + var block = [(0, _utils.assign)(testID, node.test)]; + + var stillToAdd = []; + var clearQueue = function clearQueue() { + if (stillToAdd.length) { + block.push((0, _babelTypes.ifStatement)(testID, (0, _babelTypes.blockStatement)(stillToAdd))); + stillToAdd = []; + } + }; + node.consequent.body.forEach(function (stmt) { + if ((0, _babelTypes.isIfStatement)(stmt)) { + clearQueue(); + stmt.test = (0, _babelTypes.logicalExpression)('&&', testID, stmt.test); + if (stmt.alternate) { + stmt.alternate = (0, _babelTypes.blockStatement)([(0, _babelTypes.ifStatement)(testID, stmt.alternate)]); + } + block.push(stmt); + } else { + stillToAdd.push(stmt); + } + }); + clearQueue(); + extendElse(block[block.length - 1], (node.alternate || {}).body || []); + path.replaceWithMultiple(block); + })(); + } + } +}; + +var containsReturnOrAwait = (0, _utils.matcher)(['ReturnStatement', 'AwaitExpression'], _utils.NoSubFunctionsVisitor); + +var SecondPassIfVisitor = exports.SecondPassIfVisitor = (0, _jsExtend.extend)({ + IfStatement: function IfStatement(path) { + var alt = path.node.alternate; + if (!path.node.consequent.body.length && alt && alt.body.length) { + path.node.consequent = path.node.alternate; + path.node.alternate = null; + path.node.test = (0, _babelTypes.unaryExpression)('!', path.node.test); + } + var ifContainsAwait = (0, _utils.containsAwait)(path.get('consequent')); + var elseContainsAwait = (0, _utils.containsAwait)(path.get('alternate')); + + var node = path.node; + + if (ifContainsAwait) { + node.consequent = wrapIfBranch(node.consequent); + } + if (elseContainsAwait) { + node.alternate = wrapIfBranch(node.alternate); + } + if (ifContainsAwait || elseContainsAwait) { + path.replaceWith((0, _babelTypes.awaitExpression)((0, _utils.wrapFunction)((0, _babelTypes.blockStatement)([node])))); + } + }, + BlockStatement: function BlockStatement(path) { + // Converts + // + // var _test = a(); + // if (_test && b()) { + // return c(); + // } + // return d(); + // + // into: + // + // var _test = a(); + // if (_test && b()) { + // return c(); + // } else { + // return d(); + // } + // + // ... which has at every point in time only two choices: returning + // directly out of the function, or continueing on. That's what's required + // for a nice conversion to Promise chains. + for (var i = 0; i < path.node.body.length; i++) { + var subNode = path.node.body[i]; + if ((0, _babelTypes.isReturnStatement)(subNode)) { + // remove everything in the block after the return - it's never going + // to be executed anyway. + path.node.body.splice(i + 1); + } + if (!(0, _babelTypes.isIfStatement)(subNode)) { + continue; + } + var lastStmt = subNode.consequent.body[subNode.consequent.body.length - 1]; + if (!(0, _babelTypes.isReturnStatement)(lastStmt)) { + continue; + } + var remainder = path.node.body.splice(i + 1); + if (!lastStmt.argument) { + // chop off the soon to be useless return statement + subNode.consequent.body.splice(-1); + } + extendElse(subNode, remainder); + } + } +}, _utils.NoSubFunctionsVisitor); + +var wrapIfBranch = function wrapIfBranch(branch) { + return (0, _babelTypes.blockStatement)([(0, _babelTypes.returnStatement)((0, _utils.wrapFunction)(branch))]); +}; + +function extendElse(ifStmt, extraBody) { + var body = ((ifStmt.alternate || {}).body || []).concat(extraBody); + if (body.length) { + ifStmt.alternate = (0, _babelTypes.blockStatement)(body); + } +}
\ No newline at end of file diff --git a/babel-plugin-async-to-promises/lib/index.js b/babel-plugin-async-to-promises/lib/index.js new file mode 100644 index 0000000..9d7952b --- /dev/null +++ b/babel-plugin-async-to-promises/lib/index.js @@ -0,0 +1,123 @@ +'use strict'; + +var _babelHelperHoistVariables = require('babel-helper-hoist-variables'); + +var _babelHelperHoistVariables2 = _interopRequireDefault(_babelHelperHoistVariables); + +var _babelTypes = require('babel-types'); + +var _refactor = require('./refactor'); + +var _promisechain = require('./promisechain'); + +var _promisechain2 = _interopRequireDefault(_promisechain); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +module.exports = function () { + return { + visitor: WrapperVisitor, + manipulateOptions: function manipulateOptions(opts, parserOpts) { + parserOpts.plugins.push('asyncFunctions'); + } + }; +}; + +var depth = 0; +var respID = void 0, + errID = void 0; + +var WrapperVisitor = { + // Because only ES5 is really supported, force this plugin to run as late as + // possible. At least the normal (es2015 preset) transforms have happened by + // then. + Program: { + exit: function exit(path) { + respID = path.scope.generateUid('resp'); + errID = path.scope.generateUid('err'); + path.traverse(MainVisitor); + // inline functions + path.traverse(InliningVisitor); + } + } +}; + +var MainVisitor = { + Function: { + enter: function enter(path) { + depth++; + var node = path.node; + + if (node.async) { + (function () { + var decls = []; + var addVarDecl = function addVarDecl(id) { + return decls.push((0, _babelTypes.variableDeclarator)(id)); + }; + // hoist variables + (0, _babelHelperHoistVariables2.default)(path, addVarDecl); + + // info gathering for this/arguments during the refactoring + var argumentsID = (0, _babelTypes.identifier)(path.scope.generateUid('arguments')); + var used = { argumentsID: false }; + + var newBody = []; + var addFunctionDecl = function addFunctionDecl(func) { + return newBody.push(func); + }; + + // refactor code + var args = { argumentsID: argumentsID, used: used, addVarDecl: addVarDecl, addFunctionDecl: addFunctionDecl, respID: respID, errID: errID }; + path.traverse(_refactor.RefactorVisitor, args); + // add this/arguments vars if necessary + if (used.argumentsID) { + decls.push((0, _babelTypes.variableDeclarator)(argumentsID, (0, _babelTypes.identifier)('arguments'))); + } + if (decls.length) { + newBody.push((0, _babelTypes.variableDeclaration)('var', decls)); + } + + // transformations that can only be done after all others. + path.traverse(_refactor.IfRefactorVisitor); + + // build the promise chain + var chain = new _promisechain2.default(depth > 1, node.dirtyAllowed, respID, errID); + chain.add(path.get('body.body')); + newBody.push((0, _babelTypes.returnStatement)(chain.toAST())); + + // combine all the newly generated stuff. + node.body = (0, _babelTypes.blockStatement)(newBody); + node.async = false; + })(); + } + }, + exit: function exit() { + depth--; + } + } +}; + +var InliningVisitor = { + BlockStatement: function BlockStatement(path) { + // inline blocks. Included because babel-template otherwise creates empty + // blocks. + if ((0, _babelTypes.isBlockStatement)(path.parent)) { + path.replaceWithMultiple(path.node.body); + } + }, + ReturnStatement: function ReturnStatement(path) { + // return function () { ...body... }() becomes: ...body... + var call = path.node.argument; + var inlineable = (0, _babelTypes.isCallExpression)(call) && !call.arguments.length && (0, _babelTypes.isFunctionExpression)(call.callee) && !call.callee.id && !call.callee.params.length && (0, _babelTypes.isBlockStatement)(call.callee.body) && !Object.keys(path.get('argument.callee').scope.bindings).length; + if (inlineable) { + path.replaceWithMultiple(call.callee.body.body); + } + }, + CallExpression: function CallExpression(path) { + // function () { return x; }() becomes x + var inlineable = !path.node.arguments.length && (0, _babelTypes.isFunctionExpression)(path.node.callee) && !path.node.callee.id && !path.node.callee.params.length && (0, _babelTypes.isBlockStatement)(path.node.callee.body) && path.node.callee.body.body.length === 1 && (0, _babelTypes.isReturnStatement)(path.node.callee.body.body[0]) && path.node.callee.body.body[0].argument; + if (inlineable) { + path.replaceWith(path.node.callee.body.body[0].argument); + } + } +};
\ No newline at end of file diff --git a/babel-plugin-async-to-promises/lib/looprefactor.js b/babel-plugin-async-to-promises/lib/looprefactor.js new file mode 100644 index 0000000..226dbc0 --- /dev/null +++ b/babel-plugin-async-to-promises/lib/looprefactor.js @@ -0,0 +1,257 @@ +'use strict'; + +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; +};
\ No newline at end of file diff --git a/babel-plugin-async-to-promises/lib/promisechain.js b/babel-plugin-async-to-promises/lib/promisechain.js new file mode 100644 index 0000000..d06f9ef --- /dev/null +++ b/babel-plugin-async-to-promises/lib/promisechain.js @@ -0,0 +1,167 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _babelTypes = require('babel-types'); + +var _jsExtend = require('js-extend'); + +var _utils = require('./utils'); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var PromiseChain = function () { + // add, addCatch and addFinally were designed to be called only one time each + // at most. Call them more at your own risk. + // + // addCatch() and addFinally() are not guaranteed to handle return values + // correctly. FIXME. + + function PromiseChain(inner, dirtyAllowed, respName, errName) { + _classCallCheck(this, PromiseChain); + + this._inner = inner; + this._dirtyAllowed = dirtyAllowed; + this._respName = respName; + this._errName = errName; + + this._ast = (0, _babelTypes.callExpression)((0, _babelTypes.memberExpression)((0, _babelTypes.identifier)('Promise'), (0, _babelTypes.identifier)('resolve')), []); + } + + _createClass(PromiseChain, [{ + key: 'add', + value: function add(block) { + var _this = this; + + if (!block.length) { + return; + } + var current = this._addLink('then', []); + block.forEach(function (path) { + var awaitInfos = []; + path.traverse(PromisifyPrepVisitor, { awaitInfos: awaitInfos, respName: _this._respName }); + + awaitInfos.forEach(function (awaitInfo) { + current.body.push((0, _babelTypes.returnStatement)(awaitInfo.arg)); + var params = awaitInfo.passID ? [(0, _babelTypes.identifier)(_this._respName)] : []; + current = _this._addLink('then', params); + }); + if (path.node) { + current.body.push(path.node); + } + }); + } + }, { + key: '_addLink', + value: function _addLink(type, params, secondParams) { + this._cleanup(); + + var current = { body: [] }; + var handlerBody = (0, _babelTypes.blockStatement)(current.body); + var handlers = [(0, _babelTypes.arrowFunctionExpression)(params, handlerBody, false)]; + + if (secondParams) { + current.secondBody = []; + var secondHandlerBody = (0, _babelTypes.blockStatement)(current.secondBody); + handlers.push((0, _babelTypes.arrowFunctionExpression)(secondParams, secondHandlerBody, false)); + } + + var method = (0, _babelTypes.memberExpression)(this._ast, (0, _babelTypes.identifier)(type)); + this._ast = (0, _babelTypes.callExpression)(method, handlers); + + return current; + } + }, { + key: '_cleanup', + value: function _cleanup() { + // if resolving to non-undefined when there is no return is allowed, and + // the last part of the chain is .then(function () {}), then chop off that + // part + var chopOff = this._dirtyAllowed && this._ast.callee.property.name === 'then' && this._ast.arguments.length === 1 && !this._ast.arguments[0].body.body.length; + if (chopOff) { + this._ast = this._ast.callee.object; + } + } + }, { + key: 'addCatch', + value: function addCatch(block, errID) { + var current = this._addLink('catch', [errID]); + var catchChain = this._subChain(); + catchChain.add(block); + current.body.push((0, _babelTypes.returnStatement)(catchChain.toAST())); + } + }, { + key: '_subChain', + value: function _subChain() { + return new PromiseChain(true, true, this._respName, this._errName); + } + }, { + key: 'addFinally', + value: function addFinally(block) { + var errID = (0, _babelTypes.identifier)(this._errName); + var current = this._addLink('then', [], [errID]); + + var finallyChain = this._subChain(); + + // disable optimalizations + finallyChain._inner = false; + finallyChain._dirtyAllowed = false; + finallyChain.add(block); + var secondAST = (0, _babelTypes.cloneDeep)(finallyChain.toAST()); + // smuggle in the throw statement + secondAST.arguments[0].body.body.push((0, _babelTypes.throwStatement)(errID)); + current.secondBody.push((0, _babelTypes.returnStatement)(secondAST)); + + // re-enable optimalizations + finallyChain._inner = true; + finallyChain._dirtyAllowed = true; + var ast = (0, _babelTypes.returnStatement)(finallyChain.toAST()); + current.body.push(ast); + } + }, { + key: 'toAST', + value: function toAST() { + this._cleanup(); + + var callee = this._ast.callee.object.callee; + if (this._inner && callee && callee.object.name === 'Promise') { + // only one handler to the promise - because we're in an inner function + // there's no reason to wrap the handler in promise code. Convenienly, + // such a handler is inlineable later on. + // + // Summary: + // ``Promise.resolve().then(function () {...})`` + // becomes + // ``function () {...}()`` + return (0, _babelTypes.callExpression)(this._ast.arguments[0], []); + } + return this._ast; + } + }]); + + return PromiseChain; +}(); + +exports.default = PromiseChain; + + +var PromisifyPrepVisitor = (0, _jsExtend.extend)({ + AwaitExpression: { + exit: function exit(path) { + // exit so awaits are evaluated inside out if there are multiple in + // the expression + var info = { arg: path.node.argument }; + if ((0, _babelTypes.isExpressionStatement)(path.parent)) { + path.remove(); + } else { + info.passID = true; + path.replaceWith((0, _babelTypes.identifier)(this.respName)); + } + this.awaitInfos.push(info); + } + } +}, _utils.NoSubFunctionsVisitor);
\ No newline at end of file diff --git a/babel-plugin-async-to-promises/lib/refactor.js b/babel-plugin-async-to-promises/lib/refactor.js new file mode 100644 index 0000000..608eeec --- /dev/null +++ b/babel-plugin-async-to-promises/lib/refactor.js @@ -0,0 +1,285 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RefactorVisitor = exports.IfRefactorVisitor = undefined; + +var _babelTypes = require('babel-types'); + +var _jsExtend = require('js-extend'); + +var _promisechain = require('./promisechain'); + +var _promisechain2 = _interopRequireDefault(_promisechain); + +var _utils = require('./utils'); + +var _ifrefactor = require('./ifrefactor'); + +var _looprefactor = require('./looprefactor'); + +var _looprefactor2 = _interopRequireDefault(_looprefactor); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var IfRefactorVisitor = exports.IfRefactorVisitor = _ifrefactor.SecondPassIfVisitor; + +var RefactorVisitor = exports.RefactorVisitor = (0, _jsExtend.extend)({ + AwaitExpression: function AwaitExpression(path) { + // ``return await x`` becomes just ``return x`` + if ((0, _babelTypes.isReturnStatement)(path.parent)) { + path.replaceWith(path.node.argument); + } + }, + BinaryExpression: function BinaryExpression(path) { + // a() + await b + // + // -> + // + // _temp = a(), _temp + await b + // + // to make sure the execution order is correct. This provides a nice trick: + // if you don't care about evaluation order and have one await-ed item in + // your binary expression, put it on the left side of the operator. + + if ((0, _utils.containsAwait)(path.get('right')) && !path.node.left.isTemp) { + var tmp = (0, _babelTypes.identifier)(path.scope.generateUid('temp')); + tmp.isTemp = true; + this.addVarDecl(tmp); + var assignment = (0, _utils.assign)(tmp, path.node.left); + path.node.left = tmp; + insertBefore(path, assignment); + } + }, + ArrayExpression: function ArrayExpression(path) { + // [a(), await b()] + // + // -> + // + // await Promise.all([ + // function () {return a();}(), + // function () {return await b();}() + // ]) + // + // (which is optimized away to:) + // + // await Promise.all([a(), b()]) + + if (path.get('elements').slice(1).some(_utils.containsAwait)) { + var elements = path.node.elements.map(function (element) { + return (0, _utils.wrapFunction)((0, _babelTypes.blockStatement)([(0, _babelTypes.returnStatement)(element)])); + }); + var promiseAll = (0, _babelTypes.memberExpression)((0, _babelTypes.identifier)('Promise'), (0, _babelTypes.identifier)('all')); + path.replaceWith((0, _babelTypes.awaitExpression)((0, _babelTypes.callExpression)(promiseAll, [(0, _babelTypes.arrayExpression)(elements)]))); + } + }, + CallExpression: function CallExpression(path) { + var _this = this; + + // call(a(), await b()) + // + // -> + // + // _temp = [a(), await b()], call(_temp[0], _temp[1]) + + if (path.get('arguments').slice(1).some(_utils.containsAwait)) { + (function () { + var tmp = (0, _babelTypes.identifier)(path.scope.generateUid('temp')); + _this.addVarDecl(tmp); + var assignment = (0, _utils.assign)(tmp, (0, _babelTypes.arrayExpression)(path.node.arguments)); + path.node.arguments = path.node.arguments.map(function (_, i) { + return (0, _babelTypes.memberExpression)(tmp, (0, _babelTypes.numericLiteral)(i), true); + }); + insertBefore(path, assignment); + })(); + } + }, + ObjectExpression: function ObjectExpression(path) { + var _this2 = this; + + // {a: a(), b: await b()} + // + // -> + // + // _temp = {}, _temp.a = a(), _temp.b = await b(), _temp + + if (path.get('properties').slice(1).some(_utils.containsAwait)) { + (function () { + var tmp = (0, _babelTypes.identifier)(path.scope.generateUid('temp')); + _this2.addVarDecl(tmp); + var assignments = [(0, _utils.assign)(tmp, (0, _babelTypes.objectExpression)([]))]; + path.node.properties.forEach(function (property) { + var member = (0, _babelTypes.memberExpression)(tmp, property.key); + assignments.push((0, _utils.assign)(member, property.value)); + }); + path.replaceWith(tmp); + insertBefore(path, assignments); + })(); + } + }, + + TryStatement: { + exit: function exit(path) { + // changes a try/catch that contains an await in a promise chain that uses + // .catch() + // + // uses exit() to make sure nested try/catch-es are converted correctly + // too. + + if ((0, _utils.containsAwait)(path)) { + var subChain = new _promisechain2.default(true, true, this.respID, this.errID); + subChain.add(path.get('block.body')); + if (path.node.handler) { + subChain.addCatch(path.get('handler.body.body'), path.node.handler.param); + } + if (path.node.finalizer) { + subChain.addFinally(path.get('finalizer.body')); + } + path.replaceWith((0, _utils.awaitStatement)(subChain.toAST())); + } + } + }, + ConditionalExpression: function ConditionalExpression(path) { + var node = path.node; + + var leftHasAwait = (0, _utils.containsAwait)(path.get('consequent')); + var rightHasAwait = (0, _utils.containsAwait)(path.get('alternate')); + if (leftHasAwait) { + node.consequent = wrapAwaitContaining(node.consequent); + } + if (rightHasAwait) { + node.alternate = wrapAwaitContaining(node.alternate); + } + if (leftHasAwait || rightHasAwait) { + path.replaceWith((0, _babelTypes.awaitExpression)(path.node)); + } + }, + LogicalExpression: function LogicalExpression(path) { + // a && (await b) becomes: + // await a && async function () { + // return await b(); + // }() + if ((0, _utils.containsAwait)(path.get('right'))) { + path.node.right = wrapAwaitContaining(path.node.right); + path.replaceWith((0, _babelTypes.awaitExpression)(path.node)); + } + }, + SequenceExpression: function SequenceExpression(path) { + // a, await b, await c becomes: + // await async function() { + // a; + // await b; + // return await c; + // } + + if ((0, _utils.containsAwait)(path)) { + // don't include the last item yet + var exprs = path.node.expressions; + var body = exprs.slice(0, exprs.length - 1).map(function (expr) { + return (0, _babelTypes.expressionStatement)(expr); + }); + // because that one gets a return statement + body.push((0, _babelTypes.returnStatement)(exprs[exprs.length - 1])); + path.replaceWith((0, _babelTypes.awaitExpression)((0, _utils.wrapFunction)((0, _babelTypes.blockStatement)(body)))); + } + }, + Identifier: function Identifier(path) { + if (path.node.name === 'arguments' && !path.scope.hasOwnBinding('arguments')) { + path.replaceWith(this.argumentsID); + this.used.argumentsID = true; + } + }, + SwitchStatement: function SwitchStatement(path) { + // converts a switch statement in a bunch of if statements that compare the + // discriminant to each test. Falling through is handled by a 'match' + // variable, and the break statement is handled by a variable 'brokenOut'. + // Cases after the default case are repeated so the default case can fall + // through (but in such a way that they won't match again if the default + // isn't falling through) + + var discrID = (0, _babelTypes.identifier)(path.scope.generateUid('discriminant')); + var matchID = (0, _babelTypes.identifier)(path.scope.generateUid('match')); + var brokenID = (0, _babelTypes.identifier)(path.scope.generateUid('brokenOut')); + this.addVarDecl(discrID); + this.addVarDecl(matchID); + this.addVarDecl(brokenID); + + // replace break statements with assignment expressions + path.traverse(SwitchBreakReplacementVisitor, { brokenID: brokenID }); + + var stmts = []; + var notBroken = (0, _babelTypes.unaryExpression)('!', brokenID); + var defaultIdx = void 0; + path.node.cases.forEach(function (caseNode, i) { + // add normal checks + if (!caseNode.test) { + defaultIdx = i; + return; + } + + // Seems like a weird order? Maybe, but it does prevent the + // BinaryExpression refactorer to make too much of a mess for the sake of + // strict execution order correctness. + var isOwnMatch = (0, _babelTypes.binaryExpression)('===', caseNode.test, discrID); + var isMatch = (0, _babelTypes.logicalExpression)('||', matchID, isOwnMatch); + var test = (0, _babelTypes.logicalExpression)('&&', notBroken, isMatch); + stmts.push((0, _babelTypes.ifStatement)(test, (0, _babelTypes.blockStatement)(caseNode.consequent.concat([(0, _utils.assign)(matchID, (0, _babelTypes.booleanLiteral)(true))])))); + }); + + if (typeof defaultIdx !== 'undefined') { + (function () { + // add default case + var notMatch = (0, _babelTypes.unaryExpression)('!', matchID); + var defaultTest = (0, _babelTypes.logicalExpression)('&&', notBroken, notMatch); + var body = path.node.cases[defaultIdx].consequent; + path.node.cases.slice(defaultIdx + 1).forEach(function (caseNode) { + // add fall through cases after default - still guarded by the default + // check + body.push((0, _babelTypes.ifStatement)(notBroken, (0, _babelTypes.blockStatement)(caseNode.consequent))); + }); + stmts.push((0, _babelTypes.ifStatement)(defaultTest, (0, _babelTypes.blockStatement)(body))); + })(); + } + + path.replaceWithMultiple([(0, _utils.assign)(discrID, path.node.discriminant), (0, _utils.assign)(matchID, (0, _babelTypes.booleanLiteral)(false)), (0, _utils.assign)(brokenID, (0, _babelTypes.booleanLiteral)(false))].concat(stmts)); + }, + FunctionDeclaration: function FunctionDeclaration(path) { + this.addFunctionDecl(path.node); + path.remove(); + }, + FunctionExpression: function FunctionExpression(path) { + if (path.node.id && path.parent.type !== 'ObjectProperty') { + path.node.type = 'FunctionDeclaration'; + this.addFunctionDecl(path.node); + path.replaceWith(path.node.id); + } + } +}, _ifrefactor.FirstPassIfVisitor, _looprefactor2.default, + // TODO: don't touch sub switch statements. Enabling the following should be a + // start. + //SwitchStatement(path) { + // path.skip(); + //} + _utils.NoSubFunctionsVisitor); + +function insertBefore(path, node) { + // prevent unnecessary sequence expressions. In normal JS they might be + // elegant and thus nice for Babel, but their async wrapper is ugly. + if ((0, _babelTypes.isExpressionStatement)(path.parent) || (0, _babelTypes.isReturnStatement)(path.parent)) { + path.parentPath.insertBefore(node); + } else { + path.insertBefore(node); + } +} + +var SwitchBreakReplacementVisitor = (0, _jsExtend.extend)({ + BreakStatement: function BreakStatement(path) { + // TODO: don't execute any code after the break assignment + path.replaceWith((0, _utils.assign)(this.brokenID, (0, _babelTypes.booleanLiteral)(true))); + } +}, _utils.NoSubFunctionsVisitor); + +var wrapAwaitContaining = function wrapAwaitContaining(node) { + return (0, _utils.wrapFunction)((0, _babelTypes.blockStatement)([(0, _babelTypes.returnStatement)(node)])); +};
\ No newline at end of file diff --git a/babel-plugin-async-to-promises/lib/utils.js b/babel-plugin-async-to-promises/lib/utils.js new file mode 100644 index 0000000..708adab --- /dev/null +++ b/babel-plugin-async-to-promises/lib/utils.js @@ -0,0 +1,55 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.assign = exports.awaitStatement = exports.containsAwait = exports.NoSubFunctionsVisitor = undefined; +exports.matcher = matcher; +exports.wrapFunction = wrapFunction; + +var _babelTypes = require('babel-types'); + +var _jsExtend = require('js-extend'); + +var NoSubFunctionsVisitor = exports.NoSubFunctionsVisitor = { + Function: function Function(path) { + path.skip(); + } +}; + +var containsAwait = exports.containsAwait = matcher(['AwaitExpression'], NoSubFunctionsVisitor); + +function matcher(types, base) { + var MatchVisitor = (0, _jsExtend.extend)({}, base); + types.forEach(function (type) { + MatchVisitor[type] = function (path) { + this.match.found = true; + path.stop(); + }; + }); + return function (path) { + if (!path.node) { + return false; + } + if (types.indexOf(path.node.type) !== -1) { + return true; + } + var match = {}; + path.traverse(MatchVisitor, { match: match }); + return match.found; + }; +} + +function wrapFunction(body) { + var func = (0, _babelTypes.functionExpression)(null, [], body, false, true); + func.dirtyAllowed = true; + return (0, _babelTypes.callExpression)(func, []); +} + +var awaitStatement = exports.awaitStatement = function awaitStatement(arg) { + return (0, _babelTypes.expressionStatement)((0, _babelTypes.awaitExpression)(arg)); +}; + +var assign = exports.assign = function assign(a, b) { + return (0, _babelTypes.expressionStatement)((0, _babelTypes.assignmentExpression)('=', a, b)); +};
\ No newline at end of file diff --git a/babel-plugin-async-to-promises/package.json b/babel-plugin-async-to-promises/package.json new file mode 100644 index 0000000..d13f0b1 --- /dev/null +++ b/babel-plugin-async-to-promises/package.json @@ -0,0 +1,49 @@ +{ + "name": "babel-plugin-async-to-promises", + "version": "1.0.5", + "description": "Transpile ES7 async/await to vanilla ES6 Promise chains", + "repository": "marten-de-vries/kneden", + "author": "Marten de Vries", + "main": "lib/index.js", + "devDependencies": { + "babel-cli": "^6.4.5", + "babel-core": "^6.3.17", + "babel-eslint": "^6.0.0-beta.6", + "babel-istanbul": "^0.8.0", + "babel-preset-es2015": "^6.3.13", + "babel-register": "^6.4.3", + "eslint": "^2.0.0", + "mocha": "^2.2.5" + }, + "scripts": { + "clean": "rm -rf lib", + "build": "babel src -d lib", + "test": "npm run test:lint && npm run test:cov", + "test:cov": "babel-node node_modules/.bin/babel-istanbul cover _mocha && babel-istanbul check-coverage --statements 100 --functions 100 --branches 100 --lines 100", + "test:js": "mocha --compilers js:babel-register", + "test:lint": "eslint src/*.js test/index.js && eslint test/fixtures/*/*.js", + "test:watch": "npm run test:js -- --watch", + "prepublish": "npm run clean && npm run build" + }, + "keywords": [ + "es", + "7", + "6", + "babel", + "promise", + "async", + "await", + "promises", + "function", + "functions", + "plugin", + "babel-plugin" + ], + "dependencies": { + "babel-helper-hoist-variables": "^6.5.0", + "babel-template": "^6.3.13", + "babel-types": "^6.5.2", + "js-extend": "^1.0.1" + }, + "license": "ISC" +} |