microsoft / microsoft/tsyringe

Circular solution failed in nodejs (NextJs - Api function area)

Open
#154 5 comments 3 reactions 1 assignee View on GitHub

@Xapphire13 is already working on this.

Since Jan 5, 2021.

awaiting triage
Dominant language
TypeScript
Stars
6k
Forks
184
Avg merge
3m
Merged PRs (30d)
1

Description

Describe the bug
Deal Tsyringe team,
First i want say thankyou for your masterpiece, tsyringe is wonderful and fast! Below is a issue i found when implement tsyringe to my project use NextJs as base:

in your document has section explains about circular dependencies when example:

@injectable()
export class Foo {
  constructor(@inject(delay(() => Bar)) public bar: Bar) {}
}

@injectable()
export class Bar {
  constructor(@inject(delay(() => Foo)) public foo: Foo) {}
}

// construction of foo is possible
const foo = container.resolve(Foo);

// property bar will hold a proxy that looks and acts as a real Bar instance.
foo.bar instanceof Bar; // true

This work well if put into test file and test use jest + ts-jest. but failed when add to a nextjs-api file. Look like circular not working correct in Nodejs ENV. Error shown:

ReferenceError: Cannot access 'Bar' before initialization. 

Attention that Nextjs use typescript along with Babel. After read next example in document i see other case you show how to resolve circular issue with Interface, this make me idea maybe issue start from how babel reference mismatch class with interface, so i try to change constructor(@inject(delay(() => Bar)) public bar: Bar) {} to constructor(@inject(delay(() => Bar)) public bar: typeof Bar) {} and code working.

I'm has not enough experience to go deeper issue, but i guess issue because code after compile think ...: Bar as a class , not reference as interface, and at the time 2 classes circular one of them is not initialization yet.

Of couse use interface as middleware is a option, but it duplicate a lot of work in project if project service classes is huge. Maybe you guys can research to have better solution.

To Reproduce

  1. create new project nextjs use yarn create next-app. Setup file and typescript follow nextjs instruction.
  2. modify tsconfig (see below)
  3. add file babelrc and add plugins (see below). This part in tsyring's document still lack info. If someone has time maybe can help clear exactly what babel plugins require for tsyringe in browser/node env.
  4. create new file in pages/api folder named test.ts add content (see below).
  5. run api , error will show.
  6. change constructors of 2 classes : Foo/Bar to : typeof Foo/Bar and run api again.

tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./*"
      ]
    },
    "experimentalDecorators": true,
    "emitDecoratorMetadata": false,
    "strictPropertyInitialization": false,
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

.babelrc

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

next js test api - pages/api/test.ts

import 'reflect-metadata';
import { NextApiHandler } from 'next';
import { container, delay, inject, injectable } from 'tsyringe';

@injectable()
export class Foo {
  constructor(@inject(delay(() => Bar)) public bar: Bar) {}
}

@injectable()
export class Bar {
  constructor(@inject(delay(() => Foo)) public foo: Foo) {}
}

const api: NextApiHandler = async (req, res) => {
  const test = container.resolve(Foo);
  console.log(test);
  return;
};
export default api;

Expected behavior
Find solution where we can reference Class as Interface.

Version:
"tsyringe": "^4.4.0",

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.