leonardfactory / babel-plugin-transform-typescript-metadata

Babel plugin to emit decorator metadata like typescript compiler
MIT License
215 stars 17 forks source link

✨ Move param dec to class #24

Closed wtho closed 4 years ago

wtho commented 4 years ago

The parameter decorator is now not applied on the class after the class declaration, but instead added as class decorator itself.

This is imitated from TypeScript compilation. It leads to more consistency between compilers and increases compatibility of libraries that depend on this behavior.

Example

@Injectable()
export class Decoratee {
  constructor(@Inject() param1: String) { }
}

Compilation using TSC

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

To understand the change in this PR, we should also have a look at tslib.__param:

__param = function (paramIndex, decorator) {
  return function (target, key) { decorator(target, key, paramIndex); }
};

So looking at tslib_1.__param(idx, Inject()) and knowing that Inject() will return a decorator function with the two arguments target and function, the __param call basically is the decorator

function(target, key) { return (0, Inject)()(target, key, idx) }

Compilation using babel and this plugin, before this change

var Decoratee = (
  _dec = (0, Injectable)(),
  _dec2 = Reflect.metadata("design:paramtypes", [String]),
  _dec(_class = _dec2(_class = function Decoratee(token) {
    _classCallCheck(this, Decoratee);
  }) || _class) || _class);
exports.Decoratee = Decoratee;
(0, Inject)()(target, undefined, 0);

Compilation using babel with this PR change

var Decoratee = (
  _dec = (0, Injectable)(),
  _dec2 = function _dec2(target) { return (0, Inject)()(target, undefined, 0); },
  _dec3 = Reflect.metadata("design:paramtypes", [String]),
  _dec(_class = _dec2(_class = _dec3(_class = function Decoratee(token) {
    _classCallCheck(this, Decoratee);
  }) || _class) || _class) || _class);
exports.Decoratee = Decoratee;

As Decorators are applied in reverse order (in the example above dec3, then dec2, and then dec), some libraries may rely on the class decorator (here dec) being applied after the parameter decorator (dec2). Angular's Dependency Injection in fact does rely on this. This PR solves this.

How it works

The new function in parameterVisitor transforms the previously postponed decorator call into a classical decorator, that gets called with the others. It wraps the expression the same way tslib.__param does inside another function call, as it has to be passed previously.

Side Effects

If the class has only a parameter decorator, no class decorator, previously Reflect.metadat('design:paramtypes', ... was never called on the class. Now that we transform the parameter decorator to a kind of class decorator, it will be called anyway. This leads to the novel possibility of metadata about constructor parameter being available through reflection at runtime, although no true class decorator is applied. This coincidentally solves #22.

To Clarify

TODO

Solves #23, #22

codecov[bot] commented 4 years ago

Codecov Report

Merging #24 into master will decrease coverage by 0.36%. The diff coverage is 92.85%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #24      +/-   ##
==========================================
- Coverage   91.89%   91.52%   -0.37%     
==========================================
  Files           4        4              
  Lines         111      118       +7     
  Branches       41       45       +4     
==========================================
+ Hits          102      108       +6     
- Misses          9       10       +1
Impacted Files Coverage Δ
src/metadata/metadataVisitor.ts 93.75% <100%> (+0.89%) :arrow_up:
src/parameter/parameterVisitor.ts 95.83% <90.9%> (-4.17%) :arrow_down:

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 8ab68d7...1371f6b. Read the comment docs.

wtho commented 4 years ago

Another compilation divergence came accross:

// example class

class Decoratee {
  someMethod(@Inject() param: String) {}
}

Compiled by TS

class Decoratee {
    someMethod(param) { }
}
tslib_1.__decorate([
    tslib_1.__param(0, Inject()),
    tslib_1.__metadata("design:type", Function),
    tslib_1.__metadata("design:paramtypes", [String]),
    tslib_1.__metadata("design:returntype", void 0)
], Decoratee.prototype, "someMethod", null);
exports.Decoratee = Decoratee;

Compiled by babel

var Decoratee =
/*#__PURE__*/
function () {
  function Decoratee() {
    _classCallCheck(this, Decoratee);
  }
  _createClass(Decoratee, [{
    key: "someMethod",
    value: function someMethod(param) {}
  }]);
  return Decoratee;
}();
exports.Decoratee = Decoratee;
(0, Inject)()(Decoratee.prototype, "someMethod", 0);

TS adds more metadata. Should we handle this in this PR as well?


~Another question: Can I change~ https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata/blob/8ab68d73cdfeda3ebf9f31d2dec892ca696fb77b/src/metadata/serializeType.ts#L1 ~to import { types as t } from '@babel/core'; so that the CI passes?~ - I see, you just added it in the last commit for a reason.

wtho commented 4 years ago

So I implemented the decorator installation anyway (by decorator installation I mean placing the decorator in the class/field decorator array instead of adding it after the class).

I extended it quite a bit by now, so please have a good look and tell me if I should split it in several PRs or if you want something changed. I am quite excited that babel comes this close to TypeScript with just some wrapping.

Metadata on Methods (other than the constructor)

I added the design:type decorator to methods as well, I think there is no reason we do not install it.

The design:returntype decorator on the other hand seems to be trickier, TypeScript gets the node type during compilation, whereas babel does not analyze it. We would have to do some quite complicated work, so I think this is not required until someone requests it. I still left it in for, commented out.

wtho commented 4 years ago

Hey @leonardfactory, can you have a look at this PR? Is there anything I should change?

leonardfactory commented 4 years ago

Hi @wtho! sorry for the delay but I've been busy these days. Your job seems outstanding. I'm reviewing it but I'll need some time to do some tests and check it 👍

wtho commented 4 years ago

and should not break existing projects

I think it won't behave differently in most projects, although some might depend on the way or order the decorators are applied. I personally would prefer to release a major version, but it's up to you.

If you want me to add additional notes to the README or changelog, I'll be happy to do so!

wtho commented 4 years ago

@leonardfactory we are still waiting on this feature. Would be awesome if you could have another look at it.

I already fixed the changes requested, and thinking again about it being breaking, I think it does not have to be. It adds some additional decorators, but does not remove any that were applied before.

If you are hesitant to merge and deploy, I would be happy if you could already publish a beta version.

Cheers!

codecov-io commented 4 years ago

Codecov Report

Merging #24 into master will decrease coverage by 0.36%. The diff coverage is 92.85%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #24      +/-   ##
==========================================
- Coverage   91.89%   91.52%   -0.37%     
==========================================
  Files           4        4              
  Lines         111      118       +7     
  Branches       41       45       +4     
==========================================
+ Hits          102      108       +6     
- Misses          9       10       +1
Impacted Files Coverage Δ
src/metadata/metadataVisitor.ts 93.75% <100%> (+0.89%) :arrow_up:
src/parameter/parameterVisitor.ts 95.83% <90.9%> (-4.17%) :arrow_down:
src/metadata/serializeType.ts 89.7% <0%> (ø) :arrow_up:

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 3b1ece6...5dfcc90. Read the comment docs.

leonardfactory commented 4 years ago

@wtho I'm willing to merge and release this asap, however got some issues with type declarations from master. Just let me check them.