diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b529260..5d3a6e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [6.x, 8.x, 10.x, 12.x] + node-version: [8.x, 10.x, 12.x, 14.x] runs-on: ${{ matrix.os }} diff --git a/src/generator-to-promise.ts b/src/generator-to-promise.ts deleted file mode 100644 index 69053e5..0000000 --- a/src/generator-to-promise.ts +++ /dev/null @@ -1,83 +0,0 @@ -interface Deferred { - promise: Promise; - resolve: (value?: T | PromiseLike) => void; - reject: (reason?: any) => void; -} - -function isGenerator(fn: any): fn is Generator { - return fn && fn.next && fn.throw; -} - -function isGeneratorFunction(fn: any): fn is GeneratorFunction { - return ( - typeof fn == 'function' && fn.constructor.name == 'GeneratorFunction' - ); -} - -function createDeferred(): Deferred { - let r; - let j; - const promise = new Promise( - ( - resolve: (value?: T | PromiseLike) => void, - reject: (reason?: any) => void - ): void => { - r = resolve; - j = reject; - } - ); - if (!r || !j) { - throw new Error('Creating Deferred failed'); - } - return { promise, resolve: r, reject: j }; -} - -export default function generatorFnToPromise( - generatorFunction: any -): (...args: any[]) => Promise { - if (!isGeneratorFunction(generatorFunction)) { - if (typeof generatorFunction === 'function') { - return function(this: any, ...args: any[]) { - return Promise.resolve(true).then(() => { - return generatorFunction.apply(this, args); - }); - }; - } - throw new Error('The given function must be a generator function'); - } - - return function(this: any, ...args: any[]): Promise { - const generator = generatorFunction.apply(this, args); - return generatorToPromise(generator); - }; -} - -function generatorToPromise(this: any, generator: any): Promise { - const deferred = createDeferred(); - (function next(err?: Error | null, value?: any) { - let genState = null; - try { - if (err) { - genState = generator.throw(err); - } else { - genState = generator.next(value); - } - } catch (e) { - genState = { value: Promise.reject(e), done: true }; - } - - if (isGenerator(genState.value)) { - genState.value = generatorToPromise(genState.value); - } - - if (genState.done) { - deferred.resolve(genState.value); - } else { - Promise.resolve(genState.value) - .then(promiseResult => next(null, promiseResult)) - .catch(err => next(err)); - } - })(); - - return deferred.promise; -} diff --git a/src/index.ts b/src/index.ts index 4f3f68b..162399a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,6 @@ import { parseScript } from 'esprima'; import { visit, namedTypes as n, builders as b } from 'ast-types'; import { Context, RunningScriptOptions, runInNewContext } from 'vm'; -import _supportsAsync from './supports-async'; -import generatorToPromiseFn from './generator-to-promise'; - /** * Compiles sync JavaScript code into JavaScript with async Functions. * @@ -18,8 +15,7 @@ import generatorToPromiseFn from './generator-to-promise'; function degenerator( code: string, - _names: degenerator.DegeneratorNames, - { output = 'async' }: degenerator.DegeneratorOptions = {} + _names: degenerator.DegeneratorNames ): string { if (!Array.isArray(_names)) { throw new TypeError('an array of async function "names" is required'); @@ -76,7 +72,7 @@ function degenerator( shouldDegenerate = true; } return false; - } + }, }); if (!shouldDegenerate) { @@ -84,12 +80,8 @@ function degenerator( } // Got a "function" expression/statement, - // convert it into an async or generator function - if (output === 'async') { - path.node.async = true; - } else if (output === 'generator') { - path.node.generator = true; - } + // convert it into an async function + path.node.async = true; // Add function name to `names` array if (!checkName(path.node.id.name, names)) { @@ -98,7 +90,7 @@ function degenerator( } this.traverse(path); - } + }, }); } while (lastNamesLength !== names.length); @@ -112,19 +104,10 @@ function degenerator( const delegate = false; const { name, - parent: { node: pNode } + parent: { node: pNode }, } = path; - let expr; - if (output === 'async') { - expr = b.awaitExpression(path.node, delegate); - } else if (output === 'generator') { - expr = b.yieldExpression(path.node, delegate); - } else { - throw new Error( - 'Only "async" and "generator" are allowd `output` values' - ); - } + const expr = b.awaitExpression(path.node, delegate); if (n.CallExpression.check(pNode)) { pNode.arguments[name] = expr; @@ -134,7 +117,7 @@ function degenerator( } this.traverse(path); - } + }, }); return generate(ast); @@ -143,24 +126,16 @@ function degenerator( namespace degenerator { export type DegeneratorName = string | RegExp; export type DegeneratorNames = DegeneratorName[]; - export type DegeneratorOutput = 'async' | 'generator'; - export interface DegeneratorOptions { - output?: DegeneratorOutput; - } - export interface CompileOptions - extends DegeneratorOptions, - RunningScriptOptions { + export interface CompileOptions extends RunningScriptOptions { sandbox?: Context; } - export const supportsAsync = _supportsAsync; - export function compile( + export function compile( code: string, returnName: string, names: DegeneratorNames, options: CompileOptions = {} - ): T { - const output = _supportsAsync ? 'async' : 'generator'; - const compiled = degenerator(code, names, { ...options, output }); + ): (...args: A) => Promise { + const compiled = degenerator(code, names); const fn = runInNewContext( `${compiled};${returnName}`, options.sandbox, @@ -171,23 +146,25 @@ namespace degenerator { `Expected a "function" to be returned for \`${returnName}\`, but got "${typeof fn}"` ); } - if (isAsyncFunction(fn)) { - return fn; - } else { - const rtn = (generatorToPromiseFn(fn) as unknown) as T; - Object.defineProperty(rtn, 'toString', { - value: fn.toString.bind(fn), - enumerable: false - }); - return rtn; - } + const r = function (this: any, ...args: A): Promise { + try { + const p = fn.apply(this, args); + if (typeof p.then === 'function') { + return p; + } + return Promise.resolve(p); + } catch (err) { + return Promise.reject(err); + } + }; + Object.defineProperty(r, 'toString', { + value: fn.toString.bind(fn), + enumerable: false, + }); + return r; } } -function isAsyncFunction(fn: any): boolean { - return typeof fn === 'function' && fn.constructor.name === 'AsyncFunction'; -} - /** * Returns `true` if `node` has a matching name to one of the entries in the * `names` array. diff --git a/src/supports-async.ts b/src/supports-async.ts deleted file mode 100644 index 2452324..0000000 --- a/src/supports-async.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { runInNewContext } from 'vm'; - -const supportsAsync = ((): boolean => { - try { - const fn = runInNewContext('(async function () {})'); - return fn.constructor.name === 'AsyncFunction'; - } catch (err) { - return false; - } -})(); - -export default supportsAsync; diff --git a/test/assignment.expected.js b/test/assignment.expected.js index 2412680..b9905f9 100644 --- a/test/assignment.expected.js +++ b/test/assignment.expected.js @@ -3,9 +3,9 @@ var biz = foo; function foo() { return 42; } -function* bar() { - return yield baz(); +async function bar() { + return await baz(); } -function* bir() { - return yield biz(); +async function bir() { + return await biz(); } diff --git a/test/basic.expected.js b/test/basic.expected.js index abd8ec4..0c9eb5a 100644 --- a/test/basic.expected.js +++ b/test/basic.expected.js @@ -1,3 +1,3 @@ -function* foo() { - return (yield a('bar')) || (yield b()); +async function foo() { + return await a('bar') || await b(); } diff --git a/test/get-example.expected.js b/test/get-example.expected.js index aad7195..ee7d5ce 100644 --- a/test/get-example.expected.js +++ b/test/get-example.expected.js @@ -1,7 +1,7 @@ -function* myFn() { - var one = yield get('https://google.com'); - var two = yield get('http://nodejs.org'); - var three = JSON.parse(yield get('http://jsonip.org')); +async function myFn() { + var one = await get('https://google.com'); + var two = await get('http://nodejs.org'); + var three = JSON.parse(await get('http://jsonip.org')); return [ one, two, diff --git a/test/multiple.expected.js b/test/multiple.expected.js index 19d7ef1..cfc875a 100644 --- a/test/multiple.expected.js +++ b/test/multiple.expected.js @@ -1,9 +1,9 @@ -function* foo() { - return yield baz(); +async function foo() { + return await baz(); } -function* bar() { - return yield foo(baz); +async function bar() { + return await foo(baz); } -function* baz() { - return yield bar(); +async function baz() { + return await bar(); } diff --git a/test/pac-resolver-gh-3.expected.js b/test/pac-resolver-gh-3.expected.js index 9372ecd..06d5986 100644 --- a/test/pac-resolver-gh-3.expected.js +++ b/test/pac-resolver-gh-3.expected.js @@ -1,11 +1,11 @@ -function* FindProxyForURL(url, host) { - if (yield isHostInAnySubnet(host, [ +async function FindProxyForURL(url, host) { + if (await isHostInAnySubnet(host, [ '10.1.2.0', '10.1.3.0' ], '255.255.255.0')) { return 'HTTPS proxy.example.com'; } - if (yield isHostInAnySubnet(host, [ + if (await isHostInAnySubnet(host, [ '10.2.2.0', '10.2.3.0' ], '255.255.255.0')) { @@ -13,10 +13,10 @@ function* FindProxyForURL(url, host) { } return 'DIRECT'; } -function* isHostInAnySubnet(host, subnets, mask) { +async function isHostInAnySubnet(host, subnets, mask) { var subnets_length = subnets.length; for (i = 0; i < subnets_length; i++) { - if (yield isInNet(host, subnets[i], mask)) { + if (await isInNet(host, subnets[i], mask)) { return true; } } diff --git a/test/partial.expected.js b/test/partial.expected.js index 6ba96be..396fb4d 100644 --- a/test/partial.expected.js +++ b/test/partial.expected.js @@ -1,12 +1,12 @@ -function* foo() { - return yield baz(); +async function foo() { + return await baz(); } -function* bar() { - return yield foo(baz); +async function bar() { + return await foo(baz); } -function* baz() { - return yield bar(); +async function baz() { + return await bar(); } function shouldntChange() { return 42; -} \ No newline at end of file +} diff --git a/test/test.ts b/test/test.ts index 49a133a..fefee6f 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,69 +1,34 @@ import fs from 'fs'; import path from 'path'; import assert from 'assert'; -import degenerator, { compile, supportsAsync } from '../src'; +import degenerator, { compile } from '../src'; describe('degenerator()', () => { - describe('`supportsAsync`', () => { - it('should export boolean `supportsAsync`', () => { - assert.equal(typeof supportsAsync, 'boolean'); - }); + it('should support "async" output functions', () => { + function aPlusB(a: () => string, b: () => string): string { + return a() + b(); + } + const compiled = degenerator('' + aPlusB, ['a']); + assert.equal( + compiled.replace(/\s+/g, ' '), + 'async function aPlusB(a, b) { return await a() + b(); }' + ); }); - - describe('"async" output', () => { - it('should support "async" output functions', () => { - function aPlusB(a: () => string, b: () => string): string { - return a() + b(); - } - const compiled = degenerator('' + aPlusB, ['a'], { - output: 'async' - }); - assert.equal( - compiled.replace(/\s+/g, ' '), - 'async function aPlusB(a, b) { return await a() + b(); }' - ); - }); - it('should be the default "output" mode (with options)', () => { - function foo(a: () => string): string { - return a(); - } - const compiled = degenerator('' + foo, ['a'], {}); - assert.equal( - compiled.replace(/\s+/g, ' '), - 'async function foo(a) { return await a(); }' - ); - }); - it('should be the default "output" mode (without options)', () => { - function foo(a: () => string): string { - return a(); - } - const compiled = degenerator('' + foo, ['a']); - assert.equal( - compiled.replace(/\s+/g, ' '), - 'async function foo(a) { return await a(); }' - ); - }); - }); - - describe('"generator" output', () => { - it('should support "generator" output functions', () => { - function aPlusB(a: () => string, b: () => string): string { - return a() + b(); - } - const compiled = degenerator('' + aPlusB, ['a'], { - output: 'generator' - }); - assert.equal( - compiled.replace(/\s+/g, ' '), - 'function* aPlusB(a, b) { return (yield a()) + b(); }' - ); - }); + it('should be the default "output" mode (without options)', () => { + function foo(a: () => string): string { + return a(); + } + const compiled = degenerator('' + foo, ['a']); + assert.equal( + compiled.replace(/\s+/g, ' '), + 'async function foo(a) { return await a(); }' + ); }); describe('"expected" fixture tests', () => { fs.readdirSync(__dirname) .sort() - .forEach(n => { + .forEach((n) => { if (n === 'test.js') return; if (/\.expected\.js$/.test(n)) return; if (/\.ts$/.test(n)) return; @@ -71,7 +36,7 @@ describe('degenerator()', () => { const expectedName = `${path.basename(n, '.js')}.expected.js`; - it(`${n} → ${expectedName}`, function() { + it(`${n} → ${expectedName}`, function () { const sourceName = path.resolve(__dirname, n); const compiledName = path.resolve(__dirname, expectedName); const js = fs.readFileSync(sourceName, 'utf8'); @@ -89,9 +54,7 @@ describe('degenerator()', () => { names = [/.*/]; } - const compiled = degenerator(js, names, { - output: 'generator' - }); + const compiled = degenerator(js, names); assert.equal( compiled.trim().replace(/\r/g, ''), expected.trim().replace(/\r/g, '') @@ -101,23 +64,17 @@ describe('degenerator()', () => { }); describe('`compile()`', () => { - it('should compile code into an invocable async function', () => { + it('should compile code into an invocable async function', async () => { const a = (v: string) => Promise.resolve(v); const b = () => 'b'; function aPlusB(v: string): string { return a(v) + b(); } - const fn = compile<(v: string) => Promise>( - '' + aPlusB, - 'aPlusB', - ['a'], - { - sandbox: { a, b } - } - ); - return fn('c').then((val: string) => { - assert.equal(val, 'cb'); + const fn = compile('' + aPlusB, 'aPlusB', ['a'], { + sandbox: { a, b }, }); + const val = await fn('c'); + assert.equal(val, 'cb'); }); it('should contain the compiled code in `toString()` output', () => { const a = () => 'a'; @@ -130,14 +87,10 @@ describe('degenerator()', () => { 'aPlusB', ['b'], { - sandbox: { a, b } + sandbox: { a, b }, } ); - if (supportsAsync) { - assert(/await b\(\)/.test(fn + '')); - } else { - assert(/yield b\(\)/.test(fn + '')); - } + assert(/await b\(\)/.test(fn + '')); }); it('should be able to await non-promises', () => { const a = () => 'a'; @@ -150,10 +103,10 @@ describe('degenerator()', () => { 'aPlusB', ['a'], { - sandbox: { a, b } + sandbox: { a, b }, } ); - return fn().then((val: string) => { + return fn().then((val) => { assert.equal(val, 'ab'); }); }); @@ -163,14 +116,9 @@ describe('degenerator()', () => { function aPlusB(): string { return a() + b(); } - const fn = compile<() => Promise>( - '' + aPlusB, - 'aPlusB', - [], - { - sandbox: { a, b } - } - ); + const fn = compile('' + aPlusB, 'aPlusB', [], { + sandbox: { a, b }, + }); return fn().then((val: string) => { assert.equal(val, 'ab'); }); @@ -204,12 +152,9 @@ describe('degenerator()', () => { function b() { return false; } - const fn = compile<() => Promise>( - `${ifA};${a}`, - 'ifA', - ['b'], - { sandbox: { b } } - ); + const fn = compile(`${ifA};${a}`, 'ifA', ['b'], { + sandbox: { b }, + }); return fn().then((val: string) => { assert.equal(val, 'foo'); });