diff --git a/README.md b/README.md index bf40dcb..3a34e67 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ NoDent NoDent is a small module for Nodejs that implements the JavaScript ES7 keywoards `async` and `await`. These make writing, reading and understanding asynchronous and callback methods more implicit and embedded in the language. It works by (optionally) transforming JavaScript when it is loaded into Node. -This README assumes you're using Nodent v2.x.x - see [Upgrading](#upgrading) if your upgrading from an earlier version. +This README assumes you're using Nodent v3.x.x - see [Upgrading](#upgrading) if your upgrading from an earlier version, and keep an eye out for the asides that look like.. + +> **v2.x** users - changes between NoDent v2 and v3 are highlighted like this Contents -------- @@ -23,7 +25,9 @@ Contents * [API](#api) * [Built\-in conversions and helpers](#built-in-conversions-and-helpers) * [Testing](#testing) + * [Upgrading](#upgrading) * [Changelog](#changelog) + * [Credits](#credits) Online demo =========== @@ -49,7 +53,9 @@ To use NoDent, you need to: require('nodent')() ; -This must take place early in your app, and need only happen once per app - there is no need to `require('nodent')` in more than one file, once it is loaded it will process any files ending in ".njs" or containing a `use nodent` directive at the top of a .js file. +This must take place early in your app, and need only happen once per app - there is no need to `require('nodent')` in more than one file, once it is loaded it will process any files ending in `use nodent` directive at the top of a .js file. + +> **v2.x** users, the '.njs' extension is still supported, but not recommended You can't use the directive, or any other Nodent features in the file that initially `require("nodent")()`. If necessary, have a simple "loader.js" that requires Nodent and then requires your first Nodented file, or start your app with nodent from the command line: @@ -104,8 +110,8 @@ The testing options `--parseast` and `--minast` output the source as parsed into The full list of options is: -|option|Description| -|-------------------|--------------------------------------| +|option |Description | +|-------------------|---------------------------------------| | --fromast | Input is a JSON representation of an ESTree | --parseast | Parse the input and output the ES7 specification ESTree as JSON | --pretty | Parse the input and output the JS un-transformed @@ -114,14 +120,17 @@ The full list of options is: | --minast | Same as --ast, but omit all the source-mapping and location information from the tree | --exec | Execute the transformed code | --sourcemap | Produce a source-map in the transformed code +| --runtime | Include the nodent runtime in the output Code generation options: -|option|Description| -|-------|----------| +|option |Description | +|-------------------|---------------------------------------| | --use=_mode_ | Ignore any "use nodent" directive in the source file, and force compilation _mode_ to be `es7`,`promises`,`generators`. `engine` or `default` | --wrapAwait | Allow `await` with a non-Promise expression [more info...](#differences-from-the-es7-specification) | --lazyThenables | Evaluate async bodies lazily in 'es7' mode. See the [Changelog](#changelog) for 2.4.0 for more information +| --noruntime | Compile code (in -promises or -engine mode) for execution with no runtime requirement at all +| --es6target | Compile code assuming an ES6 target (As of v3.0.0 this doesn't do anything different. When it does the exact ES6 features will be listed) Use within your Node scripts ============================ @@ -129,7 +138,7 @@ There is no need to use the command line at all if you want to do is use `async` ES7 and Promises ---------------- -Nodent can generate code that implements `async` and `await` using basic ES5 JavaScript, Promises (via a third party library or module, or an ES5+/6 platform) or Generators (ES6). Using the directive: +Nodent can generate code that implements `async` and `await` using basic ES5 JavaScript, Promises (via Nodent's built-in library, a third party library or module, or an ES5+/6 platform) or Generators (ES6). Using the directive: 'use nodent'; @@ -142,8 +151,6 @@ Within your package.json, you can have named sets of pre-defined options, which 'use nodent-generators'; 'use nodent-engine'; -The ES7 proposal for async and await specifies the syntactic elements `async` and `await` (i.e. where they can be placed), the execution semantics (how they affect flow of execution), but also the types involved. In particular, `async` functions are specified to return a Promise, and await should be followed by an expression that evaluates to a Promise. The proposal also contains an implementation based on generators. - ### Which one should you use? All the implementations work with each other - you can mix and match. If you're unsure as to which will suit your application best, or want to try them all out `'use nodent';` will use a 'default' configuration you can determine in your application's package.json. See [Advanced Configuration](#advanced-configuration) for details. @@ -151,13 +158,15 @@ All the implementations work with each other - you can mix and match. If you're `use nodent-es7` - it's the most compatible as it doesn't require any platform support such as Promises or Generators, and works on a wide range of desktop and mobile browsers. #### Shipping an app or module within Node, npm or [modern browsers supporting Promises](http://kangax.github.io/compat-table/es6/#test-Promise) -`use nodent-promises` provides the most compatibility between modules and apps. If your module or library targets Node earlier than v4.1.x, you should install a Promise library (e.g. rsvp, when, bluebird) or use `nodent.Thenable` or `nodent.EagerThenable()` to expose the Promise API. +`use nodent-promises` provides the most compatibility between modules and apps. If your module or library targets Node earlier than v4.1.x, you should install a Promise library (e.g. rsvp, when, bluebird) or use `nodent.Thenable` to expose the Promise API. In promises mode, there is no need for a runtime at all. Specifying the option `use nodent-promises {"noRuntime":true}` will generate pure ES5 code at the cost of some loss in performance and increase in code size. + +> **v2.x** users: `nodent.EagerThenable()` is still defined, but as of v3 is the same as the `Thenable` implementation. #### Generators -`use nodent-generators` generates code which is reasonably easy to follow, but is best not used for anything beyond experimentation as it requires an advanced browser on the client-side, or Node v4.x.x. The performance and memory overhead of generators is poor - currently (Node v5.9.1) averaging 4 times slower compared to the es7 with 'lazyThenables'. +`use nodent-generators` generates code which is reasonably easy to follow, but is best not used for anything beyond experimentation as it requires an advanced browser on the client-side, or Node v4.x.x. The performance and memory overhead of generators is poor - currently (Node v6.6.0) averaging 3.5 times slower compared to the es7 with 'lazyThenables'. #### Engine -`use nodent-engine` does _not_ transpile standard ES7 async/await constructs, but only transpiles the additional non-standard features provided by nodent - await anywhere, async getters, async return and throw. At the time of writing, not many runtimes implement async and await - Chrome v53 does with command line flags, and Edge 14 are examples. On Chrome, performance is better than generators, but not quite as good as Promises, and still 40% slower than ES7 mode. +`use nodent-engine` does _not_ transpile standard ES7 async/await constructs, but only transpiles the additional non-standard features provided by nodent - await anywhere, async getters, async return and throw. At the time of writing, not many runtimes implement async and await - Chrome v53 does with command line flags, and Edge 14 are examples. On Chrome, performance is better than generators, but not quite as good as Promises, and still less than half the speed of ES7 mode. In promises mode, there is no need for a runtime at all. Specifying the option `use nodent-engine {"noRuntime":true}` will generate pure ES5 code at the cost of some loss in performance and increase in code size. Use within a browser ==================== @@ -192,21 +201,21 @@ The currently supported options are: generators:, // Compile in generator mode (like 'use nodent-generators') engine:, // Compile in engine mode (like 'use nodent-engine') sourcemap:, // Create a sourcemap for the browser's debugger - wrapAwait: // Allow 'await' on non-Promise expressions - lazyThenables: // Evaluate async bodies lazily in 'es7' mode. See the Changelog for 2.4.0 for more information + wrapAwait:, // Allow 'await' on non-Promise expressions + lazyThenables:, // Evaluate async bodies lazily in 'es7' mode. See the Changelog for 2.4.0 for more information + noRuntime:, // Only compatible with promises & engine. Generate pure ES5 code for an environment that support Promises natively or as a global declaration. Currently about 15% slower that using the built-in runtime $asyncbind. Default is false. + es6target: // Compile code assuming an ES6 target (As of v3.0.0 this doesn't do anything different. When it does the exact ES6 features will be listed) } setHeaders: function(response) {} // Called prior to outputting compiled code to allow for headers (e.g. cache settings) to be sent Note that parsing of script tags within HTML is relatively simple - the parsing is based on regex and is therefore easily confused by JS strings that contain the text 'script', or malformed/nested tags. Ensure you are parsing accurate HTML to avoid these errors. Scripts inline in HTML do not support source-mapping at present. -At runtime (i.e. in the browser), you'll need to provide some support routines: +If you're using a modern browser with Promise support (or including a third-party promise library), you can specify the compiler filag `noRuntime: true`, which will generate pure ES5 code at the cost of some loss in performance and increase in code size. Otherwise, you'll need to provide some support routines at runtime (i.e. in the browser): * `Function.prototype.$asyncbind` * `Function.prototype.$asyncspawn` if you're using generators -* `Object.$makeThenable` if you're using the `wrapAwait` option (see [await with a non-Promise](#differences-from-the-es7-specification) -* `wndow.$error` if you use await outside of an async function, to catch unhandled errors, for example: - -This are generated automatically in the transpiled files when you set the `runtime` option, and declared when Nodent is loaded (so they are already avaiable for use within Node). +* `Object.$makeThenable` if you're using the `wrapAwait` option and not using promises (see [await with a non-Promise](#differences-from-the-es7-specification) +* `window.$error` if you use await outside of an async function, to catch unhandled errors, for example: // Called when an async function throws an exception during // asynchronous operations and the calling synchronous function has returned. @@ -215,6 +224,8 @@ This are generated automatically in the transpiled files when you set the `runti throw ex ; }; +This are generated automatically in the transpiled files when you set the `runtime` option, and declared when Nodent is loaded (so they are already avaiable for use within Node). + Further information on using Nodent in the browser can be found at https://github.com/MatAtBread/nodent/issues/2. Other options: Babel & Browserify @@ -224,102 +235,7 @@ You can also invoke nodent from browserify as a [plugin](https://www.npmjs.com/p Async and Await syntax and usage ====================== -The following sections show you how to declare, call and use async functions using the ES7 `async` and `await` keywords, as well as from existing ES6 and ES5 code. - -Declaring Async Functions -------------------------- -To declare an asynchronous function, put `async` in front of the definition. `async` is an ES7 keyword. You shouldn't use it as a top level identifier (variable or function name) in ES7 code. This is how it looks: - - async function myFunc(args) { - body ; - return expr ; - } - -[_TRY-IT_](http://nodent.mailed.me.uk/#async%20function%20myFunc(args)%20%7B%0A%20%20body%20%3B%0A%20%20return%20expr%20%3B%0A%7D) - -(NB: There are other mappings too, like checking for nested functions and try catch blocks, but the essence is demonstrated in the example above). - - async function myFunc(args) { - if (!args) - throw new Error("Missing parameters") ; - return doSomething(args) ; - } - -[_TRY-IT_](http://nodent.mailed.me.uk/#async%20function%20myFunc(args)%20%7B%0A%20%20if%20(!args)%0A%20%20%20%20throw%20new%20Error(%22Missing%20parameters%22)%20%3B%0A%20%20return%20doSomething(args)%20%3B%0A%7D%0A) - -Like any function returning a Promise, you invoke the function and use the Promise via code such as: - - myFunc(args).then(function(returnValue){ - -- do something -- // Success! Use returnValue - }, function(exception) { - -- do something else // Bad! Handle the error - }) ; - -Async invocation ----------------- - -Thhe ES7 keyword `await` is implemented as a unary prefix operator (in the same kind of place you might find 'typeof' or 'delete', and also before object member definitions). It is this transformation that stops all the crazy indenting that async callbacks generate. - - var result = await myFunc(args) ; - moreStuff(result) ; - -This is transformed into the code: - - return myFunc(args).then(function($await_myFunc$1) { - var result = $await_myFunc$1 ; - moreStuff(result) ; - },$error) ; - -[_TRY-IT_](http://nodent.mailed.me.uk/#var%20result%20%3D%20await%20myFunc(args)%20%3B%0AmoreStuff(result)%20%3B%0A) - -Awaiting multiple times ------------------------ - -A statement or expression can combine multiple async results, for example: - - console.log(await as1(1),await as2("hello")+await as1(3)) ; -[_TRY-IT_](http://nodent.mailed.me.uk/#console.log(await%20as1(1)%2Cawait%20as2(%22hello%22)%2Bawait%20as1(3))%3B) - -This is both syntactically and semantically meaningful, but in the current implementation will call the async functions serially (note: the order in which they are invoked is not guaranteed). It might well be more efficient in this case to use the 'map' cover function (see below) to execute all the functions in parallel first, and use the results: - - var nodent = require('nodent')() ; - var map = nodent.require('map') ; - - // Execute all the async functions at the same time - mapped = await map([as1(1),as2("hello"),as1(3)]) ; - // When they're done: - console.log(mapped[0],mapped[1]+mapped[2]) ; - -Most Promise libraries have a function called `Promise.all()`, which is similar to `nodent.map`. `nodent.map` is more flexible in that `Promise.all()` only accepts arrays whereas `map` can map Objects and apply a specific async function to each value in the Array/Object. See below for more details and examples). Any values passed to `map` that are not Thenable (i.e. Promises or async function calls) are simply passed through unchanged. - -Invoking async functions from ES5/6 ------------------------------------ - -As described above, the return type from an async function is a Promise (or, to be accurate it's whatever type you assign the scoped variable `Promise` - if this is `nodent.Thenable`, then it has a `then()` member, and behaves enough like a Promise to work with Promises/A+-compliant libraries). So, to invoke an `async function` from a normal ES5 script you can use the code: - - // An async function defined somewhere - async function readFile() { ... } - - // Calling it using ES5 syntax in a non-nodented way - readFile(filename).then(function(data){ - ... - },function(err){ - .... - }) ; - -Similarly, you can wait for any Promise with the `await`keyword - i.e. not just functions you defined yourself as `async`: - - // The elasticsearch library returns a Promise if you don't supply a callback - var resultPromise = elasticsearch.search(query) ; - console.log(await resultPromise) ; - -or just: - - console.log(await elasticsearch.search(query)) ; - -Defining async functions from ES5 ---------------------------------- -Use nodent! And any function returning a Promise (or Thenable) can be used with `await`. +You can find out more about defining and calling async functions [here](./docs/syntax.md). There's plenty on the web too. Gotchas and ES7 compatibility =========================== @@ -329,91 +245,98 @@ Async programming with Nodent (or ES7) is much easier and simpler to debug than Differences from the ES7 specification -------------------------------------- -* **async getters** - - Nodent permits a class or object definition to define async getters: +You can continue to use all the Nodent extensions with async/await capable engines. In the `use nodent-engine` mode, all ES7 standard async/await constructs are passed through unchanged, and only functions that use a Nodent extension are transformed. - async get data() { ... } - get async data() { ... } +Extensions to the specification: - This syntax is currently not supported by any other ES7 parsers and must be substituted with something an internal async IIFE: +* **async getters** and ** static async** class members: - get data() { return (async function(){ - ... - })() } +Nodent permits a class or object definition to define async getters: - Nodent logs a warning when it detects this situation. - -* **case without break** - - As of the current version, `case` blocks without a `break;` that fall thorugh into the following `case` do not transform correctly if they contain an `await` expression. Re-work each `case` to have it's own execution block ending in `break`, `return` or `throw`. Nodent logs a warning when it detects this situation. + async get data() { ... } + get async data() { ... } + + class MyClass { + static async name() { ... } + } * **await outside async** - The ES7 async-await spec states that you can only use await inside an async function. This generates a warning in nodent, but is permitted. The synchronous return value from the function is compilation mode dependent. In practice this means that the standard, synchronous function containing the `await` does not have a useful return value of it's own. +The ES7 async-await spec states that you can only use await inside an async function. This generates a warning in nodent, but is permitted. The synchronous return value from the function is compilation mode dependent. In practice this means that the standard, synchronous function containing the `await` does not have a useful return value of it's own. * **async return/throw** - The statements `async return ` and `async throw ` are proposed extensions to the ES7 standard (see https://github.com/lukehoban/ecmascript-asyncawait/issues/38). The alternative to this syntax is to use a standard ES5 declaration returning a Promise. See [below](#exiting-async-functions-from-callbacks) for details. +The statements `async return ` and `async throw ` are proposed extensions to the ES7 standard (see https://github.com/lukehoban/ecmascript-asyncawait/issues/38). The alternative to this syntax is to use a standard ES5 declaration returning a Promise. See [below](#exiting-async-functions-from-callbacks) for details. + +Known differences from the specification: * **AsyncFunction** - The [`AsyncFunction`](#asyncfunction) type is _not_ defined by default, but is returned via the expression `require('nodent')(...).require('asyncfunction')`. +The [`AsyncFunction`](docs/helpers.md#asyncfunction) type is _not_ defined by default, but is returned via the expression `require('nodent')(...).require('asyncfunction')`. The `AsyncFunction` constructor allows you to create async functions on the fly, just as the standard `Function` constructor does. + +* **case without break** + +As of the current version, `case` blocks without a `break;` that fall thorugh into the following `case` do not transform correctly if they contain an `await` expression. Re-work each `case` to have it's own execution block ending in `break`, `return` or `throw`. Nodent logs a warning when it detects this situation. * **await non-Promise** - Although not explicitly allowed in the specification, the template implementation allows an application to `await` on a non-Promise value (this occurs because the template implementation wraps every generated value in a Promise). So the statement: +The ES7 specification allows an application to `await` on a non-Promise value (this occurs because the template implementation wraps every generated value in a Promise). So the statement: var x = await 100 ; // 100 - ...is valid. Nodent, by default, does _not_ allow this behaviour (you'll get a run-time error about '100.then is not a function'. Generally, this is not a problem in that you obviously only want to wait on asynchronous things (and not numbers, strings or anything else). However, there is one unpleasant edge case, which is where an expression _might_ be a Promise (my advice is to never write code like this, and avoid code that does). +...is valid. Nodent, by default, does _not_ allow this behaviour (you'll get a run-time error about '100.then is not a function'. Generally, this is not a problem in that you obviously only want to wait on asynchronous things (and not numbers, strings or anything else). However, there is one unpleasant edge case, which is where an expression _might_ be a Promise (my advice is to never write code like this, and avoid code that does). var x = await maybeThisIsAPromise() ; - In this case, the expression will need wrapping before it is awaited on by Nodent. You can emulate this behaviour by specifying the code-generation flag 'wrapAwait' in your package.json or after the nodent directive: +In this case, the expression will need wrapping before it is awaited on by Nodent. You can emulate this behaviour by specifying the code-generation flag 'wrapAwait' in your package.json or after the nodent directive: 'use nodent {"wrapAwait":true}'; - Wrapping every value in a Promise (or Thenable for -es7 mode) increases the time taken to invoke an async function by about 20%. An alternative to - wrapping everything is to only wrap expression where this might be the case explicitly: +Wrapping every value in a Promise increases the time taken to invoke an async function by about 20%. An alternative to wrapping everything is to only wrap expression where this might be the case explicitly: + + var x = await Promise.resolve(maybeThisIsAPromise()) ; - var x = await Promise.resolve(maybeThisIsAPromise()) ; +or - or + var isThenable = require('nodent').isThenable ; + ... + var x = maybeThisIsAPromise() ; + if (isThenable(x)) + x = await x ; - var x = maybeThisIsAPromise() ; - if (require('nodent').isThenable(x)) - x = await x ; +The second implementation avoids the expense (20%) of wrapping every return value in a Promise, with the extra code for testing if it is a Promise before awaiting on it. * **lazyThenables** - Invoking an async function _without_ a preceding `await` (simply by calling it) executes the function body but you can't get the result. This is useful for initiating 'background' things, or running async functions for their side effects (Note: this behaviour only applied to ES7-mode from version 2.4.0). This is in compliance with the ES7 specification. +> **v2.x** users - lazyThenables are _only_ available in -es7 mode in v3, and the nodent.Thenable implementation is _not_ lazy, as it was in v2.x. + +Invoking an async function _without_ a preceding `await` (simply by calling it) executes the function body but you can't get the result. This is useful for initiating 'background' things, or running async functions for their side effects. This is in compliance with the ES7 specification. - However, this has a significant performance overhead. For maximum performance, you can specify this code generation option in `use nodent-es7` mode, or use the `nodent.Thenable` in place of Promises in other modes. In this case, if you call the async function the body _is not actually executed_ until resolved with an `await` (or a `.then()`). If you know your code always uses `await`, you can use this option to improve performance. +However, this has a performance overhead. For maximum performance, you can specify this code generation option in `use nodent-es7 {"lazyThenables":true}` mode. In this case mode, if you call the async function the body _is not actually executed_ until resolved with an `await` (or a `.then()`). If you know your code always uses `await`, you can use this option to improve performance. - In `use nodent-promises` mode, it is the implementation of the Promise that determines the execution semantics. The table below is a summary of modes and execution semantics. You can test the performance on your own hardware with the following command. Note the relative performance is a worst case, since the test does nothing other than make async calls in a loop. +In `use nodent-promises` mode, it is the implementation of the Promise that determines the execution scheduling and performance. The table below is a summary of modes and execution semantics. You can test the performance on your own hardware with the following command. Note the relative performance is a worst case, since the test does nothing other than make async calls in a loop. ./nodent.js tests --generators tests/semantics/perf.js -| Mode | Flags / Implementation | Lazy / Eager | Possibly sync resolution | Performance (relative) | -|------|----------|----|--------------------------|------------------------| -| es7 | lazyThenable | Lazy | Yes | 1.0 -| es7 | (none)| Eager | Yes | 2.3x slower -| promises | nodent.Thenable | Lazy | Yes | 1.0 -| promises | nodent.EagerThenable() | Eager | No | 2.4x slower -| promises | node 5.9 native | Eager | No | 3.8x slower -| promises | bluebird 3.3.4 | Eager | No | 2.0x slower -| promises | rsvp 3.2.1 | Eager | No | 1.6x slower -| promises | when 3.7.7 | Eager | No | 1.6x slower -| generators | nodent.Thenable | Lazy | Yes | 6.5x slower -| generators | nodent.EagerThenable() | Eager | No | 9.0x slower -| generators | node 5.9 native | Eager | No | 12.0x slower -| generators | bluebird 3.3.4 | Eager | No | 8.5x slower -| generators | rsvp 3.2.1 | Eager | No | 7.8x slower -| generators | when 3.7.7 | Eager | No | 9.1x slower +| Mode | Flags / Implementation | Lazy / Eager | Possibly sync resolution | Performance (relative) | +|-----------|----------|----|--------------------------|------------------------| +| es7 | lazyThenable | Lazy | Yes | 1.0 +| es7 | (none)| Eager | No | 1.7x slower +| promises | nodent | Eager | No | 1.7x slower +| promises | node 6.6 native | Eager | No | 5.2x slower +| promises | bluebird 3.4.6 | Eager | No | 2.0x slower +| promises | rsvp 3.3.1 | Eager | No | 2.2x slower +| promises | when 3.7.7 | Eager | No | 1.6x slower +| generators | nodent | Eager | No | 7.5x slower +| generators | node 6.6 native | Eager | No | 15.0x slower +| generators | bluebird 3.4.6 | Eager | No | 8.5x slower +| generators | rsvp 3.3.1 | Eager | No | 7.6x slower +| generators | when 3.7.7 | Eager | No | 8.3x slower All other JavaScript ES5/6/2015 constructs will be transformed as necessary to implement `async` and `await`. +> **v2.x** users - note the timings and execution semantics for Thenable (and EagerThenable) have changed: they are now fully Promise/A+ compliant, meaning they resolve asynchronously and evaluate eagerly. Only -es7 lazyThenable mode might resolve synchronously. + Exiting async functions from callbacks --------------------------------------- @@ -426,78 +349,42 @@ Specifically in Nodent (not specified by ES7), you can interface an ES7 async fu },t) ; } -This works because Nodent translates this into: - - function sleep(t) { - return new Promise(function($return, $error) { - setTimeout(function(){ - return $return(undefined) ; - },t); - }); - } [_TRY-IT_](http://nodent.mailed.me.uk/#async%20function%20sleep%28t%29%20{%0A%20%20%20%20setTimeout%28function%28%29{%0A%20%20%20%20%20%20%20%20%2F%2F%20NB%3A%20%22return%20async%22%20and%20%22throw%20async%22%20are%20NOT%20ES7%20standard%20syntax%0A%20%20%20%20%20%20%20%20async%20return%20undefined%20%3B%0A%20%20%20%20}%2Ct%29%20%3B%0A}) -Similarly, `async throw ` causes the inner callback to make the container async function throw and exception. The `async return` and `async throw` statements are NOT ES7 standards (see [https://github.com/tc39/ecmascript-asyncawait/issues/38](https://github.com/tc39/ecmascript-asyncawait/issues/38)). If you want your code to remain compatible with standard ES7 implementations when the arrive, use the second form above, which is what nodent would generate and is therefore ES5/6/7 compatible. - -Missing out await ------------------ -Forgetting to put `await` in front of an async call is easy, and usually not what you want - you'll get a Thenable (or Promise). This can be useful though, when you need a reference to an async function: - - var fn ; - if (x) - fn = test(x) ; // 'test' is async - don't await - else - fn = testDefault() ; // testDefault is async - don't await - - return await fn ; // Now await for which function fn refers to - - - async function f() { return true ; } - var x = f() ; // 'x' = a Thenable object - - // Call x using nodent - try { - result = await x ; - ... - } catch(error) { - ... - } - // or call x from ES5/6 - x.then(function(result){ - ... - },function(error){ - ... - }) ; +Similarly, `async throw ` causes the inner callback to make the container async function throw an exception. The `async return` and `async throw` statements are NOT ES7 standards (see [https://github.com/tc39/ecmascript-asyncawait/issues/38](https://github.com/tc39/ecmascript-asyncawait/issues/38)). If you want your code to remain compatible with standard ES7 implementations when the arrive, use the second form above, which is what nodent would generate and is therefore ES5/6/7 compatible. Advanced Configuration ====================== Nodent has two sets of configuration values: * one controls the **runtime environment** - catching unhandled errors, handling warnings from Nodent and stack mapping, etc. -* the other controls **code generation** - whether to generate code that uses Promises or generators (or not), whether to await on non-Promise values, etc. +* the other controls **code generation** - whether to generate code that uses Promises or generators (or not), whether to await on non-Promise values, whether to include the runtime code, etc. The first is defined _once_ per installation (Nodent contained as dependencies within dependencies have their own, per-installation, instances). You can 'redefine' the values, but the effect is to overwrite existing settings. These are specified as the first argument when you `require('nodent')(options)`. Details of the options are [below](#api). The second set is defined per-file for each file that Nodent loads and compiles. The options are: -|Member| Type | | -|-------|-----|------------------------| -|es7|boolean|set by the directive `use nodent-es7` -|promises|boolean|set by the directive `use nodent-promises` -|generators|boolean|set by the directive `use nodent-generators` -|engine|boolean|set by the directive `use nodent-engine` -|wrapAwait|boolean|default: false [more info...](#differences-from-the-es7-specification) -|sourcemap|boolean|default:true - generate a source-map in the output JS -|parser|object|default:{sourceType:'script'} - passed to [Acorn](https://github.com/ternjs/acorn) to control the parser -|mapStartLine|int|default:0 - initial line number for the source-map -|generatedSymbolPrefix|string|used to disambiguate indentifiers created by the compiler +|Member | Type | | +|---------------|-------|-----------------------| +|es7 |boolean| set by any `use nodent...` directive +|promises |boolean| set by the directive `use nodent-promises` and `use nodent-generators` +|generators |boolean| set by the directive `use nodent-generators` +|engine |boolean| set by the directive `use nodent-engine` +|wrapAwait |boolean| default: false - allow `await` followed by a non-Promise [more info...](#differences-from-the-es7-specification) +|sourcemap |boolean| default: true - generate a source-map in the output JS +|noRuntime |boolean| default: false - generate pure ES5 code with external dependencies. The code is bigger and slower, and only works with -promises or -engine +|es6target |boolean| default: false - use ES6 constructs like arrow functions to improve code speed and size +|parser |object | default: {sourceType:'script'} - passed to [Acorn](https://github.com/ternjs/acorn) to control the parser +|mapStartLine |int | default: 0 - initial line number for the source-map +|generatedSymbolPrefix | string | default '$': string used to disambiguate indentifiers created by the compiler The members $return, $error, $arguments, $asyncspawn, $asyncbind, $makeThenable represent the symbols generated by the compiler. You could change them to avoid name clashes, but this is not recommended. When determining what options to use when compiling an individual file, nodent follows the sequence: -* Use the set specified after the 'use nodent-' directive. For example 'use nodent-promises' uses a predefined set called 'promises'. Other predefined sets are 'es7', 'generators' and 'engine'. If the `use nodent` doesn't have a name, the internal name "default" is used. -* Apply any modifications contained within the package.json two directories above where nodent is installed (typically the location of your application). The package.json can (optionally) contain a 'nodent' section to define your own sets of options. For example, to create a set to be used by files containing a `use nodent-myapp` directive: +* Use the set specified after the `use nodent-` directive. For example `use nodent-promises` uses a predefined set called 'promises'. Other predefined sets are 'es7', 'generators' and 'engine'. If the `use nodent` doesn't have a name, the internal name "default" is used. + +* Apply any modifications contained within the package.json containing your module or application. The package.json can (optionally) contain a 'nodent' section to define your own sets of options. For example, to create a set to be used by files containing a `use nodent-myapp` directive: "nodent":{ "directive":{ @@ -508,13 +395,15 @@ When determining what options to use when compiling an individual file, nodent f } } - You can also set options for the pre-defined sets here (default,es7,promises,generators,engine). +You can also change options for the pre-defined sets here (default, es7, promises, generators, engine). + +> **v2.x** users - Until v2.5.4, Nodent would typically look for your application's package.json. All later versions use the installation location of the calling code, so an application, module and sub-module can all have their own settings and defaults. -* Finally, nodent applies any options specified _within_ the directive, but after the name. The options are strict JSON and cannot be an expression. This is useful for quickly testing options, but is probably a bad idea if applied to very many files. One exception is rare use of the `wrapAwait` options, which has a performance overhead and few genuine use-cases. For example, to create the same effect as the 'myapp' set above: +* Finally, nodent applies any options specified _within_ the directive, but after the name. The options are strict JSON and cannot be an expression. This is useful for quickly testing options, but is probably a bad idea if applied to very many files. One exception is rare use of the `wrapAwait` option, which has a performance overhead and few genuine use-cases. For example, to create the same effect as the 'myapp' set above: 'use nodent-promises {"wrapAwait":true}'; -You can programmatically set these options _before_ creating the nodent compiler (but after requiring nodent) by using the setDefaultCompileOptions() and setCompileOptions() API calls. +You can programmatically set these options _before_ creating the nodent compiler (but after requiring nodent) by using the setDefaultCompileOptions() and setCompileOptions() API calls, however it is more flexible and less likely to clash with another module if you use the techniques above. Within a nodented file, the special symbol `__nodent` is expanded out to the current option set. It is not a variable and cannot be assigned to - it is an object literal. This has few useful use-cases, except for testing. An example is [here](https://github.com/MatAtBread/nodent/blob/master/tests/semantics/await.wrap.js) @@ -526,30 +415,32 @@ Create an instance of a nodent compiler: Options: -|Member| Type | | -|-------|-----|------------------------| -|dontMapStackTraces|boolean|default: false -|asyncStackTrace|boolean|default: false - chain stack traces across `await` for easier debugging. Note this has a significant impact on memory requirements (and some performance penalty) at runtime, and should not be used in production environments. -|augmentObject|boolean|Adds asyncify(PromiseProvider) and isThenable() to Object.prototype, making expressions such as `var client = new DB().asyncify(Promise)` and `if (abc.isThenable()) await abc()` less verbose -|extension|string|extension for files to be compiled (default: '.njs'). Note that this is unused if the file has a `use nodent-` directive. -|log (msg)|function|Called when nodent has a warning of similar to show. By default they are passed to console.warn(). Set this member to, for example, suppress logging +| Member | Type | | +|--------------------|--------|------------------------| +| dontMapStackTraces |boolean | default: false +| augmentObject |boolean | Adds `asyncify(PromiseProvider)` and `isThenable()` to Object.prototype, making expressions such as `var client = new DB().asyncify(Promise)` and `if (abc.isThenable()) await abc()` less verbose +| extension | string | extension for files to be compiled (default: '.njs'). Note that this is unused if the file has a `use nodent-` directive. +| log(msg) |function| Called when nodent has a warning or similar to show. By default they are passed to console.warn(). Set this member to change how to record logging, or to `false` to disable logging. + +> **v2.x** The flag 'asyncStackTrace' has been removed as modern debuggers can do this better than nodent can. You can specify it, but it is ignored. Return: a 'nodent' compiler object with the following properties: -|Member| Type | | -|-------|-----|------------------------| -|version|string|The currently installed version| -|asyncify (PromiseProvider)|function|Return a function to convert an object with callback members to one with Thenable members. `asyncify` is also a meta-property (see below) -|Thenable (function)|function|Implements a minimal `.then()` member to interface with Promises. `Thenable` is also a meta-property (see below) -|EagerThenable() (function)|function|Implements `.then()` with the same execution semantics as a Promise (eager evaluation and asynchronous resolution), but without chaining. `EagerThenable()` is also a meta-property (see below) -|require (moduleName,options)|object|Import an async helper module| -|generateRequestHandler (path, matchRegex, options)|function|Create a function use with Express or Connect that compiles files for a browser on demand - like a magic version of the 'static' middleware -|isThenable (object)|boolean|Return boolean if the supplied argument is Thenable (i.e. has an executable `then` member). All Promises, `nodent.EagerThenable()` and `nodent.Thenable` return true -|$asyncbind|function|Required runtime in ES7/Promises mode -|$asyncspawn|function|Required runtime in generator mode +| Member | Type | | +|-----------------------------------------------|-----------|-----------------------------------| +|version |string | The currently installed version +|asyncify (PromiseProvider) |function | Return a function to convert an object with functions with callbacks to ones with async function members. `asyncify` is also a meta-property (see below) +|Thenable(function) EagerThenable()(function) |function | Nodent's in-built Promise implementation. `Thenable` is also a meta-property (see below) +|require(moduleName,options) |object | Import an async helper module| +|generateRequestHandler(path, matchRegex, options)|function | Create a function use with Express or Connect that compiles files for a browser on demand - like a magic version of the 'static' middleware +|isThenable (object) |boolean | Return boolean if the supplied argument is Thenable (i.e. has an executable `then` member). All Promises, `nodent.EagerThenable()` and `nodent.Thenable` return true +|$asyncbind |function | Runtime required in -promises and -engine mode if not compiled with `noRuntime:true` +|$asyncspawn |function | Runtime required in generator mode Note the nodent object has other members used for implementation - these are subject to change and are not part of the API. +> **v2.x** users - nodent.Thenable and nodent.EagerThenable() are now full Promises/A+-compliant Promise implementations. There is no external access to the synchronous 'Thenable' used in -es7-lazyThenables mode. + Meta-API -------- You can over-ride certain defaults and access values that are global to the process (as opposed to module by module) by instantiating nodent _without_ an argument: @@ -560,9 +451,8 @@ The available meta-properties are: | Member| Type | | |-------|-----|------------------------| -|Thenable|function|Default thenable protocol implementation| -|EagerThenable|function|EagerThenable() protocol factory| -|asyncify|object|Method to transform methods from callbacks to async functions by wrapping in Thenables| +|Thenable |function|Nodent's built in Promise/A+ implementation| +|asyncify |object|Method to transform methods from callbacks to async functions by wrapping in Promises| |setDefaultCompileOptions (compiler[,env])|function|Set the defaults for the compiler and environment. This should be called before the first compiler is created. The default environment options (`log augmentObject extension dontMapStackTraces asyncStackTrace`) will be used when the corresponding option is missing when the compiler is created. The compiler options (`sourcemap` and default symbol names) must be set before the first compiler is created. The other compilation options (`es7 promises generators engine`) are set by the corresponding directive| |setCompileOptions (name,compiler)|function|Set the compilation options for a named [directive](#advanced-configuration) for the compiler. This should be called before the first compiler is created. @@ -582,231 +472,36 @@ The return ('compiler') has the additional, instance specific properties specifi Built-in conversions and helpers ============================== -Nodentify has a (small but possibly growing) set of covers for common Node modules. You specify these through the `require` function: - - var nodent = require('nodent')() ; - var nhttp = nodent.require('http') ; - -Some covers can accept a configuation object, in this case specify the options in the second parameter: - - var http = nodent.require('http',{autoProtocol:true}) ; - -"http" and "https" ------------------- -The nodent version of http.get returns a Thenable: - - nhttp.get(options).then(function(response){},function(error){}) ; - -Hopefully you'll recognise this and be able to see you can now invoke it like: - - response = await nhttp.get(options) ; - -To make life even easier, the response is covered too, just before the first callback is invoked with an addition async function called "wait", that waits for a named event. The whole setup is therefore: - - var nodent = require('nodent')() ; // You have to do this somewhere to enable nodent - ... - var http = nodent.require('http') ; - - // Make a request. Nodent creates the callbacks, etc. for you - // and so you can read the line as "wait for the response to be generated" - var response = await http.get("http://npmjs.org/~matatbread") ; - - var body = "" ; - response.on('data',function(chunk){ body += chunk ;} ; - - // Wait for the "end" event - await response.wait('end') ; - - // The response is complete, print it out - console.log('The response is:\n"+body) ; - -http.request is similar, but not identical as you will need access to the request object to end it (amongst other things): - - var req = await http.request(options) ; - req.end() ; // Do whatever you need to with the request - // Wait for the "response" event - var response = await req.wait('response') ; - var body = "" ; - response.on('data',function(chunk){ body += chunk ;} ; - // Wait for the response to be completed - await response.wait('end') ; - console.log('The response is:\n"+body) ; - -The convenience function http.getBody(options) asynchronously gets a body encoded in UTF-8: - - console.log('The response is:", - await http.getBody("http://www.example.com/something")) ; - -The "http" cover (not https) can accept a single configuration option 'autoProtocol' that makes get(), request() and getBody() examine the passed url string or URL and use either http or https automatically. The default is "false", meaning request URLs via https will fail with a protocol mismatch. - -"map" ------ -The nodent cover "map" works like an aynchronous, parallel object/array mapper, similar to Array.prototype.map() or Promsie.all(). The map function takes three parameters: - -* the entity to iterate over, -* optionally an object in which to place the results (they are returned from the async map in any case), -* the async function to call on each iteration. - -The function completes when all the aync-iteration function calls have completed (via a return or exception). The order of execution of each async function is not guarenteed. When complete, the async-return is a complementary object or array containing the mapped values as returned asynchronously. If present, the return values are placed into the optional second parameter. If omitted, a new object or array is created to hold the results. The initial argument (the entity to iterate over) can be either: - -* An Object - each field is passed to the async-iterator function -* An Array - each element is passed to the async-iterator function -* A single Number - the async function is invoked with the integer values 0 to Number-1 -* An array or Object of async functions - each function in the array is invoked asynchronously. In this case the third parameter must be omitted. - -Example: mapping an object - - // Use nodent.map - var map = nodent.require('map') ; - - // Asynchronously map every key in "myObject" by adding 1 to the value of the key - mapped = await map(myObject,async function(key){ - // This can be async without issues - return myObject[key]+1 ; - }) ; - // All done - mapped contains the new object with all the elements "incremeneted" - - -Example: map an array of URLs to their content - - // Use nodent.map & http - var map = nodent.require('map') ; - var http = nodent.require('http') ; - - mapped = await map(['www.google.com','www.bbc.co.uk'],async function(value,index){ - // Get the URL body asynchronously. - return await http.getBody("http://"+value) ; - }) ; - // All done - mapped is the new array containing the bodies - -Example: iterate through a set of integer values and do something asynchronous with each one. - - // Use nodent.map & http - var map = nodent.require('map') ; - var http = nodent.require('http') ; - - mapped = await map(3,async function(i){ - // Get the URL body asynchronously. - return await nodent.http.getBody("http://example.com/cgi?test="+i) ; - }) ; - // All done - mapped is the new array containing the bodies - -Example: execute arbitrary async functions in parallel and return when they are all complete - - // Use nodent.map - var map = nodent.require('map') ; - - mapped = await map([asyncFn("abc"),asyncFn2("def")]) ; - - // All done - mapped is an new array containing the async-returns - -Example: execute arbitrary labelled async functions in parallel and return when they are all complete - - // Use nodent.map - var map = nodent.require('map') ; - - mapped = await map({for:asyncFn("abc"),bar:asyncFn2("def")}) ; - console.log(mapped.foo, mapped.bar) ; - - // All done - mapped is an new object containing the async-returns in each named member - -In the latter two cases, where there is only an single parameter, the async return value from `map` is a corresponding array or object to the parameter where each member has been resolved if Thenable (a Promise or async function value), or passed through unchanged if not Thenable. - -The order of execution is not guaranteed (as with all calls to map), but the completion routine will only be called when all async functions have finished either via a return or exception. the first function (at index [0]) and the async-return of the second funcrion (at index [1]). There is no programmatic limit to the number of async functions that can be passed in the array. Note that the functions have no useful parameters (use a closure or wrap the function if necessary). The order of execution is not guaranteed (as with all calls to map), but the completion routine will only be called when all async functions have finished either via a return or exception. - -### Exceptions in mapped functions -By default, in the event of an error or exception in the async-mapping function, the error value is substitued in the mapped object or array. This works well since all the exceptions will be instances of the JavaScript Error() type, and so they can be easily tested for in the mapped object after completion. - -The map() function only errors if an async function illegally returns more than once (including multiple errors or both an error and normal response). - -Alternatively, if instantiated with the option `throwOnError`, if any of the async invocations throw an exception, `map()` will throw an Error() when all the functions have completed, with a member called `results` containing the other results. To use this option: - - var map = nodent.require('map',{throwOnError:true}) ; - -Instances of 'map' are independent of each other - you can require() both the throwing and non-throwing version in different modules, or the same module as different variables. - -"asyncfunction" ---------------- - -The `AsyncFunction` type is returned by requiring 'asyncfunction'. This creates a class that can compile async functions on the fly (like `new Function()`). - -To access the type: - - var AsyncFunction = nodent.require('asyncfunction',opts) ; - -...where the `opts` parameter is optional, but if supplied contains the compiler flags as specified in [Advanced Configuration](#advanced-configuration). By default AsyncFunction uses Promises if they are defined globally, and ES7 mode otherwise. - -Once defined, you can create async functions on the fly just like normal functions: - - // Create a new async function - var add = new AsyncFunction("i","j","return i+j") ; - - console.log(add instanceof Function) // true: An AsyncFunction is also a function - console.log(add instanceof AsyncFunction) // true - console.log(add.toString()) // The original source "return i+j" - console.log(add.toES5String()) // The compiled source - console.log(await add(10,11)) // 21 - -nodent.asyncify ---------------- -This helper function wraps "normal" Node asynchronous functions (i.e. those whose final paramter is of the form `function(err,data)`) to make them usuable with `await`. For example, to asyncify the standard Node module 'fs': - - // Require 'fs' - var fs = require('fs') ; - // Get a reference to nodent.asyncify - var asyncify = require('nodent').asyncify ; - // Asyncify 'fs' - var afs = asyncify(nodent.Thenable)(fs) ; - console.log((await afs.readFile("./test/a.js")).toString()) ; - -By default, asyncify creates an object that has it's ancestor as its prototype with functions members mapped to the await call signature. -Internally, asyncify filters these so that only functions that don't end in 'Sync' and that have a member named the same without 'Sync'. -For 'fs', this works (readFile does not end in Sync, and so is mapped, readFileSync ends in 'Sync' and a member called 'readFile' exists). - -You can optionally supply your own filter to asyncify. For example to only map a function called 'queryDb': - - var aDB = asyncify(DB,function(name,newObject){ - return name=="queryDb" ; - }) ; - -You can also supply an option third parameter to asyncify() to avoid name-clashes (you often won't need this as asyncify builds a new object with the original as the prototype). - - var afs = asyncify(require('fs',null,"Async") ; - // Async version of readFile() has "Async" appended - await afs.readFileAsync("./mydata.txt") ; - -If you specifiy the environment option `augmentObject` as in `require('nodent')({augmentObject:true})` you can directly asyncify an API, for example: - - // Create a redis client from a library that can be used with await - var redis = require('redis').asyncify() ; +Nodent has a small set of covers for common Node modules. More information can be found [here](./docs/helpers.md) Testing ======= -Nodent has a test suite (in ./tests) which is itself a node package. Since it requires a bunch of Promise implementations to test against, it is NOT installed when 'nodent' is installed. The Promise implementations are option - you can run the tests using the nodent es7, Thenable and native Promises (if available) without installing any other modules. If you want to run the tests: +Nodent has a test suite (in ./tests) which is itself a node package. Since it requires a bunch of Promise implementations to test against, it is NOT installed when 'nodent' is installed. The Promise implementations are option - you can run the tests using the nodent.Thenable and native Promises (if available) without installing any other modules. To run the tests: + + npm test + +If you want to run the tests against some popular Promise libraries: cd tests npm install cd .. ./nodent.js tests -The tests themselves are normal (nodented) JavaScript files invoked with the parameters require,module and Promise. If you want to add a test, make sure it exports a single async function which the test runner can call. The async return value from this function should be `true` for success and `false` for failure. - -If you wish to add a Promise implementation to test against, add it to the dependencies in tests/package.json and give it an entry in the tests/index.js test runner. - The test runner in tests/index.js accepts the following options: ./nodent.js tests [OPTIONS] [test-files] - --quiet Suppress any errors or warnings --quick Don't target a specific execute time, just run each test once - --generators Performance test syntax transformations (the default) and generators as well + --nogenerators Performance test syntax transformations only, not generators --genonly Only run the performance tests for generator mode --engine Performance test the underlying engine's support for async and await (e.g. Chrome v53 with flags) --syntax Check the parser/output code before running semantic tests --syntaxonly Only run syntax tests --forceStrict Run the tests with a 'use strict' inserted at the top of every test file +> **v2.x** users - The flag --generators has been replaced by --nogenerators, which has the opposite sense. + Performance ----------- @@ -816,288 +511,85 @@ Run the test script without the `--quick` option to see how nodent code performs Additionally, a try the following links to test performance against Babel and Traceur. -[nodent](http://nodent.mailed.me.uk/#function%20pause%28%29%20{%0A%20%20%20%20return%20new%20Promise%28function%20%28%24return%2C%20%24error%29%20{%0A%20%20%20%20%20%20%20%20setTimeout%28function%20%28%29%20{%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%24return%280%29%3B%0A%20%20%20%20%20%20%20%20}%2C%200%29%3B%0A%20%20%20%20}%29%3B%0A}%0A%0Aasync%20function%20doNothing%28%29%20{%0A%20%20%20%20return%3B%0A}%0A%0Aasync%20function%20test%28%29%20{%0A%20%20%20%20var%20t%20%3D%20Date.now%28%29%3B%0A%20%20%20%20for%20%28var%20j%20%3D%200%3B%20j%20%3C%2050%3B%20j%2B%2B%29%20{%0A%20%20%20%20%20%20%20%20for%20%28var%20i%20%3D%200%3B%20i%20%3C%201000%3B%20i%2B%2B%29%20{%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20doNothing%28%29%3B%0A%20%20%20%20%20%20%20%20}%0A%20%20%20%20%20%20%20%20await%20pause%28%29%3B%0A%20%20%20%20}%0A%20%20%20%20return%20Date.now%28%29%20-%20t%3B%0A}%0A%0Atest%28%29.then%28alert%29%3B%0A) 456ms ('Pure ES5' mode is even faster - 261ms), or 696ms using 'Promises/Generator' mode. - -[babel](https://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=function%20pause%28%29%20{%0A%20%20%20%20return%20new%20Promise%28function%20%28%24return%2C%20%24error%29%20{%0A%20%20%20%20%20%20%20%20setTimeout%28function%20%28%29%20{%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%24return%280%29%3B%0A%20%20%20%20%20%20%20%20}%2C%200%29%3B%0A%20%20%20%20}%29%3B%0A}%0A%0Aasync%20function%20doNothing%28%29%20{%0A%20%20%20%20return%3B%0A}%0A%0Aasync%20function%20test%28%29%20{%0A%20%20%20%20var%20t%20%3D%20Date.now%28%29%3B%0A%20%20%20%20for%20%28var%20j%20%3D%200%3B%20j%20%3C%2050%3B%20j%2B%2B%29%20{%0A%20%20%20%20%20%20%20%20for%20%28var%20i%20%3D%200%3B%20i%20%3C%201000%3B%20i%2B%2B%29%20{%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20doNothing%28%29%3B%0A%20%20%20%20%20%20%20%20}%0A%20%20%20%20%20%20%20%20await%20pause%28%29%3B%0A%20%20%20%20}%0A%20%20%20%20return%20Date.now%28%29%20-%20t%3B%0A}%0A%0Atest%28%29.then%28alert%2Calert%29%3B%0A) 684ms - more than 1.5x slower - -[traceur](https://google.github.io/traceur-compiler/demo/repl.html#%2F%2F%20Options%3A%20--annotations%20--array-comprehension%20--async-functions%20--async-generators%20--exponentiation%20--export-from-extended%20--for-on%20--generator-comprehension%20--member-variables%20--proper-tail-calls%20--require%20--symbols%20--types%20%0Afunction%20pause%28%29%20{%0A%20%20%20%20return%20new%20Promise%28function%20%28%24return%2C%20%24error%29%20{%0A%20%20%20%20%20%20%20%20setTimeout%28function%20%28%29%20{%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%24return%280%29%3B%0A%20%20%20%20%20%20%20%20}%2C%200%29%3B%0A%20%20%20%20}%29%3B%0A}%0A%0Aasync%20function%20doNothing%28%29%20{%0A%20%20%20%20return%3B%0A}%0A%0Aasync%20function%20test%28%29%20{%0A%20%20%20%20var%20t%20%3D%20Date.now%28%29%3B%0A%20%20%20%20for%20%28var%20j%20%3D%200%3B%20j%20%3C%2050%3B%20j%2B%2B%29%20{%0A%20%20%20%20%20%20%20%20for%20%28var%20i%20%3D%200%3B%20i%20%3C%201000%3B%20i%2B%2B%29%20{%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20doNothing%28%29%3B%0A%20%20%20%20%20%20%20%20}%0A%20%20%20%20%20%20%20%20await%20pause%28%29%3B%0A%20%20%20%20}%0A%20%20%20%20return%20Date.now%28%29%20-%20t%3B%0A}%0A%0Atest%28%29.then%28alert%2Calert%29%3B%20%0A) 1175ms - more than 2.5x slower - -The example timings are from Chrome v49 on Mac OSX. I get even wider results with Firefox, and dramatically wider results on mobiles (nodent ES7 mode is upto 10x faster than generators and transpilers). - -The test is a simple set of nested loops calling async functions that don't do much. The purpose is to illustrate the overhead generated in the transpilation by each compiler. In reality, you'd be crazy to use async calls for everything, but very well advised to use them for I/O bound operations (network, disks, etc). In these cases, you can be reasonably certain that the overhead generated by the compilers would be small in comparison to the actual operation....but it's nice to know you're not wasting cycles, right? For those who want to know why, the real reason is the use of generators (the suggested implementation in the ES7 async/await specification), which are inefficient natively (about 50% slower than using 'nodent-promises'), and even worse when transcompiled into ES5. - -Changelog -========== - -06-Oct-16 v2.6.11 - -- Bump acorn-es7-plugin to resolve issues with 'async get(){}' -- Merge in fixes related to some loop constructs and use of 'finally{ return...}' in synchronous code called by async code - -28-Sep-16 v2.6.10 - -- Fix issue with scoping on let/const declarations in for-loop initializers -- Maintain location information when replacing/appending nodes -- Add tests for dual-loop-scope and dual-while-throw - -25-Sep-16 v.2.6.9 - -- Update acorn-es7-plugin to handle `async(()=>0)` -- Fix case where generator mode generated an illegal anonymous FunctionDefintion from an ArrowFunctionExpression that was never in an expression - - -18-Sep-16 v2.6.7, v2.6.8 - -- Handle loops with no test condition `for(;;){}` -- Don't attempt to hoist `export`ed named declarations -- Correctly identify Babel types Object/ClassMethod as scopes to avoid illegal mappings - -05-Sep-16 v2.6.6 - -- Fix JS output routine which (incorrectly) removed the asterisk from `yield *`. -- Improve syntax testing to catch differences in primitive values in ASTs - -26-Aug-16 v2.6.5 - -- Fix JS output routine which (incorrectly) removed parenthesis from `a || (()=>b)` causing a precedence problem. - -20-Aug-16 v2.6.4 - -- Fix issue with `for..of` with a `const` or destructuring initializer -- Optimize EagerThenable and guard multiple Promise resolutions - -06-Aug 16 v2.6.3 - -- Fix issue with non-trivial destructuring assignments (fixes https://github.com/MatAtBread/fast-async/issues/8 - thanks to @simonbuchan) -- Fix case where empty "else" block throws an expcetion (fixes https://github.com/MatAtBread/nodent/issues/50 - thanks to @Antender) -- Fix case where single line exported function was incorrectly hoisted with no reference (fixes https://github.com/MatAtBread/fast-async/issues/7 - thanks to @simonbuchan, @naturalethic and @nightwolfz) -- Bump acorn to v3.3.0 - -20-Jul 16 v2.6.2 - -- Update acorn-es7-plugin to fix issue with `export async` in webpack -- Fix edge case where a continuation function is treated as a block-scoped +[nodent](http://nodent.mailed.me.uk/#function%20pause()%20%7B%0A%20%20%20%20return%20new%20Promise(function%20(%24return%2C%20%24error)%20%7B%0A%20%20%20%20%20%20%20%20setTimeout(%24return%2C%200)%3B%0A%20%20%20%20%7D)%3B%0A%7D%0A%0Aasync%20function%20doNothing()%20%7B%0A%20%20%20%20return%3B%0A%7D%0A%0Aasync%20function%20test()%20%7B%0A%20%20%20%20var%20t%20%3D%20Date.now()%3B%0A%20%20%20%20for%20(var%20j%20%3D%200%3B%20j%20%3C%20100%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20for%20(var%20i%20%3D%200%3B%20i%20%3C%201000%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20doNothing()%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20await%20pause()%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20Date.now()%20-%20t%3B%0A%7D%0A%0Atest().then(alert)%3B%0A) 356ms -15-Jul 16 v2.6.0 +[babel](http://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=false&presets=stage-2&experimental=true&loose=false&spec=true&playground=false&code=function%20pause()%20%7B%0A%20%20%20%20return%20new%20Promise(function%20(%24return%2C%20%24error)%20%7B%0A%20%20%20%20%20%20%20%20setTimeout(%24return%2C%200)%3B%0A%20%20%20%20%7D)%3B%0A%7D%0A%0Aasync%20function%20doNothing()%20%7B%0A%20%20%20%20return%3B%0A%7D%0A%0Aasync%20function%20test()%20%7B%0A%20%20%20%20var%20t%20%3D%20Date.now()%3B%0A%20%20%20%20for%20(var%20j%20%3D%200%3B%20j%20%3C%20100%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20for%20(var%20i%20%3D%200%3B%20i%20%3C%201000%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20doNothing()%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20await%20pause()%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20Date.now()%20-%20t%3B%0A%7D%0A%0Atest().then(alert)%3B%0A) 1072ms - more than 3x slower -- Optimize case where 'engine' generates an 'awaitAnywhere' Promise-style callback for an async function nested inside a sync function, and `await` is legal. -- Bump version to make support for 'engine' mode easier to detect in toolchains. +[traceur](https://google.github.io/traceur-compiler/demo/repl.html#%2F%2F%20Options%3A%20--async-functions%20--source-maps%20%0Afunction%20pause()%20%7B%0A%20%20%20%20return%20new%20Promise(function%20(%24return%2C%20%24error)%20%7B%0A%20%20%20%20%20%20%20%20setTimeout(%24return%2C%200)%3B%0A%20%20%20%20%7D)%3B%0A%7D%0A%0Aasync%20function%20doNothing()%20%7B%0A%20%20%20%20return%3B%0A%7D%0A%0Aasync%20function%20test()%20%7B%0A%20%20%20%20var%20t%20%3D%20Date.now()%3B%0A%20%20%20%20for%20(var%20j%20%3D%200%3B%20j%20%3C%20100%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20for%20(var%20i%20%3D%200%3B%20i%20%3C%201000%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20doNothing()%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20await%20pause()%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20Date.now()%20-%20t%3B%0A%7D%0A%0Atest().then(alert)%3B%0A) 1175ms - more than 3x slower -14-Jul-16 v2.5.10 +The example timings are from Chrome v53 on Mac OSX. I get even wider results with Firefox, and dramatically wider results on mobiles (nodent ES7 mode is upto 10x faster than generators and transpilers). -- Add 'engine' mode that _only_ transpiles the nodent ES7 extensions (`async get x(){}`, `async throw x`, `async return` and `await` anywhere). Standard ES7 async/await constructs are passed through the compiler unchanged to be run by the underlying engine (e.g. Chrome v53 or Edge v14). -- Implement parsing for proposed ES7 Object spread `var {a,b,...x} = y ;` used in Babel (see https://github.com/MatAtBread/fast-async/issues/6) +The test is a simple set of nested loops calling async functions that don't do much. The purpose is to illustrate the overhead generated in the transpilation by each compiler. In reality, you'd be crazy to use async calls for everything, but very well advised to use them for I/O bound operations (network, disks, streams, etc). In these cases, you can be reasonably certain that the overhead generated by the compilers would be small in comparison to the actual operation....but it's nice to know you're not wasting cycles, right? For those who want to know why, the real reason is the use of generators (the suggested implementation in the ES7 async/await specification), which are inefficient natively (about 50% slower than using 'nodent-promises'), and even worse when transcompiled into ES5. -08-Jul-16 v2.5.9 - -- Revert change to runtime inclusion in 2.5.8 as it breaks some live uses of browserify/webpack, and was only included to make istanbul work -- Correctly hoist and implement block scoping for `let`, `const`, etc. Note: the actual mapping attempts to provide the most compatable code since pre-ES6 implementations of `const` differ between JS engines and strict/sloppy mode. It is possible some early code that works on non-standard JS implementations may execute differently or fail to compile. Additionally, use of `let` and `const` is slightly slower is it involves the creation of additional scopes in awaited callbacks. -- Implement dual-mode test for const scoping -- Fix typo in covers/https to prevented get() from working -- Fix path resolution for nodent.require() built-ins -- Fix typos when defining writable Object properties - -04-Jul-16 v2.5.7 - -- Correctly export MapError (for instanceof) as map.MapError - -27-Jun-16 v2.5.6 - -- Correctly bind `this` inside loops if it is referenced (https://github.com/MatAtBread/nodent/issues/39). Many thanks to https://github.com/jeffreylin for the test case and fix. -- Fix command-line option `--use=(mode)` with text input -- Bump acorn-es7-plugin to to 1.0.15 - -06-Jun-16 v2.5.5 - -- Correct hoisting of destructing var declarations (implemented in nodejs >6.0.0) - -18-May-16 v2.5.4 - -- Bump acorn-es7-plugin (parses comments between async and function correctly) -- Correct resolution of package.json for npm >v3.x to ensure modules using different nodent configurations read the 'nearest' one based on the location of the source, not the nodent installation directory - -03-May-16 v2.5.2, v2.5.3 - -- Update to latest acorn (2.5.3) -- Update acorn-es7-plugin to correctly parse the statement `export async function name(){...}` as _async function name(){...}_ is a valid named declaration. (2.5.2) - -21-Apr-16 v2.5.1 - -- Place runtimes ($asyncbind and $asyncspawn) in a separate file ('lib/runtime.js') so the dedicated Babler or other tool builder can extract them without having to include the entire compiler. - -01-Apr-16 v2.5.0 - -- Implement `nodent.EagerThenable()` to provide Promise-like (but unchainable) execution semantics (eager evaluation, asynchronous resolution) -- Implement new test harness to collate performance by mode and Promise implementation -- Allow optional passing of a Promise type to the covers http, https, map, events and movre Thenable to it's own fix to ease integration with Browserify or Webpack (specifically, these covers can be required directly as there is no hard dependancy on the 'nodent' parameter, and so no need to require the entire library into thebrowser). The default behaviour is now to use the global.Promise if present, or nodent.Thenable if not. -- Update README - -01-Mar-16 v2.4.1 - -- Significant improvement in compilation speed by re-factoring output.js (thanks to [@davidbonnet](https://github.com/davidbonnet) for pointing it out) -- Update to acorn v3.0.4 - -04-Feb-16 v2.4.0 - -- Update to [Acorn v2.7.0](https://github.com/ternjs/acorn/commit/1405436064bff087f14af55a763396aa5c0ca148). This tightens up the parsing of some ES6 edge cases and could possibly [break](https://github.com/ternjs/acorn/pull/317) old ES5 sloppy mode code -- Implement 'eager' evaluation for 'ES7' mode (promises & generators always were eager). - -02-Feb-16 v2.3.11-v2.3.13 - -- Fix issue where different versions of nodent attempt to use different loggers -- Fix typo in mapCondOp -- Improve compiler performance (approx 25%) -- Fix issues related to the generation of nested FunctionDeclarations in ES5-Strict mode -- Re-implement mapLogicalOps & mapCondOps to generate correct code for expressions like `a || b && await c`. Previous version produced code that wouldn't run. -- Allow the option `{log:false}` instead of a no-op function -- Correctly place directives at the top of the Program/function when hoisting declarations. -- Thanks to https://github.com/epoberezkin for the additional test cases and enhancements - -17-Dec-15 v2.3.10 - -- Provide the cover 'asyncfunction' which implements the type `AsyncFunction` to dynamically compile and create asynchronous functions. - -16-Dec-15 v2.3.9 - -- Correct cases where arrow functions contain deeply nested expressions containing await and logical/conditional operators -- Fix edge cases in code output (sparse array constants, object pattern precedence, generator member functions), add everything.js syntax tests - -10-Dec-15 v2.3.7 - -- Correctly asynchronize ES6 `for...in` loops. -- Update the plugin code to remove 'async' and 'await' from the super-strict keyword tests introduced in acorn v2.6.x that generate parse errors before the plugin gets a chance to manage them. Also compatible with acorn v2.5.2 as used by previous versions of nodent. -- Remove spurious 'debugger' statement, fix case where for..in body is a single expression. - -09-Dec-15 v2.3.5 - -- Correctly asynchronize ES6 `for...of` loops. - -08-Dec-15 v2.3.4 - -- Mark ArrowFunctionExpression containing a BlockStatement as having a scope (it does in Chrome 46) to constrain hoisting of variables declared inside Arrow Functions -- Correct 'return undefined' suppression in object/class methods (as well as normal functions) -- Numerous fixes to make Babel-style Object/ClassMethod play nicely with the ESTree Property & MethodDefinition (v2.3.1-2.3.3) - -07-Dec-15 v2.3.0 - -- Implement version-aware in-process JS compiler so modules built with different versions of nodent can co-exist -- Implement wrapAwait option to allow for the `await nonPromise` edge-case enabled in the standard implementation -- Implement 'optionSets' for each `use nodent` directive and allow their specification in the package.json to avoid use unnecessary use of setDefaultCompileOptions() and the consequent dependency between code and environment. -- Implement labeled `break` and `continue` containing `await` -- Only suppress the automatic insertion of `return undefined` if a function uses `async return` or `async throw`. Other async functions now return `undefined` asynchronously if the run to completion. - -04-Dec-15: v2.2.10 - -- Fix error that mangled the declaration of a `let` statement under certain conditions - -24-Nov-15: v2.2.9 - -- Report the original filename being parsed in handling SyntaxError from acorn. -- Only warn about for...in/of loops if they contain an `await` - -23-Nov-15: v2.2.8 - -- Fix case where `await` inside a non-async arrow function attempted to evaluate the await outside of the function body. Create the test case es6-object-arrow to catch this case. - -12-Nov-15: v2.2.7 - -- Correctly bind 'Finally' so that 'this' is maintained in success cases -- Return initialize from setDefaultCompileOptions() so the statement `nodent = require('nodent').setDefaultCompileOptions(...)()` works -- Fix implementation of Object.isThenable - -06-Nov-15: v2.2.6 - -- Fix incorrect 'async' value on AST Property and correctly use the Property.value.async for full compliance with the ESTree spec. -- Update to acorn-es7-plugin 1.0.9 (fixes source location for async and await, and adds tests thanks to @jamestalmage) - -04-Nov-15: v2.2.4 - -- Support enhanced ESTree constructs as used by Babel v6.x.x for [fast-async](https://www.npmjs.com/package/fast-async) Babel plugin -- Only hoist (scoped) declarations if the scope contains an 'await'. +Upgrading +--------- +v3.0.0 -30-Oct-15: v2.2.2 +Nodent v3 is a significant update. If your project isn't using any internal functions or monkey patching, you should just be able to upgrade by changing your package.json - with very few exceptions the external API and features are the same. Major changes are: -- Correct case where an ArrowFunctionExpression.body is a SequenceExpression (requires parens), e.g `x => (x,y)`, which is different from `x => x,y` -- Include parentheses in the expression +(+x) to avoid it looking like ++x +* The Promise implementation used by Nodent by default is based on the most excellent [Zousan](https://github.com/bluejava/zousan) Promise library. After a lot of looking around and testing, Zousan proved to be one of the smallest and fastest around - small enough to fit +into Nodent's runtime and faster in most practical cases than Bluebird and when. The speed is enhanced compared to most libraries as Nodent rarely generates code that relies on Promise chaining or having multiple `.then()` calls on the same Promise (v2 never did this, v3 does). Zousan is optimal in these cases as only creates the required data lazily. -29-Oct-15: v2.2.0 +* Promises are now the default execution mode. Only -es7 with lazyThenables uses the synchronous Thenable protocol. This is only retained for speed in exceptional cases. In almost all practical applications (i.e. using async functions to handle IO of some sort), the overhead is around 20% _per call_, meaning it is trivial compared to the IO operation. Nodent syntax-transformation remains around 3-4 times faster than both native generators and libraries like regenerator. -- Implement the correct conditional execution semantics for `&& || ?:` whether they contain `await` expressions or not. -- Revert to `'sourceType: 'script'` as 'module' forces 'strict' mode and breaks some existing files. You can override the sourceType (see v2.1.11) if necessary. -- Enable 'import' and 'export' statements, even in 'script' mode. Nodent does nothing with these statements, but simply passes them through to your execution platform or transpiler to implement. -- Add syntax testing to the test suite. This has been tested against over 75,000 .js files and a number of edge cases has been fixed. -- Throw a nodent::map.MapError (derived from Error) if the `map` cover encounters an error, or one of the delegates does when `throwOnError:true` +* The use of Promises/A+ compliant execution means the recursive loop asynchronisation used in Nodent v2 can be unwound. Specifically, `for`, `while` and `do...while` loops in Nodent v2 were recursive if the loop didn't yield control to an async operation. This meant that you could run out of stack on relatively trivial loop. For example: -25-Oct-15: v2.1.11 +``` +for (var i=0; i=4.0.0 or modern browsers. It is unlikely to be worthwhile specifying an es6target if you intend to transpile that es6 to es5 (e.g. with Babel) - in that case don't specify the option and compile straight to ES5. -- Rationalise CLI option parsing. Allow javascript/ast from stdin -- Fix get/set method output -- Fix method async get _() {} -- Error on method async set _() {} +* The helper function `noDentify` which was deprecated in v2 has now been removed from the main Nodent library. It can be required by `Function.prototype.noDentify = nodent().require('nodentify')` if needed. -06-Oct-15: v2.1.0 +Changelog +========== -- BREAKING CHANGE: The ES7 extensions _return async ..._ and _throw async ..._ have been changed to `async return...` and `async throw...`. This was necessary as the inability to parse 'return async function()...' unambiguously is (clearly) a mistake. If you have a large body of code using the previous syntax extension, stick to v2.0.x or earlier, however it is typically a simple search-and-replace (it was in all our code). -- `async` is now only a keyword in the correct contexts, specifically before a function declaration, function expression, arrow function or member function. Elsewhere it is parsed as an identifier (i.e. a variable, named function, etc.). This change has been made to be closer to the ES7 specification for 'async'. -- `await` is now only a keyword in the correct contexts, specifically inside an `async` function body (or arrow expression). This change has been made to be closer to the ES7 specification for 'await'. Additionally, outside of an `async` function body, nodent allows `await` where it cannot be an identifier. In practice this means almost everywhere, except when the argument to `await` is parenthesized, i.e. from a standard function you can `await x` (as before, with a warning), but you cannot `await (x)` as it parses as a function call to a a function called 'await'. Nodent translates 'await' outside a function into a ".then(...)" call. -- Added the `asyncStackTrace` environment option, which shows the current stack and the async caller (if available). +06-Oct-16 v3.0.0 [see above](#upgrading) -02-Oct-15: v2.0.4 +28-Sep-16 v2.6.10 -- Add --pretty to cli (output input, no transformation) -- Add [] as a final option to .noDentify() to forward all arguments to the callback to the awaiting call (useful for very non-standard callbacks that split results across parameters) -- Include the first line of the async stack trace (usually the message) -- Add 'npm test' script Rationalise x-* tests Add asyncify test +- Fix issue with scoping on let/const declarations in for-loop initializers +- Maintain location information when replacing/appending nodes +- Add tests for dual-loop-scope and dual-while-throw -29-Sep-15: v2.0.2 +25-Sep-16 v2.6.9 -- Add --sourcemap option to command line -- Tidy up stack trace mapping -- Add option throwOnError to covers/map (and test case) -- Remove extraneous line end after "debugger;" statement -- Only set EventEmitter.prototype.wait once (since covers can now be re-instantiated) +- Update acorn-es7-plugin to handle `async(()=>0)` +- Fix case where generator mode generated an illegal anonymous FunctionDefintion from an ArrowFunctionExpression that was never in an expression -27-Sep-15: v2.0.1 +[Older changes...](./docs/changelog-2.md) -- Fix case where `if (x) return await y ;` incorrectly evaluated the `await` before the test. +Credits +======= -23-Sep-15: v2.0.0. Initial release of Nodent v2.x.x., which has moved from UglifyJS to the acorn parser and the ESTree AST representation, mainly for performance and to support ES6 targets such as Node v4.x.x. See "upgrading" below. +Thanks to the people who reported bugs, provided test cases and fixes while I was working on v3. Here's a few that I remember, in alpha order: + +- https://github.com/Antender +- https://github.com/Arnavion +- https://github.com/Rush +- https://github.com/SimonDegraeve +- https://github.com/coudly +- https://github.com/dempfi +- https://github.com/dessaya +- https://github.com/epoberezkin +- https://github.com/jnvm +- https://github.com/naturalethic +- https://github.com/nightwolfz +- https://github.com/runn1ng +- https://github.com/simonbuchan +- https://github.com/steve-gray -Upgrading ---------- -v2.1.x BREAKING CHANGE - -The ES7 extensions _return async ..._ and _throw async ..._ have been changed to `async return...` and `async throw...`. This was necessary as the inability to parse 'return async function()...' unambiguously is (clearly) a mistake. If you have a large body of code using the previous syntax extension, stick to v2.0.x or earlier. - -Nodent v2.0.0 is a major update. There may be some breaking changes. Significant changes are: - -* Moved from Uglify2 to Acorn for input parsing, and re-written much of tree manipulation -* Supports ES6 input syntax, so suitable for adding async & await to ES6 code in Node v4.x.x -* Additional tests for interoperability between -es7, -promises and -generator mode -* Cleaner options for code generation and configuration. -* The old ( var name = function(args}{body}.$asyncbind(this) - type: 'VariableDeclaration', - kind: 'var', - declarations: [{ - type: 'VariableDeclarator', - id: ident(name), - init: { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "FunctionExpression", - "id": null, - "generator": false, - "expression": false, - "params": argnames || [], - "body": body - }, - "property": genIdent.asyncbind, - "computed": false - }, - "arguments": [{ - "type": "ThisExpression" - },binding] - } - }] - }; - } - /* Create a 'continuation' - a block of statements that have been hoisted * into a named function so they can be invoked conditionally or asynchronously */ function makeContinuation(name, body) { @@ -464,18 +462,7 @@ function asynchronize(pr, __sourceMapping, opts, logger) { function thisCall(name, args) { if (typeof name === 'string') name = ident(name); - var n = { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": name, - "property": ident('call'), - "computed": false - }, - "arguments": [{ - "type": "ThisExpression" - }].concat(args || []) - }; + var n = parser.part("$0.call($1)",[name,[{"type": "ThisExpression"}].concat(args || [])]).expr ; name.$thisCall = n; n.$thisCallName = name.name; return n; @@ -578,31 +565,17 @@ function asynchronize(pr, __sourceMapping, opts, logger) { await (async function(){ if (a) return b ; return c }) Note that 'await (async function(){})()' can be optimized to a Thenable since no args are passed - */ + */ function mapCondOp(ast, state) { if (Array.isArray(ast)) return ast.map(function (n) { - return mapCondOp(n, state); - }); + return mapCondOp(n, state); + }); parser.treeWalker(ast, function (node, descend, path) { descend(); if (node.type === 'ConditionalExpression' && (containsAwait(node.alternate) || containsAwait(node.consequent))) { var z = ident(generateSymbol("condOp")); - var xform = internalIIAFE([ - { - "type": "IfStatement", - "test": node.test, - "consequent": { - "type": "ReturnStatement", - "argument": node.consequent - }, - "alternate": null - }, - { - "type": "ReturnStatement", - "argument": node.alternate - } - ]) ; + var xform = internalIIAFE(parser.part("if ($0) return $1 ; return $2",[node.test,node.consequent,node.alternate]).body); coerce(node, xform); } }, state); @@ -620,72 +593,22 @@ function asynchronize(pr, __sourceMapping, opts, logger) { function mapLogicalOp(ast, state) { if (Array.isArray(ast)) return ast.map(function (n) { - return mapLogicalOp(n, state); - }); + return mapLogicalOp(n, state); + }); parser.treeWalker(ast, function (node, descend, path) { - function awaitedTest(truthy, falsy) { - return internalIIAFE([{ - "type": "VariableDeclaration", - "declarations": [{ - "type": "VariableDeclarator", - "id": z, - "init": null - }], - "kind": "var" - },{ - "type": "IfStatement", - "test": truthy, - "consequent": falsy, - "alternate": null - },{ - "type": "ReturnStatement", - "argument": z - }]) ; - } - descend(); if (node.type === 'LogicalExpression' && containsAwait(node.right)) { - var xform; + var codeFrag; var z = ident(generateSymbol("logical" + (node.operator === '&&' ? "And" : "Or"))); if (node.operator === '||') { - xform = awaitedTest({ - "type": "UnaryExpression", - "operator": "!", - "prefix": true, - "argument": { - "type": "AssignmentExpression", - "operator": "=", - "left": z, - "right": node.left - } - }, { - "type": "ExpressionStatement", - "expression": { - "type": "AssignmentExpression", - "operator": "=", - "left": z, - "right": node.right - } - }); + codeFrag = "var $0; if (!($0 = $1)) {$0 = $2} return $0" ; } else if (node.operator === '&&') { - xform = awaitedTest({ - "type": "AssignmentExpression", - "operator": "=", - "left": z, - "right": node.left - }, { - "type": "ExpressionStatement", - "expression": { - "type": "AssignmentExpression", - "operator": "=", - "left": z, - "right": node.right - } - }); + codeFrag = "var $0; if ($0 = $1) {$0 = $2} return $0" ; } else throw new Error(where(node) + "Illegal logical operator: "+node.operator); - coerce(node, xform); + + coerce(node, internalIIAFE(parser.part(codeFrag,[z,node.left, node.right]).body)); } }, state); return ast; @@ -741,14 +664,14 @@ function asynchronize(pr, __sourceMapping, opts, logger) { ref.replace(synthBlock); if (deferredCode.length) { var call = returnThisCall(symName); - synthBlock.body.unshift(down(makeContinuation(symName, deferredCode))); + synthBlock.body.push(down(makeContinuation(symName, deferredCode))); [ifStmt.consequent,ifStmt.alternate].forEach(function (cond) { if (!cond) return; var blockEnd; if (!examine(cond).isBlockStatement) blockEnd = cond; - else + else blockEnd = cond.body[cond.body.length - 1]; if (!(blockEnd && blockEnd.type === 'ReturnStatement')) { if (!(cond.type === 'BlockStatement')) { @@ -819,45 +742,45 @@ function asynchronize(pr, __sourceMapping, opts, logger) { if ((node.$containedAwait = !!containsAwait(node)) || (node.finalizer && inAsync(path) && (contains(node.finalizer.body,isExitStatement)))) { node.$needsMapping = true ; - var parent = getExit(path, [opts]); - if (node.finalizer && !node.handler) { - // We have a finally, but no 'catch'. Create the default catch clause 'catch(_ex) { throw _ex }' - var exSym = ident(generateSymbol("exception")); - node.handler = { - "type": "CatchClause", - "param": exSym, - "body": { - "type": "BlockStatement", - "body": [{ - "type": "ThrowStatement", - "argument": exSym - }] - } - }; - } - if (!node.handler && !node.finalizer) { - var ex = new SyntaxError(where(node.value) + "try requires catch and/or finally clause", pr.filename, node.start); - ex.pos = node.start; - ex.loc = node.loc.start; - throw ex; - } - if (node.finalizer) { - setExit(node.block, { - $error: node.$seh + "Catch", - $return: deferredFinally(node, parent.$return) - }); - setExit(node.handler, { - $error: deferredFinally(node, parent.$error), - $return: deferredFinally(node, parent.$return) - }); - } else { - setExit(node.block, { - $error: node.$seh + "Catch", - $return: parent.$return - }); + var parent = getExit(path, [opts]); + if (node.finalizer && !node.handler) { + // We have a finally, but no 'catch'. Create the default catch clause 'catch(_ex) { throw _ex }' + var exSym = ident(generateSymbol("exception")); + node.handler = { + "type": "CatchClause", + "param": exSym, + "body": { + "type": "BlockStatement", + "body": [{ + "type": "ThrowStatement", + "argument": exSym + }] + } + }; + } + if (!node.handler && !node.finalizer) { + var ex = new SyntaxError(where(node.value) + "try requires catch and/or finally clause", pr.filename, node.start); + ex.pos = node.start; + ex.loc = node.loc.start; + throw ex; + } + if (node.finalizer) { + setExit(node.block, { + $error: node.$seh + "Catch", + $return: deferredFinally(node, parent.$return) + }); + setExit(node.handler, { + $error: deferredFinally(node, parent.$error), + $return: deferredFinally(node, parent.$return) + }); + } else { + setExit(node.block, { + $error: node.$seh + "Catch", + $return: parent.$return + }); + } } } - } descend(); }); return ast; @@ -917,61 +840,21 @@ function asynchronize(pr, __sourceMapping, opts, logger) { ref.parent[ref.field].splice(ref.index,0,catcher) ; } if (node.finalizer) { - var finalizer = { - type: "VariableDeclaration", - kind: "var", - declarations: [{ - type: "VariableDeclarator", - id: ident(node.$seh + "Finally"), - init: { - type: "CallExpression", - callee: { - type: "MemberExpression", - object: { - type: 'FunctionExpression', - params: [ident(node.$seh + "Exit")], - body: { - type: 'BlockStatement', - body: [{ - type: 'ReturnStatement', - argument: { - type: 'CallExpression', - arguments: [{ - type: 'ThisExpression' - },binding.$error], - callee: { - type: 'MemberExpression', - property: genIdent.asyncbind, - object: { - type: 'FunctionExpression', - params: [ident(node.$seh + "Value")], - body: { - type: 'BlockStatement', - body: cloneNode(node.finalizer.body).concat([{ - type: 'ReturnStatement', - argument: { - type: 'BinaryExpression', - operator: '&&', - left: ident(node.$seh + "Exit"), - right: thisCall(ident(node.$seh + "Exit"), [ident(node.$seh + "Value")]) - } - }]) - } - } - } - } - }] - } - }, - "property": genIdent.asyncbind, - "computed": false - }, - "arguments": [{ - "type": "ThisExpression" - }] - } - }] - }; + var finalizer = parser.part( + "var $decl = (function ($exit) { "+ + " return (function ($value) { "+ + " {$:body} "+ + " return $exit && ($exit.call(this, $value)); "+ + " }).$asyncbind(this, $error); "+ + "}).$asyncbind(this);", { + decl:ident(node.$seh + "Finally"), + exit:ident(node.$seh + "Exit"), + value:ident(node.$seh + "Value"), + body:cloneNode(node.finalizer.body), + error:binding.$error, + asyncbind:genIdent.asyncbind + }).body[0]; + afterDirectives(ref.parent[ref.field],[finalizer]); var callFinally = returnThisCall(deferredFinally(node, ctnName && ident(ctnName))) ; catchBody.body[catchBody.length - 1] = callFinally; @@ -1032,7 +915,7 @@ function asynchronize(pr, __sourceMapping, opts, logger) { var errMsg = where(node) + "'await' used inside non-async function. "; if (opts.promises) errMsg += "'return' value Promise runtime-specific"; - else + else errMsg += "'return' value from await is synchronous"; logger(errMsg + ". See https://github.com/MatAtBread/nodent#differences-from-the-es7-specification"); } @@ -1064,8 +947,8 @@ function asynchronize(pr, __sourceMapping, opts, logger) { // reference to 'fn'. if (stmt.self.type === 'ReturnStatement' && stmt.self.argument.type === 'CallExpression' && stmt.self.argument.arguments.length === 1 && stmt.self.argument.arguments[0].name === result.name) { returner = (callback = stmt.self.argument.callee); - // If stmt is only a reference to the result, suppress the result - // reference as it does nothing + // If stmt is only a reference to the result, suppress the result + // reference as it does nothing } else if (!(stmt.self.type === 'Identifier' || stmt.self.name === result.name || stmt.self.type === 'ExpressionStatement' && stmt.self.expression.type === 'Identifier' && stmt.self.expression.name === result.name)) { callBack.unshift(stmt.self); callback = { @@ -1086,31 +969,21 @@ function asynchronize(pr, __sourceMapping, opts, logger) { body: cloneNode(callBack) }, inAsync, containingExits) }; - else + else { callback = { - type: 'FunctionExpression', - params: [], - body: { - type: 'BlockStatement', - body: [] - } - }; + type: 'FunctionExpression', + params: [], + body: { + type: 'BlockStatement', + body: [] + } + }; + } } // Wrap the callback statement(s) in a Block and transform them if (!returner) { if (callback) { - returner = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: callback, - property: ident('$asyncbind', loc), - computed: false - }, - arguments: [{ - type: 'ThisExpression' - },containingExits.$error] - }; + returner = bindAsync(callback,containingExits.$error) ; } else { returner = { type: 'FunctionExpression', @@ -1123,28 +996,20 @@ function asynchronize(pr, __sourceMapping, opts, logger) { } } if (opts.wrapAwait) { - if (opts.promises || opts.generators) { - expr = { - type: 'CallExpression', - arguments: [expr], - callee: { - type: 'MemberExpression', - object: ident('Promise'), - property: ident('resolve') - } - }; - } else { - // ES7 makeThenable - expr = { - type: 'CallExpression', - arguments: [expr], - callee: { - type: 'MemberExpression', - object: ident('Object'), - property: ident('$makeThenable') - } - }; - } + expr = { + type: 'CallExpression', + arguments: [expr], + callee: (opts.promises || opts.generators)?{ + type: 'MemberExpression', + object: ident('Promise'), + property: ident('resolve') + }:{ + // ES7 makeThenable + type: 'MemberExpression', + object: ident('Object'), + property: ident('$makeThenable') + } + }; } var exitCall = { type: 'CallExpression', @@ -1171,87 +1036,19 @@ function asynchronize(pr, __sourceMapping, opts, logger) { function transformForIn(node, path) { var label = node.$label ; delete node.$label ; - - var idx = ident(generateSymbol("idx")) - var i = ident(generateSymbol("in")); - var it = node.left.type === 'VariableDeclaration' ? node.left.declarations[0].id : node.left; - var indexAssign = { - "type": "ExpressionStatement", - "expression": { - "type": "AssignmentExpression", - "operator": "=", - "left": it, - "right": { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": i, - "property": ident('shift'), - "computed": false - }, - "arguments": [] - } - } - }; - - var declVars = { - // var $in = [], idx ; - type:'VariableDeclaration', - kind:'var', - declarations:[{ - type:'VariableDeclarator', - id:i, - init:{ - "type": "ArrayExpression", - "elements": [] - } - },{ - type:'VariableDeclarator', - id:idx, - init:null - }] - }; - - var pushValues = { - // for (var idx in <>) $in_1.push(idx); - type:'ForInStatement', - left:idx, - right:node.right, - body:{ - "type": "ExpressionStatement", - "expression": { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": i, - "property": ident('push'), - "computed": false - }, - "arguments": [idx] - } - } - } ; - - var loop = { - // for (<>; in.length;) { <=0) { + if (n.type === 'VariableDeclaration' && (n.kind = n.kind || 'var') && kinds.indexOf(n.kind)>=0) { var p = path[0] ; // Don't hoist the LHS of for (var/let/const _ of/in ... ; ;){} if (p.field == "left" && (p.parent.type === 'ForInStatement' || p.parent.type === 'ForOfStatement')) @@ -2235,7 +2135,7 @@ function asynchronize(pr, __sourceMapping, opts, logger) { } }); } else { - ref.remove() ; + ref.remove() ; } }) ; vars = scopedNodes(node, isFreeVariable(['var']),false); @@ -2289,7 +2189,7 @@ function asynchronize(pr, __sourceMapping, opts, logger) { inStrictBody = prevScope ; } descend(); - + // It makes life easier if labeled loops have the label to hand, so we simply reference them here with a hidden $label if (node.type==='ForOfStatement' || node.type==='ForInStatement' || examine(node).isLoop) { if (path[0].parent.type==='LabeledStatement') { @@ -2318,91 +2218,20 @@ function asynchronize(pr, __sourceMapping, opts, logger) { if (node.type === 'Super') { if (path[0].parent.type === 'MemberExpression') { if (path[1].parent.type === 'CallExpression' && path[1].field === 'callee') { - if (path[0].parent.computed) { - // super[m](...) maps to: this.$superid(m).call(this,...) - r = { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "ThisExpression" - }, - "property": superID(), - "computed": false - }, - "arguments": [path[0].parent.property] - }, - "property": ident('call'), - "computed": false - }, - "arguments": [{ - "type": "ThisExpression" - }].concat(path[1].parent.arguments) - }; - path[2].replace(r); - } else { - // super.m(...) maps to: this.$superid('m').call(this,...) - r = { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "ThisExpression" - }, - "property": superID(), - "computed": false - }, - "arguments": [literal(path[0].parent.property.name)] - }, - "property": ident('call'), - "computed": false - }, - "arguments": [{ - "type": "ThisExpression" - }].concat(path[1].parent.arguments) - }; - path[2].replace(r); - } + // super[m](...) maps to: this.$superid(m).call(this,...) + r = parser.part("this.$super($field).call(this,$args)",{ + 'super':superID(), + field:path[0].parent.computed ? path[0].parent.property:literal(path[0].parent.property.name), + args:path[1].parent.arguments + }).expr ; + path[2].replace(r); } else { - if (path[0].parent.computed) { - // super[f], maps to: this.$superid(f) - r = { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "ThisExpression" - }, - "property": superID(), - "computed": false - }, - "arguments": [path[0].parent.property] - }; - path[1].replace(r); - } else { - // super.f, maps to: this.$superid('f') - r = { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "ThisExpression" - }, - "property": superID(), - "computed": false - }, - "arguments": [literal(path[0].parent.property.name)] - }; - path[1].replace(r); - } + // super[f], maps to: this.$superid(f) + r = parser.part("this.$super($field)",{ + 'super':superID(), + field:path[0].parent.computed ?path[0].parent.property : literal(path[0].parent.property.name) + }).expr ; + path[1].replace(r); } } else { logger(where(node) + "'super' in async methods must be deferenced. 'async constructor()'/'await super()' not valid."); @@ -2420,24 +2249,7 @@ function asynchronize(pr, __sourceMapping, opts, logger) { if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') { node.body.body.forEach(mapSupers(node)); if (node.$superID) { - var method = { - "type": "FunctionExpression", - "params": [ident("$field")], - "body": { - "type": "BlockStatement", - "body": [{ - "type": "ReturnStatement", - "argument": { - "type": "MemberExpression", - "object": { - "type": "Super" - }, - "property": ident("$field"), - "computed": true - } - }] - } - }; + var method = parser.part("(function($field) { return super[$field] })",{ field:ident("$field") }).expr ; if (opts.babelTree) { method.type = 'ClassMethod'; method.key = node.$superID; @@ -2522,15 +2334,22 @@ function asynchronize(pr, __sourceMapping, opts, logger) { // Coalese BlockStatements parser.treeWalker(ast, function (node, descend, path) { descend(); - var block, child; - // If this node is a block with vanilla BlockStatements (no controlling entity), merge them - if (block = examine(node).isBlockStatement) { - // Remove any empty statements from within the block - // For ES6, this needs more care, as blocks containing 'let/const/class' have a scope of their own - for (var i = 0;i < block.length; i++) { - if ((child = examine(block[i]).isBlockStatement) && !containsBlockScopedDeclarations(child)) { - if (!containsBlockScopedDeclarations(block[i])) - [].splice.apply(block, [i,1].concat(child)); + if (node.type==='ArrowFunctionExpression' + && node.body.type === 'BlockStatement' + && node.body.body.length + && node.body.body[0].type==='ReturnStatement') { + node.body = node.body.body[0].argument + } else { + var block, child; + // If this node is a block with vanilla BlockStatements (no controlling entity), merge them + if (block = examine(node).isBlockStatement) { + // Remove any empty statements from within the block + // For ES6, this needs more care, as blocks containing 'let/const/class' have a scope of their own + for (var i = 0;i < block.length; i++) { + if ((child = examine(block[i]).isBlockStatement) && !containsBlockScopedDeclarations(child)) { + if (!containsBlockScopedDeclarations(block[i])) + [].splice.apply(block, [i,1].concat(child)); + } } } } @@ -2547,7 +2366,7 @@ function asynchronize(pr, __sourceMapping, opts, logger) { // Remove any statements EXCEPT for function/var definitions if (ctn[i].type === 'VariableDeclaration' || examine(ctn[i]).isFunction && ctn[i].id) i += 1; - else + else ctn.splice(i, 1); } } @@ -2579,8 +2398,8 @@ function asynchronize(pr, __sourceMapping, opts, logger) { continuations[sym].$inlined = true; if (!examine(path[1].self).isJump) repl.push({ - type: 'ReturnStatement' - }); + type: 'ReturnStatement' + }); path[1].replace(repl); } } @@ -2661,7 +2480,7 @@ function asynchronize(pr, __sourceMapping, opts, logger) { // $return.$asyncbind(this,$error) // these can be simplified to: // $return - */ + */ return ast; } } diff --git a/lib/output.js b/lib/output.js index 1461a1e..a19fe12 100644 --- a/lib/output.js +++ b/lib/output.js @@ -112,10 +112,19 @@ function precedence(node) { function out(node,state,type) { var f = this[type || node.type] ; - if (f) + if (f) { +/* + try { + var attr = Object.keys(node).filter(k=>k[0]==='$').map(k=>k+(node[k]?"+":"-")) ; + if (attr.length) + state.write(node,"/*"+attr.join(", ")+"\u002A/ ") ; + } catch (ex) {} ; +*/ f.call(this, node, state); - else // Unknown node type - just spew its source - state.write(node,state.sourceAt(node.start,node.end)) ; + } else { + // Unknown node type - just spew its source + state.write(node,"/*"+node.type+"?*/ "+state.sourceAt(node.start,node.end)) ; + } } function expr(state, parent, node, assoc) { if (assoc===2 || diff --git a/lib/parser.js b/lib/parser.js index d812b79..3162a93 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -175,7 +175,7 @@ function acornParse(code,config) { var comments = [] ; var options = { plugins:{asyncawait:{asyncExits:true, awaitAnywhere:true}}, - ecmaVersion:7, + ecmaVersion:8, allowHashBang:true, allowReturnOutsideFunction:true, allowImportExportEverywhere:true, @@ -201,7 +201,73 @@ function acornParse(code,config) { return ast ; } +var parseCache = {} ; +function partialParse(code,args) { + if (!parseCache[code]) { + parseCache[code] = acornParse(code,{ + locations:false, + ranges:false, + onComment:null + }) ; + } + + var result = substitute(parseCache[code]) ; + return {body:result.body, expr:result.body[0].type==='ExpressionStatement' ? result.body[0].expression : null} ; + + /* parse and substitute: + * + * $1 Substitute the specified expression. If $1 occupies a slot which is an array of expressions (e.g arguments, params) + * and the passed argument is an array, subtitute the whole set + * {$:1} Substitute a single statement + * + */ + function substitute(src,dest) { + if (Array.isArray(dest) && !Array.isArray(src)) + throw new Error("Can't substitute an array for a node") ; + + dest = dest || {} ; + Object.keys(src).forEach(function(k){ + if (!(src[k] instanceof Object)) + return dest[k] = src[k] ; + + function moreNodes(v){ if (typeof v==="function") v = v() ; dest = dest.concat(v) ; return dest }; + function copyNode(v){ if (typeof v==="function") v = v() ; dest[k] = v ; return dest }; + + // The src is an array, so create/grow the destination + // It could an an array of expressions $1,$2,$3 or statements $:1;$:2;$:3; + if (Array.isArray(src[k])) + return dest[k] = substitute(src[k],[]) ; + + var p ; + if (Array.isArray(dest)) + p = moreNodes ; + else + p = copyNode ; + + // Substitute a single identifier $.. with an expression (TODO: test provided arg is an expression node) + if (src[k].type==='Identifier' && src[k].name[0]==='$') + return p(args[src[k].name.slice(1)]) ; + + // Substitute a single labeled statement $:.. with a statement (TODO: test provided arg is a statement node) + if (src[k].type === 'LabeledStatement' && src[k].label.name==='$') { + var spec = src[k].body.expression ; + return p(args[spec.name || spec.value]) ; + } + + // Magic label to set call a function to modify a statement node $$method: + // The newNode = args.method(oldNode) + if (src[k].type === 'LabeledStatement' && src[k].label.name.slice(0,2)==='$$') { + return p(args[src[k].label.name.slice(2)](substitute(src[k]).body)) ; + } + + return p(substitute(src[k])) ; + }) ; + return dest ; + } +} + module.exports = { + part:partialParse, parse: acornParse, treeWalker:treeWalker, _acorn:acorn diff --git a/lib/runtime.js b/lib/runtime.js index d6973c5..3229224 100644 --- a/lib/runtime.js +++ b/lib/runtime.js @@ -1,29 +1,96 @@ -var $asyncbind = new Function("self","catcher", -(" var resolver = this; "+ -" if (catcher===true) { "+ -" if (!Function.prototype.$asyncbind.EagerThenable) "+ -" Function.prototype.$asyncbind.EagerThenable = "+require('./eager.js').toString()+"(); "+ -" return new (Function.prototype.$asyncbind.EagerThenable)(boundThen); "+ -" } "+ -" if (catcher) { "+ -" if (Function.prototype.$asyncbind.wrapAsyncStack) "+ -" catcher = Function.prototype.$asyncbind.wrapAsyncStack(catcher); "+ -" return then; "+ -" } "+ -" function then(result,error){ "+ -" try { "+ -" return result && (result instanceof Object) && typeof result.then==='function' "+ -" ? result.then(then,catcher) : resolver.call(self,result,error||catcher); "+ -" } catch (ex) { "+ -" return (error||catcher)(ex); "+ -" } "+ -" } "+ -" function boundThen(result,error) { "+ -" return resolver.call(self,result,error); "+ -" } "+ -" boundThen.then = boundThen; "+ -" return boundThen; ") -.replace(/\s+/g,' ')) ; +"use strict"; +/* + * $asyncbind has multiple uses, depending on the parameter list. It is in Function.prototype, so 'this' is always a function + * + * 1) If called with a single argument (this), it is used when defining an async function to ensure when + * it is invoked, the correct 'this' is present, just like "bind" + * 2) If called with a second parameter ("catcher") and catcher!==true it is being used to invoke an async + * function where the second parameter is the error callback (for sync exceptions and to be passed to + * nested async calls) + * 3) If called with the second parameter===true, it is the same use as (1), but the function is wrapped + * in an 'EagerThenable' as well bound to 'this'. + * It is the same as calling new EagerThenable(this), where 'this' is the function being bound/wrapped + * 4) If called with the second parameter===0, it is the same use as (1), but the function is wrapped + * in a 'Thenable', which executes lazily and can resolve synchronously. + * It is the same as calling new Thenable(fn), where 'this' is the function being bound/wrapped + */ + +function processIncludes(includes,input) { + var src = input.toString() ; + var t = "return "+src ; + var args = src.match(/.*\(([^)]*)\)/)[1] ; + var re = /!!!'([^']*)'/g ; + var m = [] ; + while (1) { + var mx = re.exec(t) ; + if (mx) + m.push(mx) ; + else break ; + } + m.reverse().forEach(function(e){ + t = t.slice(0,e.index)+includes[e[1]]+t.substr(e.index+e[0].length) ; + }) ; + t = t.replace(/\/\*[^*]*\*\//g,' ').replace(/\s+/g,' ') ; + return new Function(args,t)() ; +} + +var $asyncbind = processIncludes({ + zousan:require('./zousan').toString(), + thenable:require('./thenableFactory').toString() +}, +function $asyncbind(self,catcher) { + "use strict"; + if (!Function.prototype.$asyncbind) + Function.prototype.$asyncbind = $asyncbind ; + + if (!$asyncbind.trampoline) { + $asyncbind.trampoline = function trampoline(t,x,s,e){ + return function b(q) { + while (q) { + if (q.then) + return q.then(b, e); + try { + if (q.pop) { + if (q.length) + return q.pop() ? x.call(t) : q; + q = s; + } else + q = q.call(t) + } catch (r) { + return e(r); + } + } + } + }; + } + if (!$asyncbind.LazyThenable) { + $asyncbind.LazyThenable = !!!'thenable'(); + $asyncbind.EagerThenable = $asyncbind.Thenable = ($asyncbind.EagerThenableFactory = !!!'zousan')(); + } + + var resolver = this; + switch (catcher) { + case true: + return new ($asyncbind.Thenable)(boundThen); + case 0: + return new ($asyncbind.LazyThenable)(boundThen); + case undefined: + /* For runtime compatibility with Nodent v2.x, provide a thenable */ + boundThen.then = boundThen ; + return boundThen ; + default: + return function(){ + try { + return resolver.apply(self,arguments); + } catch(ex) { + return catcher(ex); + } + } + } + function boundThen() { + return resolver.apply(self,arguments); + } +}) ; function $asyncspawn(promiseProvider,self) { var genF = this ; @@ -62,6 +129,7 @@ function $asyncspawn(promiseProvider,self) { }); } +$asyncbind() ; module.exports = { $asyncbind:$asyncbind, $asyncspawn:$asyncspawn diff --git a/nodent.js b/nodent.js index 3b6de1e..51b8d4d 100755 --- a/nodent.js +++ b/nodent.js @@ -43,7 +43,9 @@ var config = { // set within the directive as a JSON-encoded extension var initialCodeGenOpts = { + noRuntime:false, lazyThenables:false, + es6target:false, noUseDirective:false, wrapAwait:null, mapStartLine:0, @@ -93,7 +95,11 @@ function globalErrorHandler(err) { /* Extract compiler options from code (either a string or AST) */ var useDirective = /^\s*['"]use\s+nodent-?([a-zA-Z0-9]*)?(\s*.*)?['"]\s*;/ - +var runtimes = require('./lib/runtime') ; +var $asyncbind = runtimes.$asyncbind ; +var $asyncspawn = runtimes.$asyncspawn ; +var Thenable = $asyncbind.Thenable ; + function noLogger(){} function isDirective(node){ @@ -103,6 +109,7 @@ function isDirective(node){ } function parseCompilerOptions(code,log,filename) { + if (!log) log = console.warn.bind(console) ; var regex, set, parseOpts = {} ; if (typeof code=="string") { if (regex = code.match(useDirective)) { @@ -127,23 +134,23 @@ function parseCompilerOptions(code,log,filename) { set = "default" ; regex = [null,null,"{}"] ; } - if (set) { - try { - if (!filename) - filename = require('path').resolve('.') ; - else if (require('fs').lstatSync(filename).isDirectory()) - filename = require('path').dirname(filename) ; - - var packagePath = require('resolve').sync('package.json',{ - moduleDirectory:[''], - extensions:[''], - basedir:filename - }) ; - var packageOptions = JSON.parse(fs.readFileSync(packagePath)).nodent.directive[set] ; - } catch(ex) { - // Meh - } - } + if (set) { + try { + if (!filename) + filename = require('path').resolve('.') ; + else if (require('fs').lstatSync(filename).isDirectory()) + filename = require('path').dirname(filename) ; + + var packagePath = require('resolve').sync('package.json',{ + moduleDirectory:[''], + extensions:[''], + basedir:filename + }) ; + var packageOptions = JSON.parse(fs.readFileSync(packagePath)).nodent.directive[set] ; + } catch(ex) { + // Meh + } + } try { parseOpts = copyObj([optionSets[set],packageOptions,regex[2] && JSON.parse(regex[2])]); } catch(ex) { @@ -155,6 +162,11 @@ function parseCompilerOptions(code,log,filename) { log("No valid 'use nodent' directive, assumed -es7 mode") ; parseOpts = optionSets.es7 ; } + + if (parseOpts.generators || parseOpts.engine) + parseOpts.promises = true ; + if (parseOpts.promises) + parseOpts.es7 = true ; return parseOpts ; } return null ; // No valid nodent options @@ -184,53 +196,6 @@ function btoa(str) { return buffer.toString('base64'); } -/** - * NoDentify (make async) a general function. - * The format is function(a,b,cb,d,e,f){}.noDentify(cbIdx,errorIdx,resultIdx) ; - * for example: - * http.aGet = http.get.noDentify(1) ; // The 1st argument (from zero) is the callback. errorIdx is undefined (no error) - * - * The function is transformed from: - * http.get(opts,function(result){}) ; - * to: - * http.aGet(opts).then(function(result){}) ; - * - * @params - * idx The argument index that is the 'callback'. 'undefined' for the final parameter - * errorIdx The argument index of the callback that holds the error. 'undefined' for no error value - * resultIdx The argument index of the callback that holds the result. - * 'undefined' for the argument after the errorIdx (errorIdx != undefined) - * [] returns all the arguments - * promiseProvider For promises, set this to the module providing Promises. - */ -function noDentify(idx,errorIdx,resultIdx,promiseProvider) { - promiseProvider = promiseProvider || Thenable ; - var fn = this ; - return function() { - var scope = this ; - var args = Array.prototype.slice.apply(arguments) ; - var resolver = function(ok,error) { - if (undefined==idx) // No index specified - use the final (unspecified) parameter - idx = args.length ; - if (undefined==errorIdx) // No error parameter in the callback - just pass to ok() - args[idx] = ok ; - else { - args[idx] = function() { - var err = arguments[errorIdx] ; - if (err) - return error(err) ; - if (Array.isArray(resultIdx) && resultIdx.length===0) - return ok(arguments) ; - var result = arguments[resultIdx===undefined?errorIdx+1:resultIdx] ; - return ok(result) ; - } ; - } - return fn.apply(scope,args) ; - } - return new promiseProvider(resolver) ; - }; -} - function compileNodentedFile(nodent,log) { log = log || nodent.log ; return function(mod, filename, parseOpts) { @@ -370,52 +335,6 @@ function parseCode(code,origFilename,__sourceMapping,opts) { } } -/* -$asyncbind has multple execution paths, determined by the arguments, to fulfil all runtime requirements in a single function -so it can be serialized via toString() for transport to a browser. It is always called with this=Function (ie. it is attached -to Function.prototype) - -The paths are: - $asyncbind(obj) // Simply return this function bound to obj - $asyncbind(obj,true) // Call this function, bound to obj, returning a (possibly resolved) Thenable for its completion - $asyncbind(obj,function(exception){}) // Return a Thenable bound to obj, passing exceptions to the specified handler when (if) it throws -*/ - -var runtimes = require('./lib/runtime') ; -var $asyncbind = runtimes.$asyncbind ; -var $asyncspawn = runtimes.$asyncspawn ; - -function wrapAsyncStack(catcher) { - var context = {} ; - Error.captureStackTrace(context,$asyncbind) ; - return function wrappedCatch(ex){ - if (ex instanceof Error && context) { - try { - ex.stack = //+= "\n\t...\n"+ - ex.stack.split("\n").slice(0,3) - .filter(function(s){ - return !s.match(/^\s*at.*nodent\.js/) ; - }).join("\n")+ - ex.stack.split("\n").slice(3).map(function(s){return "\n "+s}).join("")+ - context.stack.split("\n").slice(2) - .filter(function(s){ - return !s.match(/^\s*at.*nodent\.js/) ; - }) - .map(function(s,idx){ - return idx?"\n"+s:s.replace(/^(\s*)at /g,"\n$1await ") - }).join("") ; - } catch (stackError) { - // Just fall through and don't modify the stack - } - context = null ; - } - return catcher.call(this,ex) ; - } ; -} - - -var Thenable = require('./lib/thenable') ; - /* NodentCompiler prototypes, that refer to 'this' */ function requireCover(cover,opts) { opts = opts || {} ; @@ -537,10 +456,13 @@ NodentCompiler.prototype.setOptions = function(members){ delete this.options.log ; return this ; }; + +$asyncbind.call($asyncbind) ; + NodentCompiler.prototype.version = require("./package.json").version ; NodentCompiler.prototype.Thenable = Thenable ; -NodentCompiler.prototype.EagerThenable = require('./lib/eager.js') ; -NodentCompiler.prototype.isThenable = Thenable.isThenable ; +NodentCompiler.prototype.EagerThenable = $asyncbind.EagerThenableFactory ; +NodentCompiler.prototype.isThenable = function(x) { return x && x instanceof Object && typeof x.then==="function"} ; NodentCompiler.prototype.asyncify = asyncify ; NodentCompiler.prototype.require = requireCover ; NodentCompiler.prototype.generateRequestHandler = generateRequestHandler ; @@ -593,7 +515,7 @@ function setGlobalEnvironment(initOpts) { Object.$makeThenable Object.prototype.isThenable Object.prototype.asyncify - Function.prototype.noDentify + Function.prototype.noDentify // Moved to a cover as of v3.0.0 Function.prototype.$asyncspawn Function.prototype.$asyncbind Error.prepareStackTrace @@ -613,12 +535,6 @@ function setGlobalEnvironment(initOpts) { enumerable:false, configurable:true }; - augmentFunction.noDentify = { - value:noDentify, - configurable:true, - enumerable:false, - writable:true - } ; try { Object.defineProperties(Function.prototype,augmentFunction) ; } catch (ex) { @@ -632,9 +548,6 @@ function setGlobalEnvironment(initOpts) { global[defaultCodeGenOpts[defaultCodeGenOpts.$error]] = globalErrorHandler ; } - if (initOpts.asyncStackTrace) - $asyncbind.wrapAsyncStack = wrapAsyncStack ; - // "Global" options: // If anyone wants to augment Object, do it. The augmentation does not depend on the config options if (initOpts.augmentObject) { @@ -666,7 +579,6 @@ function initialize(initOpts){ * augmentObject:boolean, * extension:string? * dontMapStackTraces:boolean - * asyncStackTrace:boolean */ if (!initOpts) initOpts = {} ; @@ -848,8 +760,8 @@ initialize.setCompileOptions = function(set,compiler) { }; initialize.asyncify = asyncify ; -initialize.Thenable = Thenable ; -initialize.EagerThenable = require('./lib/eager.js') ; +initialize.Thenable = $asyncbind.Thenable ; +initialize.EagerThenable = $asyncbind.EagerThenableFactory ; module.exports = initialize ; @@ -953,7 +865,9 @@ function runFromCLI(){ sourcemap:cli.sourcemap, wrapAwait:cli.wrapAwait, lazyThenables:cli.lazyThenables, - noUseDirective:cli.use?true:false + noUseDirective:cli.use?true:false, + noRuntime:cli.noruntime, + es6target:cli.es6target, }); var nodent = initialize({ diff --git a/package.json b/package.json index 0353396..3852e52 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,22 @@ { "name": "nodent", - "version": "2.6.12", + "version": "3.0.0", "description": "NoDent - Asynchronous Javascript language extensions", "main": "nodent.js", "scripts": { - "cover": "istanbul cover ./nodent.js tests -- --quick --quiet --syntax --generators --forceStrict ; open ./coverage/lcov-report/index.html", - "test": "cd tests && npm i --prod && cd .. && ./nodent.js tests --syntax --quick --quiet --generators", + "cover": "istanbul cover ./nodent.js tests -- --quick --syntax --forceStrict ; open ./coverage/lcov-report/index.html", + "test": "cd tests && npm i --prod && cd .. && node --expose-gc ./nodent.js tests --syntax --quick", "test-loader": "cd tests/loader/app && npm test && cd ../../..", "start": "./nodent.js" }, - "bin": { - "nodentjs": "./nodent.js" + "bin":{ + "nodentjs":"./nodent.js" }, "dependencies": { - "acorn": "3.3.0", + "acorn": ">=2.5.2", "acorn-es7-plugin": "^1.1.3", "resolve": "1.1.7", - "source-map": "0.5.3" + "source-map": "0.5.6" }, "repository": { "type": "git", diff --git a/tests/index.js b/tests/index.js index 46d1850..7e7cbdd 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,5 +1,6 @@ 'use nodent-es7 {"lazyThenables":true}'; 'use strict'; + /* Run all the scripts in ./tests compiled for ES7 and Promises */ var fs = require('fs'); var path = require('path'); @@ -7,6 +8,8 @@ var color = require('colors'); var nodent = require('../nodent')({ log: function (msg) {} }); +var nativePromise = global.Promise ; +var gc = global.gc || function(){} ; var AsyncFunction = nodent.require('asyncfunction') ; var map = nodent.require('map'); var spaces = ' ' ; @@ -20,10 +23,12 @@ global.sleep = async function sleep(t) { }, t); }; global.breathe = async function breathe() { - var t = Date.now(); + var hr = process.hrtime(); setImmediate(function () { try { - async return Date.now() - t; + var t = process.hrtime(hr); + t = t[0] * 1000 + t[1] / 1e6 ; + async return t; } catch (ex) { async throw ex; } @@ -31,21 +36,17 @@ global.breathe = async function breathe() { }; var providers = []; providers.push({ - name: '(none)', + name: '{es7:true}', p: null }); providers.push({ - name: 'nodent.Thenable', + name: 'nodent', p: nodent.Thenable }); -providers.push({ - name: 'nodent.Eager', - p: nodent.EagerThenable() -}); -if (global.Promise) { +if (nativePromise) { providers.push({ name: 'native', - p: global.Promise + p: nativePromise }); } function makePromiseCompliant(module, promise, resolve) { @@ -55,6 +56,7 @@ function makePromiseCompliant(module, promise, resolve) { } var promiseImpls = providers.length; + try { var bluebird = require('bluebird'); bluebird.config({ @@ -77,13 +79,19 @@ try { p: makePromiseCompliant(require('when'), 'promise', 'resolve') }); } catch (ex) {} +try { + providers.push({ + name: 'promisejs.org', + p: require('promise') + }); +} catch (ex) {} try { providers.push({ name: 'promiscuous', p: require('promiscuous') }); } catch (ex) {} -var useQuick = false, quiet = false, useGenerators = false, useGenOnly = false, syntaxTest = 0, forceStrict = "", useEngine = false ; +var useQuick = false, quiet = false, useGenerators = undefined, useGenOnly = false, syntaxTest = 0, forceStrict = "", useEngine = false ; var idx; for (idx = 0; idx < process.argv.length; idx++) { var fqPath = path.resolve(process.argv[idx]); @@ -104,18 +112,11 @@ for (; idx < process.argv.length; idx++) { } catch (ex) { console.log(("V8 "+process.versions.v8+" does not support async/await (try a later version of nodejs). Skipping some tests. ").yellow) ; } + } else if (arg == '--nogenerators') { + useGenerators = false; } else if (arg == '--generators' || arg == '--genonly') { - try { - useGenOnly = arg == '--genonly'; - eval("var temp = new Promise(function(){}) ; function* x(){ return }"); - useGenerators = true; - if (useGenOnly) - providers.splice(1, 1); - } catch (ex) { - console.log(("V8 "+process.versions.v8+" does not support Promises or Generators (try a later version of nodejs). Skipping some tests. ").yellow) ; - if (useGenOnly) - process.exit(-1); - } + useGenOnly = arg == '--genonly'; + useGenerators = true; } else if (arg == '--output' || arg == '--es7' || arg == '--save') { console.log('Option '.grey+arg+' is deprecated and will be ignored.'.grey) } else if (arg == '--quiet') { @@ -128,6 +129,19 @@ for (; idx < process.argv.length; idx++) { break; } } + +if (useGenerators !== false) useGenerators = true ; +if (useGenerators) { + try { + eval("var temp = new Promise(function(){}) ; function* x(){ return }"); + } catch (ex) { + console.log(("V8 "+process.versions.v8+" does not support Promises or Generators (try a later version of nodejs). Skipping some tests. ").yellow) ; + if (useGenOnly) + process.exit(-1); + useGenerators = false ; + } +} + function pad(s, n) { return (" " + s).substr(-(n || 32)); } @@ -170,34 +184,52 @@ files.forEach(function (n) { "async function _a() { "+forceStrict + code+" }" ; } var compileException = false; - for (var type = useEngine?-1:0;type < (useGenerators ? 12 : 8); type++) { - var opts = {}; // es7:true } ; + for (var type = useEngine?-1:0;type < 16; type++) { + var opts = {}; if (type==-1) opts.engine = true ; else { - if (!(type & 1)) + if (!(type & 12) && !(type & 1)) opts.lazyThenables = true; if (type & 2) opts.wrapAwait = true; - if (type & 4) + if (type & 4) { opts.promises = true; - if (type & 8) + if (type & 1) + opts.noRuntime = true ; + } + if (type & 8) { + if (!useGenerators) + continue ; opts.generators = true; + if (opts.noRuntime) + continue ; + } else if (useGenOnly) + continue ; + if (!(type & (4|8))) opts.es7 = true; } + types[type] = Object.keys(opts).toString() ; - var pr, tCompiler = process.hrtime(); - pr = nodent.compile(forceStrict + code, n, opts).code; - - tTotalCompilerTime += time(tCompiler); try { - test[i].fn[type] = new Function("module", "require", "Promise", "__unused", "nodent", "DoNotTest", pr); - } catch (ex) { - if (!compileException) { - console.warn(test[i].name+(" not supported by V8 "+process.versions.v8+" (try a later version of nodejs): ").yellow+ex.message.red) ; + var pr, tCompiler = process.hrtime(); + pr = nodent.compile(forceStrict + code, n, opts).code; + tTotalCompilerTime += time(tCompiler); + try { + test[i].fn[type] = new Function("module", "require", "Promise", "__unused", "nodent", "DoNotTest", pr); + } catch (ex) { + if (!compileException) + console.warn(test[i].name+(" not supported by V8 "+process.versions.v8+" (try a later version of nodejs): ").yellow+ex.message.red) ; compileException = true ; + test[i].fn[type] = function(m) { + m.exports = DoNotTest ; + } } + } catch (ex) { + if (!compileException) + console.warn(test[i].name+(" nodent failed to compile - FAIL ").yellow+ex.message.red) ; + compileException = true ; test[i].fn[type] = function(m) { m.exports = DoNotTest ; } @@ -207,96 +239,135 @@ files.forEach(function (n) { }); console.log("Total compile time:", ((tTotalCompilerTime | 0) + "ms").yellow); if (useQuick) - console.log(('Note: Timings with '+'--quick'.underline+' are subject to significant GC jitter. Remove '+'--quick'.underline+' for accurate timing comparison')); + console.log('Note:'.cyan,'Timings with '+'--quick'.underline+' are subject to significant GC jitter. Remove '+'--quick'.underline+' for accurate timing comparison'); if (promiseImpls == providers.length) console.log('To test against some popular Promise implementations,', 'cd tests && npm i && cd ..'.yellow); +var testCache = {} ; async function runTest(test, provider, type) { if (provider.p && !(type & (4 | 8))) { return { result: DoNotTest }; } - await breathe(); - var m = {}, result; - test.fn[type](m, require, provider.p || DoNotTest, undefined, nodent, DoNotTest); - var t = process.hrtime(); + await sleep(1); + var key = [test.name,provider.name,type].join(); + var m = {}; + if (!testCache[key]) { + test.fn[type](m, require, provider.p || DoNotTest, undefined, nodent, DoNotTest); + testCache[key] = m ; + } else { + m = testCache[key] ; + } + var returned = false ; + function onResult(r) { + if (tID) { + clearTimeout(tID) ; + tID = null ; + } + if (returned) + return ; + returned = true ; + async return { + alwaysQuick: m.exports.alwaysQuick, + t: time(t), + result: r + } + } try { - result = await m.exports(); - if (result != true) - throw result; + var thenable = m.exports(); + var tID = setTimeout(function(){ + tID = null ; + if (returned) + return ; + console.log("Timeout".red,test.name,provider.name.yellow); + onResult(new Error("Timed out")) ; + },10000) ; + var t = process.hrtime(); + thenable.then(onResult,onResult) ; } catch (ex) { - result = ex; + return { + alwaysQuick: m.exports.alwaysQuick, + t: t?time(t):10000, + result: ex + } } - return { - alwaysQuick: m.exports.alwaysQuick, - t: time(t), - result: result - }; } try { - var result, byType = {}, byProvider = {}, byTest = {}, table = []; - for (i = 0;i < test.length; i++) { + var result, byType = {}, byProvider = {}, byTest = {}, table = [], fails = [], tMedian = 0, nMedian = 0 ; + nextTest: for (i = 0;i < test.length; i++) { var benchmark = null; - for (var j = 0;j < providers.length; j++) { - process.stdout.write('\r- Test: ' + test[i].name + ' using ' + providers[j].name.yellow + spaces + '\r'); + nextProvider: for (var j = 0;j < providers.length; j++) { + try { + process.stdout.write('\r- Test: ' + test[i].name + ' using ' + providers[j].name.yellow + spaces + '\n'); + gc() ; - for (var type = useGenOnly ? 8 : 0;type < (useGenerators ? 12 : 8); type++) { - var ticks = []; - table[type] = table[type] || []; - table[type][j] = table[type][j] || []; - // Warm up V8 - result = await runTest(test[i], providers[j], type); - if (result.result !== true) { - if (result.result !== DoNotTest) { - console.log(test[i].name, '\u2717'.red, types[type].red, providers[j].name.red, result.result.toString().red, spaces); - type = 32767; - j = providers.length; - } - continue; - } - var t = 0; - var cond = result.alwaysQuick?function(){ return ticks.length < result.alwaysQuick } : ( useQuick ? function () { - return ticks.length < 2; - } : function () { - return t < 100 || ticks.length < 20; - }); - while (cond()) { - result = await runTest(test[i], providers[j], type); - ticks.push(result.t); - t += result.t; - } - ticks = ticks.sort(); - var median = ticks[ticks.length / 2 | 0]; - var metric = median; - if (!benchmark) - benchmark = metric; - metric = metric / benchmark * 100; - result = { - value: result.result, - metric: metric, - provider: providers[j].name, - type: types[type], - test: test[i].name - }; - table[type][j].push(metric); - byType[types[type]] = byType[types[type]] || []; - byType[types[type]].push(result); - byProvider[providers[j].name] = byProvider[providers[j].name] || []; - byProvider[providers[j].name].push(result); + for (var type in types) if (Object.getOwnPropertyDescriptor(types,type)) { + if (!(type & 1) && (type&8)) + continue ; - } - console.log(spaces+'\n') ; - var lines = 2+showPerformanceTable() ; - while (lines--) { - process.stdout.write('\u001B[1A') ; + table[type] = table[type] || []; + table[type][j] = table[type][j] || []; + // Warm up V8 + result = await runTest(test[i], providers[j], type); + if (result.result !== true) { + if (result.result !== DoNotTest) { + console.log(test[i].name, '\u2717'.red, types[type].red, providers[j].name.red, result.result.toString().red, spaces); + type = 32767; + continue nextProvider ; + } + continue; + } + var t = 0; + var ticks = []; + var cond = result.alwaysQuick?function(){ return ticks.length < result.alwaysQuick } : ( useQuick ? function () { + return ticks.length < 2; + } : function () { + return (t < 100 || ticks.length < 20) && ticks.length < 200; + }); + while (cond()) { + result = await runTest(test[i], providers[j], type); + ticks.push(result.t); + t += result.t; + } + ticks = ticks.sort(); + var median = ticks[ticks.length / 2 | 0]; + var metric = median; + if (!benchmark) { + benchmark = metric; + tMedian = tMedian+benchmark ; + nMedian += 1 ; + } + metric = metric / benchmark * 100; + result = { + value: result.result, + metric: metric, + provider: providers[j].name, + type: types[type], + test: test[i].name + }; + table[type][j].push(metric); + byType[types[type]] = byType[types[type]] || []; + byType[types[type]].push(result); + byProvider[providers[j].name] = byProvider[providers[j].name] || []; + byProvider[providers[j].name].push(result); + } + console.log(spaces+'\n') ; + var lines = 2+showPerformanceTable() ; + while (lines--) { + process.stdout.write('\u001B[1A') ; + } + } catch(ex) { + fails.push(test[i].name.yellow + ' using ' + providers[j].name.yellow + ': ',ex.toString().red); + } finally { + process.stdout.write('\u001B[1A') ; } } } - process.stdout.write('\u001B['+type+'B') ; - console.log('\n\n\n\n') ; + console.log('\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nBenchmark execution time: '+((tMedian/nMedian)+' ms').cyan) ; + console.log(fails.join("\n")) ; function showPerformanceTable() { var i,j,lines = 0 ; diff --git a/tests/loader/app/index.js b/tests/loader/app/index.js index 6ac2504..2a89289 100644 --- a/tests/loader/app/index.js +++ b/tests/loader/app/index.js @@ -1,2 +1,3 @@ -var nodent = require('nodent')() ; +var nodent = require('../../../../nodent')() ; +console.log("App nodent version",nodent.version) ; require('./main')(nodent) ; diff --git a/tests/loader/app/main.js b/tests/loader/app/main.js index fc0e7b9..a72e11a 100644 --- a/tests/loader/app/main.js +++ b/tests/loader/app/main.js @@ -1,7 +1,8 @@ 'use nodent-promises'; module.exports = function(nodent) { - var loaded = await require('loader-module')() ; + var loaded = await require('../loader-module')() ; + console.log("loader-module nodent version",loaded.version) ; var self = {version: nodent.version, options:__nodent} ; if (self.version > loaded.version && loaded.version === "2.3.0" @@ -11,7 +12,9 @@ module.exports = function(nodent) { } else { console.log(loaded) ; console.log(self) ; - throw new Error('versionAwareLoader test FAILED') ; + var ex = new Error('versionAwareLoader test FAILED') ; + ex.stack = "" ; + throw ex ; } } diff --git a/tests/loader/app/package.json b/tests/loader/app/package.json index a209723..47c3d1e 100644 --- a/tests/loader/app/package.json +++ b/tests/loader/app/package.json @@ -4,12 +4,8 @@ "description": "Test app using current Nodent version but depending on an ealier version", "main": "index.js", "scripts": { - "test": "echo '### This test requires npm >2.x ###' && npm i && node ." + "test": "echo '### This test requires npm >2.x ###' && cd ../loader-module && npm i && cd ../app && npm i && node ." }, "author": "", - "license": "ISC", - "dependencies": { - "loader-module": "file:../loader-module", - "nodent": "../../../../nodent" - } + "license": "ISC" } diff --git a/tests/package.json b/tests/package.json index 794305c..c115f7c 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,18 +6,19 @@ "test": "cd .. && npm test" }, "repository": { - "type": "git", - "url": "https://github.com/MatAtBread/nodent.git" - }, + "type": "git", + "url": "https://github.com/MatAtBread/nodent.git" + }, "author": "", "license": "ISC", "dependencies": { - "colors": "^1.1.2" + "colors": "^1.1.2" }, "devDependencies": { - "bluebird": "^3.3.4", + "promise": "^7.1.1", + "bluebird": "^3.4.6", "promiscuous": "^0.6.0", - "rsvp": "^3.2.1", + "rsvp": "^3.3.1", "when": "^3.7.7" } } diff --git a/tests/semantics/asyncify.js b/tests/semantics/asyncify.js index 1d2afa0..b2a7d43 100644 --- a/tests/semantics/asyncify.js +++ b/tests/semantics/asyncify.js @@ -1,9 +1,10 @@ -var nodent = require('../../nodent')() ; var map = nodent.require('map',{throwOnError:true}) ; +Function.prototype.noDentify = nodent.require('nodentify') ; + Function.prototype.asAsync = function(){ var fn = this ; var args = Array.prototype.slice.call(arguments) ; - return nodent.Thenable(function(ok,err){ + return new nodent.Thenable(function(ok,err){ ok(fn.apply(this,await map(args))) ; }) ; } diff --git a/tests/semantics/es6-for-of.js b/tests/semantics/es6-for-of.js index 54a009a..97f6f26 100644 --- a/tests/semantics/es6-for-of.js +++ b/tests/semantics/es6-for-of.js @@ -2,8 +2,8 @@ Object.defineProperty(Array.prototype,'pushAsync',{ writable:true, configurable:true, value:async function() { - [].push.apply(this,arguments) ; - } + [].push.apply(this,arguments) ; + } }) ; async function log() { diff --git a/tests/semantics/es6-interop.js b/tests/semantics/es6-interop.js index ecdf021..ff18749 100644 --- a/tests/semantics/es6-interop.js +++ b/tests/semantics/es6-interop.js @@ -7,10 +7,16 @@ var gen = [ function* doWeKnowGenerators(){} async function inter(i,j) { - return (gen[j].name == await gen[j].call() && await gen[j].call() == await gen[i].consume(gen[j].call))?1:100 ; + return (gen[j].name == await gen[j].call() && await gen[j].call() == await gen[i].consume(gen[j].call))?1:100 ; } async function test() { + for (var q=0; q