I've created an exclusively decorator-driven AOP library that supports Before, AfterReturning, AfterThrowing, AfterFinally, and Around advice (a la AspectJ). It's called @scispike/aspectify.
It works great using all synchronous code. All synchronous tests pass just fine.
Problem occurs when I attempt to decorate an asynchronous method with asynchronous advice (that is, with a decorator that evaluates to an asynchronous function). I get a syntax error from Babel when issuing mocha --opts mocha.opts 'src/test/unit/**/*.spec.js' | npx bunyan
-- (via npm run u
-- see package.json and my mocha.opts
):
$ npm --version
6.4.1
$ node --version
v10.14.2
$ npm run u
npm WARN lifecycle The node binary used for scripts is /Users/matthewadams/.asdf/shims/node but npm is using /Users/matthewadams/.asdf/installs/nodejs/10.14.2/bin/node itself. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.
> @scispike/[email protected] u /Users/matthewadams/dev/scispike/aspectify
> mocha --opts mocha.opts 'src/test/unit/**/*.spec.js' | npx bunyan
/Users/matthewadams/dev/scispike/aspectify/src/test/unit/async/before.spec.js:50
go(delayMillis, value) {
^^
SyntaxError: Unexpected identifier
at new Script (vm.js:79:7)
at createScript (vm.js:251:10)
at Object.runInThisContext (vm.js:303:10)
at Module._compile (internal/modules/cjs/loader.js:657:28)
at Module._compile (/Users/matthewadams/dev/scispike/aspectify/node_modules/pirates/lib/index.js:99:24)
at Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Object.newLoader [as .js] (/Users/matthewadams/dev/scispike/aspectify/node_modules/pirates/lib/index.js:104:7)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Module.require (internal/modules/cjs/loader.js:637:17)
at require (internal/modules/cjs/helpers.js:22:18)
at /Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/lib/mocha.js:250:27
at Array.forEach (<anonymous>)
at Mocha.loadFiles (/Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/lib/mocha.js:247:14)
at Mocha.run (/Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/lib/mocha.js:576:10)
at Object.<anonymous> (/Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/bin/_mocha:637:18)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
at startup (internal/bootstrap/node.js:282:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:743:3)
^Cmake: *** [.] Interrupt: 2
npm ERR! code ELIFECYCLE
npm ERR! errno 130
npm ERR! @scispike/[email protected] u: `mocha --opts mocha.opts 'src/test/unit/**/*.spec.js' | npx bunyan`
npm ERR! Exit status 130
npm ERR!
npm ERR! Failed at the @scispike/[email protected] u script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/matthewadams/.npm/_logs/2019-04-01T17_17_04_757Z-debug.log
For those still with me, the test containing before.spec.js:50 is:
describe('parameterless before advice', function () {
it('should work', async function () {
let count = 0
const delay = 10
const val = 1
const ParameterlessBeforeCount = Before(async thisJoinPoint => {
await pause(delay + 100)
count++
})
class Class {
@ParameterlessBeforeCount
async go (delayMillis, value) { // <-- THIS IS LINE 50
await pause(delayMillis)
return value
}
}
const c = new Class()
const v = await c.go(delay, val)
expect(count).to.equal(1)
expect(v).to.equal(val)
})
Edit 1, as requested by @hackape:
If you clone the git repo, cd into it, issue npm install
then npm run transpile
, all code can be found in lib/main
and lib/test
. To run unit tests in place, run npm run u
; to run transpiled unit tests, run npm run unit
. In any case, here's the transpiled code.
Transpiled file lib/main/Advice.js
:
'use strict';
/**
* Returns a decorator that applies advice of any type.
* @param modify [{function}] Function that takes a `thisJoinPointStaticPart` that can be used to modify the decorated member.
* @param before [{function}] Function that takes a `thisJointPoint` that runs before execution proceeds.
* @param afterReturning [{function}] Function that takes a `thisJointPoint` that runs after execution normally completes.
* @param afterThrowing [{function}] Function that takes a `thisJointPoint` that runs after execution completes with an error.
* @param afterFinally [{function}] Function that takes a `thisJointPoint` that runs after execution completes via `finally`.
* @param around [{function}] Function that takes a `thisJointPoint` that leaves it to the developer to control behavior; no other advice functions are called.
* @return {Function}
* @private
*/
const Advice = ({ modify, before, afterReturning, afterThrowing, afterFinally, around } = {}) => {
return (clazz, name, originalDescriptor) => {
const advisedDescriptor = { ...originalDescriptor };
let value;
let get;
let set;
if (originalDescriptor.value && typeof originalDescriptor.value === 'function') value = originalDescriptor.value;
if (originalDescriptor.get && typeof originalDescriptor.get === 'function') get = originalDescriptor.get;
if (originalDescriptor.set && typeof originalDescriptor.set === 'function') set = originalDescriptor.set;
const thisJoinPointStaticPart = {
clazz,
name,
descriptors: {
original: originalDescriptor,
advised: advisedDescriptor } };
if (get || set) thisJoinPointStaticPart.accessor = true;
if (value) thisJoinPointStaticPart.method = true;
if (modify) {
modify(thisJoinPointStaticPart);
}
const createAdvisedFn = function (originalFn) {
return function advisedFn(...args) {
const thisJoinPoint = {
thiz: this,
args,
fullName: thisJoinPointStaticPart.name,
...thisJoinPointStaticPart };
if (thisJoinPoint.accessor) {
if (args.length === 0) {
thisJoinPoint.get = thisJoinPoint.fullName = `get ${thisJoinPoint.name}`;
} else {
thisJoinPoint.set = thisJoinPoint.fullName = `set ${thisJoinPoint.name}`;
}
}
const proceed = ({ thiz, args: newArgs } = {}) => originalFn.apply(thiz || this, newArgs || args);
if (around) {
thisJoinPoint.proceed = proceed;
return around(thisJoinPoint);
}
let returnValue;
let error;
try {
if (before) {
before(thisJoinPoint);
}
returnValue = proceed();
if (afterReturning) {
afterReturning({ returnValue, thisJoinPoint });
}
return returnValue;
} catch (e) {
error = e;
if (afterThrowing) {
afterThrowing({ error, thisJoinPoint });
}
throw error;
} finally {
if (afterFinally) {
afterFinally({ returnValue, error, thisJoinPoint });
}
}
};
};
if (value) {
advisedDescriptor.value = createAdvisedFn(value);
} else {
if (get) advisedDescriptor.get = createAdvisedFn(get);
if (set) advisedDescriptor.set = createAdvisedFn(set);
}
return advisedDescriptor;
};
};
const Around = (advice, modify) => Advice({ around: advice, modify });
const Before = (advice, modify) => Advice({ before: advice, modify });
const AfterReturning = (advice, modify) => Advice({ afterReturning: advice, modify });
const AfterThrowing = (advice, modify) => Advice({ afterThrowing: advice, modify });
const AfterFinally = (advice, modify) => Advice({ afterFinally: advice, modify });
module.exports = {
Around,
Before,
AfterReturning,
AfterThrowing,
AfterFinally };
//# sourceMappingURL=<snipped>
Transpiled file lib/test/async/before.spec.js
:
/* global it, describe */
'use strict';function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {var desc = {};Object.keys(descriptor).forEach(function (key) {desc[key] = descriptor[key];});desc.enumerable = !!desc.enumerable;desc.configurable = !!desc.configurable;if ('value' in desc || desc.initializer) {desc.writable = true;}desc = decorators.slice().reverse().reduce(function (desc, decorator) {return decorator(target, property, desc) || desc;}, desc);if (context && desc.initializer !== void 0) {desc.value = desc.initializer ? desc.initializer.call(context) : void 0;desc.initializer = undefined;}if (desc.initializer === void 0) {Object.defineProperty(target, property, desc);desc = null;}return desc;}
const chai = require('chai');
const expect = chai.expect;
chai.use(require('dirty-chai'));
const { Before } = require('../../../main/Advice');
const pause = require('./pause');
class Class {
async go(delayMillis, value) {
await pause(delayMillis);
return value;
}}
describe('unit tests of asynchronous before advice', function () {
describe('base Class', function () {
it('should work', async function () {
const c = new Class();
const v = await c.go(10, 1);
expect(v).to.equal(1);
});
it('subclass should work', async function () {
class Subclass extends Class {
async go(d, v) {
return super.go(d, v);
}}
const c = new Subclass();
const v = await c.go(10, 1);
expect(v).to.equal(1);
});
});
describe('parameterless before advice', function () {
it('should work', async function () {var _class;
let count = 0;
const delay = 10;
const val = 1;
const ParameterlessBeforeCount = Before(async thisJoinPoint => {
await pause(delay + 100);
count++;
});let
Class = (_class = class Class {
async
go(delayMillis, value) {
await pause(delayMillis);
return value;
}}, (_applyDecoratedDescriptor(_class.prototype, "go", [ParameterlessBeforeCount], Object.getOwnPropertyDescriptor(_class.prototype, "go"), _class.prototype)), _class);
const c = new Class();
const v = await c.go(delay, val);
expect(count).to.equal(1);
expect(v).to.equal(val);
});
});
});
//# sourceMappingURL=<snipped>
Transpiled file lib/test/async/pause.js
:
'use strict';
const pause = async (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));
module.exports = pause;
//# sourceMappingURL=<snipped>
I try clone your repo, set
retainLines: false
, and the syntax error goes away.Looks like a bug in babel,
retainLines
probably doesn't play well with async function syntax.Full steps to reproduce my result:
If you're seeing different behavior, it could be difference in our env. Try start from a fresh git clone.
Also, you can try babel REPL to see what configuration produce the correct code. I setup a basic example for you: link