1

I started a new typescript project based an old project of mime (which started as plain javascript) and I can't get any non-trivial injection working. I know about nothing about babel configuration (because of staring with create-react-app). I tried the recommended compilerOptions and it didn't work either..... my setup may be a mess, but that's how it evolved.

Feel free to get the whole project from github (get it and run npm install && npm start) or continue reading.

import 'reflect-metadata';
import { Container } from 'inversify';
import React from 'react';
import ReactDOM from 'react-dom';
import { injectable } from 'inversify';

@injectable()
export class Simple {
}

@injectable()
export class Composed {
    simple: Simple;

    // ERROR: Module parse failed: Unexpected character '@'
    // constructor(@inject simple: Simple) {
    //  this.simple = simple;
    // }

    // Error: Missing required @inject or @multiInject annotation in: argument 0 in class Composed.
    constructor(simple: Simple) {
        this.simple = simple;
    }
}

const container = new Container();
container.bind(Simple).toSelf();
container.bind(Composed).toSelf();
console.log(container.get(Composed));

function Main() {
    return <div>
        {JSON.stringify(container.get(Composed))}
    </div>;
}

ReactDOM.render(<Main/>, document.getElementById('root'));

package.json

{
    "name": "inversify-problem",
    "version": "0.0.1",
    "description": "",
    "private": true,
    "dependencies": {
        "@material-ui/core": "^4.9.8",
        "@material-ui/icons": "^4.9.1",
        "array.prototype.flatmap": "^1.2.3",
        "customize-cra": "^0.9.1",
        "deep-equal": "^2.0.1",
        "eslint": "^6.8.0",
        "inversify": "^5.0.1",
        "json-stable-stringify": "^1.0.1",
        "mobx": "^5.15.4",
        "mobx-decorators": "^6.0.1",
        "mobx-react": "^6.1.8",
        "mobx-state-tree": "^3.15.0",
        "notistack": "^0.9.9",
        "react": "^16.13.1",
        "react-app-polyfill": "^1.0.6",
        "react-app-rewire-mobx": "^1.0.9",
        "react-app-rewired": "^2.1.5",
        "react-dom": "^16.13.1",
        "reflect-metadata": "^0.1.13",
        "resize-observer-polyfill": "^1.5.1",
        "shallowequal": "^1.1.0",
        "styled-components": "^5.0.1",
        "ts-enum-util": "^4.0.1"
    },
    "devDependencies": {
        "babel-plugin-import": "^1.13.0",
        "babel-plugin-styled-components": "^1.10.7",
        "@types/classnames": "^2.2.10",
        "@types/deep-equal": "^1.0.1",
        "@types/json-stable-stringify": "^1.0.32",
        "@types/jest": "^25.1.4",
        "@types/node": "^13.9.8",
        "@types/react": "^16.9.29",
        "@types/react-dom": "^16.9.5",
        "eslint-plugin-unused-imports": "^0.1.2",
        "tslint-etc": "^1.10.1",
        "react-scripts": "^3.4.1",
        "typescript": "^3.8.3"
    },
    "scripts": {
        "start": "react-app-rewired start",
        "build": "react-app-rewired build",
        "test": "react-app-rewired test",
        "eslint": "eslint --fix $(find src -name '*.ts' -o  -name '*.tsx')",
        "eject": "react-scripts eject"
    },
    "eslintConfig": {
        "extends": "react-app",
        "parserOptions": {
            "ecmaFeatures": {
                "legacyDecorators": true
            }
        }
    },
    "browserslist": [
        ">0.2%",
        "not dead",
        "not ie <= 11",
        "not op_mini all"
    ]
}

tsconfig.json

{
    "compilerOptions": {
        "target": "ES5",
        "sourceMap": true,
        "downlevelIteration": true,
        "importHelpers": true,
        "lib": [
            "dom",
            "dom.iterable",
            "esnext",
            "es6",
            "webworker",
            "es2015.collection",
            "es2015.iterable",
            "es2019"
        ],
        "types": [
            "node",
            "reflect-metadata"
        ],
        "allowJs": false,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "strictNullChecks": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "noImplicitAny": true,
        "noEmitOnError": false,
        "incremental": false,
        "jsx": "preserve",
        "noEmit": true
    },
    "include": [
        "src/index.tsx",
        "src/mg/extras.d.ts"
    ]
}

config-overrides.js

const {
    override,
} = require("customize-cra");

module.exports = override(
);

Debugging

I tried to debug the problem, but found not much out. There are no metadata, no attempt is made to parse Function.prototype.toString() like in this answer. There's something wrong with my configuration, but that thing is pretty opaque to me.

maaartinus
  • 44,714
  • 32
  • 161
  • 320

2 Answers2

2

You are struggling because the react babel config does not allow decorators, so what you can do is:

$ npm run eject
$ npm install react-scripts # if you use an older npm installation then you need to add --save
$ npm install -D @babel/plugin-proposal-decorators 
$ npm install -D babel-plugin-parameter-decorator

Your package.json is now expanded with a lot of options, find the babel section and change it as follows:

  "babel": {
    "plugins": [
      [
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        "babel-plugin-parameter-decorator"
      ]
    ],
    "presets": [
      // ...
      ["@babel/preset-typescript", { "onlyRemoveTypeImports": true }]
    ]
  }

now you can at least use field injection as follows:

// ...
import { inject, injectable } from 'inversify';
// ...

@injectable()
class Composed {
    private readonly simple: Simple;

    public constructor(@inject(Simple) simple: Simple) {
        this.simple = simple;
    }
}

//...

The solution is based on this:

For more info on why decorators isn't supported:

malat
  • 12,152
  • 13
  • 89
  • 158
Rohde Fischer
  • 1,248
  • 2
  • 10
  • 32
  • 1
    Somehow, it didn't work for me, see https://github.com/Maaartinus/inversify-problem/commit/32c5f90cb33fad2a28817968f55e5c97dfb7568d. I also don't understand why the class-based approach (no constructor params annotations) doesn't work. I use annotations with mobx in my real-live project where I encountered this problem. – maaartinus Apr 26 '20 at 03:13
  • Wondering if I forgot something in my answer, you're right, I can confirm this on your code too :/ You can do the injection on the field though, so it's likely the `babel-plugin-parameter-decorator` that doesn't get registered correctly :/ – Rohde Fischer Apr 26 '20 at 08:29
  • I'm a bit pressed on time, so I can't dive further into it for now. However, this should work at least: ```typescript @injectable() export class Composed { @inject(Simple) private readonly simple!: Simple; } ``` it's not as nice, but it should keep you going. I updated the config a bit in the answer to reflect best practice and fix the accidental change from preset `react-app` to `react-native`. – Rohde Fischer Apr 26 '20 at 10:01
1

I had the same solution as posted but instead of using package.json, I used config-overrides.js:

const {
  override,
  addDecoratorsLegacy,
  addBabelPlugin,
} = require("customize-cra");

module.exports = override(
  addDecoratorsLegacy(),
  addBabelPlugin("babel-plugin-parameter-decorator")
);

To be honest, I didn't like this solution so I didn't post it...

Maxim Mazurok
  • 3,856
  • 2
  • 22
  • 37
  • 2
    This solution might have one advantage. I suspect (without checking) that the `addDecoratorsLegacy`-wrapper has been made to account for the different ways of adding the decorators. My solution only works with Babel 7 and above, there's a different plugin for earlier Babel versions – Rohde Fischer Apr 24 '20 at 12:01