├── docs ├── index.md ├── child-injector.md ├── faq.md ├── Injector.md ├── token.md └── aspect.md ├── .eslintignore ├── src ├── typings.d.ts ├── constants.ts ├── index.ts ├── helper │ ├── index.ts │ ├── utils.ts │ ├── injector-helper.ts │ ├── parameter-helper.ts │ ├── event.ts │ ├── dep-helper.ts │ ├── reflect-helper.ts │ ├── is-function.ts │ ├── provider-helper.ts │ └── hook-helper.ts ├── factoryHelper.ts ├── compose.ts ├── error.ts ├── decorator.ts ├── types.ts └── injector.ts ├── scripts ├── jest-setup.ts └── versionUpdater.js ├── .vscode ├── settings.json └── launch.json ├── .npmignore ├── .husky ├── pre-push ├── pre-commit └── commit-msg ├── tests ├── cases │ └── issue-115 │ │ ├── const.ts │ │ ├── TestClass.ts │ │ └── index.test.ts ├── helper │ ├── utils-helper.test.ts │ ├── provider-helper.test.ts │ ├── injector-helper.test.ts │ ├── dep-helper.test.ts │ ├── hook-helper.test.ts │ ├── parameter-helper.test.ts │ ├── reflect-helper.test.ts │ ├── is-function.test.ts │ ├── event.test.ts │ └── compose.test.ts ├── injector │ ├── hasInstance.test.ts │ ├── overrideProviders.test.ts │ ├── strictMode.test.ts │ ├── domain.test.ts │ ├── dynamicMultiple.test.ts │ ├── tag.test.ts │ └── dispose.test.ts ├── decorators │ └── Autowired.test.ts ├── providers │ └── useAlias.test.ts ├── decorator.test.ts ├── aspect.test.ts ├── use-case.test.ts └── injector.test.ts ├── codecov.yml ├── .prettierrc ├── tsconfig.lib.json ├── tsconfig.esm.json ├── jest.config.js ├── tsconfig.json ├── .github └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .eslintrc.js ├── LICENSE ├── .gitignore ├── package.json ├── CHANGELOG.md ├── README-zh_CN.md └── README.md /docs/index.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/child-injector.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | esm 4 | types 5 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /scripts/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | coverage 3 | node_modules 4 | test 5 | example 6 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn test 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /tests/cases/issue-115/const.ts: -------------------------------------------------------------------------------- 1 | export const TestToken = Symbol('token'); 2 | export const TestClassToken = Symbol('TestClassToken'); 3 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | // VERSION GENERATED BY standard-version. 2 | // See package.json > standard-version for details. 3 | export const VERSION = '2.1.0'; 4 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0.5% 7 | if_ci_failed: error 8 | -------------------------------------------------------------------------------- /docs/Injector.md: -------------------------------------------------------------------------------- 1 | # Injector 2 | 3 | The `Injector` is where we used to get instance, register provider. 4 | 5 | ## Example 6 | 7 | ```ts 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 120, 6 | "proseWrap": "never", 7 | "endOfLine": "lf" 8 | } 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './decorator'; 3 | export * from './injector'; 4 | export * from './factoryHelper'; 5 | 6 | export { markInjectable, setParameters, getInjectableOpts } from './helper'; 7 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "lib": ["es2015"], 6 | "outDir": "lib", 7 | "declaration": false 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /docs/token.md: -------------------------------------------------------------------------------- 1 | # Token 2 | 3 | A token is used to exchange the real value in the IoC container, so it should be a global unique value. 4 | 5 | The definition of token is: 6 | 7 | ```ts 8 | type Token = string | symbol | Function; 9 | ``` 10 | -------------------------------------------------------------------------------- /tests/cases/issue-115/TestClass.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Autowired } from '../../../src'; 2 | 3 | import { TestToken } from './const'; 4 | 5 | @Injectable() 6 | export class TestClass { 7 | @Autowired(TestToken) 8 | public value!: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/helper/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dep-helper'; 2 | export * from './injector-helper'; 3 | export * from './parameter-helper'; 4 | export * from './provider-helper'; 5 | export * from './utils'; 6 | export * from './is-function'; 7 | export * from './hook-helper'; 8 | export * from './event'; 9 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "esm", 5 | "module": "es2022", 6 | "target": "es2022", 7 | "lib": ["es2022"], 8 | "declaration": true, 9 | "declarationDir": "types" 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /src/factoryHelper.ts: -------------------------------------------------------------------------------- 1 | import { FactoryFunction } from './types'; 2 | 3 | import type { Injector } from './injector'; 4 | 5 | export function asSingleton(func: FactoryFunction): FactoryFunction { 6 | let instance: T | undefined; 7 | return (injector: Injector) => { 8 | if (instance === undefined) { 9 | instance = func(injector); 10 | } 11 | return instance; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/helper/utils.ts: -------------------------------------------------------------------------------- 1 | export function uniq(arr: T[]): T[] { 2 | const set = new Set(arr); 3 | return Array.from(set); 4 | } 5 | 6 | export function flatten(list: Array) { 7 | const result: T[] = []; 8 | 9 | for (const item of list) { 10 | if (Array.isArray(item)) { 11 | result.push(...flatten(item)); 12 | } else { 13 | result.push(item); 14 | } 15 | } 16 | 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | coveragePathIgnorePatterns: ['/node_modules/', '/test/'], 6 | coverageThreshold: { 7 | global: { 8 | branches: 10, 9 | functions: 10, 10 | lines: 10, 11 | statements: 10, 12 | }, 13 | }, 14 | setupFilesAfterEnv: ['/scripts/jest-setup.ts'], 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "lib": ["es2020"], 6 | "strict": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "pretty": false, 10 | "noEmitOnError": false, 11 | "noFallthroughCasesInSwitch": true, 12 | "downlevelIteration": false, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "moduleResolution": "node" 16 | }, 17 | "include": ["src", "tests"] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest Current File", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["${relativeFile}"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/cases/issue-115/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '../../../src'; 2 | import { TestClass } from './TestClass'; 3 | import { TestClassToken, TestToken } from './const'; 4 | 5 | const injector = new Injector(); 6 | injector.addProviders({ 7 | token: TestToken, 8 | useValue: 'test', 9 | }); 10 | injector.addProviders({ 11 | token: TestClassToken, 12 | useClass: TestClass, 13 | }); 14 | 15 | describe('issue-115', () => { 16 | it('should work', () => { 17 | expect(injector.get(TestClassToken).value).toBe('test'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | ci: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Use Node.js 14.x 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: 14.x 15 | - name: Run CI 16 | run: | 17 | npm install 18 | npm run ci 19 | - name: Upload coverage to Codecov 20 | uses: codecov/codecov-action@v3 21 | with: 22 | token: ${{ secrets.CORE_CODECOV_TOKEN }} 23 | directory: ./coverage 24 | -------------------------------------------------------------------------------- /scripts/versionUpdater.js: -------------------------------------------------------------------------------- 1 | // 替换 ./src/constants.ts 中的版本号 2 | // 使用的是 standard-version 提供的能力 3 | 4 | // 匹配版本号的正则表达式 5 | const REGEX = /(?<=const VERSION = ')(.*)(?=';)/g; 6 | 7 | module.exports.readVersion = function (contents) { 8 | console.log('read file', contents); 9 | const version = contents.match(REGEX)[0]; 10 | console.log('return version', version); 11 | return version; 12 | }; 13 | 14 | module.exports.writeVersion = function (contents, version) { 15 | console.log('write file', contents); 16 | return contents.replace(REGEX, () => { 17 | console.log('replace version with', version); 18 | return version; 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /tests/helper/utils-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as Helper from '../../src/helper/utils'; 2 | 3 | describe('utils helper', () => { 4 | it('uniq', () => { 5 | const obj = {}; 6 | const arr = [1, 2, obj, obj, 3]; 7 | const result = Helper.uniq(arr); 8 | expect(result).toEqual([1, 2, obj, 3]); 9 | }); 10 | 11 | it('flatten', () => { 12 | const arr = [[1, 2, 3], [4, 5, 6], 7, 8]; 13 | const result = Helper.flatten(arr); 14 | expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); 15 | 16 | const deepArr = [[1, 2, [3, 4, [5, [6]], 7], 8, [9, [10], 11], 12], 13, 14, 15]; 17 | const deepResult = Helper.flatten(deepArr); 18 | expect(deepResult).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | parser: '@typescript-eslint/parser', 7 | plugins: ['@typescript-eslint'], 8 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 9 | rules: { 10 | 'no-prototype-builtins': 'warn', 11 | '@typescript-eslint/no-explicit-any': 0, 12 | '@typescript-eslint/ban-types': [ 13 | 'warn', 14 | { 15 | types: { 16 | Function: false, 17 | }, 18 | }, 19 | ], 20 | '@typescript-eslint/no-this-alias': [ 21 | 'error', 22 | { 23 | allowDestructuring: false, 24 | allowedNames: ['self', 'injector'], 25 | }, 26 | ], 27 | 'no-void': [ 28 | 'error', 29 | { 30 | allowAsStatement: true, 31 | }, 32 | ], 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | schedule: 9 | - cron: '17 22 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: ['javascript'] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v2 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v2 39 | -------------------------------------------------------------------------------- /tests/injector/hasInstance.test.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector, Autowired } from '../../src'; 2 | 3 | describe('hasInstance', () => { 4 | @Injectable() 5 | class A {} 6 | 7 | @Injectable() 8 | class B { 9 | @Autowired() 10 | a!: A; 11 | } 12 | 13 | @Injectable({ multiple: true }) 14 | class C {} 15 | 16 | it('能够通过 hasInstance 查到单例对象的存在性', () => { 17 | const token = 'token'; 18 | const instance = {}; 19 | 20 | const token2 = 'token2'; 21 | const instance2 = true; 22 | 23 | const provider = { token, useValue: instance }; 24 | const injector = new Injector([provider, B, C, { token: token2, useValue: instance2 }]); 25 | 26 | expect(injector.hasInstance(instance)).toBe(true); 27 | 28 | // 支持 primitive 的判断 29 | expect(injector.hasInstance(instance2)).toBe(true); 30 | 31 | const b = injector.get(B); 32 | expect(injector.hasInstance(b)).toBe(true); 33 | 34 | const c = injector.get(C); 35 | expect(injector.hasInstance(c)).toBe(true); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/injector/overrideProviders.test.ts: -------------------------------------------------------------------------------- 1 | import { Injector, Injectable } from '../../src'; 2 | 3 | describe('overrideProviders', () => { 4 | let injector: Injector; 5 | 6 | @Injectable() 7 | class A {} 8 | 9 | beforeEach(() => { 10 | injector = new Injector(); 11 | }); 12 | 13 | it('使用 overrideProviders 会覆盖原有的 Provider', () => { 14 | injector.addProviders(A); 15 | const a1 = injector.get(A); 16 | 17 | injector.overrideProviders({ token: A, useValue: '' }); 18 | const a2 = injector.get(A); 19 | 20 | expect(a1).toBeInstanceOf(A); 21 | expect(a2).toBe(''); 22 | }); 23 | 24 | it('使用 addProviders 覆盖原有的 Provider', () => { 25 | injector.addProviders(A); 26 | const a1 = injector.get(A); 27 | 28 | injector.addProviders({ token: A, useValue: 'a2' }); 29 | const a2 = injector.get(A); 30 | 31 | injector.addProviders({ token: A, useValue: 'a3', override: true }); 32 | const a3 = injector.get(A); 33 | 34 | expect(a1).toBeInstanceOf(A); 35 | expect(a2).toBeInstanceOf(A); 36 | expect(a3).toBe('a3'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup Node.js 14.x to publish to npmjs.org 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '14.x' 20 | registry-url: 'https://registry.npmjs.org' 21 | 22 | - name: Install Packages 23 | run: yarn install --frozen-lockfile 24 | 25 | - name: Build 26 | run: yarn build 27 | 28 | - name: Test 29 | run: yarn test 30 | env: 31 | CI: true 32 | 33 | - name: Generate Release Body 34 | run: npx extract-changelog-release > RELEASE_BODY.md 35 | 36 | - name: Publish to NPM 37 | run: npm publish 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | 41 | - name: Create GitHub Release 42 | uses: ncipollo/release-action@v1 43 | with: 44 | bodyFile: 'RELEASE_BODY.md' 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | draft: true 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Alibaba Group Holding Limited, Ant Group Co. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/injector/strictMode.test.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector, Inject } from '../../src'; 2 | import * as InjectorError from '../../src/error'; 3 | 4 | describe('严格模式', () => { 5 | let injector: Injector; 6 | 7 | beforeEach(() => { 8 | injector = new Injector([], { strict: true }); 9 | }); 10 | 11 | it('严格模式下,没有预先添加 Provider 的时候,会报错', () => { 12 | @Injectable() 13 | class T {} 14 | 15 | expect(() => injector.get(T)).toThrow(InjectorError.noProviderError(T)); 16 | }); 17 | 18 | it('严格模式下,构造函数的参数依赖会报错', () => { 19 | const token = Symbol('noop'); 20 | 21 | @Injectable() 22 | class T { 23 | constructor(@Inject(token) public a: string) {} 24 | } 25 | 26 | expect(() => { 27 | injector.addProviders(T); 28 | injector.get(T); 29 | }).toThrow(InjectorError.noProviderError(token)); 30 | }); 31 | 32 | it('严格模式下,overrideProviders 能正常生效', () => { 33 | const token = Symbol('noop'); 34 | 35 | @Injectable() 36 | class T {} 37 | 38 | @Injectable() 39 | class T2 {} 40 | 41 | injector.addProviders(T); 42 | const t1 = injector.get(T); 43 | expect(t1).toBeInstanceOf(T); 44 | 45 | injector.overrideProviders({ token: T, useClass: T2 }); 46 | const t2 = injector.get(T); 47 | expect(t2).toBeInstanceOf(T2); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/decorators/Autowired.test.ts: -------------------------------------------------------------------------------- 1 | import * as Error from '../../src/error'; 2 | import { Autowired, Injectable } from '../../src'; 3 | 4 | describe('Autowired decorator', () => { 5 | it('do not support number type', () => { 6 | expect(() => { 7 | @Injectable() 8 | class B { 9 | @Autowired() 10 | a!: number; 11 | } 12 | return B; 13 | }).toThrow(); 14 | }); 15 | it('will throw error if the Token is not defined, when performing dependency injection.', () => { 16 | expect(() => { 17 | interface A { 18 | log(): void; 19 | } 20 | @Injectable() 21 | class B { 22 | @Autowired() 23 | a!: A; 24 | } 25 | return B; 26 | }).toThrow(Error.tokenInvalidError(class B {}, 'a', Object)); 27 | }); 28 | 29 | it('define dependencies with null, expect an error', () => { 30 | expect(() => { 31 | interface A { 32 | log(): void; 33 | } 34 | @Injectable() 35 | class B { 36 | @Autowired(null as any) 37 | a!: A; 38 | } 39 | return B; 40 | }).toThrow(); 41 | }); 42 | 43 | it('Define dependencies using the original Number, expect an error', () => { 44 | expect(() => { 45 | interface A { 46 | log(): void; 47 | } 48 | @Injectable() 49 | class B { 50 | @Autowired(Number) 51 | a!: A; 52 | } 53 | return B; 54 | }).toThrow(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/compose.ts: -------------------------------------------------------------------------------- 1 | interface CallStack { 2 | depth: number; 3 | } 4 | 5 | export type Optional = Omit & Partial>; 6 | 7 | export type Context = { 8 | proceed(): Promise | void; 9 | } & T; 10 | 11 | export type PureContext = Optional, 'proceed'>; 12 | 13 | export interface Composed { 14 | (ctx: PureContext): Promise | void; 15 | } 16 | 17 | export interface Middleware { 18 | (ctx: Context): Promise | void; 19 | } 20 | 21 | function dispatch( 22 | middlewareList: Middleware[], 23 | idx: number, 24 | stack: CallStack, 25 | ctx: PureContext, 26 | ): Promise | void { 27 | if (idx <= stack.depth) { 28 | throw new Error('ctx.proceed() called multiple times'); 29 | } 30 | 31 | stack.depth = idx; 32 | 33 | let maybePromise: Promise | void | undefined; 34 | 35 | if (idx < middlewareList.length) { 36 | const middleware = middlewareList[idx]; 37 | 38 | maybePromise = middleware({ 39 | ...ctx, 40 | proceed: () => dispatch(middlewareList, idx + 1, stack, ctx), 41 | } as Context); 42 | } else if (ctx.proceed) { 43 | maybePromise = ctx.proceed(); 44 | } 45 | 46 | return maybePromise; 47 | } 48 | 49 | export default function compose(middlewareList: Middleware[]): Composed { 50 | return (ctx) => { 51 | const stack: CallStack = { depth: -1 }; 52 | return dispatch(middlewareList, 0, stack, ctx); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/helper/injector-helper.ts: -------------------------------------------------------------------------------- 1 | import { InstanceOpts } from '../types'; 2 | import type { Injector } from '../injector'; 3 | import { VERSION } from '../constants'; 4 | 5 | const INJECTOR_KEY = Symbol('INJECTOR_KEY'); 6 | 7 | export function getInjectorOfInstance(instance: object): Injector | null { 8 | return (instance as any)[INJECTOR_KEY] || null; 9 | } 10 | 11 | export function setInjector(instance: any, injector: object) { 12 | (instance as any)[INJECTOR_KEY] = injector; 13 | } 14 | 15 | export function removeInjector(instance: any) { 16 | delete (instance as any)[INJECTOR_KEY]; 17 | } 18 | 19 | const INJECTABLE_KEY = Symbol('INJECTABLE_KEY'); 20 | const defaultInstanceOpts: InstanceOpts = {}; 21 | 22 | export function markInjectable(target: object, opts: InstanceOpts = defaultInstanceOpts) { 23 | // 合并的时候只合并当前对象的数据 24 | const currentOpts = Reflect.getOwnMetadata(INJECTABLE_KEY, target); 25 | Reflect.defineMetadata(INJECTABLE_KEY, { ...opts, ...currentOpts, version: VERSION }, target); 26 | } 27 | 28 | export function getInjectableOpts(target: object): InstanceOpts | undefined { 29 | // 可注入性的参数可以继承自父级 30 | return Reflect.getMetadata(INJECTABLE_KEY, target); 31 | } 32 | 33 | export function isInjectable(target: object) { 34 | return !!getInjectableOpts(target); 35 | } 36 | 37 | export function createIdFactory(name: string) { 38 | let idx = 0; 39 | return { 40 | next() { 41 | return `${name}_${idx++}`; 42 | }, 43 | }; 44 | } 45 | 46 | export const injectorIdGenerator = createIdFactory('Injector'); 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | esm 3 | types 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | dist 85 | 86 | yarn.lock 87 | -------------------------------------------------------------------------------- /src/helper/parameter-helper.ts: -------------------------------------------------------------------------------- 1 | import { Token, ParameterOpts } from '../types'; 2 | import { createConstructorMetadataManager } from './reflect-helper'; 3 | 4 | const PARAMETER_KEY = Symbol('PARAMETER_KEY'); 5 | const parametersMeta = createConstructorMetadataManager(PARAMETER_KEY); 6 | 7 | function getParameters(target: object): Token[] { 8 | return parametersMeta.get(target) || []; 9 | } 10 | 11 | export function setParameters(target: object, parameters: Token[]) { 12 | return parametersMeta.set(parameters, target); 13 | } 14 | 15 | const TOKEN_KEY = Symbol('TOKEN_KEY'); 16 | const tokenMeta = createConstructorMetadataManager(TOKEN_KEY); 17 | 18 | function getParameterTokens(target: object): Array { 19 | return tokenMeta.get(target) || []; 20 | } 21 | 22 | export function setParameterIn(target: object, opts: ParameterOpts, index: number) { 23 | const tokens = [...getParameterTokens(target)]; 24 | tokens[index] = opts; 25 | return tokenMeta.set(tokens, target); 26 | } 27 | 28 | export function getParameterOpts(target: object) { 29 | const parameters = getParameters(target).map((token) => ({ token })); 30 | const tokens = getParameterTokens(target); 31 | 32 | return mergeParameters(parameters, tokens); 33 | } 34 | 35 | export function getParameterDeps(target: object) { 36 | const opts = getParameterOpts(target); 37 | return opts.map(({ token }) => token); 38 | } 39 | 40 | function mergeParameters(first: ParameterOpts[], second: Array) { 41 | const arr: ParameterOpts[] = []; 42 | 43 | const len = Math.max(first.length, second.length); 44 | if (len === 0) { 45 | return []; 46 | } 47 | 48 | for (let i = 0; i < len; i++) { 49 | const item = second[i]; 50 | if (item) { 51 | arr[i] = item; 52 | } else { 53 | arr[i] = first[i]; 54 | } 55 | } 56 | 57 | return arr; 58 | } 59 | -------------------------------------------------------------------------------- /src/helper/event.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modified from https://github.com/opensumi/utils/blob/main/packages/events/src/index.ts 3 | */ 4 | 5 | export type Handler = (...args: T) => void; 6 | 7 | export class EventEmitter> { 8 | private _listeners: Map = new Map(); 9 | 10 | on(event: Event, listener: Handler) { 11 | if (!this._listeners.has(event)) { 12 | this._listeners.set(event, []); 13 | } 14 | this._listeners.get(event)!.push(listener); 15 | 16 | return () => this.off(event, listener); 17 | } 18 | 19 | off(event: Event, listener: Handler) { 20 | if (!this._listeners.has(event)) { 21 | return; 22 | } 23 | const listeners = this._listeners.get(event)!; 24 | const index = listeners.indexOf(listener); 25 | if (index !== -1) { 26 | listeners.splice(index, 1); 27 | } 28 | } 29 | 30 | once(event: Event, listener: Handler) { 31 | const remove: () => void = this.on(event, (...args: Parameters>) => { 32 | remove(); 33 | listener.apply(this, args); 34 | }); 35 | 36 | return remove; 37 | } 38 | 39 | emit(event: Event, ...args: Parameters>) { 40 | if (!this._listeners.has(event)) { 41 | return; 42 | } 43 | [...this._listeners.get(event)!].forEach((listener) => listener.apply(this, args)); 44 | } 45 | 46 | hasListener(event: Event) { 47 | return this._listeners.has(event); 48 | } 49 | 50 | getListeners(event: Event) { 51 | return this._listeners.get(event) || []; 52 | } 53 | 54 | dispose() { 55 | this._listeners.clear(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/aspect.md: -------------------------------------------------------------------------------- 1 | # Aspect 2 | 3 | ## Quick Start 4 | 5 | ```ts 6 | const AToken = Symbol('AToken'); 7 | interface AToken { 8 | add(a: number): number; 9 | } 10 | 11 | @Injectable() 12 | class AImpl { 13 | private sum = 0; 14 | 15 | add(a: number) { 16 | this.sum += a; 17 | return this.sum; 18 | } 19 | } 20 | 21 | @Aspect() 22 | @Injectable() 23 | class OneAspectOfA { 24 | // 拦截 AToken 实现 class 的 add 方法,在 add 方法执行前,将它的参数乘以2 25 | @Before(AToken, 'add') 26 | beforeAdd(joinPoint: IBeforeJoinPoint) { 27 | const [a] = joinPoint.getArgs(); 28 | joinPoint.setArgs([a * 2]); 29 | } 30 | @After(AToken, 'add') 31 | afterAdd(joinPoint: IAfterJoinPoint) { 32 | const ret = joinPoint.getResult(); 33 | joinPoint.setResult(ret * 5); // 将返回值乘以5 34 | } 35 | } 36 | 37 | // 必须添加 Aspect 的 class 38 | injector.addProviders(OneAspectOfA); 39 | ``` 40 | 41 | 这种情况下,第一次调用 `const hookedSum = injector.get(AToken).add(2)` 后, AImpl 中的 sum 为 4, hookedSum 为 20 42 | 43 | ## Other hooks 44 | 45 | ```ts 46 | @Around(AToken, 'add') 47 | aroundAdd(joinPoint: IAroundJoinPoint) { 48 | const [a, b] = joinPoint.getArgs(); 49 | if (a === b) { 50 | console.log('adding two same numbers'); 51 | } 52 | joinPoint.proceed(); 53 | const result = joinPoint.getResult(); 54 | if (result === 10) { 55 | joinPoint.setResult(result * 10); 56 | } 57 | } 58 | 59 | @AfterReturning(AToken, 'add') 60 | afterReturningAdd(joinPoint: IAfterReturningJoinPoint) { 61 | const ret = joinPoint.getResult(); 62 | console.log('the return value is ' + ret); 63 | } 64 | 65 | @AfterThrowing(AToken, 'add') afterThrowingAdd(joinPoint: IAfterReturningJoinPoint) { 66 | const error = joinPoint.getError(); console.error('产生了一个错误', error); 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /src/helper/dep-helper.ts: -------------------------------------------------------------------------------- 1 | import { flatten, uniq } from './utils'; 2 | import { Token } from '../types'; 3 | import { getParameterDeps } from './parameter-helper'; 4 | import { createConstructorMetadataManager } from './reflect-helper'; 5 | 6 | const DEP_KEY = Symbol('DEP_KEY'); 7 | const depMeta = createConstructorMetadataManager(DEP_KEY); 8 | 9 | function getDeps(target: object): Token[] { 10 | return depMeta.get(target) || []; 11 | } 12 | 13 | export function addDeps(target: object, ...tokens: Token[]) { 14 | const deps = getDeps(target); 15 | return depMeta.set(deps.concat(tokens), target); 16 | } 17 | 18 | function getAllDepsWithScanned(targets: Token[], scanned: Token[]): Token[] { 19 | const deps: Token[] = []; 20 | 21 | for (const target of targets) { 22 | // only function types has dependency 23 | if (typeof target !== 'function' || scanned.includes(target)) { 24 | continue; 25 | } else { 26 | scanned.push(target); 27 | } 28 | 29 | // Find the dependencies of the target, the dependencies of the constructor, and the dependencies of the dependencies 30 | const targetDeps = getDeps(target); 31 | const parameters = getParameterDeps(target); 32 | const spreadDeeps = getAllDepsWithScanned(targetDeps, scanned); 33 | 34 | deps.push(...targetDeps, ...parameters, ...spreadDeeps); 35 | } 36 | 37 | return deps; 38 | } 39 | 40 | function getDepsWithCache(target: Token, cache: Map): Token[] { 41 | if (cache.has(target)) { 42 | return cache.get(target)!; 43 | } 44 | 45 | const scanned: Token[] = []; 46 | const deps = uniq(getAllDepsWithScanned([target], scanned)); 47 | cache.set(target, deps); 48 | return deps; 49 | } 50 | 51 | const allDepsCache = new Map(); 52 | 53 | /** 54 | * get all dependencies of input tokens. 55 | * @param tokens 56 | */ 57 | export function getAllDeps(...tokens: Token[]): Token[] { 58 | const depsArr = tokens.map((item) => getDepsWithCache(item, allDepsCache)); 59 | return uniq(flatten(depsArr)); 60 | } 61 | -------------------------------------------------------------------------------- /tests/helper/provider-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as Helper from '../../src/helper/provider-helper'; 2 | import { 3 | Token, 4 | TypeProvider, 5 | ValueProvider, 6 | FactoryProvider, 7 | ClassProvider, 8 | CreatorStatus, 9 | Injectable, 10 | } from '../../src'; 11 | 12 | describe('provider helper', () => { 13 | @Injectable() 14 | class A {} 15 | const clsToken: Token = A; 16 | const strToken: Token = 'strToken'; 17 | const symbolToken: Token = Symbol('symbolToken'); 18 | 19 | const typeProvider: TypeProvider = A; 20 | const valueProvider: ValueProvider = { 21 | token: A, 22 | useValue: new A(), 23 | }; 24 | const factoryProvider: FactoryProvider = { 25 | token: A, 26 | useFactory: () => new A(), 27 | }; 28 | const classProvider: ClassProvider = { 29 | token: A, 30 | useClass: A, 31 | }; 32 | 33 | it('getProvidersFromTokens', () => { 34 | const ret = Helper.getProvidersFromTokens([clsToken, strToken, symbolToken]); 35 | expect(ret).toEqual([A]); 36 | }); 37 | 38 | it('parseTokenFromProvider', () => { 39 | expect(Helper.parseTokenFromProvider(typeProvider)).toBe(A); 40 | expect(Helper.parseTokenFromProvider(valueProvider)).toBe(A); 41 | expect(Helper.parseTokenFromProvider(factoryProvider)).toBe(A); 42 | expect(Helper.parseTokenFromProvider(classProvider)).toBe(A); 43 | }); 44 | 45 | it('parseCreatorFromProvider', () => { 46 | expect(Helper.parseCreatorFromProvider(typeProvider)).toMatchObject({ 47 | parameters: [], 48 | useClass: A, 49 | }); 50 | 51 | const creator = Helper.parseCreatorFromProvider(valueProvider); 52 | expect(creator.instances?.has(A)).toBeTruthy; 53 | expect(creator.status).toBe(CreatorStatus.done); 54 | 55 | expect(Helper.parseCreatorFromProvider(factoryProvider)).toMatchObject({ 56 | useFactory: factoryProvider.useFactory, 57 | }); 58 | 59 | expect(Helper.parseCreatorFromProvider(classProvider)).toMatchObject({ 60 | parameters: [], 61 | useClass: A, 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/helper/reflect-helper.ts: -------------------------------------------------------------------------------- 1 | function findConstructor(target: object) { 2 | return typeof target === 'object' ? target.constructor : target; 3 | } 4 | 5 | function getConstructorMetadata(metadataKey: any, target: object, propertyKey?: string | symbol) { 6 | const constructor = findConstructor(target); 7 | if (propertyKey == null) { 8 | return Reflect.getMetadata(metadataKey, constructor); 9 | } else { 10 | return Reflect.getMetadata(metadataKey, constructor, propertyKey); 11 | } 12 | } 13 | 14 | function defineConstructorMetadata( 15 | metadataKey: any, 16 | metadataValue: any, 17 | target: object, 18 | propertyKey?: string | symbol, 19 | ) { 20 | const constructor = findConstructor(target); 21 | if (propertyKey == null) { 22 | return Reflect.defineMetadata(metadataKey, metadataValue, constructor); 23 | } else { 24 | return Reflect.defineMetadata(metadataKey, metadataValue, constructor, propertyKey); 25 | } 26 | } 27 | 28 | export function createConstructorMetadataManager(metadataKey: any) { 29 | return { 30 | get(target: object, propertyKey?: string | symbol) { 31 | return getConstructorMetadata(metadataKey, target, propertyKey); 32 | }, 33 | set(metadataValue: any, target: object, propertyKey?: string | symbol) { 34 | return defineConstructorMetadata(metadataKey, metadataValue, target, propertyKey); 35 | }, 36 | }; 37 | } 38 | 39 | export function createMetadataManager(metadataKey: any) { 40 | return { 41 | get(target: object, propertyKey?: string | symbol) { 42 | if (propertyKey == null) { 43 | return Reflect.getMetadata(metadataKey, target); 44 | } else { 45 | return Reflect.getMetadata(metadataKey, target, propertyKey); 46 | } 47 | }, 48 | set(metadataValue: any, target: object, propertyKey?: string | symbol) { 49 | if (propertyKey == null) { 50 | return Reflect.defineMetadata(metadataKey, metadataValue, target); 51 | } else { 52 | return Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey); 53 | } 54 | }, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /tests/helper/injector-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as Helper from '../../src/helper/injector-helper'; 2 | import { ConstructorOf, InstanceOpts } from '../../src'; 3 | 4 | // eslint-disable-next-line 5 | const pkg = require('../../package.json'); 6 | 7 | describe('injector helper', () => { 8 | let Parent: ConstructorOf; 9 | let Constructor: ConstructorOf; 10 | 11 | beforeEach(() => { 12 | Parent = class {}; 13 | Constructor = class extends Parent {}; 14 | }); 15 | 16 | it('设置可注入,并正常读取', () => { 17 | Helper.markInjectable(Constructor); 18 | expect(Helper.isInjectable(Constructor)).toBe(true); 19 | expect(Helper.getInjectableOpts(Constructor)).toEqual({ version: pkg.version }); 20 | }); 21 | 22 | it('设置注入配置,并正常读取', () => { 23 | const opts = {}; 24 | Helper.markInjectable(Constructor, opts); 25 | expect(Helper.isInjectable(Constructor)).toBe(true); 26 | expect(Helper.getInjectableOpts(Constructor)).toEqual({ version: pkg.version }); 27 | }); 28 | 29 | it('设置父亲的注入配置,并读取', () => { 30 | const opts = {}; 31 | Helper.markInjectable(Parent, opts); 32 | expect(Helper.isInjectable(Constructor)).toBe(true); 33 | expect(Helper.getInjectableOpts(Constructor)).toEqual({ version: pkg.version }); 34 | }); 35 | 36 | it('InstanceOpts 只会和自己的数据合并', () => { 37 | const parentOpts: InstanceOpts = { multiple: true }; 38 | const childOpts: InstanceOpts = { tag: 'tag' }; 39 | 40 | Helper.markInjectable(Parent, parentOpts); 41 | expect(Helper.getInjectableOpts(Constructor)).toEqual({ ...parentOpts, version: pkg.version }); 42 | 43 | Helper.markInjectable(Constructor, childOpts); 44 | expect(Helper.getInjectableOpts(Constructor)).toEqual({ ...childOpts, version: pkg.version }); 45 | 46 | const childOpts2: InstanceOpts = { domain: 'domain' }; 47 | Helper.markInjectable(Constructor, childOpts2); 48 | expect(Helper.getInjectableOpts(Constructor)).toEqual({ ...childOpts, ...childOpts2, version: pkg.version }); 49 | }); 50 | 51 | it('不同版本设置的时候,会报错', () => { 52 | const opts = {}; 53 | Helper.markInjectable(Parent, opts); 54 | expect(Helper.isInjectable(Constructor)).toBe(true); 55 | expect(Helper.getInjectableOpts(Constructor)).toEqual({ version: pkg.version }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/helper/is-function.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Provider, 3 | Token, 4 | TypeProvider, 5 | ClassProvider, 6 | FactoryProvider, 7 | ValueProvider, 8 | InstanceCreator, 9 | ValueCreator, 10 | ClassCreator, 11 | FactoryCreator, 12 | CreatorStatus, 13 | AliasProvider, 14 | AliasCreator, 15 | } from '../types'; 16 | import { isInjectable } from './injector-helper'; 17 | 18 | export function isTypeProvider(provider: Provider | Token): provider is TypeProvider { 19 | return typeof provider === 'function'; 20 | } 21 | 22 | export function isClassProvider(provider: Provider): provider is ClassProvider { 23 | return !!(provider as ClassProvider).useClass; 24 | } 25 | 26 | export function isFactoryProvider(provider: Provider): provider is FactoryProvider { 27 | return !!(provider as FactoryProvider).useFactory; 28 | } 29 | 30 | export function isValueProvider(provider: Provider): provider is ValueProvider { 31 | return Object.prototype.hasOwnProperty.call(provider, 'useValue'); 32 | } 33 | 34 | export function isAliasProvider(provider: Provider): provider is AliasProvider { 35 | return Object.prototype.hasOwnProperty.call(provider, 'useAlias'); 36 | } 37 | 38 | export function isInjectableToken(token: Token): token is TypeProvider { 39 | return typeof token === 'function' && isInjectable(token); 40 | } 41 | 42 | const errorConstructors = new Set([Object, String, Number, Boolean]); 43 | 44 | const tokenTypes = new Set(['function', 'string', 'symbol']); 45 | export function isToken(token: any): token is Token { 46 | if (typeof token === 'function') { 47 | return !errorConstructors.has(token); 48 | } 49 | 50 | return tokenTypes.has(typeof token); 51 | } 52 | 53 | export function isValueCreator(creator: InstanceCreator): creator is ValueCreator { 54 | return (creator as ValueCreator).status === CreatorStatus.done; 55 | } 56 | 57 | export function isClassCreator(creator: InstanceCreator): creator is ClassCreator { 58 | return !!(creator as ClassCreator).useClass; 59 | } 60 | 61 | export function isFactoryCreator(creator: InstanceCreator): creator is FactoryCreator { 62 | return !!(creator as FactoryCreator).useFactory; 63 | } 64 | 65 | export function isAliasCreator(creator: InstanceCreator): creator is AliasCreator { 66 | return Object.prototype.hasOwnProperty.call(creator, 'useAlias'); 67 | } 68 | -------------------------------------------------------------------------------- /tests/helper/dep-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as Helper from '../../src/helper/dep-helper'; 2 | import { ConstructorOf } from '../../src'; 3 | 4 | describe('dep helper', () => { 5 | let Parent: ConstructorOf; 6 | let Constructor: ConstructorOf; 7 | 8 | beforeEach(() => { 9 | Parent = class {}; 10 | Constructor = class extends Parent {}; 11 | }); 12 | 13 | it('没有定义依赖的时候查询出来是空的', () => { 14 | const depsFromConstructor = Helper.getAllDeps(Constructor); 15 | expect(depsFromConstructor).toEqual([]); 16 | 17 | const instance = new Constructor(); 18 | const depsFromInstance = Helper.getAllDeps(instance); 19 | expect(depsFromInstance).toEqual([]); 20 | }); 21 | 22 | it('基本的依赖定义', () => { 23 | const dep = 'Test'; 24 | Helper.addDeps(Constructor, dep); 25 | 26 | const depsFromConstructor = Helper.getAllDeps(Constructor); 27 | expect(depsFromConstructor).toEqual([dep]); 28 | }); 29 | 30 | it('在父级进行依赖定义', () => { 31 | const dep = 'Test'; 32 | Helper.addDeps(Parent, dep); 33 | 34 | const depsFromConstructor = Helper.getAllDeps(Constructor); 35 | expect(depsFromConstructor).toEqual([dep]); 36 | }); 37 | 38 | it('依赖取值出来应该是去重的结果', () => { 39 | const dep = 'Test'; 40 | Helper.addDeps(Parent, dep, dep); 41 | 42 | const depsFromConstructor = Helper.getAllDeps(Constructor); 43 | expect(depsFromConstructor).toEqual([dep]); 44 | }); 45 | 46 | it('在父级进行依赖定义,并且再新定义', () => { 47 | const dep1 = 'dep1'; 48 | Helper.addDeps(Parent, dep1); 49 | 50 | const dep2 = 'dep2'; 51 | Helper.addDeps(Constructor, dep2); 52 | 53 | const depsFromParentConstructor = Helper.getAllDeps(Parent); 54 | expect(depsFromParentConstructor).toEqual([dep1]); 55 | 56 | const depsFromConstructor = Helper.getAllDeps(Constructor); 57 | expect(depsFromConstructor).toEqual([dep1, dep2]); 58 | }); 59 | 60 | it('当前一个依赖包含了后面的所有依赖的时候,应该正确解析', () => { 61 | const Dep1 = class {}; 62 | const Dep2 = class {}; 63 | const Dep3 = class {}; 64 | 65 | Helper.addDeps(Dep1, Dep2, Dep3); 66 | Helper.addDeps(Dep2, Dep3); 67 | 68 | const deps = Helper.getAllDeps(Dep1, Dep2); 69 | const depsAgain = Helper.getAllDeps(Dep2); 70 | 71 | expect(deps).toEqual([Dep2, Dep3]); 72 | expect(depsAgain).toEqual([Dep3]); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/helper/hook-helper.test.ts: -------------------------------------------------------------------------------- 1 | import { HookStore, applyHooks, isHooked } from '../../src/helper'; 2 | import { HookType } from '../../src'; 3 | 4 | describe('hook store test', () => { 5 | const token1 = Symbol(); 6 | 7 | it('可以创建和删除hook', () => { 8 | const hookStore = new HookStore(); 9 | const disposer = hookStore.createHooks([ 10 | { 11 | hook: () => undefined, 12 | method: 'method', 13 | target: token1, 14 | type: HookType.Before, 15 | }, 16 | { 17 | hook: () => undefined, 18 | method: 'method', 19 | target: token1, 20 | type: HookType.After, 21 | }, 22 | ]); 23 | expect(hookStore.getHooks(token1, 'method').length).toBe(2); 24 | disposer.dispose(); 25 | expect(hookStore.getHooks(token1, 'method').length).toBe(0); 26 | disposer.dispose(); 27 | (hookStore as any).hooks.get(token1).clear(); 28 | disposer.dispose(); 29 | (hookStore as any).hooks.delete(token1); 30 | disposer.dispose(); 31 | }); 32 | 33 | it('子store中可以拿到父store的hook', () => { 34 | const hookStore = new HookStore(); 35 | const childHookStore = new HookStore(hookStore); 36 | const disposer = hookStore.createHooks([ 37 | { 38 | hook: () => undefined, 39 | method: 'method', 40 | target: token1, 41 | type: HookType.Before, 42 | }, 43 | ]); 44 | childHookStore.createHooks([ 45 | { 46 | hook: () => undefined, 47 | method: 'method', 48 | target: token1, 49 | type: HookType.After, 50 | }, 51 | ]); 52 | expect(hookStore.getHooks(token1, 'method').length).toBe(1); 53 | expect(childHookStore.getHooks(token1, 'method').length).toBe(2); 54 | }); 55 | 56 | it('apply Hook测试', () => { 57 | const valueRet = applyHooks('1', token1, new HookStore()); 58 | expect(valueRet).toBe('1'); 59 | 60 | const hookStore = new HookStore(); 61 | hookStore.createHooks([ 62 | { 63 | hook: () => undefined, 64 | method: 'a', 65 | target: token1, 66 | type: HookType.After, 67 | }, 68 | ]); 69 | const objectRet = applyHooks({}, token1, hookStore); 70 | expect(isHooked(objectRet)).toBeTruthy(); 71 | 72 | const objectRetNoHooks = applyHooks({}, token1, new HookStore()); 73 | expect(isHooked(objectRetNoHooks)).toBeFalsy(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/helper/provider-helper.ts: -------------------------------------------------------------------------------- 1 | import { Provider, Token, Tag, InstanceCreator, CreatorStatus, InstanceOpts } from '../types'; 2 | import { 3 | isValueProvider, 4 | isClassProvider, 5 | isTypeProvider, 6 | isFactoryProvider, 7 | isInjectableToken, 8 | isAliasProvider, 9 | } from './is-function'; 10 | import { getParameterOpts } from './parameter-helper'; 11 | import { getAllDeps } from './dep-helper'; 12 | import { getInjectableOpts } from './injector-helper'; 13 | import { noInjectableError } from '../error'; 14 | 15 | export function getProvidersFromTokens(targets: Token[]) { 16 | const spreadDeps: Token[] = getAllDeps(...targets); 17 | const allDeps = targets.concat(spreadDeps); 18 | 19 | return allDeps.filter(isInjectableToken); 20 | } 21 | 22 | export function parseTokenFromProvider(provider: Provider): Token { 23 | if (isTypeProvider(provider)) { 24 | return provider; 25 | } else { 26 | return provider.token; 27 | } 28 | } 29 | 30 | export function hasTag(target: T): target is T & { tag: Tag } { 31 | if (typeof target === 'function') { 32 | return false; 33 | } else { 34 | return Object.prototype.hasOwnProperty.call(target, 'tag'); 35 | } 36 | } 37 | 38 | export function parseCreatorFromProvider(provider: Provider): InstanceCreator { 39 | const basicObj = isTypeProvider(provider) 40 | ? {} 41 | : { 42 | dropdownForTag: provider.dropdownForTag, 43 | tag: provider.tag, 44 | }; 45 | 46 | if (isValueProvider(provider)) { 47 | return { 48 | instances: new Set([provider.useValue]), 49 | isDefault: provider.isDefault, 50 | status: CreatorStatus.done, 51 | ...basicObj, 52 | }; 53 | } else if (isFactoryProvider(provider)) { 54 | return { 55 | isDefault: provider.isDefault, 56 | useFactory: provider.useFactory, 57 | ...basicObj, 58 | }; 59 | } else if (isAliasProvider(provider)) { 60 | return { 61 | useAlias: provider.useAlias, 62 | }; 63 | } else { 64 | const isDefault = isClassProvider(provider) ? provider.isDefault : false; 65 | const useClass = isClassProvider(provider) ? provider.useClass : provider; 66 | 67 | const opts = getInjectableOpts(useClass); 68 | if (!opts) { 69 | throw noInjectableError(useClass); 70 | } 71 | 72 | const parameters = getParameterOpts(useClass); 73 | 74 | return { 75 | isDefault, 76 | opts, 77 | parameters, 78 | useClass, 79 | ...basicObj, 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opensumi/di", 3 | "version": "2.1.0", 4 | "description": "A dependency injection tool for Javascript.", 5 | "license": "MIT", 6 | "module": "esm/index.js", 7 | "main": "lib/index.js", 8 | "types": "types/index.d.ts", 9 | "scripts": { 10 | "prepare": "husky install", 11 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 12 | "build": "npm run build:lib && npm run build:esm", 13 | "build:lib": "rm -rf lib && tsc -p tsconfig.lib.json", 14 | "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json", 15 | "test": "jest --coverage tests/**", 16 | "test:watch": "yarn test --watch", 17 | "ci": "npm run lint && npm run test", 18 | "prepublishOnly": "npm run build", 19 | "prerelease": "npm run lint && npm run test && npm run build", 20 | "release": "commit-and-tag-version --npmPublishHint 'echo Just Push code to remote repo, npm publish will be done by CI.'", 21 | "release:beta": "npm run release -- --prerelease beta" 22 | }, 23 | "devDependencies": { 24 | "@commitlint/cli": "17.2.0", 25 | "@commitlint/config-conventional": "17.2.0", 26 | "@types/jest": "29.2.2", 27 | "@types/node": "18.11.9", 28 | "@typescript-eslint/eslint-plugin": "5.42.1", 29 | "@typescript-eslint/parser": "5.42.1", 30 | "commit-and-tag-version": "^11.2.3", 31 | "commitlint": "17.2.0", 32 | "eslint": "8.27.0", 33 | "eslint-config-prettier": "8.5.0", 34 | "husky": "8.0.2", 35 | "jest": "29.3.0", 36 | "lint-staged": "13.0.3", 37 | "prettier": "2.7.1", 38 | "reflect-metadata": "^0.1.13", 39 | "ts-jest": "29.0.3", 40 | "typescript": "4.8.4" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git@github.com:opensumi/di.git" 45 | }, 46 | "engines": { 47 | "node": ">=8.0.0" 48 | }, 49 | "lint-staged": { 50 | "*.ts": [ 51 | "eslint --fix", 52 | "prettier --write" 53 | ] 54 | }, 55 | "commitlint": { 56 | "extends": [ 57 | "@commitlint/config-conventional" 58 | ] 59 | }, 60 | "keywords": [ 61 | "di", 62 | "injector" 63 | ], 64 | "files": [ 65 | "esm", 66 | "types", 67 | "lib" 68 | ], 69 | "standard-version": { 70 | "bumpFiles": [ 71 | { 72 | "filename": "./src/constants.ts", 73 | "updater": "./scripts/versionUpdater.js" 74 | }, 75 | { 76 | "filename": "package.json", 77 | "type": "json" 78 | } 79 | ] 80 | }, 81 | "publishConfig": { 82 | "access": "public", 83 | "registry": "https://registry.npmjs.org" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/injector/domain.test.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector } from '../../src'; 2 | 3 | describe('domain', () => { 4 | let injector: Injector; 5 | 6 | beforeEach(() => { 7 | injector = new Injector([]); 8 | }); 9 | 10 | it('if domain is not found, getFromDomain will not throw error', () => { 11 | @Injectable() 12 | class T {} 13 | 14 | injector.addProviders(T); 15 | const result = injector.getFromDomain('domain'); 16 | expect(result).toEqual([]); 17 | }); 18 | 19 | it('if domain is registered, we can get instance through domain', () => { 20 | @Injectable({ domain: 'domain' }) 21 | class T {} 22 | 23 | // T is not registered in injector 24 | const result = injector.getFromDomain('domain'); 25 | expect(result).toEqual([]); 26 | const a = injector.get(T); 27 | expect(a).toBeInstanceOf(T); 28 | const result2 = injector.getFromDomain('domain'); 29 | expect(result2).toEqual([a]); 30 | }); 31 | 32 | it('Use string as domain', () => { 33 | @Injectable({ domain: 'domain' }) 34 | class T {} 35 | 36 | @Injectable({ domain: 'domain' }) 37 | class K {} 38 | 39 | injector.addProviders(T, K); 40 | const result = injector.getFromDomain('domain'); 41 | expect(result.length).toBe(2); 42 | expect(result[0]).toBeInstanceOf(T); 43 | expect(result[1]).toBeInstanceOf(K); 44 | }); 45 | 46 | it('Use Token as domain', () => { 47 | const domain = Symbol('domain'); 48 | 49 | @Injectable({ domain }) 50 | class T {} 51 | 52 | @Injectable({ domain }) 53 | class K {} 54 | 55 | injector.addProviders(T, K); 56 | const result = injector.getFromDomain(domain); 57 | expect(result.length).toBe(2); 58 | expect(result[0]).toBeInstanceOf(T); 59 | expect(result[1]).toBeInstanceOf(K); 60 | }); 61 | 62 | it('cross multiple domains', () => { 63 | @Injectable({ domain: ['domain1', 'domain2'] }) 64 | class T {} 65 | 66 | @Injectable({ domain: ['domain3', 'domain2'] }) 67 | class K {} 68 | 69 | injector.addProviders(T, K); 70 | const result1 = injector.getFromDomain('domain1'); 71 | expect(result1.length).toBe(1); 72 | 73 | const result2 = injector.getFromDomain('domain2'); 74 | expect(result2.length).toBe(2); 75 | 76 | const result3 = injector.getFromDomain('domain3'); 77 | expect(result3.length).toBe(1); 78 | }); 79 | it('child injector can get instance from parent injector', () => { 80 | @Injectable({ domain: 'domain' }) 81 | class T {} 82 | 83 | injector.addProviders(T); 84 | const childInjector = injector.createChild([]); 85 | const result = childInjector.getFromDomain('domain'); 86 | expect(result.length).toBe(1); 87 | expect(result[0]).toBeInstanceOf(T); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/helper/parameter-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as Helper from '../../src/helper/parameter-helper'; 2 | import { ConstructorOf } from '../../src'; 3 | 4 | describe('parameter helper', () => { 5 | let Parent: ConstructorOf; 6 | let Constructor: ConstructorOf; 7 | 8 | beforeEach(() => { 9 | Parent = class {}; 10 | Constructor = class extends Parent {}; 11 | }); 12 | 13 | it('什么都没定义的时候结果是空的', () => { 14 | const ret = Helper.getParameterDeps(Constructor); 15 | expect(ret).toEqual([]); 16 | }); 17 | 18 | it('基本的添加依赖', () => { 19 | const dep = 'dep'; 20 | Helper.setParameters(Constructor, [dep]); 21 | 22 | const ret = Helper.getParameterDeps(Constructor); 23 | expect(ret).toEqual([dep]); 24 | }); 25 | 26 | it('指定第二个参数的依赖', () => { 27 | const dep1 = 'dep'; 28 | Helper.setParameters(Constructor, [dep1]); 29 | 30 | const dep2 = 'dep2'; 31 | Helper.setParameterIn(Constructor, { token: dep2 }, 1); 32 | 33 | const ret = Helper.getParameterDeps(Constructor); 34 | expect(ret).toEqual([dep1, dep2]); 35 | }); 36 | 37 | it('给父类添加依赖,能个正常查出', () => { 38 | const dep = 'dep'; 39 | Helper.setParameters(Parent, [dep]); 40 | 41 | const ret = Helper.getParameterDeps(Constructor); 42 | expect(ret).toEqual([dep]); 43 | }); 44 | 45 | it('给父类指定依赖,能够正常查出', () => { 46 | const dep1 = 'dep'; 47 | Helper.setParameters(Parent, [dep1]); 48 | 49 | const dep2 = 'dep2'; 50 | Helper.setParameterIn(Parent, { token: dep2 }, 1); 51 | 52 | const ret = Helper.getParameterDeps(Constructor); 53 | expect(ret).toEqual([dep1, dep2]); 54 | }); 55 | 56 | it('从子类设置的依赖能够覆盖父类的依赖', () => { 57 | Helper.setParameters(Parent, ['parent', 'parent']); 58 | const parentDeps = Helper.getParameterDeps(Parent); 59 | expect(parentDeps).toEqual(['parent', 'parent']); 60 | 61 | Helper.setParameters(Constructor, ['child', 'child']); 62 | const childDeps = Helper.getParameterDeps(Constructor); 63 | expect(childDeps).toEqual(['child', 'child']); 64 | }); 65 | 66 | it('不同位置的 Token 描述能够合并', () => { 67 | Helper.setParameterIn(Parent, { token: 'parent' }, 0); 68 | Helper.setParameterIn(Constructor, { token: 'child' }, 1); 69 | const deps = Helper.getParameterDeps(Constructor); 70 | expect(deps).toEqual(['parent', 'child']); 71 | }); 72 | 73 | it('能够得到构造依赖和 Token 定义的结果产物', () => { 74 | Helper.setParameters(Constructor, ['parameter1', 'parameter2']); 75 | Helper.setParameterIn(Constructor, { token: 'token' }, 1); 76 | 77 | const deps = Helper.getParameterDeps(Constructor); 78 | expect(deps).toEqual(['parameter1', 'token']); 79 | 80 | const opts = Helper.getParameterOpts(Constructor); 81 | expect(opts).toEqual([{ token: 'parameter1' }, { token: 'token' }]); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { Context, Token } from './types'; 2 | 3 | function stringify(target: object | Token) { 4 | if (typeof target === 'object') { 5 | return target.constructor.name; 6 | } else if (typeof target === 'function') { 7 | return target.name; 8 | } else { 9 | return String(target); 10 | } 11 | } 12 | 13 | export function noProviderError(...tokens: Token[]) { 14 | return new Error(`Cannot find Provider of ${tokens.map((t) => stringify(t)).join(', ')}`); 15 | } 16 | 17 | export function tagOnlyError(expectTag: string, currentTag: string) { 18 | return new Error(`Expect creating class in Injector with tag: ${expectTag}, but current: ${currentTag}.`); 19 | } 20 | 21 | export function noInjectableError(target: object) { 22 | return new Error( 23 | `Target ${stringify(target)} has not decorated by Injectable. Maybe you have multiple packages installed.`, 24 | ); 25 | } 26 | 27 | export function notInjectError(target: object, index: number) { 28 | return new Error( 29 | `The ${index}th constructor parameter of ${stringify( 30 | target, 31 | )} has not decorated by \`Inject\`, or maybe you want to set \`multiple\` in the Injectable decorator.`, 32 | ); 33 | } 34 | 35 | export function tokenInvalidError(target: object, key: Token, token: any) { 36 | const tokenType = String(token); 37 | const reason = 38 | '(1) Please check your `tsconfig.json` to enable `emitDecoratorMetadata` and `experimentalDecorators`. (2) Has not defined token cause TS compiled to Object. (3) Has circular dependencies cause reading property error.'; 39 | return new Error( 40 | `Autowired error: The type of property ${String(key)} of ${stringify( 41 | target, 42 | )} is unsupported. Allowed type: string/symbol/function, but received "${tokenType}". ${reason}`, 43 | ); 44 | } 45 | 46 | export function noInjectorError(target: object) { 47 | return new Error(`Cannot find the Injector of ${stringify(target)}`); 48 | } 49 | 50 | export function circularError(target: object, ctx: Context) { 51 | const tokenTrace = [] as string[]; 52 | let current: Context | undefined = ctx; 53 | while (current) { 54 | tokenTrace.push(stringify(current.token)); 55 | current = current.parent; 56 | } 57 | 58 | const traceResult = tokenTrace.reverse().join(' > '); 59 | 60 | return new Error(`Detected circular dependencies when creating ${stringify(target)}. ` + traceResult); 61 | } 62 | 63 | export function aliasCircularError(paths: Token[], current: Token) { 64 | return new Error( 65 | `useAlias registration cycle detected! ${[...paths, current].map((v) => stringify(v)).join(' -> ')}`, 66 | ); 67 | } 68 | 69 | export function noInstancesInCompletedCreatorError(token: Token) { 70 | /* istanbul ignore next */ 71 | return new Error(`Cannot find value of ${stringify(token)} in a completed creator.`); 72 | } 73 | -------------------------------------------------------------------------------- /tests/helper/reflect-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as Helper from '../../src/helper/reflect-helper'; 2 | import { ConstructorOf } from '../../src'; 3 | 4 | describe('reflect helper', () => { 5 | let Parent: ConstructorOf; 6 | let Constructor: ConstructorOf; 7 | 8 | beforeEach(() => { 9 | Parent = class {}; 10 | Constructor = class extends Parent {}; 11 | }); 12 | 13 | it('从构造函数设置 Meta 数据', () => { 14 | const instance = new Constructor(); 15 | const propertyKey = 'propertyKey'; 16 | const meta = Helper.createConstructorMetadataManager(Symbol()); 17 | expect(meta.get(Constructor)).toBeUndefined(); 18 | expect(meta.get(Constructor, 'propertyKey')).toBeUndefined(); 19 | 20 | const value1 = {}; 21 | meta.set(value1, Constructor); 22 | expect(meta.get(Constructor)).toBe(value1); 23 | expect(meta.get(instance)).toBe(value1); 24 | 25 | const value2 = {}; 26 | meta.set(value2, Constructor, propertyKey); 27 | expect(meta.get(Constructor, propertyKey)).toBe(value2); 28 | expect(meta.get(instance, propertyKey)).toBe(value2); 29 | 30 | const parentMata = Helper.createConstructorMetadataManager(Symbol()); 31 | const value3 = {}; 32 | parentMata.set(value3, Parent); 33 | expect(parentMata.get(Constructor)).toBe(value3); 34 | expect(parentMata.get(instance)).toBe(value3); 35 | 36 | const value4 = {}; 37 | parentMata.set(value4, Parent, propertyKey); 38 | expect(parentMata.get(Constructor, propertyKey)).toBe(value4); 39 | expect(parentMata.get(instance, propertyKey)).toBe(value4); 40 | }); 41 | 42 | it('从实例对象设置 Meta 数据', () => { 43 | const instance = new Constructor(); 44 | const propertyKey = 'propertyKey'; 45 | const meta = Helper.createConstructorMetadataManager(Symbol()); 46 | expect(meta.get(Constructor)).toBeUndefined(); 47 | expect(meta.get(Constructor, 'propertyKey')).toBeUndefined(); 48 | 49 | const value1 = {}; 50 | meta.set(value1, instance); 51 | expect(meta.get(Constructor)).toBe(value1); 52 | expect(meta.get(instance)).toBe(value1); 53 | 54 | const value2 = {}; 55 | meta.set(value2, instance, propertyKey); 56 | expect(meta.get(Constructor, propertyKey)).toBe(value2); 57 | expect(meta.get(instance, propertyKey)).toBe(value2); 58 | }); 59 | 60 | it('一般的 Meta 数据设置', () => { 61 | const propertyKey = 'propertyKey'; 62 | const meta = Helper.createMetadataManager(Symbol()); 63 | expect(meta.get(Constructor)).toBeUndefined(); 64 | expect(meta.get(Constructor, 'propertyKey')).toBeUndefined(); 65 | 66 | const value1 = {}; 67 | meta.set(value1, Constructor); 68 | expect(meta.get(Constructor)).toBe(value1); 69 | 70 | const value2 = {}; 71 | meta.set(value2, Constructor, propertyKey); 72 | expect(meta.get(Constructor, propertyKey)).toBe(value2); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/helper/is-function.test.ts: -------------------------------------------------------------------------------- 1 | import * as Helper from '../../src/helper/is-function'; 2 | import { 3 | Token, 4 | ValueProvider, 5 | TypeProvider, 6 | FactoryProvider, 7 | ClassProvider, 8 | markInjectable, 9 | CreatorStatus, 10 | } from '../../src'; 11 | 12 | describe('is function', () => { 13 | class A {} 14 | const clsToken: Token = A; 15 | const strToken: Token = 'strToken'; 16 | const symbolToken: Token = Symbol('symbolToken'); 17 | 18 | const typeProvider: TypeProvider = A; 19 | const valueProvider: ValueProvider = { 20 | token: A, 21 | useValue: new A(), 22 | }; 23 | const factoryProvider: FactoryProvider = { 24 | token: A, 25 | useFactory: () => new A(), 26 | }; 27 | const classProvider: ClassProvider = { 28 | token: A, 29 | useClass: A, 30 | }; 31 | 32 | it('isTypeProvider', () => { 33 | expect(Helper.isTypeProvider(typeProvider)).toBe(true); 34 | expect(Helper.isTypeProvider(strToken)).toBe(false); 35 | }); 36 | 37 | it('isClassProvider', () => { 38 | expect(Helper.isClassProvider(classProvider)).toBe(true); 39 | expect(Helper.isClassProvider(valueProvider)).toBe(false); 40 | }); 41 | 42 | it('isFactoryProvider', () => { 43 | expect(Helper.isFactoryProvider(factoryProvider)).toBe(true); 44 | expect(Helper.isFactoryProvider(valueProvider)).toBe(false); 45 | }); 46 | 47 | it('isValueProvider', () => { 48 | const emptyValueProvider = { token: '1', useValue: '' }; 49 | expect(Helper.isValueProvider(emptyValueProvider)).toBe(true); 50 | 51 | const nullValueProvider = { token: '1', useValue: null }; 52 | expect(Helper.isValueProvider(nullValueProvider)).toBe(true); 53 | 54 | const undefinedValueProvider = { token: '1', useValue: undefined }; 55 | expect(Helper.isValueProvider(undefinedValueProvider)).toBe(true); 56 | 57 | expect(Helper.isValueProvider(valueProvider)).toBe(true); 58 | expect(Helper.isValueProvider(factoryProvider)).toBe(false); 59 | }); 60 | 61 | it('isInjectableToken', () => { 62 | expect(Helper.isInjectableToken(strToken)).toBe(false); 63 | expect(Helper.isInjectableToken(clsToken)).toBe(false); 64 | 65 | class B {} 66 | markInjectable(B); 67 | expect(Helper.isInjectableToken(B)).toBe(true); 68 | }); 69 | 70 | it('isToken', () => { 71 | expect(Helper.isToken(strToken)).toBe(true); 72 | expect(Helper.isToken(clsToken)).toBe(true); 73 | expect(Helper.isToken(symbolToken)).toBe(true); 74 | expect(Helper.isToken(1)).toBe(false); 75 | }); 76 | 77 | it('isValueCreator', () => { 78 | expect(Helper.isValueCreator(factoryProvider)).toBe(false); 79 | expect(Helper.isValueCreator({ status: CreatorStatus.done, instances: new Set([A]) })).toBe(true); 80 | }); 81 | 82 | it('isFactoryCreator', () => { 83 | expect(Helper.isClassCreator(factoryProvider)).toBe(false); 84 | expect(Helper.isClassCreator({ opts: {}, parameters: [], useClass: A })).toBe(true); 85 | }); 86 | 87 | it('isFactoryCreator', () => { 88 | expect(Helper.isFactoryCreator(factoryProvider)).toBe(true); 89 | expect(Helper.isFactoryCreator({ opts: {}, parameters: [], useClass: A })).toBe(false); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/injector/dynamicMultiple.test.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector, Autowired, Inject } from '../../src'; 2 | import * as InjectorError from '../../src/error'; 3 | 4 | describe('动态多例创建', () => { 5 | it('能够动态传递 Arguments, 且使用覆盖的实现', () => { 6 | @Injectable() 7 | class AClass {} 8 | 9 | class Parent { 10 | @Autowired() 11 | a!: AClass; 12 | 13 | constructor(public d: symbol) {} 14 | } 15 | 16 | @Injectable() 17 | class Child extends Parent {} 18 | 19 | const injector = new Injector([AClass, { token: Parent, useClass: Child }]); 20 | const dynamic = Symbol('dynamic'); 21 | const instance = injector.get(Parent, [dynamic]); 22 | expect(instance).toBeInstanceOf(Child); 23 | expect(instance.a).toBeInstanceOf(AClass); 24 | expect(instance.d).toBe(dynamic); 25 | }); 26 | 27 | it('能够动态传递 Arguments, 且使用单例的实现', () => { 28 | @Injectable() 29 | class AClass {} 30 | 31 | class Parent { 32 | @Autowired() 33 | a!: AClass; 34 | 35 | constructor(@Inject('d') public d: symbol) {} 36 | } 37 | 38 | @Injectable() 39 | class ChildImpl extends Parent {} 40 | const persistArgs = Symbol('persist'); 41 | const dynamicArgs = Symbol('dynamic'); 42 | 43 | const injector = new Injector([ 44 | AClass, 45 | { token: 'd', useValue: persistArgs }, 46 | { token: Parent, useClass: ChildImpl }, 47 | ]); 48 | 49 | const persistOne = injector.get(Parent); 50 | expect(persistOne).toBeInstanceOf(ChildImpl); 51 | expect(persistOne.a).toBeInstanceOf(AClass); 52 | expect(persistOne.d).toBe(persistArgs); 53 | 54 | const dynamicOne = injector.get(Parent, [dynamicArgs]); 55 | expect(dynamicOne).toBeInstanceOf(ChildImpl); 56 | expect(dynamicOne.a).toBeInstanceOf(AClass); 57 | expect(dynamicOne.d).toBe(dynamicArgs); 58 | expect(dynamicOne.a).toBe(persistOne.a); 59 | }); 60 | 61 | it('能够动态传递 Arguments, 且使用 Value 覆盖的实现', () => { 62 | @Injectable() 63 | class AClass {} 64 | 65 | class Parent { 66 | @Autowired() 67 | a!: AClass; 68 | 69 | constructor(public d: symbol) {} 70 | } 71 | 72 | const dynamic = Symbol('dynamic'); 73 | class OtherImpl { 74 | a = new AClass(); 75 | 76 | d = dynamic; 77 | } 78 | 79 | const injector = new Injector([AClass, { token: Parent, useValue: new OtherImpl() }]); 80 | const instance = injector.get(Parent, [dynamic]); 81 | expect(instance).toBeInstanceOf(OtherImpl); 82 | expect(instance.a).toBeInstanceOf(AClass); 83 | expect(instance.d).toBe(dynamic); 84 | }); 85 | 86 | it('传递创建多例的参数到错误的 Token 中,会创建不了多例', () => { 87 | @Injectable() 88 | class AClass {} 89 | 90 | const dynamic = Symbol('dynamic'); 91 | expect(() => { 92 | const injector = new Injector([AClass]); 93 | injector.get('Token' as any, [dynamic]); 94 | }).toThrow(InjectorError.noProviderError('Token')); 95 | }); 96 | it('支持使用 token 来创建多例', () => { 97 | @Injectable({ multiple: true }) 98 | class MultipleCase { 99 | constructor(public d: number) {} 100 | getNumber() { 101 | return this.d; 102 | } 103 | } 104 | 105 | const token = Symbol('token'); 106 | const injector = new Injector([ 107 | { 108 | token, 109 | useClass: MultipleCase, 110 | }, 111 | ]); 112 | 113 | const a1 = injector.get(token, [1]); 114 | const a2 = injector.get(token, [2]); 115 | expect(a1.getNumber()).toBe(1); 116 | expect(a2.getNumber()).toBe(2); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/injector/tag.test.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector, Autowired } from '../../src'; 2 | import * as InjectorError from '../../src/error'; 3 | 4 | describe('Tag', () => { 5 | let injector: Injector; 6 | 7 | beforeEach(() => { 8 | injector = new Injector(); 9 | }); 10 | 11 | it('能够正常从 Tag 获取对象', () => { 12 | const token = Symbol('token'); 13 | const tag = 'tag'; 14 | const value = Symbol('value'); 15 | 16 | injector.addProviders({ 17 | tag, 18 | token, 19 | useValue: value, 20 | }); 21 | 22 | const instance = injector.get(token, { tag }); 23 | expect(instance).toBe(value); 24 | 25 | expect(() => { 26 | injector.get(token); 27 | }).toThrow(InjectorError.noProviderError(token)); 28 | }); 29 | 30 | it('空字符串能够作为 Tag 正常创建对象', () => { 31 | const token = Symbol('token'); 32 | const tag = ''; 33 | const value = Symbol('value'); 34 | 35 | injector.addProviders({ 36 | tag, 37 | token, 38 | useValue: value, 39 | }); 40 | 41 | const instance = injector.get(token, { tag }); 42 | expect(instance).toBe(value); 43 | 44 | expect(() => { 45 | injector.get(token); 46 | }).toThrow(InjectorError.noProviderError(token)); 47 | }); 48 | 49 | it('数字 0 能够作为 Tag 正常创建对象', () => { 50 | const token = Symbol('token'); 51 | const tag = 0; 52 | const value = Symbol('value'); 53 | 54 | injector.addProviders({ 55 | tag, 56 | token, 57 | useValue: value, 58 | }); 59 | 60 | const instance = injector.get(token, { tag }); 61 | expect(instance).toBe(value); 62 | 63 | expect(() => { 64 | injector.get(token); 65 | }).toThrow(InjectorError.noProviderError(token)); 66 | }); 67 | 68 | it('数字 1 能够作为 Tag 正常创建对象', () => { 69 | const token = Symbol('token'); 70 | const tag = 1; 71 | const value = Symbol('value'); 72 | 73 | injector.addProviders({ 74 | tag, 75 | token, 76 | useValue: value, 77 | }); 78 | 79 | const instance = injector.get(token, { tag }); 80 | expect(instance).toBe(value); 81 | 82 | expect(() => { 83 | injector.get(token); 84 | }).toThrow(InjectorError.noProviderError(token)); 85 | }); 86 | 87 | it('没有定义 Tag Provider 的时候会获取默认值', () => { 88 | const token = Symbol('token'); 89 | const tag = 'tag'; 90 | const value = Symbol('value'); 91 | 92 | injector.addProviders({ 93 | token, 94 | useValue: value, 95 | }); 96 | 97 | const instance = injector.get(token, { tag }); 98 | expect(instance).toBe(value); 99 | 100 | const instanceWithoutTag = injector.get(token); 101 | expect(instanceWithoutTag).toBe(value); 102 | }); 103 | 104 | it('使用 Autowired 能够正常获取值', () => { 105 | const token = Symbol('token'); 106 | const tag = 'tag'; 107 | const value = Symbol('value'); 108 | 109 | injector.addProviders({ 110 | token, 111 | useValue: value, 112 | }); 113 | 114 | @Injectable() 115 | class TagParent { 116 | @Autowired(token, { tag }) 117 | tagValue: any; 118 | } 119 | 120 | const parent = injector.get(TagParent); 121 | expect(parent).toBeInstanceOf(TagParent); 122 | expect(parent.tagValue).toBe(value); 123 | }); 124 | 125 | it('Child 能够正确获取 Parent 的 Tag 对象', () => { 126 | const token = Symbol('token'); 127 | const tag = 'tag'; 128 | const value = Symbol('value'); 129 | 130 | injector.addProviders({ 131 | tag, 132 | token, 133 | useValue: value, 134 | }); 135 | 136 | const childInjector = injector.createChild(); 137 | const parentTagToken = injector.exchangeToken(token, tag); 138 | const childTagToken = childInjector.exchangeToken(token, tag); 139 | expect(parentTagToken).toBe(childTagToken); 140 | 141 | const instance = childInjector.get(token, { tag }); 142 | expect(instance).toBe(value); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /tests/providers/useAlias.test.ts: -------------------------------------------------------------------------------- 1 | import { Autowired, Injectable, Injector } from '../../src'; 2 | import { aliasCircularError } from '../../src/error'; 3 | 4 | describe('useAlias is work', () => { 5 | it('can alias useValue', () => { 6 | const injector = new Injector(); 7 | const single1 = { company: 'AntGroup' }; 8 | const tokenA = Symbol('tokenA'); 9 | const tokenB = Symbol('tokenB'); 10 | injector.addProviders( 11 | { 12 | token: tokenA, 13 | useValue: single1, 14 | }, 15 | { 16 | token: tokenB, 17 | useAlias: tokenA, 18 | }, 19 | ); 20 | const aa = injector.get(tokenA); 21 | const bb = injector.get(tokenB); 22 | expect(aa).toBe(bb); 23 | }); 24 | it('can alias useClass', () => { 25 | const injector = new Injector(); 26 | @Injectable() 27 | class A {} 28 | 29 | const tokenA = Symbol('tokenA'); 30 | const tokenB = Symbol('tokenB'); 31 | injector.addProviders( 32 | { 33 | token: tokenA, 34 | useClass: A, 35 | }, 36 | { 37 | token: tokenB, 38 | useAlias: tokenA, 39 | }, 40 | ); 41 | const aa = injector.get(tokenA); 42 | const bb = injector.get(tokenB); 43 | expect(aa).toBe(bb); 44 | }); 45 | it('can alias useFactory', () => { 46 | const injector = new Injector(); 47 | const tokenA = Symbol('tokenA'); 48 | const tokenB = Symbol('tokenB'); 49 | injector.addProviders( 50 | { 51 | token: tokenA, 52 | useFactory: () => () => 1, 53 | }, 54 | { 55 | token: tokenB, 56 | useAlias: tokenA, 57 | }, 58 | ); 59 | const aa = injector.get(tokenA); 60 | const bb = injector.get(tokenB); 61 | expect(aa()).toEqual(bb()); 62 | }); 63 | it('can resolve nested alias', () => { 64 | const injector = new Injector(); 65 | const single1 = { company: 'AntGroup' }; 66 | const tokenA = Symbol('tokenA'); 67 | const tokenB = Symbol('tokenB'); 68 | const tokenC = Symbol('tokenC'); 69 | injector.addProviders( 70 | { 71 | token: tokenA, 72 | useValue: single1, 73 | }, 74 | { 75 | token: tokenB, 76 | useAlias: tokenA, 77 | }, 78 | { 79 | token: tokenC, 80 | useAlias: tokenB, 81 | }, 82 | ); 83 | const aa = injector.get(tokenA); 84 | const bb = injector.get(tokenB); 85 | expect(aa).toBe(bb); 86 | const cc = injector.get(tokenC); 87 | expect(aa).toBe(cc); 88 | }); 89 | it('can detect useAlias Token registration cycle', () => { 90 | const injector = new Injector(); 91 | const single1 = { company: 'AntGroup' }; 92 | const tokenA = Symbol('tokenA'); 93 | const tokenB = Symbol('tokenB'); 94 | const tokenC = Symbol('tokenC'); 95 | 96 | expect(() => { 97 | injector.addProviders( 98 | { 99 | token: tokenA, 100 | useValue: single1, 101 | }, 102 | { 103 | token: tokenB, 104 | useAlias: tokenC, 105 | }, 106 | { 107 | token: tokenC, 108 | useAlias: tokenB, 109 | }, 110 | ); 111 | // Because tokenC is added later, so the first one in the array is tokenC, and tokenC alias to tokenB. 112 | // So the cycle is C **alias to** B **alias to** C 113 | }).toThrowError(aliasCircularError([tokenC, tokenB], tokenC)); 114 | }); 115 | it('dispose alias will delete all its token', () => { 116 | const injector = new Injector(); 117 | const instantiateSpy = jest.fn(); 118 | @Injectable() 119 | class A { 120 | constructor() { 121 | instantiateSpy(); 122 | } 123 | } 124 | 125 | const tokenA = Symbol('tokenA'); 126 | const tokenB = Symbol('tokenB'); 127 | @Injectable() 128 | class DisposeCls { 129 | @Autowired(tokenA) 130 | a!: A; 131 | @Autowired(tokenB) 132 | aa!: A; 133 | } 134 | 135 | injector.addProviders( 136 | { 137 | token: tokenA, 138 | useClass: A, 139 | }, 140 | { 141 | token: tokenB, 142 | useAlias: tokenA, 143 | }, 144 | ); 145 | 146 | const disposeCls = injector.get(DisposeCls); 147 | expect(disposeCls.a).toBeInstanceOf(A); 148 | expect(disposeCls.a).toBe(disposeCls.aa); 149 | expect(instantiateSpy).toBeCalledTimes(1); 150 | 151 | injector.disposeOne(tokenB); 152 | expect(instantiateSpy).toBeCalledTimes(1); 153 | expect(disposeCls.a).toBeInstanceOf(A); 154 | expect(disposeCls.a).toBe(disposeCls.aa); 155 | expect(instantiateSpy).toBeCalledTimes(2); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /tests/decorator.test.ts: -------------------------------------------------------------------------------- 1 | import * as Error from '../src/error'; 2 | import { Autowired, Injectable, Inject, Injector, getInjectableOpts } from '../src'; 3 | 4 | // eslint-disable-next-line 5 | const pkg = require('../package.json'); 6 | 7 | describe('decorators', () => { 8 | it('Injectable 的时候允许多次描述', () => { 9 | @Injectable({ multiple: true }) 10 | @Injectable({ domain: [] }) 11 | class A {} 12 | 13 | @Injectable({ domain: [] }) 14 | @Injectable({ multiple: true }) 15 | class B {} 16 | 17 | const optsA = getInjectableOpts(A); 18 | expect(optsA).toEqual({ multiple: true, domain: [], version: pkg.version }); 19 | 20 | const optsB = getInjectableOpts(B); 21 | expect(optsB).toEqual({ multiple: true, domain: [], version: pkg.version }); 22 | }); 23 | 24 | it('构造函数进行依赖注入的时候,没有定义 Token 会报错', () => { 25 | expect(() => { 26 | @Injectable() 27 | class B { 28 | constructor(public a: any) {} 29 | } 30 | return B; 31 | }).toThrow(Error.notInjectError(class B {}, 0)); 32 | }); 33 | 34 | it('使用数字作为 Token 进行依赖注入的时候,没有定义 Token 会报错', () => { 35 | expect(() => { 36 | @Injectable() 37 | class B { 38 | constructor(@Inject(1 as any) public a: any) {} 39 | } 40 | return B; 41 | }).toThrow(Error.notInjectError(class B {}, 0)); 42 | }); 43 | 44 | it('单纯的对象使用 Autowired 进行装饰的时候,找不到 Injector 的报错', () => { 45 | @Injectable() 46 | class A {} 47 | 48 | class B { 49 | @Autowired() 50 | a!: A; 51 | } 52 | 53 | const b = new B(); 54 | 55 | expect(() => { 56 | return b.a; 57 | }).toThrow(Error.noInjectorError(b)); 58 | }); 59 | 60 | it('父类被 Injectable 装饰过,子类可以创建', () => { 61 | @Injectable() 62 | class A {} 63 | class B extends A {} 64 | 65 | const injector = new Injector([B]); 66 | expect(injector.get(B)).toBeInstanceOf(B); 67 | }); 68 | 69 | it('多例模式下,每个对象只会创建一个依赖', () => { 70 | @Injectable({ multiple: true }) 71 | class A {} 72 | 73 | @Injectable() 74 | class D {} 75 | 76 | @Injectable() 77 | class B { 78 | @Autowired() 79 | a!: A; 80 | 81 | @Autowired() 82 | d!: D; 83 | } 84 | 85 | @Injectable() 86 | class C { 87 | @Autowired() 88 | a!: A; 89 | 90 | @Autowired() 91 | d!: D; 92 | } 93 | 94 | const injector = new Injector(); 95 | const b = injector.get(B); 96 | const c = injector.get(C); 97 | 98 | expect(b.a).toBe(b.a); 99 | expect(c.a).toBe(c.a); 100 | expect(b.a).not.toBe(c.a); 101 | expect(b.a).not.toBe(b.d); 102 | expect(b.d).toBe(c.d); 103 | }); 104 | 105 | it('多例模式下,多次对同一个对象进行依赖,应该有多个实例', () => { 106 | @Injectable({ multiple: true }) 107 | class A {} 108 | 109 | @Injectable() 110 | class B { 111 | @Autowired() 112 | a1!: A; 113 | 114 | @Autowired() 115 | a2!: A; 116 | } 117 | 118 | const injector = new Injector(); 119 | const b = injector.get(B); 120 | expect(b.a1).toBe(b.a1); 121 | expect(b.a2).toBe(b.a2); 122 | expect(b.a1).not.toBe(b.a2); 123 | }); 124 | 125 | it('装饰器解析出来的依赖不是正确的 Token 的时候需要报错', () => { 126 | expect(() => { 127 | @Injectable() 128 | class InjectError { 129 | constructor(public a: string) {} 130 | } 131 | }).toThrow(Error.notInjectError(class InjectError {}, 0)); 132 | 133 | expect(() => { 134 | @Injectable({ multiple: true }) 135 | class NoError { 136 | constructor(public a: string) {} 137 | } 138 | }).not.toThrow(); 139 | 140 | expect(() => { 141 | interface AA { 142 | a: string; 143 | } 144 | 145 | @Injectable() 146 | class InjectError { 147 | constructor(public a: AA) {} 148 | } 149 | }).toThrow(Error.notInjectError(class InjectError {}, 0)); 150 | 151 | expect(() => { 152 | enum AAEnum { 153 | aa, 154 | } 155 | 156 | @Injectable() 157 | class InjectError { 158 | constructor(public aa: AAEnum) {} 159 | } 160 | }).toThrow(Error.notInjectError(class InjectError {}, 0)); 161 | 162 | expect(() => { 163 | @Injectable() 164 | class InjectError { 165 | constructor(public a: number) {} 166 | } 167 | }).toThrow(Error.notInjectError(class InjectError {}, 0)); 168 | 169 | expect(() => { 170 | @Injectable() 171 | class InjectError { 172 | constructor(public a: 1) {} 173 | } 174 | }).toThrow(Error.notInjectError(class InjectError {}, 0)); 175 | 176 | expect(() => { 177 | @Injectable() 178 | class InjectError { 179 | constructor(public a: boolean) {} 180 | } 181 | }).toThrow(Error.notInjectError(class InjectError {}, 0)); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /tests/helper/event.test.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '../../src/helper/event'; 2 | 3 | describe('event emitter', () => { 4 | it('basic usage', () => { 5 | const emitter = new EventEmitter<{ 6 | [key: string]: [string]; 7 | }>(); 8 | 9 | const spy = jest.fn(); 10 | const spy2 = jest.fn(); 11 | emitter.on('test', spy); 12 | emitter.on('foo', spy2); 13 | 14 | expect(emitter.hasListener('test')).toBe(true); 15 | const listeners = emitter.getListeners('test'); 16 | expect(listeners.length).toBe(1); 17 | 18 | emitter.emit('test', 'hello'); 19 | expect(spy).toBeCalledWith('hello'); 20 | emitter.off('test', spy); 21 | 22 | const listeners2 = emitter.getListeners('test'); 23 | expect(listeners2.length).toBe(0); 24 | 25 | emitter.emit('test', 'hello'); 26 | expect(spy).toBeCalledTimes(1); 27 | 28 | emitter.once('test', spy); 29 | emitter.emit('test', 'hello'); 30 | expect(spy).toBeCalledTimes(2); 31 | emitter.emit('test', 'hello'); 32 | expect(spy).toBeCalledTimes(2); 33 | 34 | emitter.off('bar', spy); 35 | 36 | emitter.dispose(); 37 | 38 | emitter.emit('test', 'hello'); 39 | expect(spy).toBeCalledTimes(2); 40 | }); 41 | 42 | it('many listeners listen to one event', () => { 43 | const emitter = new EventEmitter<{ 44 | [key: string]: [string]; 45 | }>(); 46 | const spy = jest.fn(); 47 | const spy2 = jest.fn(); 48 | emitter.on('test', spy); 49 | emitter.on('test', spy2); 50 | emitter.emit('test', 'hello'); 51 | expect(spy).toBeCalledWith('hello'); 52 | expect(spy2).toBeCalledWith('hello'); 53 | 54 | emitter.off('test', spy); 55 | emitter.emit('test', 'hello'); 56 | expect(spy).toBeCalledTimes(1); 57 | expect(spy2).toBeCalledTimes(2); 58 | 59 | emitter.dispose(); 60 | }); 61 | 62 | it('can dispose event listener by using returned function', () => { 63 | const emitter = new EventEmitter<{ 64 | [key: string]: [string]; 65 | }>(); 66 | const spy = jest.fn(); 67 | const spy2 = jest.fn(); 68 | const spy3 = jest.fn(); 69 | const disposeSpy = emitter.on('test', spy); 70 | emitter.on('test', spy2); 71 | 72 | const disposeSpy3 = emitter.once('test', spy3); 73 | disposeSpy3(); 74 | 75 | emitter.emit('test', 'hello'); 76 | expect(spy).toBeCalledWith('hello'); 77 | expect(spy2).toBeCalledWith('hello'); 78 | 79 | disposeSpy(); 80 | emitter.emit('test', 'hello'); 81 | expect(spy).toBeCalledTimes(1); 82 | expect(spy2).toBeCalledTimes(2); 83 | expect(spy3).toBeCalledTimes(0); 84 | emitter.dispose(); 85 | }); 86 | }); 87 | 88 | describe('event emitter types', () => { 89 | it('basic usage', () => { 90 | const emitter = new EventEmitter<{ 91 | test: [string, string]; 92 | foo: [string]; 93 | }>(); 94 | 95 | const spy = jest.fn(); 96 | const spy2 = jest.fn(); 97 | 98 | emitter.on('test', spy); 99 | emitter.on('foo', spy2); 100 | 101 | expect(emitter.hasListener('test')).toBe(true); 102 | const listeners = emitter.getListeners('test'); 103 | expect(listeners.length).toBe(1); 104 | 105 | emitter.emit('test', 'hello', 'world'); 106 | expect(spy).toBeCalledWith('hello', 'world'); 107 | emitter.off('test', spy); 108 | 109 | const listeners2 = emitter.getListeners('test'); 110 | expect(listeners2.length).toBe(0); 111 | 112 | emitter.emit('test', 'hello', 'world'); 113 | expect(spy).toBeCalledTimes(1); 114 | 115 | emitter.once('test', spy); 116 | emitter.emit('test', 'hello', 'world'); 117 | expect(spy).toBeCalledTimes(2); 118 | emitter.emit('test', 'hello', 'world'); 119 | expect(spy).toBeCalledTimes(2); 120 | 121 | emitter.off('bar' as any, spy); 122 | 123 | emitter.dispose(); 124 | 125 | emitter.emit('test' as any, 'hello'); 126 | expect(spy).toBeCalledTimes(2); 127 | }); 128 | 129 | it('many listeners listen to one event', () => { 130 | const emitter = new EventEmitter<{ 131 | [key: string]: [string]; 132 | }>(); 133 | const spy = jest.fn(); 134 | const spy2 = jest.fn(); 135 | emitter.on('test', spy); 136 | emitter.on('test', spy2); 137 | emitter.emit('test', 'hello'); 138 | expect(spy).toBeCalledWith('hello'); 139 | expect(spy2).toBeCalledWith('hello'); 140 | 141 | emitter.off('test', spy); 142 | emitter.emit('test', 'hello'); 143 | expect(spy).toBeCalledTimes(1); 144 | expect(spy2).toBeCalledTimes(2); 145 | 146 | emitter.dispose(); 147 | }); 148 | 149 | it('can dispose event listener by using returned function', () => { 150 | const emitter = new EventEmitter<{ 151 | [key: string]: [string]; 152 | }>(); 153 | const spy = jest.fn(); 154 | const spy2 = jest.fn(); 155 | const spy3 = jest.fn(); 156 | const disposeSpy = emitter.on('test', spy); 157 | emitter.on('test', spy2); 158 | 159 | const disposeSpy3 = emitter.once('test', spy3); 160 | disposeSpy3(); 161 | 162 | emitter.emit('test', 'hello'); 163 | expect(spy).toBeCalledWith('hello'); 164 | expect(spy2).toBeCalledWith('hello'); 165 | 166 | disposeSpy(); 167 | emitter.emit('test', 'hello'); 168 | expect(spy).toBeCalledTimes(1); 169 | expect(spy2).toBeCalledTimes(2); 170 | expect(spy3).toBeCalledTimes(0); 171 | emitter.dispose(); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /tests/aspect.test.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Aspect, Around, IAroundJoinPoint, Injector } from '../src'; 2 | 3 | describe('aspect', () => { 4 | jest.setTimeout(50 * 1000); 5 | 6 | it('test around hook: union model1', async () => { 7 | function delay(value: number, time: number): Promise { 8 | return new Promise((resolve) => { 9 | setTimeout(() => { 10 | resolve(value); 11 | }, time); 12 | }); 13 | } 14 | 15 | @Injectable() 16 | class TestClass { 17 | async add(a: number, b: number): Promise { 18 | const data = await delay(a + b, 1000); 19 | console.log('TestClass add result', data); 20 | return data; 21 | } 22 | } 23 | 24 | @Aspect() 25 | @Injectable() 26 | class TestAspect { 27 | @Around(TestClass, 'add', { await: true }) 28 | async interceptAdd(joinPoint: IAroundJoinPoint) { 29 | expect(joinPoint.getMethodName()).toBe('add'); 30 | expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); 31 | expect(joinPoint.getThis()).toBeInstanceOf(TestClass); 32 | await joinPoint.proceed(); 33 | const result = await joinPoint.getResult(); 34 | console.log('TestAspect', result); 35 | } 36 | } 37 | 38 | @Aspect() 39 | @Injectable() 40 | class TestAspect2 { 41 | @Around(TestClass, 'add', { await: true }) 42 | async interceptAdd(joinPoint: IAroundJoinPoint) { 43 | const other = await delay(10, 1000); 44 | console.log('TestAspect2 async', other); 45 | expect(joinPoint.getMethodName()).toBe('add'); 46 | expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); 47 | expect(joinPoint.getThis()).toBeInstanceOf(TestClass); 48 | await joinPoint.proceed(); 49 | const result = await joinPoint.getResult(); 50 | console.log('TestAspect2', result); 51 | } 52 | } 53 | 54 | const injector = new Injector(); 55 | injector.addProviders(TestClass); 56 | injector.addProviders(TestAspect); 57 | injector.addProviders(TestAspect2); 58 | 59 | const testClass = injector.get(TestClass); 60 | 61 | const result = await testClass.add(1, 2); 62 | console.log('TestClass invoke result', result); 63 | 64 | expect(result).toBe(3); 65 | }); 66 | 67 | it('test union model: union model2', async () => { 68 | function delay(value: number, time: number): Promise { 69 | return new Promise((resolve) => { 70 | setTimeout(() => { 71 | resolve(value); 72 | }, time); 73 | }); 74 | } 75 | 76 | @Injectable() 77 | class TestClass { 78 | async add(a: number, b: number): Promise { 79 | const data = await delay(a + b, 1000); 80 | console.log('TestClass add result', data); 81 | return data; 82 | } 83 | } 84 | 85 | @Aspect() 86 | @Injectable() 87 | class TestAspect { 88 | @Around(TestClass, 'add', { await: true }) 89 | async interceptAdd(joinPoint: IAroundJoinPoint) { 90 | expect(joinPoint.getMethodName()).toBe('add'); 91 | expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); 92 | expect(joinPoint.getThis()).toBeInstanceOf(TestClass); 93 | joinPoint.proceed(); 94 | const result = await joinPoint.getResult(); 95 | console.log('TestAspect', result); 96 | } 97 | } 98 | 99 | @Aspect() 100 | @Injectable() 101 | class TestAspect2 { 102 | @Around(TestClass, 'add', { await: true }) 103 | async interceptAdd(joinPoint: IAroundJoinPoint) { 104 | const other = await delay(10, 1000); 105 | console.log('TestAspect2 async', other); 106 | expect(joinPoint.getMethodName()).toBe('add'); 107 | expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); 108 | expect(joinPoint.getThis()).toBeInstanceOf(TestClass); 109 | joinPoint.proceed(); 110 | const result = await joinPoint.getResult(); 111 | console.log('TestAspect2', result); 112 | } 113 | } 114 | 115 | const injector = new Injector(); 116 | injector.addProviders(TestClass); 117 | injector.addProviders(TestAspect); 118 | injector.addProviders(TestAspect2); 119 | 120 | const testClass = injector.get(TestClass); 121 | 122 | const result = await testClass.add(1, 2); 123 | console.log('TestClass invoke result', result); 124 | 125 | expect(result).toBe(3); 126 | }); 127 | 128 | it('aspect针对token注入的内容,不会多次实例化', () => { 129 | const spy = jest.fn(); 130 | 131 | @Injectable() 132 | class B { 133 | async do() { 134 | console.log('do'); 135 | } 136 | } 137 | @Aspect() 138 | @Injectable() 139 | class A { 140 | constructor() { 141 | spy(); 142 | } 143 | 144 | @Around(B, 'do', { await: true }) 145 | async aroundDo() { 146 | console.log('aroundDo'); 147 | } 148 | } 149 | const token = 'token'; 150 | 151 | const injector = new Injector(); 152 | injector.addProviders({ 153 | token, 154 | useClass: A, 155 | }); 156 | injector.addProviders(B); 157 | injector.get(token); 158 | injector.get(B); 159 | expect(spy).toHaveBeenCalledTimes(1); 160 | 161 | const b = injector.get(B); 162 | b.do(); 163 | expect(spy).toHaveBeenCalledTimes(1); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /tests/use-case.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { asSingleton, Autowired, Inject, Injectable, Injector } from '../src'; 3 | import * as Error from '../src/error'; 4 | 5 | describe('use cases', () => { 6 | it('使用 Autowired 动态注入依赖', () => { 7 | const spy = jest.fn(); 8 | @Injectable() 9 | class A { 10 | constructor() { 11 | spy(); 12 | } 13 | } 14 | 15 | @Injectable() 16 | class B { 17 | @Autowired() 18 | a!: A; 19 | } 20 | 21 | const injector = new Injector(); 22 | injector.parseDependencies(B); 23 | 24 | const b = injector.get(B); 25 | // B 被实例化出来之后,A 还没有被实例化出来 26 | expect(b).toBeInstanceOf(B); 27 | expect(spy).toBeCalledTimes(0); 28 | // A 被访问的时候才被实例化出来 29 | expect(b.a).toBeInstanceOf(A); 30 | expect(spy).toBeCalledTimes(1); 31 | }); 32 | 33 | it('使用 Inject 定义构造依赖', () => { 34 | const spy = jest.fn(); 35 | 36 | @Injectable() 37 | class A { 38 | constructor() { 39 | spy(); 40 | } 41 | } 42 | 43 | @Injectable() 44 | class B { 45 | constructor(public a: A) {} 46 | } 47 | 48 | const injector = new Injector(); 49 | injector.parseDependencies(B); 50 | 51 | const b = injector.get(B); 52 | expect(spy).toBeCalledTimes(1); 53 | expect(b).toBeInstanceOf(B); 54 | expect(b.a).toBeInstanceOf(A); 55 | }); 56 | 57 | it('单例与多例模式', () => { 58 | @Injectable() 59 | class Single {} 60 | 61 | @Injectable({ multiple: true }) 62 | class Multiple {} 63 | 64 | const injector = new Injector([Single, Multiple]); 65 | const single1 = injector.get(Single); 66 | const single2 = injector.get(Single); 67 | expect(single1).toBe(single2); 68 | 69 | const multiple1 = injector.get(Multiple); 70 | const multiple2 = injector.get(Multiple); 71 | expect(multiple1).not.toBe(multiple2); 72 | }); 73 | 74 | it('使用 Token 进行依赖注入', () => { 75 | const AToken = Symbol('A'); 76 | 77 | interface A { 78 | log(): void; 79 | } 80 | 81 | @Injectable() 82 | class AImpl implements A { 83 | log() { 84 | // nothing 85 | } 86 | } 87 | 88 | @Injectable() 89 | class B { 90 | constructor(@Inject(AToken) public a: A) {} 91 | } 92 | 93 | @Injectable() 94 | class C { 95 | @Autowired(AToken) 96 | a!: A; 97 | } 98 | 99 | const injector = new Injector([ 100 | B, 101 | C, 102 | { 103 | token: AToken, 104 | useClass: AImpl, 105 | }, 106 | ]); 107 | 108 | const b = injector.get(B); 109 | expect(b).toBeInstanceOf(B); 110 | expect(b.a).toBeInstanceOf(AImpl); 111 | 112 | const c = injector.get(C); 113 | expect(c).toBeInstanceOf(C); 114 | expect(c.a).toBeInstanceOf(AImpl); 115 | expect(b.a).toBe(c.a); 116 | }); 117 | 118 | it('使用 TypeProvider 创建实例', () => { 119 | @Injectable() 120 | class A {} 121 | 122 | const injector = new Injector([A]); 123 | const a = injector.get(A); 124 | expect(a).toBeInstanceOf(A); 125 | }); 126 | 127 | it('使用 ClassProvider 创建实例', () => { 128 | const token = 'Token'; 129 | 130 | @Injectable() 131 | class A {} 132 | 133 | const provider = { 134 | token, 135 | useClass: A, 136 | }; 137 | 138 | const injector = new Injector([provider]); 139 | const a = injector.get(token); 140 | expect(a).toBeInstanceOf(A); 141 | }); 142 | 143 | it('使用 ValueProvider 创建实例', () => { 144 | const token = 'Token'; 145 | 146 | @Injectable() 147 | class A {} 148 | 149 | const provider = { 150 | token, 151 | useValue: new A(), 152 | }; 153 | 154 | const injector = new Injector([provider]); 155 | const a = injector.get(token); 156 | expect(a).toBe(provider.useValue); 157 | }); 158 | 159 | it('使用 FactoryProvider 创建实例', () => { 160 | const token = 'Token'; 161 | 162 | @Injectable() 163 | class A {} 164 | 165 | const provider = { 166 | token, 167 | useFactory: () => new A(), 168 | }; 169 | 170 | const injector = new Injector([provider]); 171 | const a = injector.get(token); 172 | expect(a).toBeInstanceOf(A); 173 | }); 174 | 175 | it('FactoryProvider 是创建的多例', () => { 176 | const token = 'Token'; 177 | 178 | @Injectable() 179 | class A {} 180 | 181 | const provider = { 182 | token, 183 | useFactory: () => new A(), 184 | }; 185 | 186 | const injector = new Injector([provider]); 187 | const a = injector.get(token); 188 | const b = injector.get(token); 189 | expect(a).not.toBe(b); 190 | }); 191 | 192 | it('提供了 asSingleton 来实现工厂模式的单例', () => { 193 | const token = 'Token'; 194 | 195 | @Injectable() 196 | class A {} 197 | 198 | const provider = { 199 | token, 200 | useFactory: asSingleton(() => new A()), 201 | }; 202 | 203 | const injector = new Injector([provider]); 204 | const a = injector.get(token); 205 | const b = injector.get(token); 206 | expect(a).toBe(b); 207 | }); 208 | 209 | it('使用抽象函数作为 Token', () => { 210 | abstract class Logger { 211 | abstract log(msg: string): void; 212 | } 213 | 214 | @Injectable() 215 | class LoggerImpl implements Logger { 216 | log(msg: string) { 217 | console.log(msg); 218 | } 219 | } 220 | 221 | @Injectable() 222 | class App { 223 | @Autowired() 224 | logger!: Logger; 225 | } 226 | 227 | const injector = new Injector(); 228 | expect(() => { 229 | injector.get(Logger); 230 | }).toThrow(Error.noProviderError(Logger)); 231 | 232 | injector.addProviders({ 233 | token: Logger, 234 | useClass: LoggerImpl, 235 | }); 236 | const app = injector.get(App); 237 | expect(app.logger).toBeInstanceOf(LoggerImpl); 238 | }); 239 | 240 | it('能够动态传递 Arguments', () => { 241 | @Injectable() 242 | class A {} 243 | 244 | class T { 245 | @Autowired() 246 | a!: A; 247 | 248 | constructor(public d: symbol) {} 249 | } 250 | 251 | const injector = new Injector([A]); 252 | const dynamic = Symbol('dynamic'); 253 | const t = injector.get(T, [dynamic]); 254 | expect(t.a).toBeInstanceOf(A); 255 | expect(t.d).toBe(dynamic); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /src/decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Token, 3 | InstanceOpts, 4 | MethodName, 5 | HookType, 6 | IBeforeAspectHookFunction, 7 | IAfterAspectHookFunction, 8 | IAroundAspectHookFunction, 9 | IHookOptions, 10 | IAfterReturningAspectHookFunction, 11 | IAfterThrowingAspectHookFunction, 12 | IAroundHookOptions, 13 | } from './types'; 14 | import { 15 | addDeps, 16 | getInjectorOfInstance, 17 | getParameterDeps, 18 | isToken, 19 | markAsAspect, 20 | markAsHook, 21 | markInjectable, 22 | setParameterIn, 23 | setParameters, 24 | } from './helper'; 25 | import { noInjectorError, notInjectError, tokenInvalidError } from './error'; 26 | 27 | /** 28 | * Decorate a Class to mark it as injectable 29 | * @param opts 30 | */ 31 | export function Injectable(opts?: InstanceOpts): ClassDecorator { 32 | return (target: T) => { 33 | markInjectable(target, opts); 34 | 35 | const params = Reflect.getMetadata('design:paramtypes', target); 36 | if (Array.isArray(params)) { 37 | setParameters(target, params); 38 | 39 | // If it supports multiple instances, do not check the injectability of the constructor dependencies 40 | if (opts && opts.multiple) { 41 | return; 42 | } 43 | 44 | // Check the injectability of the constructor dependencies 45 | const depTokens = getParameterDeps(target); 46 | depTokens.forEach((item, index) => { 47 | if (!isToken(item)) { 48 | throw notInjectError(target, index); 49 | } 50 | }); 51 | } 52 | }; 53 | } 54 | 55 | interface InjectOpts { 56 | /** 57 | * Default value when the token is not found 58 | */ 59 | default?: any; 60 | } 61 | 62 | /** 63 | * Associate the constructor parameters with a specific injection token 64 | * 65 | * @param token 66 | */ 67 | export function Inject(token: Token, opts: InjectOpts = {}): ParameterDecorator { 68 | return (target, _: string | symbol | undefined, index: number) => { 69 | setParameterIn(target, { ...opts, token }, index); 70 | }; 71 | } 72 | 73 | /** 74 | * Decorator for optional dependencies in the constructor 75 | * @param token 76 | */ 77 | export function Optional(token: Token = Symbol()): ParameterDecorator { 78 | return (target, _: string | symbol | undefined, index: number) => { 79 | setParameterIn(target, { default: undefined, token }, index); 80 | }; 81 | } 82 | 83 | /** 84 | * Decorate a class attribute, and only start using the injector to create an instance when this attribute is accessed 85 | * @param token 86 | */ 87 | export function Autowired(token?: Token, opts?: InstanceOpts): PropertyDecorator { 88 | return (target: object, propertyKey: string | symbol) => { 89 | const INSTANCE_KEY = Symbol('INSTANCE_KEY'); 90 | 91 | let realToken = token as Token; 92 | if (realToken === undefined) { 93 | realToken = Reflect.getMetadata('design:type', target, propertyKey); 94 | } 95 | 96 | if (!isToken(realToken)) { 97 | throw tokenInvalidError(target, propertyKey, realToken); 98 | } 99 | 100 | // Add the dependency of the constructor 101 | addDeps(target, realToken); 102 | 103 | const descriptor: PropertyDescriptor = { 104 | configurable: true, 105 | enumerable: true, 106 | get(this: any) { 107 | if (!this[INSTANCE_KEY]) { 108 | const injector = getInjectorOfInstance(this); 109 | 110 | if (!injector) { 111 | throw noInjectorError(this); 112 | } 113 | 114 | this[INSTANCE_KEY] = injector.get(realToken, opts); 115 | injector.onceInstanceDisposed(this[INSTANCE_KEY], () => { 116 | this[INSTANCE_KEY] = undefined; 117 | }); 118 | } 119 | 120 | return this[INSTANCE_KEY]; 121 | }, 122 | }; 123 | 124 | // return a descriptor and compiler(tsc/babel/...) will automatically perform define. 125 | return descriptor; 126 | }; 127 | } 128 | 129 | // hooks start 130 | 131 | /** 132 | * mark a class as an aspect 133 | */ 134 | export function Aspect() { 135 | return (target: any) => { 136 | markAsAspect(target); 137 | }; 138 | } 139 | 140 | /** 141 | * Hook before method execution 142 | * 143 | * The order of hooks follows the onion model 144 | * @param token 145 | * @param method 146 | */ 147 | export function Before( 148 | token: Token, 149 | method: MethodName, 150 | options: IHookOptions = {}, 151 | ) { 152 | return >, K extends MethodName>( 153 | target: T, 154 | property: K, 155 | ) => { 156 | markAsHook(target, property, HookType.Before, token, method, options); 157 | }; 158 | } 159 | 160 | /** 161 | * Hook after the method ends 162 | * 163 | * The order of hooks follows the onion model 164 | * @param token 165 | * @param method 166 | */ 167 | export function After( 168 | token: Token, 169 | method: MethodName, 170 | options: IHookOptions = {}, 171 | ) { 172 | return >, K extends MethodName>( 173 | target: T, 174 | property: K, 175 | ) => { 176 | markAsHook(target, property, HookType.After, token, method, options); 177 | }; 178 | } 179 | 180 | /** 181 | * around hook, this method performs consistently with the onion model 182 | * @param token 183 | * @param method 184 | * @description 185 | */ 186 | export function Around( 187 | token: Token, 188 | method: MethodName, 189 | options: IAroundHookOptions = {}, 190 | ) { 191 | return >, K extends MethodName>( 192 | target: T, 193 | property: K, 194 | ) => { 195 | markAsHook(target, property, HookType.Around, token, method, options); 196 | }; 197 | } 198 | 199 | /** 200 | * Hook after the method ends (and after callback to the outer layer). 201 | * the hook will be executed even if the method throws an error 202 | * @param token 203 | * @param method 204 | * @param options 205 | */ 206 | export function AfterReturning( 207 | token: Token, 208 | method: MethodName, 209 | options: IHookOptions = {}, 210 | ) { 211 | return >, K extends MethodName>( 212 | target: T, 213 | property: K, 214 | ) => { 215 | markAsHook(target, property, HookType.AfterReturning, token, method, options); 216 | }; 217 | } 218 | 219 | /** 220 | * Hook after the method throws an exception (or PromiseRejection) 221 | * 222 | * the hook will be executed even if the method throws an error 223 | * @param token 224 | * @param method 225 | * @param options 226 | */ 227 | export function AfterThrowing( 228 | token: Token, 229 | method: MethodName, 230 | options: IHookOptions = {}, 231 | ) { 232 | return >, K extends MethodName>( 233 | target: T, 234 | property: K, 235 | ) => { 236 | markAsHook(target, property, HookType.AfterThrowing, token, method, options); 237 | }; 238 | } 239 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. 4 | 5 | ## [2.1.0](https://github.com/opensumi/di/compare/v1.10.1...v2.1.0) (2024-01-22) 6 | 7 | 8 | ### Features 9 | 10 | * add event emitter ([6bb85ec](https://github.com/opensumi/di/commit/6bb85ec3d08f0abb6ecb63b137a0ecd3f02e1b61)) 11 | * hooks support priority option ([5f13619](https://github.com/opensumi/di/commit/5f13619eb644fa04ad486c511bcf8a18229dee5c)) 12 | * make dispose support disposing instance of useFactory ([#116](https://github.com/opensumi/di/issues/116)) ([4a9e4a3](https://github.com/opensumi/di/commit/4a9e4a345dfd1ece3f67ecca315fb0acba62025c)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * add missing dist artifacts ([bd6bbc9](https://github.com/opensumi/di/commit/bd6bbc9ec810070d4a52e298416a694732d5d797)) 18 | * factory instances should save proxied ([dad48a8](https://github.com/opensumi/di/commit/dad48a81f47033e090202667cd4b3fccb9fa8e14)) 19 | * factory support multiple value ([edaa25e](https://github.com/opensumi/di/commit/edaa25e85602374642e55948415e3a656035e06a)) 20 | * instance should be disposed ([5f09c6a](https://github.com/opensumi/di/commit/5f09c6a17dd79175c14a6a688b06892572699024)) 21 | * typo in README-zh_CN ([#120](https://github.com/opensumi/di/issues/120)) ([f41d956](https://github.com/opensumi/di/commit/f41d956b7e08bc613fabbb8ddbc537fd284bef56)) 22 | * we should listen on instance disposed ([362196f](https://github.com/opensumi/di/commit/362196f50bb4f8a476b0d8c108dfbdb1455e1d9a)) 23 | 24 | ## [2.0.0](https://github.com/opensumi/di/compare/v1.10.1...v2.0.0) (2024-01-19) 25 | 26 | ### Breaking Changes 27 | 28 | * We remove the direct import of reflect-metadata, you need import it by your self. 29 | * Remove `__id` and `__injectorId` from the instantiated instance. 30 | 31 | ### Features 32 | 33 | * hooks support priority option ([5f13619](https://github.com/opensumi/di/commit/5f13619eb644fa04ad486c511bcf8a18229dee5c)) 34 | * make dispose support disposing instance of useFactory ([#116](https://github.com/opensumi/di/issues/116)) ([4a9e4a3](https://github.com/opensumi/di/commit/4a9e4a345dfd1ece3f67ecca315fb0acba62025c)) 35 | * `hasInstance` support to check `useFactory`. 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * factory instances should save proxied ([dad48a8](https://github.com/opensumi/di/commit/dad48a81f47033e090202667cd4b3fccb9fa8e14)) 41 | * instance should be disposed ([5f09c6a](https://github.com/opensumi/di/commit/5f09c6a17dd79175c14a6a688b06892572699024)) 42 | * we should listen on instance disposed ([362196f](https://github.com/opensumi/di/commit/362196f50bb4f8a476b0d8c108dfbdb1455e1d9a)) 43 | 44 | ## [1.10.1](https://github.com/opensumi/di/compare/v1.10.0...v1.10.1) (2023-12-12) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * hooked class will be instantiate multiple times ([#119](https://github.com/opensumi/di/issues/119)) ([924bef9](https://github.com/opensumi/di/commit/924bef9e29e077fdbb6a4362d29398429de1b24e)) 50 | 51 | ## [1.10.0](https://github.com/opensumi/di/compare/v1.9.0...v1.10.0) (2023-11-09) 52 | 53 | 54 | ### Features 55 | 56 | * support dispose hooks ([#113](https://github.com/opensumi/di/issues/113)) ([a1e587c](https://github.com/opensumi/di/commit/a1e587c7bdfe2993d61c58d523a69a16d74ce599)) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * avoid instance id conflict when have multi injector ([#114](https://github.com/opensumi/di/issues/114)) ([ca3da8c](https://github.com/opensumi/di/commit/ca3da8ccd7891f1d1d2d5f4ca217c93bc650673d)) 62 | 63 | ## [1.9.0](https://github.com/opensumi/di/compare/v1.8.1...v1.9.0) (2023-10-13) 64 | 65 | 66 | ### Features 67 | 68 | * injector can get domain from parent ([#108](https://github.com/opensumi/di/issues/108)) ([07d3baf](https://github.com/opensumi/di/commit/07d3baff7f410911af05da1d6f90550b0a49b468)) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * dispose should also delete its all instance cache ([#110](https://github.com/opensumi/di/issues/110)) ([cec693b](https://github.com/opensumi/di/commit/cec693b898e8608bb6ec32207beeddbce31ec460)) 74 | 75 | ## [1.8.1](https://github.com/opensumi/di/compare/v1.8.0...v1.8.1) (2023-09-26) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * union model in promise ([#107](https://github.com/opensumi/di/issues/107)) ([015f1e2](https://github.com/opensumi/di/commit/015f1e2562ecc56851c3eb496c101403e6e91324)) 81 | 82 | ## [1.8.0](https://github.com/opensumi/di/compare/v1.7.0...v1.8.0) (2022-08-10) 83 | 84 | 85 | ### Features 86 | 87 | * support detect useAlias registration cycle ([#46](https://github.com/opensumi/di/issues/46)) ([7b00e61](https://github.com/opensumi/di/commit/7b00e612639459401f9bc8349f63be16f94466a6)) 88 | 89 | ## [1.7.0](https://github.com/opensumi/di/compare/v1.6.0...v1.7.0) (2022-07-17) 90 | 91 | 92 | ### Features 93 | 94 | * support nested useAlias ([7201cc7](https://github.com/opensumi/di/commit/7201cc7bdb18606cbb2a02ad17d5df7099e88471)) 95 | 96 | ## [1.6.0](https://github.com/opensumi/di/compare/v1.6.0-beta.0...v1.6.0) (2022-07-15) 97 | 98 | ## [1.6.0-beta.0](https://github.com/opensumi/di/compare/v1.5.1...v1.6.0-beta.0) (2022-07-15) 99 | 100 | 101 | ### Features 102 | 103 | * annotate the return type of `getFromDomain` ([#41](https://github.com/opensumi/di/issues/41)) ([8ed2853](https://github.com/opensumi/di/commit/8ed2853c3ab17c78574ee316a1f12841b44a0753)) 104 | * support detecting circular dependencies ([#38](https://github.com/opensumi/di/issues/38)) ([a44ec02](https://github.com/opensumi/di/commit/a44ec02796481680b732075f1de60d4f1bff9a4c)) 105 | 106 | ## [1.5.0](https://github.com/opensumi/di/compare/v1.4.0...v1.5.0) (2022-06-20) 107 | 108 | 109 | ### Features 110 | 111 | * create child by `this.constructor` ([#18](https://github.com/opensumi/di/issues/18)) ([11aab50](https://github.com/opensumi/di/commit/11aab503b6679f0d04e4b288304a1112519afc8a)) 112 | 113 | ## [1.4.0](https://github.com/opensumi/di/compare/v1.4.0-beta.2...v1.4.0) (2022-06-09) 114 | 115 | ## [1.4.0-beta.2](https://github.com/opensumi/di/compare/v1.4.0-beta.1...v1.4.0-beta.2) (2022-05-30) 116 | 117 | ## [1.4.0-beta.1](https://github.com/opensumi/di/compare/v1.4.0-beta.0...v1.4.0-beta.1) (2022-05-30) 118 | 119 | 120 | ### Features 121 | 122 | * add useAlias ([#17](https://github.com/opensumi/di/issues/17)) ([048c414](https://github.com/opensumi/di/commit/048c4143477e3a4cff92eb971991841dbaec7114)) 123 | 124 | ## [1.4.0-beta.0](https://github.com/opensumi/di/compare/v1.3.1...v1.4.0-beta.0) (2022-05-24) 125 | 126 | 127 | ### Features 128 | 129 | * add `asSingleton` factory helper ([#16](https://github.com/opensumi/di/issues/16)) ([40db872](https://github.com/opensumi/di/commit/40db87256f4ddab3f3f62ba35c5d383b6a63b56b)) 130 | * support asynchronous dispose ([#15](https://github.com/opensumi/di/issues/15)) ([e26b577](https://github.com/opensumi/di/commit/e26b577f75ccf32ba5a98db9e082da51409b45f5)) 131 | 132 | ### [1.3.1](https://github.com/opensumi/di/compare/v1.3.0...v1.3.1) (2022-05-06) 133 | 134 | ## 1.3.0 (2022-05-05) 135 | 136 | ### Features 137 | 138 | * support create multiple case by token ([#13](https://github.com/opensumi/di/issues/13)) ([b0217db](https://github.com/opensumi/di/commit/b0217db25ada21299a995755194e9206c00eb59c)) 139 | 140 | ## 1.1.0 (2021-12-14) 141 | 142 | ### Features 143 | 144 | * remove archived api ([239c527](https://github.com/opensumi/di/commit/239c527)) 145 | -------------------------------------------------------------------------------- /tests/helper/compose.test.ts: -------------------------------------------------------------------------------- 1 | import compose, { Middleware } from '../../src/compose'; 2 | 3 | interface ExampleContext { 4 | getName(): string; 5 | getResult(): any; 6 | } 7 | 8 | describe('di compose', () => { 9 | it('should work', async () => { 10 | const arr = [] as number[]; 11 | 12 | const mockFn = jest.fn(); 13 | 14 | const middleware1: Middleware = async (ctx) => { 15 | const name = ctx.getName(); 16 | console.log(`middleware1: ${name}`); 17 | arr.push(1); 18 | await ctx.proceed(); 19 | const result = ctx.getResult(); 20 | console.log(`middleware1 result: ${result}`); 21 | console.log(`middleware1 after: ${name}`); 22 | arr.push(6); 23 | }; 24 | 25 | const middleware2: Middleware = async (ctx) => { 26 | const name = ctx.getName(); 27 | console.log(`middleware2: ${name}`); 28 | arr.push(2); 29 | await ctx.proceed(); 30 | const result = ctx.getResult(); 31 | expect(result).toBe('final result'); 32 | console.log(`middleware2 result: ${result}`); 33 | console.log(`middleware2 after: ${name}`); 34 | arr.push(5); 35 | }; 36 | 37 | const middleware3: Middleware = async (ctx) => { 38 | const name = ctx.getName(); 39 | console.log(`middleware3: ${name}`); 40 | arr.push(3); 41 | await ctx.proceed(); 42 | const result = ctx.getResult(); 43 | expect(result).toBe('final result'); 44 | console.log(`middleware3 result: ${result}`); 45 | console.log(`middleware3 after: ${name}`); 46 | arr.push(4); 47 | }; 48 | 49 | const all = compose([middleware1, middleware2, middleware3]); 50 | let ret = undefined as any; 51 | const result = all({ 52 | getName() { 53 | return 'example'; 54 | }, 55 | async proceed() { 56 | mockFn(); 57 | ret = 'final result'; 58 | }, 59 | getResult(): any { 60 | return ret; 61 | }, 62 | }); 63 | expect(result).toBeInstanceOf(Promise); 64 | await result; 65 | expect(mockFn).toBeCalledTimes(1); 66 | expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6])); 67 | }); 68 | 69 | it('can worked with sync', () => { 70 | const middleware1: Middleware = (ctx) => { 71 | const name = ctx.getName(); 72 | console.log(`middleware1: ${name}`); 73 | ctx.proceed(); 74 | const result = ctx.getResult(); 75 | console.log(`middleware1 result: ${result}`); 76 | console.log(`middleware1 after: ${name}`); 77 | }; 78 | 79 | const middleware2: Middleware = (ctx) => { 80 | const name = ctx.getName(); 81 | console.log(`middleware2: ${name}`); 82 | ctx.proceed(); 83 | const result = ctx.getResult(); 84 | console.log(`middleware2 result: ${result}`); 85 | console.log(`middleware2 after: ${name}`); 86 | }; 87 | 88 | const all = compose([middleware1, middleware2]); 89 | let ret = undefined as any; 90 | const result = all({ 91 | getName() { 92 | return 'example'; 93 | }, 94 | proceed() { 95 | console.log('invoked'); 96 | ret = 'final result'; 97 | }, 98 | getResult(): any { 99 | return ret; 100 | }, 101 | }); 102 | expect(result).toBeFalsy(); 103 | }); 104 | 105 | it('should be able to be called multiple times', () => { 106 | interface WrappedContext { 107 | arr: number[]; 108 | push: (num: number) => void; 109 | } 110 | 111 | const middleware1: Middleware = (ctx) => { 112 | ctx.push(1); 113 | ctx.proceed(); 114 | ctx.push(4); 115 | }; 116 | 117 | const middleware2: Middleware = (ctx) => { 118 | ctx.push(2); 119 | ctx.proceed(); 120 | ctx.push(3); 121 | }; 122 | 123 | const all = compose([middleware1, middleware2]); 124 | const context1 = { 125 | arr: [], 126 | proceed() { 127 | console.log('invoked'); 128 | }, 129 | push(num) { 130 | this.arr.push(num); 131 | }, 132 | } as WrappedContext; 133 | const context2 = { 134 | arr: [], 135 | proceed() { 136 | console.log('invoked'); 137 | }, 138 | push(num) { 139 | this.arr.push(num); 140 | }, 141 | } as WrappedContext; 142 | all(context1); 143 | all(context2); 144 | 145 | expect(context1.arr).toEqual(context2.arr); 146 | }); 147 | 148 | it('will throw error when call proceed twice', async () => { 149 | interface ExampleContext { 150 | getName(): string; 151 | getResult(): any; 152 | } 153 | const middleware1: Middleware = (ctx) => { 154 | const name = ctx.getName(); 155 | console.log(`middleware1: ${name}`); 156 | ctx.proceed(); 157 | const result = ctx.getResult(); 158 | console.log(`middleware1 result: ${result}`); 159 | console.log(`middleware1 after: ${name}`); 160 | }; 161 | 162 | const middleware2: Middleware = (ctx) => { 163 | const name = ctx.getName(); 164 | console.log(`middleware2: ${name}`); 165 | ctx.proceed(); 166 | ctx.proceed(); 167 | const result = ctx.getResult(); 168 | console.log(`middleware2 result: ${result}`); 169 | console.log(`middleware2 after: ${name}`); 170 | }; 171 | 172 | const all = compose([middleware1, middleware2]); 173 | let ret = undefined as any; 174 | expect(() => { 175 | all({ 176 | getName() { 177 | return 'example'; 178 | }, 179 | proceed() { 180 | console.log('invoked'); 181 | ret = 'final result'; 182 | }, 183 | getResult(): any { 184 | return ret; 185 | }, 186 | }); 187 | }).toThrowError('ctx.proceed() called multiple times'); 188 | }); 189 | 190 | it('should reject on errors in middleware', async () => { 191 | const arr = [] as number[]; 192 | 193 | const mockFn = jest.fn(); 194 | 195 | const middleware1: Middleware = async (ctx) => { 196 | const name = ctx.getName(); 197 | console.log(`middleware1: ${name}`); 198 | arr.push(1); 199 | await ctx.proceed(); 200 | const result = ctx.getResult(); 201 | console.log(`middleware1 result: ${result}`); 202 | console.log(`middleware1 after: ${name}`); 203 | arr.push(6); 204 | }; 205 | 206 | const middleware2: Middleware = async (ctx) => { 207 | const name = ctx.getName(); 208 | console.log(`middleware2: ${name}`); 209 | arr.push(2); 210 | await ctx.proceed(); 211 | const result = ctx.getResult(); 212 | expect(result).toBe('final result'); 213 | console.log(`middleware2 result: ${result}`); 214 | console.log(`middleware2 after: ${name}`); 215 | arr.push(5); 216 | }; 217 | 218 | const middleware3: Middleware = async (ctx) => { 219 | throw new Error('error in middleware3'); 220 | }; 221 | 222 | const all = compose([middleware1, middleware2, middleware3]); 223 | let ret = undefined as any; 224 | const result = all({ 225 | getName() { 226 | return 'example'; 227 | }, 228 | async proceed() { 229 | mockFn(); 230 | ret = 'final result'; 231 | }, 232 | getResult(): any { 233 | return ret; 234 | }, 235 | }); 236 | expect(arr).toEqual(expect.arrayContaining([1, 2])); 237 | expect(result).rejects.toThrowError('error in middleware3'); 238 | }); 239 | }); 240 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Injector } from './injector'; 2 | 3 | export type ConstructorOf = new (...args: any[]) => T; 4 | export type TokenResult = T extends ConstructorOf ? R : any; 5 | 6 | export type Token = string | symbol | Function; 7 | export type Tag = string | number; 8 | export type Domain = string | symbol; 9 | 10 | // An identifier to get the Injector. 11 | export const INJECTOR_TOKEN: Token = Symbol('INJECTOR_TOKEN'); 12 | 13 | /** 14 | * Represents the state in this round of creating. 15 | */ 16 | export interface Context { 17 | injector: Injector; 18 | token: Token; 19 | creator: T; 20 | /** 21 | * Refers to the state of the last time in the recursive creation process 22 | */ 23 | parent?: Context; 24 | } 25 | 26 | // A Provider that can be directly instantiated. 27 | export type TypeProvider = ConstructorOf; 28 | 29 | interface BasicProvider { 30 | token: Token; 31 | tag?: Tag; 32 | dropdownForTag?: boolean; 33 | isDefault?: boolean; 34 | override?: boolean; 35 | } 36 | 37 | /** 38 | * Provide a `class` that is used to instantiated 39 | */ 40 | export interface ClassProvider extends BasicProvider { 41 | useClass: ConstructorOf; 42 | } 43 | 44 | /** 45 | * Provide a `value` that is used to get 46 | */ 47 | export interface ValueProvider extends BasicProvider { 48 | useValue: any; 49 | } 50 | 51 | export interface AliasProvider extends BasicProvider { 52 | useAlias: Token; 53 | } 54 | 55 | export interface FactoryFunction { 56 | (injector: Injector): T; 57 | } 58 | 59 | export interface FactoryProvider extends BasicProvider { 60 | useFactory: FactoryFunction; 61 | } 62 | 63 | export type Provider = ClassProvider | TypeProvider | ValueProvider | AliasProvider | FactoryProvider; 64 | 65 | export enum CreatorStatus { 66 | init, 67 | creating, 68 | done, 69 | } 70 | 71 | interface BasicCreator { 72 | tag?: Tag; 73 | dropdownForTag?: boolean; 74 | status?: CreatorStatus; 75 | /** 76 | * Store the instantiated objects. 77 | */ 78 | instances?: Set; 79 | /** 80 | * Represent this creator is parsed from `Parameter`. and the params of Inject has set `default` attribution. 81 | */ 82 | isDefault?: boolean; 83 | } 84 | 85 | export interface ValueCreator extends BasicCreator { 86 | status: CreatorStatus.done; 87 | } 88 | 89 | export interface ParameterOpts { 90 | token: Token; 91 | default?: any; 92 | } 93 | 94 | export interface ClassCreator extends BasicCreator { 95 | opts: InstanceOpts; 96 | parameters: ParameterOpts[]; 97 | useClass: ConstructorOf; 98 | } 99 | 100 | export interface FactoryCreator extends BasicCreator { 101 | useFactory: FactoryFunction; 102 | } 103 | 104 | export interface AliasCreator extends BasicCreator { 105 | useAlias: Token; 106 | } 107 | 108 | export type InstanceCreator = ValueCreator | ClassCreator | FactoryCreator | AliasCreator; 109 | 110 | export interface InstanceOpts { 111 | version?: string; 112 | multiple?: boolean; 113 | tag?: Tag; 114 | domain?: Domain | Domain[]; 115 | } 116 | 117 | export interface InjectorOpts { 118 | strict?: boolean; 119 | dropdownForTag?: boolean; 120 | tag?: string; 121 | } 122 | 123 | export interface AddProvidersOpts { 124 | override?: boolean; 125 | deep?: boolean; 126 | } 127 | 128 | export type MethodName = string | number | symbol; 129 | 130 | export enum HookType { 131 | Before = 'Before', 132 | After = 'After', 133 | Around = 'Around', 134 | AfterReturning = 'AfterReturning', 135 | AfterThrowing = 'AfterThrowing', 136 | } 137 | 138 | export type IBeforeAspectHookFunction = ( 139 | joinPoint: IBeforeJoinPoint, 140 | ) => void | Promise; 141 | export type IAfterAspectHookFunction = ( 142 | joinPoint: IAfterJoinPoint, 143 | ) => void | Promise; 144 | export type IAroundAspectHookFunction = ( 145 | joinPoint: IAroundJoinPoint, 146 | ) => void | Promise; 147 | export type IAfterReturningAspectHookFunction = ( 148 | joinPoint: IAfterReturningJoinPoint, 149 | ) => void | Promise; 150 | export type IAfterThrowingAspectHookFunction = ( 151 | joinPoint: IAfterThrowingJoinPoint, 152 | ) => void | Promise; 153 | 154 | export type IAspectHookTypeFunction = 155 | | IBeforeAspectHookFunction 156 | | IAfterAspectHookFunction 157 | | IAroundAspectHookFunction 158 | | IAfterReturningAspectHookFunction 159 | | IAfterThrowingAspectHookFunction; 160 | 161 | export type IInstanceHooks = Map; 162 | export type IHookMap = Map; 163 | 164 | /** 165 | * Describe how to hook a method 166 | */ 167 | export interface IAspectHook { 168 | target: Token; 169 | method: MethodName; 170 | awaitPromise?: boolean; 171 | priority?: number; 172 | type: HookType; 173 | hook: IAspectHookTypeFunction; 174 | } 175 | 176 | export type IValidAspectHook = 177 | | IBeforeAspectHook 178 | | IAfterAspectHook 179 | | IAroundAspectHook 180 | | IAfterReturningAspectHook 181 | | IAfterThrowingAspectHook; 182 | 183 | export interface IBeforeAspectHook 184 | extends IAspectHook { 185 | type: HookType.Before; 186 | hook: IBeforeAspectHookFunction; 187 | } 188 | 189 | export interface IAfterAspectHook 190 | extends IAspectHook { 191 | type: HookType.After; 192 | hook: IAfterAspectHookFunction; 193 | } 194 | 195 | export interface IAfterReturningAspectHook 196 | extends IAspectHook { 197 | type: HookType.AfterReturning; 198 | hook: IAfterReturningAspectHookFunction; 199 | } 200 | 201 | export interface IAroundAspectHook 202 | extends IAspectHook { 203 | type: HookType.Around; 204 | hook: IAroundAspectHookFunction; 205 | } 206 | 207 | export interface IAfterThrowingAspectHook 208 | extends IAspectHook { 209 | type: HookType.AfterThrowing; 210 | hook: IAfterThrowingAspectHookFunction; 211 | } 212 | 213 | export interface IJoinPoint { 214 | getThis(): ThisType; 215 | getMethodName(): MethodName; 216 | getOriginalArgs(): Args; 217 | } 218 | 219 | export interface IBeforeJoinPoint extends IJoinPoint { 220 | getArgs(): Args; 221 | setArgs(args: Args): void; 222 | } 223 | 224 | export interface IAfterJoinPoint extends IJoinPoint { 225 | getArgs(): Args; 226 | getResult(): Result; 227 | setResult(result: Result): void; 228 | } 229 | 230 | export interface IAroundJoinPoint extends IJoinPoint { 231 | getArgs(): Args; 232 | setArgs(args: Args): void; 233 | getResult(): Result; 234 | setResult(result: Result): void; 235 | proceed(): Promise | void; 236 | } 237 | 238 | export interface IAfterReturningJoinPoint extends IJoinPoint { 239 | getArgs(): Args; 240 | getResult(): Result; 241 | } 242 | 243 | export interface IAfterThrowingJoinPoint extends IJoinPoint { 244 | getError(): Error | undefined; 245 | } 246 | 247 | export interface IHookStore { 248 | createHooks(hooks: IValidAspectHook[]): IDisposable; 249 | createOneHook(hook: IValidAspectHook): IDisposable; 250 | removeOneHook(hook: IValidAspectHook): void; 251 | getHooks(token: Token, method: MethodName): IValidAspectHook[]; 252 | hasHooks(token: Token): boolean; 253 | } 254 | 255 | export interface IDisposable { 256 | dispose: () => void; 257 | } 258 | 259 | export interface IHookOptions { 260 | /** 261 | * Whether to wait for the hook (if the return value of the hook is a promise) 262 | */ 263 | await?: boolean; 264 | 265 | /** 266 | * The priority of the hook. 267 | * for `before` hooks, the higher the priority, the earlier the execution. 268 | * for `after` and `around` hooks, the higher the priority, the later the execution. 269 | * @default 0 270 | */ 271 | priority?: number; 272 | } 273 | 274 | export interface IAroundHookOptions extends IHookOptions { 275 | /** 276 | * @deprecated around hooks act as the union model, you can just use `ctx.proceed()`(no await) to invoke the next hook. 277 | */ 278 | await?: boolean; 279 | } 280 | -------------------------------------------------------------------------------- /tests/injector/dispose.test.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector, CreatorStatus, Autowired } from '../../src'; 2 | 3 | @Injectable() 4 | class A {} 5 | 6 | @Injectable() 7 | class B { 8 | @Autowired() 9 | a!: A; 10 | } 11 | 12 | describe('dispose', () => { 13 | let injector: Injector; 14 | 15 | beforeEach(() => { 16 | injector = new Injector(); 17 | }); 18 | 19 | it('销毁不存在的对象不会出错', () => { 20 | injector.disposeOne('noop'); 21 | }); 22 | 23 | it('销毁没有初始化的 Provider ', () => { 24 | const spy = jest.fn(); 25 | 26 | @Injectable() 27 | class DisposeCls { 28 | dispose = spy; 29 | } 30 | 31 | injector.addProviders(DisposeCls); 32 | injector.disposeOne(DisposeCls); 33 | injector.disposeAll(); 34 | 35 | expect(spy).toBeCalledTimes(0); 36 | }); 37 | 38 | it('成功销毁单个对象', () => { 39 | const a = injector.get(A); 40 | expect(injector.hasInstance(a)).toBeTruthy(); 41 | 42 | injector.disposeOne(A); 43 | expect(injector.hasInstance(a)).toBeFalsy(); 44 | 45 | const creator = injector.creatorMap.get(A); 46 | expect(creator!.status).toBe(CreatorStatus.init); 47 | expect(creator!.instances).toBeUndefined(); 48 | 49 | const a2 = injector.get(A); 50 | expect(a).not.toBe(a2); 51 | }); 52 | 53 | it('成功进行批量对象销毁', () => { 54 | const a = injector.get(A); 55 | const b = injector.get(B); 56 | expect(injector.hasInstance(a)).toBeTruthy(); 57 | expect(injector.hasInstance(b)).toBeTruthy(); 58 | 59 | injector.disposeAll(); 60 | expect(injector.hasInstance(a)).toBeFalsy(); 61 | expect(injector.hasInstance(b)).toBeFalsy(); 62 | 63 | const creatorA = injector.creatorMap.get(A); 64 | expect(creatorA!.status).toBe(CreatorStatus.init); 65 | expect(creatorA!.instances).toBeUndefined(); 66 | 67 | const creatorB = injector.creatorMap.get(B); 68 | expect(creatorB!.status).toBe(CreatorStatus.init); 69 | expect(creatorB!.instances).toBeUndefined(); 70 | 71 | const a2 = injector.get(A); 72 | expect(a).not.toBe(a2); 73 | }); 74 | 75 | it('销毁单个对象的时候成功调用对象的 dispose 函数', () => { 76 | const spy = jest.fn(); 77 | 78 | @Injectable() 79 | class DisposeCls { 80 | dispose = spy; 81 | } 82 | 83 | const instance = injector.get(DisposeCls); 84 | expect(injector.hasInstance(instance)).toBeTruthy(); 85 | expect(instance).toBeInstanceOf(DisposeCls); 86 | 87 | injector.disposeOne(DisposeCls); 88 | expect(injector.hasInstance(instance)).toBeFalsy(); 89 | expect(spy).toBeCalledTimes(1); 90 | 91 | injector.disposeOne(DisposeCls); 92 | expect(spy).toBeCalledTimes(1); 93 | }); 94 | 95 | it('销毁全部的时候成功调用对象的 dispose 函数', () => { 96 | const spy = jest.fn(); 97 | 98 | @Injectable() 99 | class DisposeCls { 100 | dispose = spy; 101 | } 102 | 103 | const instance = injector.get(DisposeCls); 104 | expect(injector.hasInstance(instance)).toBeTruthy(); 105 | expect(instance).toBeInstanceOf(DisposeCls); 106 | 107 | injector.disposeAll(); 108 | expect(injector.hasInstance(instance)).toBeFalsy(); 109 | expect(spy).toBeCalledTimes(1); 110 | 111 | injector.disposeOne(DisposeCls); 112 | expect(spy).toBeCalledTimes(1); 113 | }); 114 | 115 | it("dispose an instance will also dispose it's instance", () => { 116 | const spy = jest.fn(); 117 | 118 | @Injectable() 119 | class A { 120 | constructor() { 121 | spy(); 122 | } 123 | } 124 | 125 | @Injectable() 126 | class B { 127 | @Autowired() 128 | a!: A; 129 | } 130 | 131 | const instance = injector.get(B); 132 | expect(injector.hasInstance(instance)).toBeTruthy(); 133 | expect(instance).toBeInstanceOf(B); 134 | expect(instance.a).toBeInstanceOf(A); 135 | expect(spy).toBeCalledTimes(1); 136 | 137 | injector.disposeOne(A); 138 | const creatorA = injector.creatorMap.get(A)!; 139 | expect(creatorA.status).toBe(CreatorStatus.init); 140 | expect(creatorA.instances).toBeUndefined(); 141 | 142 | expect(instance.a).toBeInstanceOf(A); 143 | expect(spy).toBeCalledTimes(2); 144 | }); 145 | }); 146 | 147 | describe('dispose asynchronous', () => { 148 | let injector: Injector; 149 | 150 | beforeEach(() => { 151 | injector = new Injector(); 152 | }); 153 | 154 | it('销毁不存在的对象不会出错', async () => { 155 | await injector.disposeOne('noop'); 156 | }); 157 | 158 | it('销毁没有初始化的 Provider ', async () => { 159 | const spy = jest.fn(); 160 | 161 | @Injectable() 162 | class DisposeCls { 163 | async dispose() { 164 | spy(); 165 | } 166 | } 167 | 168 | injector.addProviders(DisposeCls); 169 | await injector.disposeOne(DisposeCls); 170 | await injector.disposeAll(); 171 | 172 | expect(spy).toBeCalledTimes(0); 173 | }); 174 | 175 | it('成功销毁单个对象', async () => { 176 | const a = injector.get(A); 177 | expect(injector.hasInstance(a)).toBeTruthy(); 178 | 179 | await injector.disposeOne(A); 180 | expect(injector.hasInstance(a)).toBeFalsy(); 181 | 182 | const creator = injector.creatorMap.get(A); 183 | expect(creator!.status).toBe(CreatorStatus.init); 184 | expect(creator!.instances).toBeUndefined(); 185 | 186 | const a2 = injector.get(A); 187 | expect(a).not.toBe(a2); 188 | }); 189 | 190 | it('成功进行批量对象销毁', async () => { 191 | const a = injector.get(A); 192 | const b = injector.get(B); 193 | expect(injector.hasInstance(a)).toBeTruthy(); 194 | expect(injector.hasInstance(b)).toBeTruthy(); 195 | 196 | await injector.disposeAll(); 197 | expect(injector.hasInstance(a)).toBeFalsy(); 198 | expect(injector.hasInstance(b)).toBeFalsy(); 199 | 200 | const creatorA = injector.creatorMap.get(A); 201 | expect(creatorA!.status).toBe(CreatorStatus.init); 202 | expect(creatorA!.instances).toBeUndefined(); 203 | 204 | const creatorB = injector.creatorMap.get(B); 205 | expect(creatorB!.status).toBe(CreatorStatus.init); 206 | expect(creatorB!.instances).toBeUndefined(); 207 | 208 | const a2 = injector.get(A); 209 | expect(a).not.toBe(a2); 210 | }); 211 | 212 | it('销毁单个对象的时候成功调用对象的 dispose 函数', async () => { 213 | const spy = jest.fn(); 214 | 215 | @Injectable() 216 | class DisposeCls { 217 | dispose = spy; 218 | } 219 | 220 | const instance = injector.get(DisposeCls); 221 | expect(injector.hasInstance(instance)).toBeTruthy(); 222 | expect(instance).toBeInstanceOf(DisposeCls); 223 | 224 | await injector.disposeOne(DisposeCls); 225 | expect(injector.hasInstance(instance)).toBeFalsy(); 226 | expect(spy).toBeCalledTimes(1); 227 | 228 | await injector.disposeOne(DisposeCls); 229 | expect(spy).toBeCalledTimes(1); 230 | }); 231 | 232 | it('销毁全部的时候成功调用对象的 dispose 函数', async () => { 233 | const spy = jest.fn(); 234 | 235 | @Injectable() 236 | class DisposeCls { 237 | async dispose() { 238 | spy(); 239 | } 240 | } 241 | const instance = injector.get(DisposeCls); 242 | expect(injector.hasInstance(instance)).toBeTruthy(); 243 | expect(instance).toBeInstanceOf(DisposeCls); 244 | 245 | await injector.disposeAll(); 246 | expect(injector.hasInstance(instance)).toBeFalsy(); 247 | expect(spy).toBeCalledTimes(1); 248 | 249 | await injector.disposeOne(DisposeCls); 250 | expect(spy).toBeCalledTimes(1); 251 | }); 252 | 253 | it("dispose creator with multiple instance will call instances's dispose method", async () => { 254 | const spy = jest.fn(); 255 | 256 | @Injectable({ multiple: true }) 257 | class DisposeCls { 258 | dispose = async () => { 259 | spy(); 260 | }; 261 | } 262 | 263 | const instance = injector.get(DisposeCls); 264 | expect(injector.hasInstance(instance)).toBeTruthy(); 265 | expect(instance).toBeInstanceOf(DisposeCls); 266 | 267 | const instance2 = injector.get(DisposeCls); 268 | expect(injector.hasInstance(instance2)).toBeTruthy(); 269 | expect(instance2).toBeInstanceOf(DisposeCls); 270 | 271 | await injector.disposeOne(DisposeCls); 272 | expect(injector.hasInstance(instance)).toBeFalsy(); 273 | expect(injector.hasInstance(instance2)).toBeFalsy(); 274 | expect(spy).toBeCalledTimes(2); 275 | 276 | await injector.disposeOne(DisposeCls); 277 | expect(spy).toBeCalledTimes(2); 278 | }); 279 | 280 | it("dispose an instance will also dispose it's instance", async () => { 281 | const spy = jest.fn(); 282 | 283 | @Injectable() 284 | class A { 285 | constructor() { 286 | spy(); 287 | } 288 | async dispose() { 289 | await new Promise((resolve) => setTimeout(resolve, 100)); 290 | } 291 | } 292 | 293 | @Injectable() 294 | class B { 295 | @Autowired() 296 | a!: A; 297 | } 298 | 299 | const instance = injector.get(B); 300 | expect(injector.hasInstance(instance)).toBeTruthy(); 301 | expect(instance).toBeInstanceOf(B); 302 | expect(instance.a).toBeInstanceOf(A); 303 | expect(spy).toBeCalledTimes(1); 304 | 305 | await injector.disposeOne(A); 306 | const creatorA = injector.creatorMap.get(A); 307 | expect(creatorA!.status).toBe(CreatorStatus.init); 308 | expect(creatorA!.instances).toBeUndefined(); 309 | 310 | expect(instance.a).toBeInstanceOf(A); 311 | expect(spy).toBeCalledTimes(2); 312 | }); 313 | 314 | it('dispose should dispose instance of useFactory', () => { 315 | const injector = new Injector(); 316 | let aValue = 1; 317 | const token = Symbol.for('A'); 318 | 319 | injector.addProviders( 320 | ...[ 321 | { 322 | token, 323 | useFactory: () => aValue, 324 | }, 325 | ], 326 | ); 327 | 328 | @Injectable() 329 | class B { 330 | @Autowired(token) 331 | a!: number; 332 | } 333 | 334 | const instance = injector.get(B); 335 | expect(injector.hasInstance(instance)).toBeTruthy(); 336 | expect(instance).toBeInstanceOf(B); 337 | expect(instance.a).toBe(1); 338 | 339 | injector.disposeOne(token); 340 | aValue = 2; 341 | expect(instance.a).toBe(2); 342 | }); 343 | }); 344 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # @opensumi/di 2 | 3 | 这个工具将会帮助你很好的实现依赖反转,而不用关注那些对象实例化的细节。同时,因为对象的实例化在注册器中进行创建,所以工厂模式和单例模式都很容易实现。 4 | 5 | ## Table of Contents 6 | 7 | - [Install](#install) 8 | - [Quick Start](#quick-start) 9 | - [API](#api) 10 | - [Examples](#examples) 11 | - [FAQ](#faq) 12 | - [Related Efforts](#related-efforts) 13 | 14 | ## Install 15 | 16 | ```sh 17 | npm install @opensumi/di --save 18 | yarn add @opensumi/di 19 | ``` 20 | 21 | 将您的 tsconfig.json 修改为包含以下设置: 22 | 23 | ```json 24 | { 25 | "compilerOptions": { 26 | "experimentalDecorators": true, 27 | "emitDecoratorMetadata": true 28 | } 29 | } 30 | ``` 31 | 32 | 为 Reflect API 添加一个 polyfill(下面的例子使用 reflect-metadata)。你可以使用: 33 | 34 | - [reflect-metadata](https://www.npmjs.com/package/reflect-metadata) 35 | - [core-js (core-js/es7/reflect)](https://www.npmjs.com/package/core-js) 36 | - [reflection](https://www.npmjs.com/package/@abraham/reflection) 37 | 38 | Reflect polyfill 的导入应该只添加一次,并且在使用 DI 之前: 39 | 40 | ```typescript 41 | // main.ts 42 | import 'reflect-metadata'; 43 | 44 | // 你的代码... 45 | ``` 46 | 47 | ## Quick Start 48 | 49 | 让我们从一个简单的例子开始: 50 | 51 | ```ts 52 | import { Injector } from '@opensumi/di'; 53 | 54 | // 创建一个 Injector,这是一个 IoC 容器 55 | const injector = new Injector(); 56 | 57 | const TokenA = Symbol('TokenA'); 58 | injector.addProviders({ 59 | token: TokenA, 60 | useValue: 1, 61 | }); 62 | injector.get(TokenA) === 1; // true 63 | ``` 64 | 65 | The `Injector` class is the starting point of all things. We create a `injector`, and we add a provider into it: 66 | 67 | ```ts 68 | injector.addProviders({ 69 | token: TokenA, 70 | useValue: 1, 71 | }); 72 | ``` 73 | 74 | We use a `ValueProvider` here, and its role is to provide a value: 75 | 76 | ```ts 77 | interface ValueProvider { 78 | token: Token; 79 | useValue: any; 80 | } 81 | ``` 82 | 83 | We have the following several kinds of the provider. According to the different Provider kinds, Injector will use different logic to provide the value that you need. 84 | 85 | ```ts 86 | type Provider = ClassProvider | ValueProvider | FactoryProvider | AliasProvider; 87 | ``` 88 | 89 | A token is used to find the real value in the Injector, so token should be a global unique value. 90 | 91 | ```ts 92 | type Token = string | symbol | Function; 93 | ``` 94 | 95 | and now we want get value from the `Injector`, just use `Injector.get`: 96 | 97 | ```ts 98 | injector.get(TokenA) === 1; 99 | ``` 100 | 101 | ### Providers 102 | 103 | 这是目前支持的 Provider 类型: 104 | 105 | #### ClassProvider 106 | 107 | 定义一个 Token 使用某个特定的构造函数的时候会用到的 Provider。 108 | 109 | ```ts 110 | interface ClassProvider { 111 | token: Token; 112 | useClass: ConstructorOf; 113 | } 114 | ``` 115 | 116 | 在依赖反转之后,构造函数都依赖抽象而不依赖实例的时候会非常有效。比如下面的例子: 117 | 118 | ```ts 119 | interface Drivable { 120 | drive(): void; 121 | } 122 | 123 | @Injectable() 124 | class Student { 125 | @Autowired('Drivable') 126 | mBike: Drivable; 127 | 128 | goToSchool() { 129 | console.log('go to school'); 130 | this.mBike.drive(); 131 | } 132 | } 133 | ``` 134 | 135 | 学生对象依赖的是一个可驾驶的交通工具,可以在创建对象的时候提供一个自行车,也可以在创建的时候提供一个汽车: 136 | 137 | ```ts 138 | @Injectable() 139 | class Car implements Drivable { 140 | drive() { 141 | console.log('by car'); 142 | } 143 | } 144 | 145 | injector.addProviders(Student, { 146 | token: 'Drivable', 147 | useClass: Car, 148 | }); 149 | 150 | const student = injector.get(Student); 151 | student.goToSchool(); // print 'go to school by car' 152 | ``` 153 | 154 | #### ValueProvider 155 | 156 | This provider is used to provide a value: 157 | 158 | ```ts 159 | interface ValueProvider { 160 | token: Token; 161 | useValue: any; 162 | } 163 | ``` 164 | 165 | ```ts 166 | const TokenA = Symbol('TokenA'); 167 | injector.addProviders({ 168 | token: TokenA, 169 | useValue: 1, 170 | }); 171 | injector.get(TokenA) === 1; // true 172 | ``` 173 | 174 | #### FactoryProvider 175 | 176 | 提供一个函数进行对象实例创建的 Provider。 177 | 178 | ```ts 179 | interface FactoryFunction { 180 | (injector: Injector): T; 181 | } 182 | interface FactoryProvider { 183 | token: Token; 184 | useFactory: FactoryFunction; 185 | } 186 | ``` 187 | 188 | 同时也提供了一些工厂模式的帮助函数: 189 | 190 | 1. `asSingleton` 191 | 192 | You can implement a singleton factory by using this helper: 193 | 194 | ```ts 195 | const provider = { 196 | token, 197 | useFactory: asSingleton(() => new A()), 198 | }; 199 | ``` 200 | 201 | #### AliasProvider 202 | 203 | Sets a token to the alias of an existing token. 204 | 205 | ```ts 206 | interface AliasProvider { 207 | // New Token 208 | token: Token; 209 | // Existing Token 210 | useAlias: Token; 211 | } 212 | ``` 213 | 214 | and then you can use: 215 | 216 | ```ts 217 | const TokenA = Symbol('TokenA'); 218 | const TokenB = Symbol('TokenB'); 219 | injector.addProviders( 220 | { 221 | token: TokenA, 222 | useValue: 1, 223 | }, 224 | { 225 | token: TokenB, 226 | useAlias: TokenA, 227 | }, 228 | ); 229 | injector.get(TokenA) === 1; // true 230 | injector.get(TokenB) === 1; // true 231 | ``` 232 | 233 | ### 对构造函数进行注入 234 | 235 | 在下面这个例子里,你会发现 `class B` 依赖于 `class A`,并且在构造函数的参数列表中声明了这个依赖关系,所以在 `B` 的实例创建过程中,Injector 会自动创建 `A` 的实例,并且注入到 `B` 的实例中。 236 | 237 | ```ts 238 | @Injectable() 239 | class A { 240 | constructor() { 241 | console.log('Create A'); 242 | } 243 | } 244 | 245 | @Injectable() 246 | class B { 247 | constructor(public a: A) {} 248 | } 249 | 250 | const injector = new Injector(); 251 | injector.addProviders(A, B); 252 | 253 | const b = injector.get(B); // 打印 'Create A' 254 | console.log(b.a instanceof A); // 打印 'true' 255 | ``` 256 | 257 | ### 使用 `@Autowired()` 进行动态注入 258 | 259 | ```ts 260 | @Injectable() 261 | class A { 262 | constructor() { 263 | console.log('Create A'); 264 | } 265 | } 266 | 267 | @Injectable() 268 | class B { 269 | @Autowired() 270 | a: A; 271 | } 272 | 273 | const injector = new Injector(); 274 | injector.addProviders(A, B); 275 | 276 | const b = injector.get(B); 277 | console.log(b.a instanceof A); // 1. 打印 'Create A', 2. 打印 'true' 278 | ``` 279 | 280 | ### 可以创建单例或者多例 281 | 282 | ```ts 283 | @Injectable() 284 | class Singleton { 285 | constructor() {} 286 | } 287 | 288 | @Injectable({ multiple: true }) 289 | class Multiton { 290 | constructor() {} 291 | } 292 | 293 | const injector = new Injector(); 294 | injector.addProviders(Singleton, Multiton); 295 | 296 | const single1 = injector.get(Singleton); 297 | const single2 = injector.get(Singleton); 298 | console.log(single1 === single2); // print 'true' 299 | 300 | const multiple1 = injector.get(Multiton); 301 | const multiple2 = injector.get(Multiton); 302 | console.log(multiple1 === multiple2); // print 'false' 303 | ``` 304 | 305 | ### 类型依赖抽象而不是依赖实现的用法 306 | 307 | ```ts 308 | const LOGGER_TOKEN = Symbol('LOGGER_TOKEN'); 309 | 310 | interface Logger { 311 | log(msg: string): void; 312 | } 313 | 314 | @Injectable() 315 | class App { 316 | @Autowired(LOGGER_TOKEN) 317 | logger: Logger; 318 | } 319 | 320 | @Injectable() 321 | class LoggerImpl implements Logger { 322 | log(msg: string) { 323 | console.log(msg); 324 | } 325 | } 326 | 327 | const injector = new Injector(); 328 | injector.addProviders(App); 329 | injector.addProviders({ 330 | token: LOGGER_TOKEN, 331 | useClass: LoggerImpl, 332 | }); 333 | 334 | const app = injector.get(App); 335 | console.log(app.logger instanceof LoggerImpl); // 打印 'true' 336 | ``` 337 | 338 | ### 使用抽象函数作为 Token 进行依赖注入 339 | 340 | ```ts 341 | abstract class Logger { 342 | abstract log(msg: string): void; 343 | } 344 | 345 | @Injectable() 346 | class LoggerImpl implements Logger { 347 | log(msg: string) { 348 | console.log(msg); 349 | } 350 | } 351 | 352 | @Injectable() 353 | class App { 354 | @Autowired() 355 | logger: Logger; 356 | } 357 | 358 | const injector = new Injector(); 359 | injector.addProviders(App); 360 | injector.addProviders({ 361 | token: Logger, 362 | useClass: LoggerImpl, 363 | }); 364 | 365 | const app = injector.get(App); 366 | console.log(app.logger instanceof LoggerImpl); // print 'true' 367 | ``` 368 | 369 | ## API 370 | 371 | ### decorator: @Injectable 372 | 373 | ```ts 374 | interface InstanceOpts { 375 | multiple?: boolean; 376 | } 377 | function Injectable(opts?: InstanceOpts): ClassDecorator; 378 | 379 | @Injectable({ multiple: true }) 380 | class A {} 381 | 382 | const injector = new Injector([A]); 383 | 384 | const a = injector.get(A); 385 | console.log(injector.hasInstance(a)); // print 'false' 386 | ``` 387 | 388 | 所有需要被 Injector 创建的构造函数都应该使用这个装饰器修饰才可以正常使用,否则会报错。 389 | 390 | - multiple: 是否启用多例模式,一旦启用了多例模式之后,Injector 将不会持有实例对象的引用。 391 | 392 | ### decorator: @Autowired 393 | 394 | ```ts 395 | function Autowired(token?: Token): PropertyDecorator; 396 | 397 | @Injectable() 398 | class A {} 399 | 400 | @Injectable() 401 | class B { 402 | @Autowired() 403 | a: A; 404 | } 405 | ``` 406 | 407 | 修饰一个属性会被注册器动态创建依赖实例,而这个依赖实例只有在被使用的时候才会被创建出来。比如上面的例子中,只有访问到 `b.a` 的时候,才会创建 A 的实例。 408 | 409 | > 需要注意的是,因为 Autowired 依赖着 Injector 的实例,所以只有从 Injector 创建出来的对象可以使用这个装饰器 410 | 411 | ### decorator: @Inject 412 | 413 | ```ts 414 | function Inject(token: string | symbol): ParameterDecorator; 415 | 416 | interface IA { 417 | log(): void; 418 | } 419 | 420 | @Injectable() 421 | class B { 422 | constructor(@Inject('IA') a: IA) {} 423 | } 424 | ``` 425 | 426 | 在构造函数进行依赖注入的时候,需要特别指定依赖 Token 的时候的装饰器。当一个构造函数依赖某个抽象,并且这个抽象是在构造函数中传递进来的时候,会需要使用这个装饰器。 427 | 428 | ### Injector.get 429 | 430 | ```ts 431 | interface Injector { 432 | get(token: ConstructorOf, args?: ConstructorParameters, opts?: InstanceOpts): TokenResult; 433 | get(token: T, opts?: InstanceOpts): TokenResult; 434 | } 435 | ``` 436 | 437 | 从 Injector 获取一个对象实例的方法,如果传递的是一个构造函数,第二个参数可以传递构造函数 Arguments 数据,此时将会直接将构造函数创建实例返回,并附加依赖注入的功能,此时的构造函数不需要被 Injectable 装饰也能正常创建对象。例如下面这样: 438 | 439 | ```ts 440 | @Injectable() 441 | class A {} 442 | 443 | class B { 444 | @Autowired() 445 | a: A; 446 | } 447 | 448 | const injector = new Injector([A]); 449 | const b = injector.get(B, []); 450 | console.log(b.a instanceof A); // print 'true' 451 | ``` 452 | 453 | ### Injector.hasInstance 454 | 455 | Whether have an instantiated object in the Injector. 456 | 457 | ### Injector.disposeOne / Injector.disposeAll 458 | 459 | 可以使用 `Injector.disposeOne` 和 `Injector.disposeAll` 来释放 Token。 460 | 461 | 这两个方法会从 DI 容器中删除当前已创建的实例,并尝试调用这个实例的 `dispose` 方法(可以没有)。 462 | 463 | ### markInjectable 464 | 465 | ```ts 466 | import { markInjectable } from '@opensumi/di'; 467 | import { Editor } from 'path/to/package'; 468 | 469 | markInjectable(Editor); 470 | ``` 471 | 472 | You can use this function to mark some Class as Injectable. 473 | 474 | ## Examples 475 | 476 | See More Examples [in the test case](test/use-case.test.ts). 477 | 478 | ## FAQ 479 | 480 | Please see [FAQ.md](docs/faq.md). 481 | 482 | ## Related Efforts 483 | 484 | - [Angular](https://angular.io/guide/dependency-injection) Dependency injection in Angular 485 | - [injection-js](https://github.com/mgechev/injection-js) It is an extraction of the Angular's ReflectiveInjector. 486 | - [InversifyJS](https://github.com/inversify/InversifyJS) A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript. 487 | - [power-di](https://github.com/zhang740/power-di) A lightweight Dependency Injection library. 488 | -------------------------------------------------------------------------------- /tests/injector.test.ts: -------------------------------------------------------------------------------- 1 | import { Autowired, Injectable, Injector, INJECTOR_TOKEN, Inject, Optional } from '../src'; 2 | import * as InjectorError from '../src/error'; 3 | import { getInjectorOfInstance } from '../src/helper'; 4 | 5 | describe('test injector work', () => { 6 | @Injectable() 7 | class A {} 8 | 9 | @Injectable() 10 | class B { 11 | @Autowired() 12 | a!: A; 13 | } 14 | 15 | @Injectable({ multiple: true }) 16 | class C {} 17 | 18 | const DToken = Symbol('D'); 19 | const EToken = Symbol('E'); 20 | 21 | @Injectable() 22 | class D { 23 | constructor(@Inject(EToken) public e: any) {} 24 | } 25 | 26 | @Injectable() 27 | class E { 28 | constructor(@Inject(DToken) public d: any) {} 29 | } 30 | 31 | it('从静态函数创建 Injector', () => { 32 | const injector = new Injector(); 33 | injector.parseDependencies(B); 34 | 35 | const b = injector.get(B); 36 | expect(b).toBeInstanceOf(B); 37 | expect(b.a).toBeInstanceOf(A); 38 | expect(injector.get(INJECTOR_TOKEN)).toBe(injector); 39 | }); 40 | 41 | it('get INJECTOR_TOKEN in the constructor', () => { 42 | const injector = new Injector(); 43 | 44 | @Injectable() 45 | class E { 46 | @Autowired(INJECTOR_TOKEN) 47 | injector!: Injector; 48 | constructor() { 49 | expect(this.injector).toBe(injector); 50 | } 51 | } 52 | 53 | const e = injector.get(E); 54 | expect(e).toBeInstanceOf(E); 55 | expect(injector.get(INJECTOR_TOKEN)).toBe(injector); 56 | }); 57 | 58 | it('使用多例模式创建对象', () => { 59 | const injector = new Injector([A, B, C]); 60 | const b1 = injector.get(B, { multiple: true }); 61 | const b2 = injector.get(B, { multiple: true }); 62 | expect(b1).toBeInstanceOf(B); 63 | expect(b2).toBeInstanceOf(B); 64 | expect(b1).not.toBe(b2); 65 | expect(b1.a).toBe(b2.a); 66 | 67 | const c1 = injector.get(C); 68 | const c2 = injector.get(C); 69 | expect(c1).not.toBe(c2); 70 | }); 71 | 72 | it('使用多例模式创建对象,构造函数依赖时不会创建多例', () => { 73 | @Injectable() 74 | class T { 75 | constructor(public a: A) {} 76 | } 77 | 78 | const injector = new Injector([T, A]); 79 | 80 | const t1 = injector.get(T, { multiple: true }); 81 | const t2 = injector.get(T, { multiple: true }); 82 | expect(t1).toBeInstanceOf(T); 83 | expect(t2).toBeInstanceOf(T); 84 | expect(t1).not.toBe(t2); 85 | expect(t1.a).toBe(t2.a); 86 | }); 87 | 88 | it('重复添加依赖,不会产生多个 Creator', () => { 89 | const injector = new Injector([B]); 90 | 91 | const b1 = injector.get(B); 92 | expect(b1).toBeInstanceOf(B); 93 | 94 | const b2 = injector.get(B); 95 | expect(b2).toBe(b1); 96 | }); 97 | 98 | it('同一个 Token 有不同的 Provider 的时候使用第一个', () => { 99 | const temp = {}; 100 | const injector = new Injector([B, { token: B, useValue: temp }]); 101 | 102 | const b = injector.get(B); 103 | expect(b).toBeInstanceOf(B); 104 | }); 105 | 106 | it('添加一个没有提供 Provider 的依赖', () => { 107 | const injector = new Injector(); 108 | expect(() => injector.get('noop')).toThrow(InjectorError.noProviderError('noop')); 109 | }); 110 | 111 | it('有循环依赖的对象创建的时候会报错', () => { 112 | const injector = new Injector([ 113 | { token: EToken, useClass: E }, 114 | { token: DToken, useClass: D }, 115 | ]); 116 | 117 | expect(() => injector.get(DToken)).toThrow( 118 | InjectorError.circularError(D, { 119 | token: DToken, 120 | parent: { 121 | token: EToken, 122 | parent: { 123 | token: DToken, 124 | }, 125 | }, 126 | } as any), 127 | ); 128 | }); 129 | 130 | it('没有定义 Injectable 的依赖', () => { 131 | class T {} 132 | expect(() => new Injector([T])).toThrow(InjectorError.noInjectableError(T)); 133 | }); 134 | 135 | describe('addProviders', () => { 136 | let injector: Injector; 137 | 138 | beforeEach(() => { 139 | injector = new Injector(); 140 | }); 141 | 142 | it('使用 addProviders 不会覆盖原有的 Provider', () => { 143 | injector.addProviders(A); 144 | const a1 = injector.get(A); 145 | 146 | injector.addProviders({ token: A, useValue: '' }); 147 | const a2 = injector.get(A); 148 | 149 | expect(a1).toBe(a2); 150 | }); 151 | }); 152 | 153 | describe('创建对象时发生异常', () => { 154 | it('创建对象时发生异常,creator 的状态会回滚', () => { 155 | const injector = new Injector(); 156 | 157 | @Injectable() 158 | class ErrorCls { 159 | constructor() { 160 | throw new Error('test'); 161 | } 162 | } 163 | 164 | expect(() => { 165 | injector.get(ErrorCls); 166 | }).toThrow('test'); 167 | 168 | const creator = injector.creatorMap.get(ErrorCls); 169 | expect(creator && creator.status).toBeUndefined(); 170 | }); 171 | }); 172 | 173 | describe('默认值', () => { 174 | it('带有默认值的对象', () => { 175 | @Injectable() 176 | class T { 177 | constructor(@Inject('a', { default: 'aaa' }) public a: string) {} 178 | } 179 | 180 | const injector = new Injector(); 181 | injector.parseDependencies(T); 182 | const t = injector.get(T); 183 | expect(t.a).toBe('aaa'); 184 | 185 | injector.addProviders({ token: 'a', useValue: 'bbb' }); 186 | const childInjector = injector.createChild([T]); 187 | const t2 = childInjector.get(T); 188 | expect(t2.a).toBe('bbb'); 189 | }); 190 | 191 | it('严格模式下,带有默认值的对象', () => { 192 | @Injectable() 193 | class T { 194 | constructor(@Inject('a', { default: 'aaa' }) public a: string) {} 195 | } 196 | 197 | const injector = new Injector([], { strict: true }); 198 | injector.addProviders(T); 199 | const t = injector.get(T); 200 | expect(t.a).toBe('aaa'); 201 | 202 | injector.addProviders({ token: 'a', useValue: 'bbb' }); 203 | const childInjector = injector.createChild([T]); 204 | const t2 = childInjector.get(T); 205 | expect(t2.a).toBe('bbb'); 206 | }); 207 | 208 | it('带有默认赋值的对象', () => { 209 | @Injectable() 210 | class T { 211 | constructor(@Optional('a') public a: string = 'aaa') {} 212 | } 213 | 214 | const injector = new Injector([T]); 215 | const t = injector.get(T); 216 | expect(t.a).toBe('aaa'); 217 | 218 | injector.addProviders({ token: 'a', useValue: 'bbb' }); 219 | const childInjector = injector.createChild([T]); 220 | const t2 = childInjector.get(T); 221 | expect(t2.a).toBe('bbb'); 222 | }); 223 | 224 | it('Optional 不传递 Token', () => { 225 | @Injectable() 226 | class T { 227 | constructor(@Optional() public a: string = 'aaa') {} 228 | } 229 | 230 | const injector = new Injector([T]); 231 | const t = injector.get(T); 232 | expect(t.a).toBe('aaa'); 233 | }); 234 | }); 235 | 236 | describe('injector 访问', () => { 237 | it('构造函数内能够访问 injector', () => { 238 | const injector = new Injector(); 239 | 240 | const testFn = jest.fn(); 241 | 242 | @Injectable() 243 | class ChildCls {} 244 | 245 | @Injectable() 246 | class ParentCls { 247 | @Autowired() 248 | private child!: ChildCls; 249 | 250 | constructor() { 251 | testFn(this.child); 252 | } 253 | } 254 | 255 | const parent = injector.get(ParentCls); 256 | expect(parent).toBeInstanceOf(ParentCls); 257 | expect(testFn).toBeCalledTimes(1); 258 | expect(testFn.mock.calls[0][0]).toBeInstanceOf(ChildCls); 259 | }); 260 | }); 261 | 262 | describe('parseDependencies', () => { 263 | it('没有提供完整属性依赖,parse 会报错', () => { 264 | const injector = new Injector(); 265 | 266 | expect(() => { 267 | injector.parseDependencies(D); 268 | }).toThrow(InjectorError.noProviderError(EToken)); 269 | }); 270 | 271 | it('没有提供完整的构造函数依赖,parse 会报错', () => { 272 | @Injectable() 273 | class T { 274 | constructor(@Inject('a') public a: string) {} 275 | } 276 | 277 | const injector = new Injector(); 278 | expect(() => { 279 | injector.parseDependencies(T); 280 | }).toThrow(InjectorError.noProviderError('a')); 281 | }); 282 | 283 | it('新的 scope 解析出来的 creator 不会覆盖父级', () => { 284 | const injector = new Injector(); 285 | const childInjector = injector.createChild(); 286 | 287 | injector.parseDependencies(A); 288 | childInjector.parseDependencies(B); 289 | 290 | const a = childInjector.get(A); 291 | const b = childInjector.get(B); 292 | expect(b.a).toBe(a); 293 | expect(childInjector.hasInstance(a)).toBeFalsy(); 294 | }); 295 | }); 296 | 297 | describe('createChild', () => { 298 | it('createChild 得到一个新的 Scope', () => { 299 | const injector = new Injector(); 300 | injector.addProviders(A); 301 | 302 | const injector1 = injector.createChild([C]); 303 | const injector2 = injector.createChild([C]); 304 | 305 | expect(injector1.get(A)).toBe(injector2.get(A)); 306 | expect(injector1.get(C)).not.toBe(injector2.get(C)); 307 | }); 308 | 309 | it('createChild 带有 tag 的 provider 下落', () => { 310 | const injector = new Injector([], { strict: true }); 311 | injector.addProviders({ 312 | dropdownForTag: true, 313 | tag: 'Tag', 314 | token: 'Token', 315 | useClass: A, 316 | }); 317 | 318 | expect(() => { 319 | injector.get('Token', { tag: 'Tag' }); 320 | }).toThrow(InjectorError.tagOnlyError('Tag', 'undefined')); 321 | 322 | const childInjector = injector.createChild([], { 323 | dropdownForTag: true, 324 | tag: 'Tag', 325 | }); 326 | const a = childInjector.get('Token', { tag: 'Tag' }); 327 | expect(a).toBeInstanceOf(A); 328 | }); 329 | 330 | it('createChild 会自动下落 strict 配置', () => { 331 | const injector = new Injector([], { strict: true }); 332 | const childInjector = injector.createChild([], { 333 | dropdownForTag: true, 334 | tag: 'Tag', 335 | }); 336 | 337 | expect(() => { 338 | childInjector.get('Token', { tag: 'Tag' }); 339 | }).toThrow(InjectorError.noProviderError('Token')); 340 | }); 341 | 342 | it('三个 Token 四个对象', () => { 343 | @Injectable() 344 | class ParentClsC {} 345 | 346 | @Injectable() 347 | class ParentClsB { 348 | @Autowired() 349 | c!: ParentClsC; 350 | } 351 | 352 | @Injectable() 353 | class ChildClsA { 354 | @Autowired() 355 | b!: ParentClsB; 356 | 357 | @Autowired() 358 | c!: ParentClsC; 359 | } 360 | 361 | const parent = new Injector(); 362 | parent.addProviders(ParentClsB); 363 | 364 | const child = parent.createChild(); 365 | child.addProviders(ChildClsA); 366 | child.addProviders(ParentClsC); 367 | 368 | const a = child.get(ChildClsA); 369 | const b = a.b; 370 | const c1 = a.c; 371 | const c2 = b.c; 372 | expect(c1).not.toBe(c2); 373 | expect(child.hasInstance(c1)).toBeTruthy(); 374 | expect(child.hasInstance(c2)).toBeFalsy(); 375 | expect(parent.hasInstance(c1)).toBeFalsy(); 376 | expect(parent.hasInstance(c2)).toBeTruthy(); 377 | }); 378 | }); 379 | 380 | describe('strict: false', () => { 381 | let injector: Injector; 382 | 383 | beforeEach(() => { 384 | injector = new Injector(); 385 | }); 386 | 387 | it('普通模式下,可以直接获取一个 Injectable 对象的实例', () => { 388 | @Injectable() 389 | class T {} 390 | 391 | const t = injector.get(T); 392 | expect(t).toBeInstanceOf(T); 393 | }); 394 | }); 395 | 396 | describe('id', () => { 397 | it('创建注册器实例的时候会有 ID 数据', () => { 398 | const injector1 = new Injector(); 399 | const injector2 = new Injector(); 400 | expect(injector1.id.startsWith('Injector')).toBeTruthy(); 401 | expect(injector1.id).not.toBe(injector2.id); 402 | }); 403 | 404 | it('could get id from the instance', () => { 405 | const injector = new Injector(); 406 | const instance1 = injector.get(A); 407 | const instance2 = injector.get(A); 408 | const instance3 = injector.get(B); 409 | 410 | expect(getInjectorOfInstance(instance1)!.id).toBe(injector.id); 411 | expect(getInjectorOfInstance(instance2)!.id).toBe(injector.id); 412 | expect(getInjectorOfInstance(instance3)!.id).toBe(injector.id); 413 | }); 414 | }); 415 | 416 | describe('extends class should also support createChild', () => { 417 | class NewInjector extends Injector { 418 | funcForNew() { 419 | return 1; 420 | } 421 | } 422 | const tokenA = Symbol('A'); 423 | const injector = new NewInjector(); 424 | injector.addProviders({ 425 | token: tokenA, 426 | useValue: 'A', 427 | }); 428 | 429 | expect(injector.get(tokenA)).toBe('A'); 430 | expect(injector.funcForNew()).toBe(1); 431 | const child = injector.createChild([ 432 | { 433 | token: tokenA, 434 | useValue: 'B', 435 | }, 436 | ]); 437 | 438 | expect(child.get(tokenA)).toBe('B'); 439 | expect(child.funcForNew()).toBe(1); 440 | }); 441 | }); 442 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @opensumi/di 2 | 3 | [中文文档](./README-zh_CN.md) 4 | 5 | [![CI](https://github.com/opensumi/di/actions/workflows/ci.yml/badge.svg)](https://github.com/opensumi/di/actions/workflows/ci.yml) [![NPM Version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url] [![Test Coverage][test-image]][test-url] [![License][license-image]][license-url] [![Bundle size for @opensumi/di][pkg-size-img]][pkg-size] 6 | 7 | [npm-image]: https://img.shields.io/npm/v/@opensumi/di.svg 8 | [npm-url]: https://www.npmjs.com/package/@opensumi/di 9 | [download-image]: https://img.shields.io/npm/dm/@opensumi/di.svg 10 | [download-url]: https://npmjs.org/package/@opensumi/di 11 | [license-image]: https://img.shields.io/npm/l/@opensumi/di.svg 12 | [license-url]: https://github.com/opensumi/di/blob/main/LICENSE 13 | [test-image]: https://codecov.io/gh/opensumi/di/branch/main/graph/badge.svg?token=07JAPLU957 14 | [test-url]: https://codecov.io/gh/opensumi/di 15 | [pkg-size]: https://pkg-size.dev/@opensumi/di 16 | [pkg-size-img]: https://pkg-size.dev/badge/bundle/15776 17 | 18 | This tool will help you achieve dependency inversion effectively without concerning the details of object instantiation. Additionally, since object instantiation is done within a registry, both the factory pattern and singleton pattern can be easily implemented. 19 | 20 | ## Table of Contents 21 | 22 | - [Install](#install) 23 | - [Quick Start](#quick-start) 24 | - [API](#api) 25 | - [Examples](#examples) 26 | - [FAQ](#faq) 27 | - [Related Efforts](#related-efforts) 28 | 29 | ## Installation 30 | 31 | ```sh 32 | npm install @opensumi/di --save 33 | yarn add @opensumi/di 34 | ``` 35 | 36 | Modify your tsconfig.json to include the following settings: 37 | 38 | ```json 39 | { 40 | "compilerOptions": { 41 | "experimentalDecorators": true, 42 | "emitDecoratorMetadata": true 43 | } 44 | } 45 | ``` 46 | 47 | Add a polyfill for the Reflect API (examples below use reflect-metadata). You can use: 48 | 49 | - [reflect-metadata](https://www.npmjs.com/package/reflect-metadata) 50 | - [core-js (core-js/es7/reflect)](https://www.npmjs.com/package/core-js) 51 | - [reflection](https://www.npmjs.com/package/@abraham/reflection) 52 | 53 | The Reflect polyfill import should only be added once, and before DI is used: 54 | 55 | ```typescript 56 | // main.ts 57 | import 'reflect-metadata'; 58 | 59 | // Your code here... 60 | ``` 61 | 62 | ## Quick Start 63 | 64 | Let's start with a simple example: 65 | 66 | ```ts 67 | import { Injector } from '@opensumi/di'; 68 | 69 | // we create a new Injector instance 70 | const injector = new Injector(); 71 | 72 | const TokenA = Symbol('TokenA'); 73 | injector.addProviders({ 74 | token: TokenA, 75 | useValue: 1, 76 | }); 77 | injector.get(TokenA) === 1; // true 78 | ``` 79 | 80 | The `Injector` class is the starting point of all things. We create a `injector`, and we add a provider into it: 81 | 82 | ```ts 83 | injector.addProviders({ 84 | token: TokenA, 85 | useValue: 1, 86 | }); 87 | ``` 88 | 89 | We use a `ValueProvider` here, and its role is to provide a value: 90 | 91 | ```ts 92 | interface ValueProvider { 93 | token: Token; 94 | useValue: any; 95 | } 96 | ``` 97 | 98 | We have the following several kinds of the provider. According to the different Provider kinds, Injector will use different logic to provide the value that you need. 99 | 100 | ```ts 101 | type Provider = ClassProvider | ValueProvider | FactoryProvider | AliasProvider; 102 | ``` 103 | 104 | A token is used to find the real value in the Injector, so token should be a global unique value. 105 | 106 | ```ts 107 | type Token = string | symbol | Function; 108 | ``` 109 | 110 | and now we want get value from the `Injector`, just use `Injector.get`: 111 | 112 | ```ts 113 | injector.get(TokenA) === 1; 114 | ``` 115 | 116 | ### Providers 117 | 118 | Here are all the providers we have: 119 | 120 | #### ClassProvider 121 | 122 | Declare a provider that includes a constructor and its token. 123 | 124 | ```ts 125 | interface ClassProvider { 126 | token: Token; 127 | useClass: ConstructorOf; 128 | } 129 | ``` 130 | 131 | After dependency inversion, constructors depending on abstractions instead of instances can be highly effective. For example, consider the following example: 132 | 133 | ```ts 134 | interface Drivable { 135 | drive(): void; 136 | } 137 | 138 | @Injectable() 139 | class Student { 140 | @Autowired('Drivable') 141 | mBike: Drivable; 142 | 143 | goToSchool() { 144 | console.log('go to school'); 145 | this.mBike.drive(); 146 | } 147 | } 148 | ``` 149 | 150 | The student object depends on a drivable mode of transportation, which can be provided either as a bicycle or a car during object creation: 151 | 152 | ```ts 153 | @Injectable() 154 | class Car implements Drivable { 155 | drive() { 156 | console.log('by car'); 157 | } 158 | } 159 | 160 | injector.addProviders(Student, { 161 | token: 'Drivable', 162 | useClass: Car, 163 | }); 164 | 165 | const student = injector.get(Student); 166 | student.goToSchool(); // print 'go to school by car' 167 | ``` 168 | 169 | #### ValueProvider 170 | 171 | This provider is used to provide a value: 172 | 173 | ```ts 174 | interface ValueProvider { 175 | token: Token; 176 | useValue: any; 177 | } 178 | ``` 179 | 180 | ```ts 181 | const TokenA = Symbol('TokenA'); 182 | injector.addProviders({ 183 | token: TokenA, 184 | useValue: 1, 185 | }); 186 | injector.get(TokenA) === 1; // true 187 | ``` 188 | 189 | #### FactoryProvider 190 | 191 | Declare a provider, and later you can use this token to invoke this factory function. 192 | 193 | ```ts 194 | interface FactoryFunction { 195 | (injector: Injector): T; 196 | } 197 | interface FactoryProvider { 198 | token: Token; 199 | useFactory: FactoryFunction; 200 | } 201 | ``` 202 | 203 | It also provides some helper functions for the factory pattern: 204 | 205 | 1. `asSingleton` 206 | 207 | You can implement a singleton factory by using this helper: 208 | 209 | ```ts 210 | const provider = { 211 | token, 212 | useFactory: asSingleton(() => new A()), 213 | }; 214 | ``` 215 | 216 | #### AliasProvider 217 | 218 | Sets a token to the alias of an existing token. 219 | 220 | ```ts 221 | interface AliasProvider { 222 | // New Token 223 | token: Token; 224 | // Existing Token 225 | useAlias: Token; 226 | } 227 | ``` 228 | 229 | and then you can use: 230 | 231 | ```ts 232 | const TokenA = Symbol('TokenA'); 233 | const TokenB = Symbol('TokenB'); 234 | injector.addProviders( 235 | { 236 | token: TokenA, 237 | useValue: 1, 238 | }, 239 | { 240 | token: TokenB, 241 | useAlias: TokenA, 242 | }, 243 | ); 244 | injector.get(TokenA) === 1; // true 245 | injector.get(TokenB) === 1; // true 246 | ``` 247 | 248 | ### Perform constructor injection 249 | 250 | In this example, you can see the `class B` depends on `class A`,And declare this dependency relationship in the parameter list of the constructor.: 251 | 252 | So, during the instantiation process of `class B`, the Injector will automatically create the `A` instance and inject it into the `B` instance. 253 | 254 | ```ts 255 | @Injectable() 256 | class A { 257 | constructor() { 258 | console.log('Create A'); 259 | } 260 | } 261 | 262 | @Injectable() 263 | class B { 264 | constructor(public a: A) {} 265 | } 266 | 267 | const injector = new Injector(); 268 | injector.addProviders(A, B); 269 | 270 | const b = injector.get(B); // print 'Create A' 271 | console.log(b.a instanceof A); // print 'true' 272 | ``` 273 | 274 | ### Use `@Autowired` for dynamic injection 275 | 276 | ```ts 277 | @Injectable() 278 | class A { 279 | constructor() { 280 | console.log('Create A'); 281 | } 282 | } 283 | 284 | @Injectable() 285 | class B { 286 | @Autowired() 287 | a: A; 288 | } 289 | 290 | const injector = new Injector(); 291 | injector.addProviders(A, B); 292 | 293 | const b = injector.get(B); 294 | console.log(b.a instanceof A); // print 'Create A'; print 'true' 295 | ``` 296 | 297 | ### Use Singleton pattern Or Multiton pattern 298 | 299 | ```ts 300 | @Injectable() 301 | class Singleton { 302 | constructor() {} 303 | } 304 | 305 | @Injectable({ multiple: true }) 306 | class Multiton { 307 | constructor() {} 308 | } 309 | 310 | const injector = new Injector(); 311 | injector.addProviders(Singleton, Multiton); 312 | 313 | const single1 = injector.get(Singleton); 314 | const single2 = injector.get(Singleton); 315 | console.log(single1 === single2); // print 'true' 316 | 317 | const multiple1 = injector.get(Multiton); 318 | const multiple2 = injector.get(Multiton); 319 | console.log(multiple1 === multiple2); // print 'false' 320 | ``` 321 | 322 | ### Depend on abstractions rather than implementations 323 | 324 | ```ts 325 | const LOGGER_TOKEN = Symbol('LOGGER_TOKEN'); 326 | 327 | interface Logger { 328 | log(msg: string): void; 329 | } 330 | 331 | @Injectable() 332 | class App { 333 | @Autowired(LOGGER_TOKEN) 334 | logger: Logger; 335 | } 336 | 337 | @Injectable() 338 | class LoggerImpl implements Logger { 339 | log(msg: string) { 340 | console.log(msg); 341 | } 342 | } 343 | 344 | const injector = new Injector(); 345 | injector.addProviders(App); 346 | injector.addProviders({ 347 | token: LOGGER_TOKEN, 348 | useClass: LoggerImpl, 349 | }); 350 | 351 | const app = injector.get(App); 352 | console.log(app.logger instanceof LoggerImpl); // print 'true' 353 | ``` 354 | 355 | ### Use an abstract class as a Token 356 | 357 | ```ts 358 | abstract class Logger { 359 | abstract log(msg: string): void; 360 | } 361 | 362 | @Injectable() 363 | class LoggerImpl implements Logger { 364 | log(msg: string) { 365 | console.log(msg); 366 | } 367 | } 368 | 369 | @Injectable() 370 | class App { 371 | @Autowired() 372 | logger: Logger; 373 | } 374 | 375 | const injector = new Injector(); 376 | injector.addProviders(App); 377 | injector.addProviders({ 378 | token: Logger, 379 | useClass: LoggerImpl, 380 | }); 381 | 382 | const app = injector.get(App); 383 | console.log(app.logger instanceof LoggerImpl); // print 'true' 384 | ``` 385 | 386 | ## API 387 | 388 | ### decorator: @Injectable 389 | 390 | ```ts 391 | interface InstanceOpts { 392 | multiple?: boolean; 393 | } 394 | function Injectable(opts?: InstanceOpts): ClassDecorator; 395 | 396 | @Injectable({ multiple: true }) 397 | class A {} 398 | 399 | const injector = new Injector([A]); 400 | 401 | const a = injector.get(A); 402 | console.log(injector.hasInstance(a)); // print 'false' 403 | ``` 404 | 405 | All constructor functions that need to be created by the Injector must be decorated with this decorator in order to work properly. Otherwise, an error will be thrown. 406 | 407 | - multiple: Whether to enable the multiple instance mode or not, once the multiple instance mode is enabled, the Injector will not hold references to instance objects. 408 | 409 | ### decorator: @Autowired 410 | 411 | ```ts 412 | function Autowired(token?: Token): PropertyDecorator; 413 | 414 | @Injectable() 415 | class A {} 416 | 417 | @Injectable() 418 | class B { 419 | @Autowired() 420 | a: A; 421 | } 422 | ``` 423 | 424 | Decorating a property will allow the registry to dynamically create a dependency instance, which will only be created when it is accessed. For example, in the given example, the instance of class A will be created only when `b.a` is accessed. 425 | 426 | > It's important to note that since `Autowired` depends on an instance of the `Injector`, only objects created by the `Injector` can use this decorator. 427 | 428 | ### decorator: @Inject 429 | 430 | ```ts 431 | function Inject(token: string | symbol): ParameterDecorator; 432 | 433 | interface IA { 434 | log(): void; 435 | } 436 | 437 | @Injectable() 438 | class B { 439 | constructor(@Inject('IA') a: IA) {} 440 | } 441 | ``` 442 | 443 | When performing dependency injection in a constructor parameter, it is necessary to specify the decorator for the dependency token when a constructor depends on an abstraction that is passed into the constructor. In such cases, you will need to use this decorator. 444 | 445 | ### Injector.get 446 | 447 | ```ts 448 | interface Injector { 449 | get(token: ConstructorOf, args?: ConstructorParameters, opts?: InstanceOpts): TokenResult; 450 | get(token: T, opts?: InstanceOpts): TokenResult; 451 | } 452 | ``` 453 | 454 | You can use this method to create an instance of a specific token from the `Injector`. 455 | 456 | if you pass a constructor as the first parameter and provide constructor arguments as the second parameter (if any), the Injector will directly create an instance of the constructor and apply dependency injection. In this case, the constructor does not need to be decorated with Injectable and can still create objects successfully. For example: 457 | 458 | ```ts 459 | @Injectable() 460 | class A {} 461 | 462 | class B { 463 | @Autowired() 464 | a: A; 465 | } 466 | 467 | const injector = new Injector([A]); 468 | const b = injector.get(B, []); 469 | console.log(b.a instanceof A); // print 'true' 470 | ``` 471 | 472 | ### Injector.hasInstance 473 | 474 | Whether have an instantiated object in the Injector. 475 | 476 | ### Injector.disposeOne / Injector.disposeAll 477 | 478 | You can use `Injector.disposeOne` to dispose of a specific instance, and `Injector.disposeAll` to dispose of all instances. 479 | 480 | These two methods will delete the instance from the Injector, and then call the `dispose` method of the instance if it exists. 481 | 482 | ### markInjectable 483 | 484 | ```ts 485 | import { markInjectable } from '@opensumi/di'; 486 | import { Editor } from 'path/to/package'; 487 | 488 | markInjectable(Editor); 489 | ``` 490 | 491 | You can use this function to mark some Class as Injectable. 492 | 493 | ## Examples 494 | 495 | See More Examples [in the test case](test/use-case.test.ts). 496 | 497 | ## FAQ 498 | 499 | Please see [FAQ.md](docs/faq.md). 500 | 501 | ## Related Efforts 502 | 503 | - [Angular](https://angular.io/guide/dependency-injection) Dependency injection in Angular 504 | - [injection-js](https://github.com/mgechev/injection-js) It is an extraction of the Angular's ReflectiveInjector. 505 | - [InversifyJS](https://github.com/inversify/InversifyJS) A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript. 506 | - [power-di](https://github.com/zhang740/power-di) A lightweight Dependency Injection library. 507 | -------------------------------------------------------------------------------- /src/helper/hook-helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IHookStore, 3 | IDisposable, 4 | Token, 5 | IHookMap, 6 | MethodName, 7 | ClassCreator, 8 | IBeforeJoinPoint, 9 | IJoinPoint, 10 | HookType, 11 | IBeforeAspectHook, 12 | IAfterThrowingAspectHook, 13 | IAfterReturningAspectHook, 14 | IAroundAspectHook, 15 | IAfterAspectHook, 16 | IAfterJoinPoint, 17 | IAroundJoinPoint, 18 | IValidAspectHook, 19 | IHookOptions, 20 | IAfterReturningJoinPoint, 21 | IAfterThrowingJoinPoint, 22 | IInstanceHooks, 23 | InstanceCreator, 24 | } from '../types'; 25 | import compose, { Middleware } from '../compose'; 26 | 27 | export const HOOKED_SYMBOL = Symbol('DI_HOOKED'); 28 | 29 | export function applyHooks(instance: T, token: Token, hooks: IHookStore): T { 30 | if (typeof instance !== 'object') { 31 | // Only object can be hook. 32 | return instance; 33 | } 34 | if (!hooks.hasHooks(token)) { 35 | return instance; 36 | } 37 | 38 | // One disadvantage of using a proxy is that it can be difficult to intercept certain methods 39 | // that are defined using bound functions. 40 | // 41 | // If the methods of a class defined using a bound function such as a = () => this.b() are used, 42 | // the b() function cannot be intercepted correctly. 43 | const hookedCache: Map = new Map(); 44 | const proxy = new Proxy(instance as any, { 45 | get: (target, prop) => { 46 | if (prop === HOOKED_SYMBOL) { 47 | return true; 48 | } 49 | const raw = target[prop]; 50 | if (typeof raw === 'function' && hooks.getHooks(token, prop).length > 0) { 51 | if (!hookedCache.has(prop)) { 52 | hookedCache.set(prop, createHookedFunction(prop, raw, hooks.getHooks(token, prop))); 53 | } 54 | return hookedCache.get(prop); 55 | } 56 | return raw; 57 | }, 58 | }); 59 | 60 | return proxy; 61 | } 62 | 63 | /** 64 | * To use hooks to assemble a final wrapped function 65 | * @param rawMethod the original method 66 | * @param hooks hooks 67 | */ 68 | export function createHookedFunction( 69 | methodName: MethodName, 70 | rawMethod: (...args: Args) => Result, 71 | hooks: Array>, 72 | ): (...args: Args) => Result { 73 | const beforeHooks: Array> = []; 74 | const afterHooks: Array> = []; 75 | const aroundHooks: Array> = []; 76 | const afterReturningHooks: Array> = []; 77 | const afterThrowingHooks: Array> = []; 78 | 79 | // Onion model 80 | hooks.forEach((h) => { 81 | if (isBeforeHook(h)) { 82 | // for the "before hook", the first one to come is executed first. 83 | beforeHooks.push(h); 84 | } else if (isAfterHook(h)) { 85 | // For the "after hook", the one that comes later is executed first. 86 | afterHooks.unshift(h); 87 | } else if (isAroundHook(h)) { 88 | // For the "around hook", the one that comes later is executed first. 89 | aroundHooks.unshift(h); 90 | } else if (isAfterReturningHook(h)) { 91 | afterReturningHooks.push(h); 92 | } else if (isAfterThrowingHook(h)) { 93 | afterThrowingHooks.push(h); 94 | } 95 | }); 96 | 97 | return function (this: any, ...args: Args) { 98 | let promise: Promise | undefined | void; 99 | let ret: Result = undefined as any; 100 | let error: Error | undefined; 101 | const self = this; 102 | const originalArgs: Args = args; 103 | 104 | try { 105 | promise = runAroundHooks(); 106 | 107 | if (promise) { 108 | // If there is one hook that is asynchronous, convert all of them to asynchronous. 109 | promise = promise.then(() => { 110 | return ret; 111 | }); 112 | return promise as any; 113 | } else { 114 | return ret; 115 | } 116 | } catch (e) { 117 | error = e as Error; 118 | runAfterThrowing(); 119 | throw e; 120 | } finally { 121 | if (error) { 122 | // noop 123 | } else if (promise) { 124 | promise.then( 125 | () => { 126 | runAfterReturning(); 127 | }, 128 | (e) => { 129 | error = e; 130 | runAfterThrowing(); 131 | }, 132 | ); 133 | } else { 134 | runAfterReturning(); 135 | } 136 | } 137 | 138 | function runAroundHooks(): Promise | void { 139 | const hooks = aroundHooks.map((v) => { 140 | const fn = v.hook as Middleware>; 141 | return fn; 142 | }); 143 | const composed = compose>(hooks); 144 | 145 | const aroundJoinPoint: IAroundJoinPoint = { 146 | getArgs: () => { 147 | return args; 148 | }, 149 | getMethodName: () => { 150 | return methodName; 151 | }, 152 | getOriginalArgs: () => { 153 | return originalArgs; 154 | }, 155 | getResult: () => { 156 | return ret; 157 | }, 158 | getThis: () => { 159 | return self; 160 | }, 161 | setArgs: (_args: Args) => { 162 | args = _args; 163 | }, 164 | setResult: (_ret: Result) => { 165 | ret = _ret; 166 | }, 167 | proceed: () => { 168 | const maybePromise = wrapped(); 169 | if (maybePromise && isPromiseLike(maybePromise)) { 170 | return maybePromise.then(() => Promise.resolve()); 171 | } 172 | }, 173 | }; 174 | 175 | return composed(aroundJoinPoint); 176 | } 177 | 178 | function runBeforeHooks(): Promise | undefined { 179 | if (beforeHooks.length === 0) { 180 | return; 181 | } 182 | let _inThisHook = true; 183 | const beforeJointPont: IBeforeJoinPoint = { 184 | getArgs: () => { 185 | return args; 186 | }, 187 | getMethodName: () => { 188 | return methodName; 189 | }, 190 | getOriginalArgs: () => { 191 | return originalArgs; 192 | }, 193 | getThis: () => { 194 | return self; 195 | }, 196 | setArgs: (_args: Args) => { 197 | if (!_inThisHook) { 198 | throw new Error('It is not allowed to set the parameters after the Hook effect time is over'); 199 | } 200 | args = _args; 201 | }, 202 | }; 203 | 204 | return runHooks(beforeHooks, beforeJointPont, () => { 205 | _inThisHook = false; 206 | }); 207 | } 208 | 209 | function runAfterHooks(): Promise | undefined { 210 | if (afterHooks.length === 0) { 211 | return; 212 | } 213 | 214 | let _inThisHook = true; 215 | const afterJoinPoint: IAfterJoinPoint = { 216 | getArgs: () => { 217 | return args; 218 | }, 219 | getMethodName: () => { 220 | return methodName; 221 | }, 222 | getOriginalArgs: () => { 223 | return originalArgs; 224 | }, 225 | getResult: () => { 226 | return ret; 227 | }, 228 | getThis: () => { 229 | return self; 230 | }, 231 | setResult: (_ret: Result) => { 232 | if (!_inThisHook) { 233 | throw new Error('It is not allowed to set the return value after the Hook effect time is over'); 234 | } 235 | ret = _ret; 236 | }, 237 | }; 238 | 239 | return runHooks(afterHooks, afterJoinPoint, () => { 240 | _inThisHook = false; 241 | }); 242 | } 243 | 244 | function runAfterReturning() { 245 | if (afterReturningHooks.length === 0) { 246 | return; 247 | } 248 | 249 | const afterReturningJoinPoint: IAfterReturningJoinPoint = { 250 | getArgs: () => { 251 | return args; 252 | }, 253 | getMethodName: () => { 254 | return methodName; 255 | }, 256 | getOriginalArgs: () => { 257 | return originalArgs; 258 | }, 259 | getResult: () => { 260 | return ret; 261 | }, 262 | getThis: () => { 263 | return self; 264 | }, 265 | }; 266 | 267 | afterReturningHooks.forEach((hook) => { 268 | try { 269 | hook.hook(afterReturningJoinPoint); 270 | } catch (e) { 271 | // no op, ignore error on AfterReturning 272 | } 273 | }); 274 | } 275 | 276 | function runAfterThrowing() { 277 | if (afterThrowingHooks.length === 0) { 278 | return; 279 | } 280 | 281 | const afterThrowingJoinPoint: IAfterThrowingJoinPoint = { 282 | getError: () => { 283 | return error; 284 | }, 285 | getMethodName: () => { 286 | return methodName; 287 | }, 288 | getOriginalArgs: () => { 289 | return originalArgs; 290 | }, 291 | getThis: () => { 292 | return self; 293 | }, 294 | }; 295 | 296 | afterThrowingHooks.forEach((hook) => { 297 | try { 298 | hook.hook(afterThrowingJoinPoint); 299 | } catch (e) { 300 | // no op, ignore error on AfterThrowing 301 | } 302 | }); 303 | } 304 | 305 | function wrapped(): Result | void | Promise { 306 | promise = runBeforeHooks(); 307 | 308 | if (promise) { 309 | promise = promise.then(() => { 310 | ret = rawMethod.apply(self, args); 311 | return; 312 | }); 313 | } else { 314 | ret = rawMethod.apply(self, args); 315 | } 316 | 317 | if (promise) { 318 | promise = promise.then(() => { 319 | return runAfterHooks(); 320 | }); 321 | } else { 322 | promise = runAfterHooks(); 323 | } 324 | 325 | if (promise) { 326 | return promise.then(() => { 327 | return ret; 328 | }); 329 | } else { 330 | return ret; 331 | } 332 | } 333 | }; 334 | } 335 | 336 | function isBeforeHook( 337 | hook: IValidAspectHook, 338 | ): hook is IBeforeAspectHook { 339 | return hook && hook.type === HookType.Before; 340 | } 341 | 342 | function isAfterHook( 343 | hook: IValidAspectHook, 344 | ): hook is IAfterAspectHook { 345 | return hook && hook.type === HookType.After; 346 | } 347 | 348 | function isAroundHook( 349 | hook: IValidAspectHook, 350 | ): hook is IAroundAspectHook { 351 | return hook && hook.type === HookType.Around; 352 | } 353 | 354 | function isAfterReturningHook( 355 | hook: IValidAspectHook, 356 | ): hook is IAfterReturningAspectHook { 357 | return hook && hook.type === HookType.AfterReturning; 358 | } 359 | 360 | function isAfterThrowingHook( 361 | hook: IValidAspectHook, 362 | ): hook is IAfterThrowingAspectHook { 363 | return hook && hook.type === HookType.AfterThrowing; 364 | } 365 | 366 | export function isPromiseLike(thing: any): thing is Promise { 367 | return !!(thing as Promise).then; 368 | } 369 | 370 | function runHooks< 371 | T extends { awaitPromise?: boolean; hook: (joinPoint: P) => Promise | void }, 372 | P extends IJoinPoint, 373 | >(hooks: T[], joinPoint: P, then: () => void) { 374 | let promise: Promise | undefined; 375 | for (const hook of hooks) { 376 | promise = runOneHook(hook, joinPoint, promise); 377 | } 378 | if (promise) { 379 | promise = promise.then(then); 380 | } else { 381 | then(); 382 | } 383 | return promise; 384 | } 385 | 386 | function runOneHook< 387 | T extends { awaitPromise?: boolean; hook: (joinPoint: P) => Promise | void }, 388 | P extends IJoinPoint, 389 | >(hook: T, joinPoint: P, promise: Promise | undefined): Promise | undefined { 390 | if (hook.awaitPromise) { 391 | promise = promise || Promise.resolve(); 392 | promise = promise.then(() => { 393 | // notice: here we return hook's result 394 | // and the return statement on the next condition branch will return undefined. 395 | return hook.hook(joinPoint); 396 | }); 397 | } else if (promise) { 398 | promise = promise.then(() => { 399 | hook.hook(joinPoint); 400 | }); 401 | } else { 402 | hook.hook(joinPoint); 403 | } 404 | return promise; 405 | } 406 | 407 | export class HookStore implements IHookStore { 408 | private hooks: IHookMap = new Map(); 409 | 410 | constructor(private parent?: IHookStore) {} 411 | 412 | createHooks(hooks: IValidAspectHook[]): IDisposable { 413 | const disposers: IDisposable[] = hooks.map((hook) => { 414 | return this.createOneHook(hook); 415 | }); 416 | return { 417 | dispose: () => { 418 | for (const toDispose of disposers) { 419 | toDispose.dispose(); 420 | } 421 | }, 422 | }; 423 | } 424 | 425 | hasHooks(token: Token) { 426 | if (this.hooks.has(token)) { 427 | return true; 428 | } 429 | if (this.parent) { 430 | return this.parent.hasHooks(token); 431 | } 432 | return false; 433 | } 434 | 435 | getHooks(token: Token, method: string | number | symbol): IValidAspectHook[] { 436 | let hooks: IValidAspectHook[] = []; 437 | if (this.parent) { 438 | hooks = hooks.concat(this.parent.getHooks(token, method)); 439 | } 440 | 441 | if (this.hooks.get(token)?.has(method)) { 442 | hooks = hooks.concat(this.hooks.get(token)!.get(method)!); 443 | } 444 | 445 | hooks.sort((a, b) => { 446 | return (b.priority || 0) - (a.priority || 0); 447 | }); 448 | 449 | return hooks; 450 | } 451 | 452 | createOneHook(hook: IValidAspectHook): IDisposable { 453 | const token = hook.target; 454 | if (!this.hooks.has(token)) { 455 | this.hooks.set(token, new Map()); 456 | } 457 | const instanceHooks = this.hooks.get(token)!; 458 | if (!instanceHooks.has(hook.method)) { 459 | instanceHooks.set(hook.method, []); 460 | } 461 | instanceHooks.get(hook.method)!.push(hook); 462 | return { 463 | dispose: () => { 464 | this.removeOneHook(hook); 465 | }, 466 | }; 467 | } 468 | 469 | removeOneHook(hook: IValidAspectHook): void { 470 | const token = hook.target; 471 | if (!this.hooks.has(token)) { 472 | return; 473 | } 474 | const instanceHooks = this.hooks.get(token)!; 475 | if (!instanceHooks.has(hook.method)) { 476 | return; 477 | } 478 | const methodHooks = instanceHooks.get(hook.method)!; 479 | const index = methodHooks.indexOf(hook); 480 | if (index > -1) { 481 | methodHooks.splice(index, 1); 482 | } 483 | } 484 | } 485 | 486 | const HOOK_KEY = Symbol('HOOK_KEY'); 487 | const ASPECT_KEY = Symbol('ASPECT_KEY'); 488 | 489 | export type IHookMetadata = Array<{ 490 | prop: MethodName; 491 | type: HookType; 492 | target: Token; 493 | targetMethod: MethodName; 494 | options: IHookOptions; 495 | }>; 496 | 497 | export function markAsAspect(target: object) { 498 | Reflect.defineMetadata(ASPECT_KEY, true, target); 499 | } 500 | 501 | export function markAsHook( 502 | target: object, 503 | prop: MethodName, 504 | type: HookType, 505 | hookTarget: Token, 506 | targetMethod: MethodName, 507 | options: IHookOptions, 508 | ) { 509 | let hooks = Reflect.getOwnMetadata(HOOK_KEY, target); 510 | if (!hooks) { 511 | hooks = []; 512 | Reflect.defineMetadata(HOOK_KEY, hooks, target); 513 | } 514 | hooks.push({ prop, type, target: hookTarget, targetMethod, options }); 515 | } 516 | 517 | export function isAspectCreator(target: InstanceCreator) { 518 | return !!Reflect.getMetadata(ASPECT_KEY, (target as ClassCreator).useClass); 519 | } 520 | 521 | export function getHookMeta(target: any): IHookMetadata { 522 | return Reflect.getOwnMetadata(HOOK_KEY, target.prototype) || []; 523 | } 524 | 525 | export function isHooked(target: any): boolean { 526 | return target && !!target[HOOKED_SYMBOL]; 527 | } 528 | -------------------------------------------------------------------------------- /src/injector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | flatten, 3 | getAllDeps, 4 | getParameterOpts, 5 | hasTag, 6 | injectorIdGenerator, 7 | isFactoryCreator, 8 | isInjectableToken, 9 | isPromiseLike, 10 | parseCreatorFromProvider, 11 | parseTokenFromProvider, 12 | uniq, 13 | } from './helper'; 14 | import { 15 | aliasCircularError, 16 | circularError, 17 | noProviderError, 18 | noInstancesInCompletedCreatorError, 19 | tagOnlyError, 20 | } from './error'; 21 | import { 22 | INJECTOR_TOKEN, 23 | Provider, 24 | Token, 25 | InstanceCreator, 26 | CreatorStatus, 27 | TokenResult, 28 | InstanceOpts, 29 | ConstructorOf, 30 | ClassCreator, 31 | InjectorOpts, 32 | AddProvidersOpts, 33 | Domain, 34 | Tag, 35 | IHookStore, 36 | IValidAspectHook, 37 | Context, 38 | ParameterOpts, 39 | IDisposable, 40 | FactoryCreator, 41 | } from './types'; 42 | import { 43 | isClassCreator, 44 | setInjector, 45 | removeInjector, 46 | isTypeProvider, 47 | applyHooks, 48 | HookStore, 49 | isAspectCreator, 50 | getHookMeta, 51 | isAliasCreator, 52 | EventEmitter, 53 | } from './helper'; 54 | 55 | export class Injector { 56 | id = injectorIdGenerator.next(); 57 | 58 | depth = 0; 59 | tag?: string; 60 | hookStore: IHookStore; 61 | parent?: Injector; 62 | 63 | private tagMatrix = new Map>(); 64 | private domainMap = new Map(); 65 | creatorMap = new Map(); 66 | 67 | private instanceDisposedEmitter = new EventEmitter(); 68 | 69 | constructor(providers: Provider[] = [], private opts: InjectorOpts = {}, parent?: Injector) { 70 | this.tag = opts.tag; 71 | 72 | if (parent) { 73 | this.parent = parent; 74 | this.depth = parent.depth + 1; 75 | this.hookStore = new HookStore(this.parent.hookStore); 76 | } else { 77 | this.hookStore = new HookStore(); 78 | } 79 | 80 | this.addProviders( 81 | { 82 | token: INJECTOR_TOKEN, 83 | useValue: this, 84 | }, 85 | ...providers, 86 | ); 87 | } 88 | 89 | createChild(providers: Provider[] = [], opts: InjectorOpts = {}): InstanceType> { 90 | const injector = new (this.constructor as ConstructorOf)(providers, { ...this.opts, ...opts }, this); 91 | 92 | if (opts.dropdownForTag) { 93 | for (const [token, creator] of this.creatorMap.entries()) { 94 | if (creator.dropdownForTag && creator.tag && opts.tag === creator.tag && !injector.creatorMap.has(token)) { 95 | injector.creatorMap.set(token, creator); 96 | 97 | const targetTokenMap = injector.tagMatrix.get(creator.tag) || new Map(); 98 | const currentTokenMap = this.tagMatrix.get(creator.tag); 99 | if (currentTokenMap) { 100 | for (const [key, value] of currentTokenMap.entries()) { 101 | targetTokenMap.set(key, value); 102 | } 103 | } 104 | injector.tagMatrix.set(creator.tag, targetTokenMap); 105 | } 106 | } 107 | } 108 | 109 | return injector; 110 | } 111 | 112 | get>(token: T, args?: ConstructorParameters, opts?: InstanceOpts): TokenResult; 113 | get(token: T, opts?: InstanceOpts): TokenResult; 114 | get = ConstructorOf>( 115 | token: T, 116 | opts?: ConstructorParameters, 117 | ): TokenResult; 118 | get = ConstructorOf>( 119 | token: T, 120 | args?: InstanceOpts | ConstructorParameters, 121 | opts?: InstanceOpts, 122 | ): TokenResult { 123 | if (!Array.isArray(args)) { 124 | opts = args; 125 | args = undefined; 126 | } 127 | 128 | let creator: InstanceCreator | null = null; 129 | let injector: Injector = this; 130 | 131 | // If user passed the args parameter, he want to instantiate the class 132 | if (args) { 133 | // At this time, it must not be singleton 134 | opts = { 135 | ...opts, 136 | multiple: true, 137 | }; 138 | // here we do not use the parsed injector(the second return value of `getCreator`) 139 | [creator] = this.getCreator(token); 140 | 141 | if (!creator) { 142 | // if there is no specific Creator, then: 143 | // 1. if token is a Class, we also allow the not-Injectable Class as Token, we just instantiate this Class 144 | if (isTypeProvider(token)) { 145 | creator = { 146 | opts: {}, 147 | parameters: [], 148 | useClass: token, 149 | }; 150 | } else { 151 | throw noProviderError(token); 152 | } 153 | } 154 | } else { 155 | // firstly, use tag to exchange token 156 | if (opts && hasTag(opts)) { 157 | const tagToken = this.exchangeToken(token, opts.tag); 158 | [creator, injector] = this.getCreator(tagToken); 159 | } 160 | 161 | // if there is no Creator, then use the token to find the Creator 162 | if (!creator) { 163 | [creator, injector] = this.getCreator(token); 164 | } 165 | 166 | // if in non-strict mode, parse dependencies and providers automatically when get 167 | if (isTypeProvider(token) && !creator && !this.opts.strict) { 168 | this.parseDependencies(token); 169 | [creator, injector] = this.getCreator(token); 170 | } 171 | } 172 | 173 | if (!creator) { 174 | throw noProviderError(token); 175 | } 176 | 177 | const ctx = { 178 | token, 179 | creator, 180 | injector, 181 | } as Context; 182 | 183 | return this.createInstance(ctx, opts, args as ConstructorParameters); 184 | } 185 | 186 | private getTokenForDomain(domain: Domain): Token[] { 187 | let tokens = this.domainMap.get(domain) || []; 188 | if (this.parent) { 189 | tokens = tokens.concat(this.parent.getTokenForDomain(domain)); 190 | } 191 | return tokens; 192 | } 193 | 194 | getFromDomain(...domains: Domain[]): Array { 195 | const tokenSet = new Set(); 196 | 197 | for (const domain of domains) { 198 | const arr = this.getTokenForDomain(domain); 199 | arr.forEach((item) => tokenSet.add(item)); 200 | } 201 | 202 | const tokens = Array.from(tokenSet); 203 | return tokens.map((token) => this.get(token)); 204 | } 205 | 206 | /** 207 | * Only check this injector whether has the singleton instance. 208 | */ 209 | hasInstance(instance: any) { 210 | for (const creator of this.creatorMap.values()) { 211 | if (creator.instances?.has(instance)) { 212 | return true; 213 | } 214 | } 215 | 216 | return false; 217 | } 218 | 219 | addProviders(...providers: Provider[]) { 220 | this.setProviders(providers); 221 | } 222 | 223 | overrideProviders(...providers: Provider[]) { 224 | this.setProviders(providers, { override: true }); 225 | } 226 | 227 | parseDependencies(...targets: Token[]) { 228 | const deepDeps: Token[] = getAllDeps(...targets); 229 | const allDeps = targets.concat(deepDeps); 230 | const providers = uniq(allDeps.filter(isInjectableToken)); 231 | this.setProviders(providers, { deep: true }); 232 | 233 | const defaultProviders = flatten( 234 | providers.map((p) => { 235 | return getParameterOpts(p); 236 | }), 237 | ) 238 | .filter((opt) => { 239 | return Object.prototype.hasOwnProperty.call(opt, 'default'); 240 | }) 241 | .map((opt) => ({ 242 | isDefault: true, 243 | token: opt.token, 244 | useValue: opt.default, 245 | })); 246 | 247 | this.setProviders(defaultProviders, { deep: true }); 248 | 249 | // make sure all dependencies have corresponding providers 250 | const notProvidedDeps = allDeps.filter((d) => !this.getCreator(d)[0]); 251 | if (notProvidedDeps.length) { 252 | throw noProviderError(...notProvidedDeps); 253 | } 254 | } 255 | 256 | exchangeToken(token: Token, tag: Tag) { 257 | const current = this.getTagToken(token, tag); 258 | if (current) { 259 | return current; 260 | } 261 | 262 | const tokenMap: Map = this.tagMatrix.get(tag) || new Map(); 263 | const tagToken = Symbol(tag); 264 | tokenMap.set(token, tagToken); 265 | this.tagMatrix.set(tag, tokenMap); 266 | return tagToken; 267 | } 268 | 269 | createHooks(hooks: IValidAspectHook[]) { 270 | return this.hookStore.createHooks(hooks); 271 | } 272 | 273 | createHook(hook: IValidAspectHook) { 274 | return this.hookStore.createOneHook(hook); 275 | } 276 | 277 | onceInstanceDisposed(instance: any, cb: () => void) { 278 | return this.instanceDisposedEmitter.once(instance, cb); 279 | } 280 | 281 | disposeOne(token: Token, key = 'dispose') { 282 | const [creator] = this.getCreator(token); 283 | if (!creator) { 284 | return; 285 | } 286 | 287 | const instances = creator.instances; 288 | 289 | let maybePromise: Promise | void | undefined; 290 | if (instances) { 291 | const disposeFns = [] as any[]; 292 | 293 | for (const item of instances.values()) { 294 | let _maybePromise: Promise | undefined; 295 | if (item && typeof item[key] === 'function') { 296 | _maybePromise = item[key](); 297 | } 298 | 299 | if (_maybePromise && isPromiseLike(_maybePromise)) { 300 | disposeFns.push( 301 | _maybePromise.then(() => { 302 | this.instanceDisposedEmitter.emit(item); 303 | }), 304 | ); 305 | } else { 306 | this.instanceDisposedEmitter.emit(item); 307 | } 308 | } 309 | 310 | maybePromise = disposeFns.length ? Promise.all(disposeFns) : undefined; 311 | } 312 | 313 | creator.instances = undefined; 314 | creator.status = CreatorStatus.init; 315 | 316 | return maybePromise; 317 | } 318 | 319 | disposeAll(key = 'dispose'): Promise { 320 | const creatorMap = this.creatorMap; 321 | 322 | const promises: (Promise | void)[] = []; 323 | 324 | for (const token of creatorMap.keys()) { 325 | promises.push(this.disposeOne(token, key)); 326 | } 327 | 328 | return Promise.all(promises).finally(() => { 329 | this.instanceDisposedEmitter.dispose(); 330 | }) as unknown as Promise; 331 | } 332 | 333 | protected getTagToken(token: Token, tag: Tag): Token | undefined | null { 334 | const tokenMap = this.tagMatrix.get(tag); 335 | 336 | if (tokenMap && tokenMap.has(token)) { 337 | return tokenMap.get(token); 338 | } else if (this.parent) { 339 | return this.parent.getTagToken(token, tag); 340 | } 341 | 342 | return null; 343 | } 344 | 345 | private setProviders(providers: Provider[], opts: AddProvidersOpts = {}) { 346 | for (const provider of providers) { 347 | const originToken = parseTokenFromProvider(provider); 348 | const token = hasTag(provider) ? this.exchangeToken(originToken, provider.tag) : originToken; 349 | const current = opts.deep ? this.getCreator(token)[0] : this.resolveToken(token)[1]; 350 | 351 | const shouldBeSet = [ 352 | // use provider's override attribute. 353 | isTypeProvider(provider) ? false : provider.override, 354 | // use opts.override. The user explicitly call `overrideProviders`. 355 | opts.override, 356 | // if this token do not have corresponding creator, use override 357 | // if the creator is default(it is a fallback value), it means we can override it. 358 | !current || current.isDefault, 359 | ].some(Boolean); 360 | 361 | if (shouldBeSet) { 362 | const creator = parseCreatorFromProvider(provider); 363 | 364 | this.creatorMap.set(token, creator); 365 | // use effect to Make sure there are no cycles 366 | void this.resolveToken(token); 367 | 368 | if (isClassCreator(creator)) { 369 | const domain = creator.opts.domain; 370 | if (domain) { 371 | const domains = Array.isArray(domain) ? domain : [domain]; 372 | this.addToDomain(domains, token); 373 | } 374 | if (isAspectCreator(creator)) { 375 | const hookMetadata = getHookMeta(creator.useClass); 376 | let toDispose: IDisposable | undefined; 377 | let instance: any; 378 | const getInstance = () => { 379 | if (!instance) { 380 | instance = this.get(token); 381 | this.onceInstanceDisposed(instance, () => { 382 | if (toDispose) { 383 | toDispose.dispose(); 384 | toDispose = undefined; 385 | } 386 | instance = undefined; 387 | // remove the aspect creator when the instance is disposed 388 | this.creatorMap.delete(creator.useClass); 389 | }); 390 | } 391 | 392 | return instance; 393 | }; 394 | 395 | const preprocessedHooks = hookMetadata.map((metadata) => { 396 | const wrapped = (...args: any[]) => { 397 | const instance = getInstance(); 398 | return instance[metadata.prop].call(instance, ...args); 399 | }; 400 | return { 401 | awaitPromise: metadata.options.await, 402 | priority: metadata.options.priority, 403 | hook: wrapped, 404 | method: metadata.targetMethod, 405 | target: metadata.target, 406 | type: metadata.type, 407 | } as IValidAspectHook; 408 | }); 409 | toDispose = this.hookStore.createHooks(preprocessedHooks); 410 | } 411 | } 412 | } 413 | } 414 | } 415 | 416 | private addToDomain(domains: Domain[], token: Token) { 417 | for (const domain of domains) { 418 | const tokens = this.domainMap.get(domain) || []; 419 | tokens.push(token); 420 | this.domainMap.set(domain, tokens); 421 | } 422 | } 423 | 424 | private resolveToken(token: Token): [Token, InstanceCreator | undefined] { 425 | let creator = this.creatorMap.get(token); 426 | 427 | const paths = [token]; 428 | 429 | while (creator && isAliasCreator(creator)) { 430 | token = creator.useAlias; 431 | 432 | if (paths.includes(token)) { 433 | throw aliasCircularError(paths, token); 434 | } 435 | paths.push(token); 436 | creator = this.creatorMap.get(token); 437 | } 438 | 439 | return [token, creator]; 440 | } 441 | 442 | getCreator(token: Token): [InstanceCreator | null, Injector] { 443 | const creator: InstanceCreator | undefined = this.resolveToken(token)[1]; 444 | 445 | if (creator) { 446 | return [creator, this]; 447 | } 448 | 449 | if (this.parent) { 450 | return this.parent.getCreator(token); 451 | } 452 | 453 | return [null, this]; 454 | } 455 | 456 | private createInstance(ctx: Context, defaultOpts?: InstanceOpts, args?: any[]) { 457 | const { creator } = ctx; 458 | 459 | if (creator.dropdownForTag && creator.tag !== this.tag) { 460 | throw tagOnlyError(String(creator.tag), String(this.tag)); 461 | } 462 | 463 | if (isClassCreator(creator)) { 464 | const opts = defaultOpts ?? creator.opts; 465 | // if a class creator is singleton, and the instance is already created, return the instance. 466 | if (creator.status === CreatorStatus.done && !opts.multiple) { 467 | if (creator.instances) { 468 | return creator.instances.values().next().value; 469 | } 470 | 471 | /* istanbul ignore next */ 472 | throw noInstancesInCompletedCreatorError(ctx.token); 473 | } 474 | 475 | return this.createInstanceFromClassCreator(ctx as Context, opts, args); 476 | } 477 | 478 | if (isFactoryCreator(creator)) { 479 | return this.createInstanceFromFactory(ctx as Context); 480 | } 481 | 482 | if (creator.instances) { 483 | return creator.instances.values().next().value; 484 | } 485 | 486 | /* istanbul ignore next */ 487 | throw noInstancesInCompletedCreatorError(ctx.token); 488 | } 489 | 490 | private createInstanceFromClassCreator(ctx: Context, opts: InstanceOpts, defaultArgs?: any[]) { 491 | const { creator, token, injector } = ctx; 492 | 493 | const cls = creator.useClass; 494 | const currentStatus = creator.status; 495 | 496 | // If you try to create an instance whose status is creating, it must be caused by circular dependencies. 497 | if (currentStatus === CreatorStatus.creating) { 498 | throw circularError(cls, ctx); 499 | } 500 | 501 | creator.status = CreatorStatus.creating; 502 | 503 | try { 504 | const args = defaultArgs ?? this.getParameters(creator.parameters, ctx); 505 | const instance = this.createInstanceWithInjector(cls, token, injector, args); 506 | creator.status = CreatorStatus.init; 507 | 508 | // if not allow multiple, save the instance in creator. 509 | if (!opts.multiple) { 510 | creator.status = CreatorStatus.done; 511 | } 512 | creator.instances ? creator.instances.add(instance) : (creator.instances = new Set([instance])); 513 | 514 | return instance; 515 | } catch (e) { 516 | // rollback the status if exception occurs 517 | creator.status = currentStatus; 518 | throw e; 519 | } 520 | } 521 | 522 | private getParameters(parameters: ParameterOpts[], state: Context) { 523 | return parameters.map((opts) => { 524 | const [creator, injector] = this.getCreator(opts.token); 525 | 526 | if (creator) { 527 | return this.createInstance( 528 | { 529 | injector, 530 | creator, 531 | token: opts.token, 532 | parent: state, 533 | }, 534 | undefined, 535 | undefined, 536 | ); 537 | } 538 | 539 | if (!creator && Object.prototype.hasOwnProperty.call(opts, 'default')) { 540 | return opts.default; 541 | } 542 | throw noProviderError(opts.token); 543 | }); 544 | } 545 | 546 | private createInstanceWithInjector(cls: ConstructorOf, token: Token, injector: Injector, args: any[]) { 547 | // when creating an instance, set injector to prototype, so that the constructor can access it. 548 | // after creating the instance, remove the injector from prototype to prevent memory leaks. 549 | setInjector(cls.prototype, injector); 550 | const ret = new cls(...args); 551 | removeInjector(cls.prototype); 552 | 553 | // mount injector on the instance, so that the inner object can access the injector in the future. 554 | setInjector(ret, injector); 555 | 556 | return applyHooks(ret, token, this.hookStore); 557 | } 558 | 559 | private createInstanceFromFactory(ctx: Context) { 560 | const { creator, token } = ctx; 561 | 562 | const value = applyHooks(creator.useFactory(this), token, this.hookStore); 563 | creator.instances ? creator.instances.add(value) : (creator.instances = new Set([value])); 564 | return value; 565 | } 566 | } 567 | --------------------------------------------------------------------------------