├── .gitignore ├── .travis.yml ├── CHANGES.markdown ├── LICENSE ├── README.markdown ├── bin └── promise-me ├── examples ├── after.ast ├── after.js ├── before.ast ├── before.js └── generate-asts.js ├── package.json ├── promise-me.js └── spec ├── basic-spec.js ├── bin-spec.js ├── fixtures ├── after │ └── simple.js └── before │ └── simple.js ├── requirejs.html └── script.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | branches: 5 | except: 6 | - website 7 | - gh-pages 8 | -------------------------------------------------------------------------------- /CHANGES.markdown: -------------------------------------------------------------------------------- 1 | v0.1.1 2 | ====== 3 | 4 | * Update escope to v0.0.13 which brings back support for script loading and 5 | require.js/AMD. 6 | 7 | v0.1.0 8 | ====== 9 | 10 | * Known issue: this version will not work with script loading and 11 | require.js/AMD as the escope package doesn't have a suitable module 12 | definition. Once https://github.com/Constellation/escope/pull/10 is merged, 13 | it will resume working. To use promise-me in the browser until then please 14 | use [Mr](https://github.com/montagejs/mr). 15 | * Update options object API of convert. See the readme for details. 16 | * Allow custom matcher, replacer and flattener functions in the options 17 | object. See the readme for more details. 18 | * Don't flatten `then()`s if either the resolutoin or rejection handler 19 | captures one or more variables from its parents. 20 | 21 | v0.0.3 22 | ====== 23 | 24 | * Add support for AMD (Require.js) and script tag loading 25 | 26 | v0.0.2 27 | ====== 28 | 29 | * Retain comments in generated code 30 | 31 | v0.0.1 32 | ====== 33 | 34 | * Initial release 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Stuart Knightley 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Promise Me [![Build Status](https://secure.travis-ci.org/Stuk/promise-me.png?branch=master)](https://travis-ci.org/Stuk/promise-me) 2 | ========== 3 | 4 | Promise Me helps you move your code from using callbacks to using [promises](http://wiki.commonjs.org/wiki/Promises/A), for example through [Q](https://github.com/kriskowal/q), [RSVP.js](https://github.com/tildeio/rsvp.js) or [when.js](https://github.com/cujojs/when). 5 | 6 | It parses your code and then manipulates the AST to transform the callbacks into calls to `then()`, including a rejection handler if you handle the original callback error. Think of it as a slightly smarter find-and-replace. It will probably break your code and require you to fix it. 7 | 8 | Try the [live demo](http://stuk.github.com/promise-me/)! 9 | 10 | Installation and usage 11 | ---------------------- 12 | 13 | ```bash 14 | $ npm install -g promise-me 15 | $ promise-me script.js 16 | ``` 17 | 18 | ### API 19 | 20 | ```javascript 21 | var promiseMe = require("promise-me"); 22 | 23 | var before = "..."; 24 | var after = promiseMe.convert(before, options); 25 | ``` 26 | 27 | #### promiseMe.convert(code, options) 28 | 29 | Convert the given code to use promises. 30 | 31 | * `{string} code` String containing Javascript code. 32 | * `{Object} options` Options for generation. 33 | * `{Object} options.parse` Options for `esprima.parse`. See http://esprima.org/doc/ . 34 | * `{Object} options.generate` Options for `escodegen.generate`. See https://github.com/Constellation/escodegen/wiki/API . 35 | * `{Function} options.matcher` A function of form `(node) => boolean`. Must accept any type of node and return true if it should be transformed into `then`, using the replacer, and false if not. See https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API for node types. 36 | * `{Function} options.replacer` A function of form `(node) => Node`. Must accept any type of node and return a new node to replace it that uses `then` instead of a callback. Will only get called if options.matcher returns true. See https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API for node types. 37 | * `{Function} options.flattener` A function of form `(node) => Node`. Must accept any type of node, and return either return the original node, or a new node with the `then` calls flattened. 38 | * `{Function} log` Function to call with log messages. 39 | * Returns `{string}` The Javascript code with callbacks replaced with `.then()` functions. 40 | 41 | 42 | Examples 43 | -------- 44 | 45 | ### Simple callback 46 | 47 | Before: 48 | ```javascript 49 | getDetails("Bob", function (err, details) { 50 | console.log(details) 51 | }); 52 | ``` 53 | 54 | After: 55 | ```javascript 56 | getDetails('Bob').then(function (details) { 57 | console.log(details); 58 | }); 59 | ``` 60 | 61 | ### Error handling 62 | 63 | Before: 64 | ```javascript 65 | getDetails("Bob", function (err, details) { 66 | if (err) { 67 | console.error(err); 68 | return; 69 | } 70 | console.log(details) 71 | }); 72 | ``` 73 | 74 | After: 75 | ```javascript 76 | getDetails('Bob').then(function (details) { 77 | console.log(details); 78 | }, function (err) { 79 | console.error(err); 80 | return; 81 | }); 82 | ``` 83 | 84 | ### Nested callbacks 85 | 86 | Before: 87 | ```javascript 88 | getDetails("Bob", function (err, details) { 89 | getLongLat(details.address, details.country, function(err, longLat) { 90 | getNearbyBars(longLat, function(err, bars) { 91 | console.log("Your nearest bar is: " + bars[0]); 92 | }); 93 | }); 94 | }); 95 | ``` 96 | 97 | After: 98 | ```javascript 99 | getDetails('Bob').then(function (details) { 100 | return getLongLat(details.address, details.country); 101 | }).then(function (longLat) { 102 | return getNearbyBars(longLat); 103 | }).then(function (bars) { 104 | console.log('Your nearest bar is: ' + bars[0]); 105 | }); 106 | ``` 107 | 108 | ### Captured variables 109 | 110 | Before: 111 | ```javascript 112 | getDetails("Bob", function (err, details) { 113 | getLongLat(details.address, details.country, function(err, longLat) { 114 | getNearbyBars(longLat, function(err, bars) { 115 | // Note the captured `details` variable 116 | console.log("The closest bar to " + details.address + " is: " + bars[0]); 117 | }); 118 | }); 119 | }); 120 | ``` 121 | 122 | After: 123 | ```javascript 124 | getDetails("Bob").then(function (details) { 125 | getLongLat(details.address, details.country).then(function (longLat) { 126 | return getNearbyBars(longLat); 127 | }).then(function (bars) { 128 | // Note the captured `details` variable 129 | console.log("The closest bar to " + details.address + " is: " + bars[0]); 130 | }); 131 | }); 132 | ``` 133 | 134 | 135 | -------------------------------------------------------------------------------- /bin/promise-me: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require("fs"); 3 | 4 | var promiseMe = require("../promise-me"); 5 | 6 | var content = fs.readFileSync(process.argv[2], "utf8"); 7 | 8 | console.log(promiseMe.convert(content)); 9 | -------------------------------------------------------------------------------- /examples/after.ast: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "ExpressionStatement", 6 | "expression": { 7 | "type": "CallExpression", 8 | "callee": { 9 | "type": "MemberExpression", 10 | "computed": false, 11 | "object": { 12 | "type": "Identifier", 13 | "name": "browser" 14 | }, 15 | "property": { 16 | "type": "Identifier", 17 | "name": "init" 18 | } 19 | }, 20 | "arguments": [ 21 | { 22 | "type": "Identifier", 23 | "name": "desired" 24 | }, 25 | { 26 | "type": "FunctionExpression", 27 | "id": null, 28 | "params": [], 29 | "defaults": [], 30 | "body": { 31 | "type": "BlockStatement", 32 | "body": [ 33 | { 34 | "type": "ExpressionStatement", 35 | "expression": { 36 | "type": "CallExpression", 37 | "callee": { 38 | "type": "MemberExpression", 39 | "computed": false, 40 | "object": { 41 | "type": "Identifier", 42 | "name": "browser" 43 | }, 44 | "property": { 45 | "type": "Identifier", 46 | "name": "get" 47 | } 48 | }, 49 | "arguments": [ 50 | { 51 | "type": "Literal", 52 | "value": "http://admc.io/wd/test-pages/guinea-pig.html" 53 | }, 54 | { 55 | "type": "FunctionExpression", 56 | "id": null, 57 | "params": [], 58 | "defaults": [], 59 | "body": { 60 | "type": "BlockStatement", 61 | "body": [ 62 | { 63 | "type": "ExpressionStatement", 64 | "expression": { 65 | "type": "CallExpression", 66 | "callee": { 67 | "type": "MemberExpression", 68 | "computed": false, 69 | "object": { 70 | "type": "CallExpression", 71 | "callee": { 72 | "type": "MemberExpression", 73 | "computed": false, 74 | "object": { 75 | "type": "CallExpression", 76 | "callee": { 77 | "type": "MemberExpression", 78 | "computed": false, 79 | "object": { 80 | "type": "Identifier", 81 | "name": "browser" 82 | }, 83 | "property": { 84 | "type": "Identifier", 85 | "name": "title" 86 | } 87 | }, 88 | "arguments": [] 89 | }, 90 | "property": { 91 | "type": "Identifier", 92 | "name": "then" 93 | } 94 | }, 95 | "arguments": [ 96 | { 97 | "type": "FunctionExpression", 98 | "id": null, 99 | "params": [ 100 | { 101 | "type": "Identifier", 102 | "name": "title" 103 | } 104 | ], 105 | "defaults": [], 106 | "body": { 107 | "type": "BlockStatement", 108 | "body": [ 109 | { 110 | "type": "ExpressionStatement", 111 | "expression": { 112 | "type": "CallExpression", 113 | "callee": { 114 | "type": "MemberExpression", 115 | "computed": false, 116 | "object": { 117 | "type": "Identifier", 118 | "name": "assert" 119 | }, 120 | "property": { 121 | "type": "Identifier", 122 | "name": "ok" 123 | } 124 | }, 125 | "arguments": [ 126 | { 127 | "type": "UnaryExpression", 128 | "operator": "~", 129 | "argument": { 130 | "type": "CallExpression", 131 | "callee": { 132 | "type": "MemberExpression", 133 | "computed": false, 134 | "object": { 135 | "type": "Identifier", 136 | "name": "title" 137 | }, 138 | "property": { 139 | "type": "Identifier", 140 | "name": "indexOf" 141 | } 142 | }, 143 | "arguments": [ 144 | { 145 | "type": "Literal", 146 | "value": "I am a page title - Sauce Labs" 147 | } 148 | ] 149 | } 150 | }, 151 | { 152 | "type": "Literal", 153 | "value": "Wrong title!" 154 | } 155 | ] 156 | } 157 | }, 158 | { 159 | "type": "ReturnStatement", 160 | "argument": { 161 | "type": "CallExpression", 162 | "callee": { 163 | "type": "MemberExpression", 164 | "computed": false, 165 | "object": { 166 | "type": "Identifier", 167 | "name": "browser" 168 | }, 169 | "property": { 170 | "type": "Identifier", 171 | "name": "elementById" 172 | } 173 | }, 174 | "arguments": [ 175 | { 176 | "type": "Literal", 177 | "value": "i am a link" 178 | } 179 | ] 180 | } 181 | } 182 | ] 183 | }, 184 | "rest": null, 185 | "generator": false, 186 | "expression": false 187 | } 188 | ] 189 | }, 190 | "property": { 191 | "type": "Identifier", 192 | "name": "then" 193 | } 194 | }, 195 | "arguments": [ 196 | { 197 | "type": "FunctionExpression", 198 | "id": null, 199 | "params": [ 200 | { 201 | "type": "Identifier", 202 | "name": "el" 203 | } 204 | ], 205 | "defaults": [], 206 | "body": { 207 | "type": "BlockStatement", 208 | "body": [ 209 | { 210 | "type": "ExpressionStatement", 211 | "expression": { 212 | "type": "CallExpression", 213 | "callee": { 214 | "type": "MemberExpression", 215 | "computed": false, 216 | "object": { 217 | "type": "Identifier", 218 | "name": "browser" 219 | }, 220 | "property": { 221 | "type": "Identifier", 222 | "name": "clickElement" 223 | } 224 | }, 225 | "arguments": [ 226 | { 227 | "type": "Identifier", 228 | "name": "el" 229 | }, 230 | { 231 | "type": "FunctionExpression", 232 | "id": null, 233 | "params": [], 234 | "defaults": [], 235 | "body": { 236 | "type": "BlockStatement", 237 | "body": [ 238 | { 239 | "type": "ExpressionStatement", 240 | "expression": { 241 | "type": "CallExpression", 242 | "callee": { 243 | "type": "MemberExpression", 244 | "computed": false, 245 | "object": { 246 | "type": "CallExpression", 247 | "callee": { 248 | "type": "MemberExpression", 249 | "computed": false, 250 | "object": { 251 | "type": "Identifier", 252 | "name": "browser" 253 | }, 254 | "property": { 255 | "type": "Identifier", 256 | "name": "eval" 257 | } 258 | }, 259 | "arguments": [ 260 | { 261 | "type": "Literal", 262 | "value": "window.location.href" 263 | } 264 | ] 265 | }, 266 | "property": { 267 | "type": "Identifier", 268 | "name": "then" 269 | } 270 | }, 271 | "arguments": [ 272 | { 273 | "type": "FunctionExpression", 274 | "id": null, 275 | "params": [ 276 | { 277 | "type": "Identifier", 278 | "name": "location" 279 | } 280 | ], 281 | "defaults": [], 282 | "body": { 283 | "type": "BlockStatement", 284 | "body": [ 285 | { 286 | "type": "ExpressionStatement", 287 | "expression": { 288 | "type": "CallExpression", 289 | "callee": { 290 | "type": "MemberExpression", 291 | "computed": false, 292 | "object": { 293 | "type": "Identifier", 294 | "name": "assert" 295 | }, 296 | "property": { 297 | "type": "Identifier", 298 | "name": "ok" 299 | } 300 | }, 301 | "arguments": [ 302 | { 303 | "type": "UnaryExpression", 304 | "operator": "~", 305 | "argument": { 306 | "type": "CallExpression", 307 | "callee": { 308 | "type": "MemberExpression", 309 | "computed": false, 310 | "object": { 311 | "type": "Identifier", 312 | "name": "location" 313 | }, 314 | "property": { 315 | "type": "Identifier", 316 | "name": "indexOf" 317 | } 318 | }, 319 | "arguments": [ 320 | { 321 | "type": "Literal", 322 | "value": "guinea-pig2" 323 | } 324 | ] 325 | } 326 | } 327 | ] 328 | } 329 | }, 330 | { 331 | "type": "ExpressionStatement", 332 | "expression": { 333 | "type": "CallExpression", 334 | "callee": { 335 | "type": "MemberExpression", 336 | "computed": false, 337 | "object": { 338 | "type": "Identifier", 339 | "name": "browser" 340 | }, 341 | "property": { 342 | "type": "Identifier", 343 | "name": "quit" 344 | } 345 | }, 346 | "arguments": [] 347 | } 348 | } 349 | ] 350 | }, 351 | "rest": null, 352 | "generator": false, 353 | "expression": false 354 | } 355 | ] 356 | } 357 | } 358 | ] 359 | }, 360 | "rest": null, 361 | "generator": false, 362 | "expression": false 363 | } 364 | ] 365 | } 366 | } 367 | ] 368 | }, 369 | "rest": null, 370 | "generator": false, 371 | "expression": false 372 | } 373 | ] 374 | } 375 | } 376 | ] 377 | }, 378 | "rest": null, 379 | "generator": false, 380 | "expression": false 381 | } 382 | ] 383 | } 384 | } 385 | ] 386 | }, 387 | "rest": null, 388 | "generator": false, 389 | "expression": false 390 | } 391 | ] 392 | } 393 | } 394 | ] 395 | } -------------------------------------------------------------------------------- /examples/after.js: -------------------------------------------------------------------------------- 1 | browser.init(desired, function () { 2 | browser.get('http://admc.io/wd/test-pages/guinea-pig.html', function () { 3 | browser.title().then(function (title) { 4 | assert.ok(~title.indexOf('I am a page title - Sauce Labs'), 'Wrong title!'); 5 | return browser.elementById('i am a link'); 6 | }).then(function (el) { 7 | browser.clickElement(el, function () { 8 | browser.eval('window.location.href').then(function (location) { 9 | assert.ok(~location.indexOf('guinea-pig2')); 10 | browser.quit(); 11 | }); 12 | }); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/before.ast: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "ExpressionStatement", 6 | "expression": { 7 | "type": "CallExpression", 8 | "callee": { 9 | "type": "MemberExpression", 10 | "computed": false, 11 | "object": { 12 | "type": "CallExpression", 13 | "callee": { 14 | "type": "Identifier", 15 | "name": "one" 16 | }, 17 | "arguments": [] 18 | }, 19 | "property": { 20 | "type": "Identifier", 21 | "name": "then" 22 | } 23 | }, 24 | "arguments": [ 25 | { 26 | "type": "FunctionExpression", 27 | "id": null, 28 | "params": [ 29 | { 30 | "type": "Identifier", 31 | "name": "dataA" 32 | } 33 | ], 34 | "defaults": [], 35 | "body": { 36 | "type": "BlockStatement", 37 | "body": [ 38 | { 39 | "type": "ExpressionStatement", 40 | "expression": { 41 | "type": "CallExpression", 42 | "callee": { 43 | "type": "MemberExpression", 44 | "computed": false, 45 | "object": { 46 | "type": "CallExpression", 47 | "callee": { 48 | "type": "Identifier", 49 | "name": "two" 50 | }, 51 | "arguments": [ 52 | { 53 | "type": "Identifier", 54 | "name": "dataA" 55 | } 56 | ] 57 | }, 58 | "property": { 59 | "type": "Identifier", 60 | "name": "then" 61 | } 62 | }, 63 | "arguments": [ 64 | { 65 | "type": "FunctionExpression", 66 | "id": null, 67 | "params": [ 68 | { 69 | "type": "Identifier", 70 | "name": "dataB" 71 | } 72 | ], 73 | "defaults": [], 74 | "body": { 75 | "type": "BlockStatement", 76 | "body": [ 77 | { 78 | "type": "ReturnStatement", 79 | "argument": { 80 | "type": "CallExpression", 81 | "callee": { 82 | "type": "Identifier", 83 | "name": "three" 84 | }, 85 | "arguments": [ 86 | { 87 | "type": "Identifier", 88 | "name": "dataB" 89 | } 90 | ] 91 | } 92 | } 93 | ] 94 | }, 95 | "rest": null, 96 | "generator": false, 97 | "expression": false 98 | } 99 | ] 100 | } 101 | } 102 | ] 103 | }, 104 | "rest": null, 105 | "generator": false, 106 | "expression": false 107 | } 108 | ] 109 | } 110 | } 111 | ] 112 | } -------------------------------------------------------------------------------- /examples/before.js: -------------------------------------------------------------------------------- 1 | getDetails("Bob", function (err, details) { 2 | getLongLat(details.address, details.country, function(err, longLat) { 3 | getNearbyBars(longLat, function(err, bars) { 4 | // Note the captured `details` variable 5 | console.log("The closest bar to " + details.address + " is: " + bars[0]); 6 | }); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/generate-asts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require("fs"); 4 | var esprima = require("esprima"); 5 | 6 | 7 | function generateAst(js) { 8 | return JSON.stringify( 9 | esprima.parse(js), 10 | null, 4 11 | ); 12 | } 13 | 14 | function transform(name) { 15 | var content = generateAst(fs.readFileSync(name + ".js", "utf8")); 16 | fs.writeFileSync(name + ".ast", content, "utf8"); 17 | } 18 | 19 | transform("before"); 20 | transform("after"); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promise-me", 3 | "version": "0.1.1", 4 | "description": "Code transformer to change Node-style callbacks into promises.", 5 | "main": "promise-me.js", 6 | "preferGlobal": true, 7 | "bin": "./bin/promise-me", 8 | "scripts": { 9 | "test": "jasmine-node spec" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Stuk/promise-me.git" 14 | }, 15 | "keywords": [ 16 | "promise", 17 | "esprima", 18 | "transform" 19 | ], 20 | "author": "Stuart Knightley", 21 | "license": "BSD", 22 | "dependencies": { 23 | "esprima": "~1.0.2", 24 | "escodegen": "~0.0.15", 25 | "estraverse": "~0.0.4", 26 | "escope": "~0.0.13" 27 | }, 28 | "devDependencies": { 29 | "jasmine-node": "1.0.x", 30 | "requirejs": "2.1.x" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /promise-me.js: -------------------------------------------------------------------------------- 1 | /* 2 | Promise Me 3 | Copyright (c) 2013, Stuart Knightley 4 | All rights reserved. 5 | 6 | Licenced under the New BSD License. See LICENSE for details. 7 | */ 8 | 9 | // UMD from https://github.com/umdjs/umd/blob/master/commonjsStrict.js 10 | (function (root, factory) { 11 | if (typeof exports === "object") { 12 | // CommonJS 13 | factory(exports, 14 | require("esprima"), 15 | require("escodegen"), 16 | require("estraverse"), 17 | require("escope") 18 | ); 19 | } else if (typeof define === "function" && define.amd) { 20 | // AMD. Register as an anonymous module. 21 | /*global define*/ 22 | define(["exports", 23 | "esprima", 24 | "escodegen", 25 | "estraverse", 26 | "escope" 27 | ], factory); 28 | } else { 29 | // Browser globals 30 | // The auxilary files also attach to the promiseMe root object, so 31 | // don't redefine if it exists 32 | factory((root.promiseMe = root.promiseMe || {}), 33 | root.esprima, 34 | root.escodegen, 35 | root.estraverse, 36 | root.escope 37 | ); 38 | } 39 | }(this, function (exports, esprima, escodegen, estraverse, escope) { 40 | 41 | function curry(fn) { 42 | var args = Array.prototype.slice.call(arguments, 1); 43 | return function() { 44 | return fn.apply(this, args.concat( 45 | Array.prototype.slice.call(arguments))); 46 | }; 47 | } 48 | 49 | function isObject(obj) { 50 | return obj !== null && typeof obj === "object"; 51 | } 52 | 53 | function combine(source, obj) { 54 | for(var prop in source) { 55 | if (typeof obj[prop] === "undefined") { 56 | obj[prop] = source[prop]; 57 | } else if (isObject(obj[prop]) || isObject(source[prop])) { 58 | if(!isObject(obj[prop]) || !isObject(source[prop])) { 59 | throw new TypeError("Cannot combine object with non-object, '" + prop + "'"); 60 | } else { 61 | obj[prop] = combine(obj[prop], source[prop]); 62 | } 63 | } else { 64 | obj[prop] = source[prop]; 65 | } 66 | } 67 | return obj; 68 | } 69 | 70 | var parseOptions = { 71 | loc: true, 72 | range: true, 73 | raw: true, 74 | tokens: true, 75 | comment: true 76 | }; 77 | 78 | var genOptions = { 79 | parse: esprima.parse, 80 | comment: true 81 | }; 82 | 83 | var defaultOptions = { 84 | parse: parseOptions, 85 | generate: genOptions, 86 | 87 | matcher: hasNodeCallback, 88 | replacer: replaceNodeCallback, 89 | flattener: thenFlattener, 90 | }; 91 | 92 | /** 93 | * Convert the given code to use promises. 94 | * @param {string} code String containing Javascript code. 95 | * @param {Object} options Options for generation. 96 | * @param {Object} options.parse Options for esprima.parse. See http://esprima.org/doc/ . 97 | * @param {Object} options.generate Options for escodegen.generate. See https://github.com/Constellation/escodegen/wiki/API . 98 | * @param {Function} options.matcher A function of form `(node) => boolean`. Must 99 | * accept any type of node and return true if it should be transformed into 100 | * `then`, using the replacer, and false if not. See 101 | * https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API for node 102 | * types. 103 | * @param {Function} options.replacer A function of form `(node) => Node`. Must 104 | * accept any type of node and return a new node to replace it that uses `then` 105 | * instead of a callback. Will only get called if options.matcher returns true. See 106 | * https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API for node 107 | * types. 108 | * @param {Function} options.flattener A function of form `(node) => Node`. Must 109 | * accept any type of node, and return either return the original node, or a new 110 | * node with the `then` calls flattened. 111 | * @param {Function} log Function to call with log messages. 112 | * @return {string} The Javascript code with callbacks replaced with 113 | * .then() functions. 114 | */ 115 | exports.convert = function(code, options, log) { 116 | if (options) { 117 | options = combine(defaultOptions, options); 118 | } else { 119 | options = defaultOptions; 120 | } 121 | 122 | // Parse 123 | var ast = esprima.parse(code, options.parse); 124 | 125 | // Add comments to nodes. 126 | ast = escodegen.attachComments(ast, ast.comments, ast.tokens); 127 | 128 | // Do the magic 129 | estraverse.replace(ast, { 130 | leave: curry(callbackReplacer, options.matcher, options.replacer) 131 | }); 132 | estraverse.replace(ast, { 133 | enter: options.flattener 134 | }); 135 | 136 | // generate 137 | return escodegen.generate(ast, options.generate); 138 | }; 139 | 140 | /** 141 | * Visitor to each node that replaces Node callbacks with then calls. 142 | * @param {Function} matcher A function of form `(node) => boolean`. Should 143 | * accept any type of node and return true if it should be transformed into 144 | * `then` using the replacer and false if not. 145 | * @param {Function} replacer A function of form `(node) => Node`. Should 146 | * accept any type of node and return a new node that uses `then`. 147 | * @param {Object} node Any type of node. 148 | * @return {Object|undefined} If the CallExpression has a node callback returns 149 | * a CallExpression node that calls ".then" on the result of the original 150 | * function call, or undefined. 151 | */ 152 | function callbackReplacer(matcher, replacer, node) { 153 | if(matcher(node)) { 154 | // create a call to .then() 155 | return replacer(node); 156 | } 157 | } 158 | 159 | /** 160 | * Checks if a CallExpression has a node callback. Heuristic: last argument 161 | * is a FunctionExpression, and that FunctionExpression has two arguments. 162 | * TODO: check first FunctionExpression argument is called something like "err", 163 | * or "error". Maybe that would be too presumptuous. 164 | * @param {Object} node CallExpression node. 165 | * @return {Boolean} true if the node has a Node callback, false otherwise. 166 | */ 167 | exports.NODE_MATCHER = hasNodeCallback; 168 | function hasNodeCallback(node) { 169 | var args = node.arguments; 170 | return node.type === "CallExpression" && 171 | args.length && 172 | args[args.length - 1].type === "FunctionExpression" && 173 | args[args.length - 1].params.length === 2; 174 | } 175 | 176 | exports.NODE_REPLACER = replaceNodeCallback; 177 | function replaceNodeCallback(node) { 178 | // the called function 179 | var func = node; 180 | // arguments to called function 181 | var args = func.arguments; 182 | // the last argument is the callback we need to turn into promise 183 | // handlers 184 | var callback = args.pop(); 185 | 186 | // TODO retain comments 187 | return { 188 | "type": "CallExpression", 189 | "callee": { 190 | "type": "MemberExpression", 191 | "computed": false, 192 | "object": func, 193 | "property": { 194 | "type": "Identifier", 195 | "name": "then" 196 | } 197 | }, 198 | "arguments": callbackToThenArguments(callback) 199 | }; 200 | } 201 | 202 | /** 203 | * Convert a Node callback to arguments for a then call. 204 | * Removes the first (error) argument from the function. Checks if the error 205 | * arg is handled, if so it creates a rejection handler function. 206 | * @param {Object} callback FunctionExpression node. 207 | * @return {Array} Array of one or two FunctionExpression nodes. 208 | */ 209 | function callbackToThenArguments(callback) { 210 | var thenArgs = [callback]; 211 | 212 | var errorArg = callback.params.shift(); 213 | 214 | var errback = getErrorHandler(callback, errorArg); 215 | if (errback) { 216 | thenArgs.push(errback); 217 | } 218 | 219 | return thenArgs; 220 | } 221 | 222 | /** 223 | * Checks if the given FunctionExpression checks if the given error arg is 224 | * checked and handled. If so, returns a function containing the contents 225 | * of the if block. 226 | * @param {Object} callback FunctionExpression node. 227 | * @param {string} errorArg The name of the error argument. 228 | * @return {Object|undefined} A FunctionExpression node if the error was 229 | * handled, or undefined if not. 230 | */ 231 | function getErrorHandler(callback, errorArg) { 232 | var errorArgName = errorArg.name; 233 | if (callback.body.type === 'BlockStatement') { 234 | var body = callback.body.body; 235 | for (var i = 0, len = body.length; i < len; i++) { 236 | // Only matches 237 | // if (error) ... 238 | // TODO: think about matching if (err !== null) and others 239 | if ( 240 | body[i].type === "IfStatement" && 241 | body[i].test.type === 'Identifier' && 242 | body[i].test.name === errorArgName 243 | ) { 244 | var handler = body.splice(i, 1)[0].consequent; 245 | 246 | if (handler.type !== "BlockStatement") { 247 | handler = { 248 | "type": "BlockStatement", 249 | "body": [handler] 250 | }; 251 | } 252 | 253 | return { 254 | "type": "FunctionExpression", 255 | "id": null, 256 | // give the new function the same error argument 257 | "params": [errorArg], 258 | "defaults": [], 259 | // the body is the body of the if 260 | "body": handler, 261 | "rest": null, 262 | "generator": false, 263 | "expression": false 264 | }; 265 | } 266 | 267 | } 268 | } 269 | } 270 | 271 | /** 272 | * Visitor that looks for promises as the last statement of a function, and 273 | * moves the ".then" call to be called on the result of the function call. 274 | * TODO: check if the "then" function (or any of its children) close on any 275 | * variables in this function. If so we shouldn't flatten. 276 | * @param {Object} node Any type of node. 277 | * @return {Object|undefined} If the last statment is a promise, then returns 278 | * a CallExpression that calls ".then" on the result of the original function 279 | * call. 280 | */ 281 | exports.FLATTENER = thenFlattener; 282 | function thenFlattener(node) { 283 | if (isThenCallWithThenCallAsLastStatement(node)) { 284 | var resolvedFn = node.arguments[0]; 285 | var body = resolvedFn.body.body; 286 | var lastStatement = body[body.length - 1]; 287 | 288 | var functionCall = lastStatement.expression.callee.object; 289 | var thenArguments = lastStatement.expression.arguments; 290 | 291 | // escope only works with full programs, so lets make one out of 292 | // our FunctionExpression 293 | var program = { 294 | "type": "Program", 295 | "body": [{ 296 | "type": "ExpressionStatement", 297 | "expression": resolvedFn 298 | }] 299 | }; 300 | 301 | 302 | var root = escope.analyze(program); 303 | // List of all the identifiers used that were not defined in the 304 | // resolvedFn scope 305 | var parentIdentifiers = root.scopes[1].through; 306 | // List of all the identifiers used that were not defined in the `then` 307 | // resolved handler scope 308 | var resolveIdentifiers = root.acquire(thenArguments[0]).through; 309 | 310 | // If the `then` handler references variables from outside of its scope 311 | // that its parent doesn't, then they must have been captured from 312 | // the parent, and we cannot flatten, so just return the original node 313 | for (var i = resolveIdentifiers.length - 1; i >= 0; i--) { 314 | if (parentIdentifiers.indexOf(resolveIdentifiers[i]) === -1) { 315 | return node; 316 | } 317 | } 318 | 319 | // same for rejection handler 320 | if (thenArguments.length >= 2) { 321 | var rejectIdentifiers = root.acquire(thenArguments[1]).through; 322 | for (i = rejectIdentifiers.length - 1; i >= 0; i--) { 323 | if (parentIdentifiers.indexOf(rejectIdentifiers[i]) === -1) { 324 | return node; 325 | } 326 | } 327 | } 328 | 329 | // Change last statement to just return the function call 330 | body[body.length - 1] = { 331 | type: "ReturnStatement", 332 | argument: functionCall 333 | }; 334 | 335 | // Wrap the outer function call in a MemberExpression, so that we can 336 | // call then(thenArguments) on the result (which is the return value, 337 | // which is the return value of functionCall) 338 | return thenFlattener({ 339 | type: "CallExpression", 340 | callee: { 341 | type: "MemberExpression", 342 | computed: false, 343 | object: node, 344 | property: { 345 | type: "Identifier", 346 | name: "then" 347 | } 348 | }, 349 | arguments: thenArguments 350 | }); 351 | } else { 352 | return node; 353 | } 354 | } 355 | /** 356 | * Checks if this is a ".then" CallExpression, and if so checks if the last 357 | * statment is a function call with ".then" called on it. 358 | * @param {Object} node Any type node. 359 | * @return {Boolean} 360 | */ 361 | function isThenCallWithThenCallAsLastStatement(node) { 362 | var callee, firstArg, firstArgBody; 363 | if (doesMatch(node, { 364 | type: "CallExpression", 365 | callee: { 366 | type: "MemberExpression", 367 | property: { 368 | type: "Identifier", 369 | name: "then" 370 | } 371 | }, 372 | arguments: [ 373 | { 374 | type: "FunctionExpression", 375 | body: { 376 | type: "BlockStatement" 377 | } 378 | } 379 | ] 380 | })) { 381 | var body = node.arguments[0].body.body; 382 | var lastStatement = body[body.length - 1]; 383 | return doesMatch(lastStatement, { 384 | type: "ExpressionStatement", 385 | expression: { 386 | type: "CallExpression", 387 | callee: { 388 | type: "MemberExpression", 389 | property: { 390 | type: "Identifier", 391 | name: "then" 392 | } 393 | } 394 | } 395 | }); 396 | } 397 | 398 | return false; 399 | } 400 | 401 | /** 402 | * Returns true if and only if for every property in matchObject, object 403 | * contains the same property with the same value. Objects are compared deeply. 404 | * Short circuits, so if a property does not exist in object then no errors are 405 | * raised. 406 | * The is means object can contain other properties that are not defined in 407 | * matchObject 408 | * @param {any} object The object to test. 409 | * @param {any} matchObject The object that object must match. 410 | * @return {boolean} See above. 411 | */ 412 | function doesMatch(object, matchObject) { 413 | if (!object || matchObject === null || typeof matchObject !== "object") { 414 | return object === matchObject; 415 | } 416 | 417 | return Object.keys(matchObject).every(function(prop) { 418 | return doesMatch(object[prop], matchObject[prop]); 419 | }); 420 | } 421 | 422 | })); // UMD 423 | -------------------------------------------------------------------------------- /spec/basic-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe,before,it,expect,after */ 2 | 3 | // UMD from https://github.com/umdjs/umd/blob/master/commonjsStrict.js 4 | (function (root, factory) { 5 | if (typeof exports === "object") { 6 | // CommonJS 7 | factory(require("../promise-me")); 8 | } else if (typeof define === "function" && define.amd) { 9 | // AMD. Register as an anonymous module. 10 | /*global define*/ 11 | define(["../promise-me"], factory); 12 | } else { 13 | // Browser globals 14 | factory(root.promiseMe); 15 | } 16 | }(this, function (promiseMe) { 17 | 18 | function convertFunctionToString(fn) { 19 | var lines = fn.toString().split("\n"); 20 | lines.pop(); 21 | lines.shift(); 22 | lines = lines.map(function(line) { 23 | return line.trim(); 24 | }); 25 | return lines.join("\n"); 26 | } 27 | 28 | // This utility function allows us to write tests as Javascript functions 29 | // instead of strings. Note: the functions are never executed so they don't 30 | // need to make semantic sense. 31 | var compare = function(before, after) { 32 | before = convertFunctionToString(before); 33 | after = convertFunctionToString(after); 34 | 35 | expect(promiseMe.convert( 36 | before, 37 | {generate: {format: {indent: {style: '', base: 0 }}}} 38 | )).toEqual(after); 39 | }; 40 | 41 | describe("promise-me basics", function() { 42 | // defined to avoid jshint errors 43 | var a, b, c, d, e, f, g, h, i, j; 44 | 45 | it("changes a callback to then", function() { 46 | compare(function() { 47 | a(function (err, value) { 48 | console.log(value); 49 | }); 50 | }, function() { 51 | a().then(function (value) { 52 | console.log(value); 53 | }); 54 | }); 55 | }); 56 | 57 | it("doesn't change a callback to then when it doesn't have two parameters", function() { 58 | compare(function() { 59 | a(function (err) { 60 | console.log(err); 61 | }); 62 | }, function() { 63 | a(function (err) { 64 | console.log(err); 65 | }); 66 | }); 67 | 68 | compare(function() { 69 | a(function (err, value, more) { 70 | console.log(value, more); 71 | }); 72 | }, function() { 73 | a(function (err, value, more) { 74 | console.log(value, more); 75 | }); 76 | }); 77 | }); 78 | 79 | describe("error handler", function() { 80 | it("creates a rejection handler from if statement", function() { 81 | compare(function() { 82 | a(function (err, value) { 83 | if(err) { 84 | return; 85 | } 86 | console.log(value); 87 | }); 88 | }, function() { 89 | a().then(function (value) { 90 | console.log(value); 91 | }, function (err) { 92 | return; 93 | }); 94 | }); 95 | }); 96 | 97 | it("handles consequant that isn't a block statement", function() { 98 | compare(function() { 99 | a(function (err, value) { 100 | if(err) return; 101 | console.log(value); 102 | }); 103 | }, function() { 104 | a().then(function (value) { 105 | console.log(value); 106 | }, function (err) { 107 | return; 108 | }); 109 | }); 110 | }); 111 | 112 | xit("handles if else/alternate", function() { 113 | compare(function() { 114 | a(function (err, value) { 115 | if(err) { 116 | console.error(err); 117 | } else { 118 | console.log(value); 119 | } 120 | }); 121 | }, function() { 122 | a().then(function (value) { 123 | console.log(value); 124 | }, function (err) { 125 | console.error(err); 126 | }); 127 | }); 128 | }); 129 | }); 130 | 131 | describe("nested", function() { 132 | describe("callbacks", function() { 133 | it("are transformed into a chained thens", function() { 134 | compare(function () { 135 | a(function (errA, valueA) { 136 | b(valueA, function (errB, valueB) { 137 | console.log(valueB); 138 | }); 139 | }); 140 | }, function() { 141 | a().then(function (valueA) { 142 | return b(valueA); 143 | }).then(function (valueB) { 144 | console.log(valueB); 145 | }); 146 | }); 147 | 148 | compare(function () { 149 | a(function (errA, valueA) { 150 | b(valueA, function (errB, valueB) { 151 | c(valueB, function (errC, valueC) { 152 | console.log(valueC); 153 | }); 154 | }); 155 | }); 156 | }, function() { 157 | a().then(function (valueA) { 158 | return b(valueA); 159 | }).then(function (valueB) { 160 | return c(valueB); 161 | }).then(function (valueC) { 162 | console.log(valueC); 163 | }); 164 | }); 165 | }); 166 | 167 | it("with rejection handlers are transformed into a chained thens", function() { 168 | compare(function () { 169 | a(function (errA, valueA) { 170 | if (errA) { 171 | console.error(errA); 172 | return; 173 | } 174 | b(valueA, function (errB, valueB) { 175 | console.log(valueB); 176 | }); 177 | }); 178 | }, function() { 179 | a().then(function (valueA) { 180 | return b(valueA); 181 | }, function (errA) { 182 | console.error(errA); 183 | return; 184 | }).then(function (valueB) { 185 | console.log(valueB); 186 | }); 187 | }); 188 | 189 | }); 190 | }); 191 | 192 | // disabled for the moment, as I don't think these are necessary for 193 | // version 1 194 | describe("thens", function() { 195 | it("are transformed into chained thens", function() { 196 | compare(function() { 197 | a().then(function (valueA) { 198 | b(valueA).then(function (c) {}); 199 | }); 200 | }, function() { 201 | a().then(function (valueA) { 202 | return b(valueA); 203 | }).then(function (c) { 204 | }); 205 | }); 206 | }); 207 | 208 | xit("that are also chained are flattened", function() { 209 | compare(function() { 210 | a().then(function (valueA) { 211 | b(valueA).then(function (c) {}).then(function (d) {}); 212 | }); 213 | }, function() { 214 | a().then(function (valueA) { 215 | return b(valueA); 216 | }).then(function (c) {}).then(function (d) {}); 217 | }); 218 | }); 219 | 220 | xit("that are called on an object are flattened", function() { 221 | compare(function() { 222 | a().then(function (valueA) { 223 | b(valueA).then(function(c) {}).then(function(d) {}); 224 | }); 225 | }, function() { 226 | a().then(function (valueA) { 227 | return b(valueA); 228 | }).then(function (c) {}).then(function (d) {}); 229 | }); 230 | }); 231 | }); 232 | 233 | describe("scopes", function() { 234 | it("doesn't flatten functions that capture arguments", function() { 235 | compare(function() { 236 | a(function (errA, valueA) { 237 | b(valueA, function (errB, valueB) { 238 | c(function (errC, valueC) { 239 | d(valueB, valueC, function(errD, valueD) { 240 | console.log(valueD); 241 | }); 242 | }); 243 | }); 244 | }); 245 | }, function() { 246 | a().then(function (valueA) { 247 | return b(valueA); 248 | }).then(function (valueB) { 249 | c().then(function (valueC) { 250 | return d(valueB, valueC); 251 | }).then(function (valueD) { 252 | console.log(valueD); 253 | }); 254 | }); 255 | }); 256 | }); 257 | it("doesn't flatten resolution handler that captures variables", function() { 258 | compare(function() { 259 | a(function (errA, valueA) { 260 | b(valueA, function (errB, valueB) { 261 | var x = valueB + 2; 262 | c(function (errC, valueC) { 263 | d(x, valueC, function(errD, valueD) { 264 | console.log(valueD); 265 | }); 266 | }); 267 | }); 268 | }); 269 | }, function() { 270 | a().then(function (valueA) { 271 | return b(valueA); 272 | }).then(function (valueB) { 273 | var x = valueB + 2; 274 | c().then(function (valueC) { 275 | return d(x, valueC); 276 | }).then(function (valueD) { 277 | console.log(valueD); 278 | }); 279 | }); 280 | }); 281 | }); 282 | it("doesn't flatten rejection handler that captures variables", function() { 283 | compare(function() { 284 | a(function (errA, valueA) { 285 | b(valueA, function (errB, valueB) { 286 | var x = valueB + 2; 287 | c(function (errC, valueC) { 288 | if (errC) { 289 | console.error(errC + " " + x); 290 | return; 291 | } 292 | 293 | d(valueC, function(errD, valueD) { 294 | console.log(valueD); 295 | }); 296 | }); 297 | }); 298 | }); 299 | }, function() { 300 | a().then(function (valueA) { 301 | return b(valueA); 302 | }).then(function (valueB) { 303 | var x = valueB + 2; 304 | c().then(function (valueC) { 305 | return d(valueC); 306 | }, function (errC) { 307 | console.error(errC + " " + x); 308 | return; 309 | }).then(function (valueD) { 310 | console.log(valueD); 311 | }); 312 | }); 313 | }); 314 | }); 315 | }); 316 | }); 317 | 318 | describe("comments", function() { 319 | it("keeps comments", function() { 320 | compare(function() { 321 | a(function (err, value) { 322 | // log the value 323 | console.log(value); 324 | }); 325 | }, function() { 326 | a().then(function (value) { 327 | // log the value 328 | console.log(value); 329 | }); 330 | }); 331 | }); 332 | }); 333 | 334 | }); 335 | 336 | })); // UMD 337 | -------------------------------------------------------------------------------- /spec/bin-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe,before,it,expect,after,waitsFor */ 2 | 3 | // No UMD, only for nodejs 4 | var exec = require("child_process").exec, 5 | fs = require("fs"); 6 | 7 | describe("promise-me binary", function() { 8 | 9 | it("converts a simple file", function() { 10 | var after = fs.readFileSync("spec/fixtures/after/simple.js", "utf8"); 11 | 12 | var child = exec("./bin/promise-me spec/fixtures/before/simple.js", 13 | function (error, stdout, stderr) { 14 | expect(error).toBe(null); 15 | expect(stderr).toEqual(""); 16 | expect(stdout).toEqual(after); 17 | }); 18 | 19 | waitsFor(function() { return child.exitCode !== null; }); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /spec/fixtures/after/simple.js: -------------------------------------------------------------------------------- 1 | getDetails("Bob").then(function (details) { 2 | console.log(details); 3 | }); 4 | -------------------------------------------------------------------------------- /spec/fixtures/before/simple.js: -------------------------------------------------------------------------------- 1 | getDetails("Bob", function (err, details) { 2 | console.log(details); 3 | }); 4 | -------------------------------------------------------------------------------- /spec/requirejs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RequireJS Jasmine Spec Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /spec/script.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jasmine Spec Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | --------------------------------------------------------------------------------