leonardfactory / babel-plugin-transform-typescript-metadata

Babel plugin to emit decorator metadata like typescript compiler

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

order of applied class/constr-param decorators different than in tsc

wtho opened this issue · comments

commented

When I compile

@ClassDec()
export class Decoratee {
  constructor(@ParamDec() param: string) {}
}

with tsc I end up with (I replaced some tslib calls with what they will be replaced with):

let Decoratee = class Decoratee {
    constructor(param) { }
};
Decoratee = tslib_1.__decorate([
    ClassDec(),
    ParamDec(),
    Reflect.metadata("design:paramtypes", [String])
], TokenService);

tslib then will call these three decorators on Decoratee, starting with the last one (Reflect.metadata), then alls ParamDec, and finally ClassDec`.

Using this plugin, I get something like this, with the order being differently:

var _dec, _dec2, _class;
var Decoratee = (
  _dec = (0, ClassDec)(),
  _dec2 = Reflect.metadata("design:paramtypes", [String]),
  _dec(
    _class = _dec2(
      _class = function Decoratee(param) {}
    ) || _class
  ) || _class
);
(0, ParamDec)()(Decoratee, undefined, 0);

Compiling this with babel will in some cases have different results, if libraries/frameworks rely on a consistent order. I do not know, if the order is defined in the metadata proposal, did not find anything so far. I think you mentioned this issue also at the bottom of README.md. Specifically in the Angular DI, the @Injectable decorator expects all @Injects inside the constructor parameter to already have been applied.

You're right here. I added the parameter decorator emitter (even if this is not strictly related to metadata emission) since current babel decorator plugin does not support the syntax the way we need.

The only solution I can think of, is to operate like we are actually doing and enhance the paramterVisitor function on order to emit something that can be processed by the babel-decorator plugin, but I'm not sure this is possibile due to plugin constraints.

commented

I found a solution. So what ts in combination tslib does it this:

let Decoratee = class Decoratee {
    constructor(token) { }
};
Decoratee = tslib_1.__decorate([
    Injectable(),
    tslib_1.__param(0, Inject(TOKEN)),
    tslib_1.__metadata("design:paramtypes", [String])
], Decoratee);

And tslib_1.__param(0, Inject(TOKEN)) expands to:

(function (target, key) { return (0, Inject)(TOKEN)(target, undefined, 0); })

So I tried to build the same structure and succeeded, but it bloats the code:

// parameterVisitor

if (methodPath.node.kind === 'constructor') {
  // function(target, key) { return decorator(Decoratee, undefined, 0) }
  const resultantDecorator = _core.types.decorator(
    _core.types.functionExpression(
      null, // anonymous function
      [_core.types.identifier('target')],
      _core.types.blockStatement(
        [
          _core.types.returnStatement(
            _core.types.callExpression(
              decorator.expression,
              [
                _core.types.identifier('target'),
                _core.types.identifier('undefined'),
                _core.types.numericLiteral(param.key)
              ]
            )
          )
        ]
      )
    )
  )
  classPath.node.decorators.push(resultantDecorator)
} else {

I also moved the classPath.insertAfter to the else block.

Reflect.metadata is added to the constructors later, so the error with angular DI is resolved.

Might this create issues with other DI frameworks? Do you want me to open a PR?

commented

By the way, this also resolves #22, I guess because we now put the new decorator on the class itself and this in return leads to the Reflect.metadata decorator being created.

Awesome! I'd like to see the output code, and we should add some tests.
Anyway, if you can build up something with a PR it would be cool 👍

Going to publish today 🚀