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

Property injection example from readme

elderapo opened this issue · comments

Hey! I saw this code in the readme:

import { MyService } from './MyService';
import { Configuration } from './Configuration';

@Injectable()
class AnotherService {
  @Inject()
  config: Configuration;

  constructor(private service: MyService) {}
}

I am not sure if this is a pseudo or real code with removed imports. The part I am interested about is property injection using decorators. I've been trying to get something similar to work in typescript, babel 7 and InversifyJS for the past few hours with no success. Apparently babel decorators implementation doesn't allow setting properties from decorator. Would you mind sharing some info? :)

PS. Thanks for this babel plugin btw!

Hi @elderapo! Sorry for the misleading example, it should say:

import { Injectable, Inject } from 'my-ioc-library'; // here
import { MyService } from './MyService';
import { Configuration } from './Configuration';

@Injectable()
class AnotherService {
  @Inject()
  config: Configuration;

  constructor(private service: MyService) {}
}

The part I am interested about is property injection using decorators. I've been trying to get something similar to work in typescript, babel 7 and InversifyJS for the past few hours with no success. Apparently babel decorators implementation doesn't allow setting properties from decorator.

You should be able to use the decorators in legacy mode in combination with babel-plugin-proposal-class-properties in loose mode. I'm using this configuration (the plugins order matters) and it outputs the decorators succesfully:

{
  "plugins": [
    "babel-plugin-transform-typescript-metadata",
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }],
  ],
  "presets": [
    "@babel/preset-typescript"
  ]
}

May you try with this one and let me know if it works for you / which output you get so i can work fixing it?

PS. Thanks for this babel plugin btw!

Thank you! 🎉

Are you sure it's working correctly in your project? Mind sharing what ioc library you're using?

It might use some kind of trick: property level decorator only sets metadata and class level decorator/constructor injects dependencies based on saved metadata?

I already opened issue in babel repo regarding this problem. Here is reproducible demo if you wanna play around.

I’ll try it and I’ll update you with my findings and, hopefully, a solution
thanks for the demo!

Hi @elderapo, I've found a way to make it compatible. I'll investigate these in the next days (even with other IoC libraries), but I've found that it works in the example i've set up here.

The solution is to wrap the decorator this way:

// .. your code to set up injector and get the container ..
let { lazyInject } = getDecorators(container);

// Additional function to make property decorators compatible with babel.
function fixPropertyDecorator<T extends Function>(decorator: T): T {
  return ((...args: any[]) => (
    target: any,
    propertyName: any,
    ...decoratorArgs: any[]
  ) => {
    decorator(...args)(target, propertyName, ...decoratorArgs);
    return Object.getOwnPropertyDescriptor(target, propertyName);
  }) as any;
}

const lazyInjectFix = fixPropertyDecorator(lazyInject);

The issue is solved since babel expects the decorator to be returned (not set directly on target by inversifyjs). In this way, we are just returning the decorator set by inversify and re-setting it with babel. (not sure if this double-define could be an issue, please let me know in this case.)

I am not sure if this is how it is supposed to be done but for LogUpdate decorator to work the same way in tsc and babel I had to change fixPropertyDecorator like this:

function fixPropertyDecorator<T extends Function>(decorator: T): T {
  return ((...args: any[]) => (
    target: any,
    propertyName: any,
    ...decoratorArgs: any[]
  ) => {
    decorator(...args)(target, propertyName, ...decoratorArgs);

    for (let arg of decoratorArgs) {
      if (!arg) {
        continue;
      }

      target[propertyName] = arg.initializer ? arg.initializer() : undefined;
    }

    return Object.getOwnPropertyDescriptor(target, propertyName);
  }) as any;
}

To preverse default value:

class Something {
  @LogUpdateFixed()
  public count: number = 0; // <---
}

Here is the repo.

Output with the fix:

elderapo@*****:~/nodejs/babel-7-property-decorator-issue$ yarn ts-node-start && yarn babel-start
yarn run v1.13.0
$ ts-node ./src/index.ts
Updating: undefined => 0...
default 0
Updating: 0 => 1...
Updating: 1 => 123...
Updating: 123 => -1...
Done in 1.07s.
yarn run v1.13.0
$ yarn babel-build && node ./compiled-with-babel/index.js
$ babel src --out-dir compiled-with-babel --extensions ".ts,.tsx" --source-maps inline
Successfully compiled 1 file with Babel.
Updating: undefined => 0...
default 0
Updating: 0 => 1...
Updating: 1 => 123...
Updating: 123 => -1...
Done in 0.94s.

Without:

elderapo@*****:~/nodejs/babel-7-property-decorator-issue$ yarn ts-node-start && yarn babel-start
yarn run v1.13.0
$ ts-node ./src/index.ts
Updating: undefined => 0...
default 0
Updating: 0 => 1...
Updating: 1 => 123...
Updating: 123 => -1...
Done in 1.00s.
yarn run v1.13.0
$ yarn babel-build && node ./compiled-with-babel/index.js
$ babel src --out-dir compiled-with-babel --extensions ".ts,.tsx" --source-maps inline
Successfully compiled 1 file with Babel.
default undefined
Updating: undefined => NaN...
Updating: NaN => 123...
Updating: 123 => -1...
Done in 0.73s.

With this fix decorators compiled with tsc and babel seem to act the same.

Regarding InversifyJS/IoC, I need to refactor some code and test some changes because seems like other stuff broke after changing from tsc to babel.

@elderapo the way you are dealing with args is not what I would suggest. Furthermore, initializer should be called anyway by babel, so I think it should just be merged in the original descriptor by lazyInject. Thinking about this, maybe it could be easier to just rewrite the lazyInject directly in order to respect the babel spec. I'll update you as soon as I can get something actionable.

Regarding InversifyJS/IoC, I need to refactor some code and test some changes because seems like other stuff broke after changing from tsc to babel.

If you can post any other issue I'd be glad to help, let me know

A lot of property decorators that intercept properties do something like:

const currentValue = target[propertyKey];

and then setup getters and setters. The solution with initializer will not work in these cases and all these decorators will have to be rewritten. Not sure what is the best approach to this issue.

Regarding "my other issues", I think they might be caused by isolatedModules and imports. I get Object(...) is not a function because decorators imported from "./ioc/decorators" are undefined under some circumstances. I need to investigate further if it's caused by my project structure or something else.

I'm going to close this since problem diverged from original question – hoping you've found a solution, and I'd be happy if you could share your experience here. Feel free to ping me if there's something more.