├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── prettier.config.mjs ├── renovate.json ├── rollup.config.mjs ├── scripts ├── writeCommonJsPackageJson.mjs └── writeEsmPackageJson.mjs ├── src ├── index.ts └── test │ ├── annotation │ ├── inject.test.ts │ ├── injectable.test.ts │ ├── multi_inject.test.ts │ ├── named.test.ts │ ├── optional.test.ts │ └── post_construct.test.ts │ ├── bugs │ ├── bugs.test.ts │ ├── issue_1190.test.ts │ ├── issue_1297.test.ts │ ├── issue_1416.test.ts │ ├── issue_1515.test.ts │ ├── issue_1518.test.ts │ ├── issue_1564.test.ts │ ├── issue_543.test.ts │ ├── issue_549.test.ts │ ├── issue_706.test.ts │ └── issue_928.test.ts │ ├── container │ ├── container.test.ts │ └── container_module.test.ts │ ├── features │ ├── named_default.test.ts │ ├── property_injection.test.ts │ ├── provider.test.ts │ ├── request_scope.test.ts │ └── transitive_bindings.test.ts │ ├── inversify.test.ts │ └── node │ ├── error_messages.test.ts │ ├── exceptions.test.ts │ ├── performance.test.ts │ └── proxies.test.ts ├── tsconfig.base.cjs.json ├── tsconfig.base.esm.json ├── tsconfig.base.json ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "If something isn't working as expected \U0001F914" 3 | labels: ["needs triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports and feature requests 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/jXcMagAPnm) 13 | - use [StackOverflow using the tag `inversifyjs`](https://stackoverflow.com/questions/tagged/inversifyjs) 14 | 15 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 16 | 17 | --- 18 | 19 | - type: checkboxes 20 | attributes: 21 | label: "Is there an existing issue for this?" 22 | description: "Please search [here](https://github.com/inversify/InversifyJS/issues) to see if an issue already exists for the bug you encountered" 23 | options: 24 | - label: "I have searched the existing issues" 25 | required: true 26 | 27 | - type: textarea 28 | validations: 29 | required: true 30 | attributes: 31 | label: "Current behavior" 32 | description: "How the issue manifests?" 33 | 34 | - type: textarea 35 | attributes: 36 | label: "Steps to reproduce" 37 | description: | 38 | How the issue manifests? 39 | You could leave this blank if you can't reproduce it, but please provide as much information as possible 40 | placeholder: | 41 | 1. `npm ci` 42 | 2. `npm start:dev` 43 | 3. See error... 44 | 45 | - type: textarea 46 | validations: 47 | required: true 48 | attributes: 49 | label: "Expected behavior" 50 | description: "A clear and concise description of what you expected to happened (or code)" 51 | 52 | - type: textarea 53 | validations: 54 | required: false 55 | attributes: 56 | label: "Possible solution" 57 | description: "If you have a suggestion on how to fix the bug" 58 | 59 | - type: markdown 60 | attributes: 61 | value: | 62 | --- 63 | 64 | 65 | - type: input 66 | validations: 67 | required: true 68 | attributes: 69 | label: "Package version" 70 | description: | 71 | Which version of `inversify` are you using? 72 | placeholder: "6.0.1" 73 | 74 | - type: input 75 | attributes: 76 | label: "Node.js version" 77 | description: "Which version of Node.js are you using?" 78 | placeholder: "18.0.0" 79 | 80 | - type: checkboxes 81 | attributes: 82 | label: "In which operating systems have you tested?" 83 | options: 84 | - label: macOS 85 | - label: Windows 86 | - label: Linux 87 | 88 | - type: markdown 89 | attributes: 90 | value: | 91 | --- 92 | 93 | - type: textarea 94 | attributes: 95 | label: "Stack trace" 96 | description: "If you have a stack trace, please include it here" 97 | 98 | - type: textarea 99 | attributes: 100 | label: "Other" 101 | description: | 102 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 103 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 104 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Community Ideas" 2 | description: "I have an idea or proposal \U0001F4A1!" 3 | labels: ["needs triage"] 4 | assignees: 5 | - "notaphplover" 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## :heart: We would love to hear your ideas and proposals 11 | 12 | Suggest an idea for a specific feature, product, process, anything you wish to propose to the community for comment and discussion. 13 | 14 | **NOTE:** If your idea is approved after discussion, you will be asked to provide more technical information until we get an issue to implement it. 15 | 16 | --- 17 | 18 | - type: checkboxes 19 | attributes: 20 | label: "Is there an existing proposal similar to this?" 21 | description: "Please make sure that your idea is not already proposed otherwise you will be asked to comment on the existing proposal" 22 | options: 23 | - label: "I have searched the existing proposals" 24 | required: true 25 | 26 | - type: textarea 27 | validations: 28 | required: true 29 | attributes: 30 | label: "What are you proposing?" 31 | description: "In a few sentences, describe your idea or proposal" 32 | placeholder: | 33 | My idea is ... 34 | 35 | - type: textarea 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Is there any specific group of users that will benefit from this?" 40 | description: "Highlight any research, proposals, requests or anecdotes that signal this is the right thing to build. Include links to GitHub Issues, Forums, Stack Overflow, Twitter, Etc" 41 | placeholder: | 42 | I have seen ... 43 | 44 | - type: textarea 45 | validations: 46 | required: true 47 | attributes: 48 | label: "What problems are you trying to solve?" 49 | description: "Describe the problems that this idea or proposal will solve" 50 | placeholder: | 51 | I am trying to solve ... 52 | 53 | - type: textarea 54 | validations: 55 | required: true 56 | attributes: 57 | label: "Do you have any references or examples that can illustrate your idea?" 58 | description: "If you have any references or examples that can illustrate your idea, who is using it, and how it is being used, please share them here" 59 | placeholder: | 60 | I have seen ... 61 | 62 | - type: dropdown 63 | id: idea-type 64 | validations: 65 | required: true 66 | attributes: 67 | label: "What type of idea is this?" 68 | description: "Select the type of idea that this is" 69 | multiple: false 70 | options: 71 | - "Innovation: No similar idea exists" 72 | - "Improvement of existing idea: Similar idea exists but this is an improvement" 73 | - "Copy of existing idea: Similar idea exists and this is a copy" 74 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | Testing: 11 | name: Compile source code and run tests 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | node-version: [20.x, 22.x] 16 | os: [ubuntu-latest, windows-latest] 17 | ts-project: [tsconfig.json] 18 | exclude: 19 | - node-version: 22.x 20 | os: ubuntu-latest 21 | ts-project: tsconfig.json 22 | env: 23 | TS_NODE_PROJECT: ${{ matrix.ts-project }} 24 | 25 | steps: 26 | - name: Checkout Project 27 | uses: actions/checkout@v4 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | cache: npm 33 | registry-url: https://registry.npmjs.org/ 34 | - run: npm ci 35 | - run: npm run build 36 | - name: Run tests 37 | run: npm test 38 | 39 | Upload_Coverage_Report: 40 | name: Upload coverage report to codecov 41 | environment: CI 42 | env: 43 | TS_NODE_PROJECT: tsconfig.json 44 | needs: [Testing] 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout Project 48 | uses: actions/checkout@v4 49 | with: 50 | fetch-depth: 0 51 | - name: Use Node.js 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: 22.16.0 55 | cache: npm 56 | registry-url: https://registry.npmjs.org/ 57 | - name: Install Dependencies 58 | run: npm ci 59 | - run: npm run build 60 | - name: Run linter 61 | run: npm run lint 62 | - name: Run tests 63 | run: npm test --coverage 64 | - name: Codecov Upload 65 | uses: codecov/codecov-action@v5 66 | with: 67 | directory: coverage/ 68 | fail_ci_if_error: true 69 | token: ${{ secrets.CODECOV_TOKEN }} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | 13 | # Dependency directory 14 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 15 | node_modules 16 | 17 | dist 18 | dts 19 | lib 20 | temp 21 | es 22 | 23 | type_definitions/inversify/*.js 24 | 25 | src/*.js 26 | src/**/*.js 27 | 28 | src/*.js.map 29 | src/**/*.js.map 30 | 31 | *.d.ts 32 | *.tsbuildinfo 33 | src/annotation/*.d.ts 34 | src/bindings/*.d.ts 35 | src/constants/*.d.ts 36 | src/container/*.d.ts 37 | src/planning/*.d.ts 38 | src/resolution/*.d.ts 39 | src/syntax/*.d.ts 40 | 41 | test/*.js 42 | test/**/*.js 43 | test/*.js.map 44 | test/**/*.js.map 45 | src/**/*.js.map 46 | src/*.js.map 47 | type_definitions/**/*.js 48 | .DS_store 49 | .idea 50 | 51 | .nyc_output 52 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .gitignore 3 | .nyc_output 4 | .vscode 5 | build 6 | CODE_OF_CONDUCT.md 7 | CONTRIBUTING.md 8 | coverage 9 | eslint.config.mjs 10 | ISSUE_TEMPLATE.md 11 | **/*.ts 12 | !lib/cjs/**/*.d.ts 13 | lib/esm/**/*.d.ts.map 14 | !lib/esm/index.d.ts 15 | !lib/esm/index.d.ts.map 16 | lib/*/test/** 17 | mocha.opts 18 | prettier.config.mjs 19 | PULL_REQUEST_TEMPLATE.md 20 | renovate.json 21 | rollup.config.mjs 22 | scripts 23 | src 24 | temp 25 | tsconfig.json 26 | tsconfig.base.cjs.json 27 | tsconfig.base.esm.json 28 | tsconfig.base.json 29 | tsconfig.cjs.json 30 | tsconfig.cjs.tsbuildinfo 31 | tsconfig.esm.json 32 | wiki 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project from 5.0.0 forward will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | 11 | ### Changed 12 | 13 | ### Removed 14 | 15 | ## [7.5.2] 16 | 17 | ### Changed 18 | - Fixed `Container.snapshot` so snapshot bindings are not updated after a snapshot is taken. 19 | - Fixed a memory leak affecting child containers. 20 | 21 | ## [7.5.1] 22 | 23 | ### Changed 24 | - Updated `Container.get` like methods to properly set bindings when autobind mode is set: 25 | - `@injectable` scopes properly handled. 26 | - Autobind mode no longer creates duplicated bindings. 27 | 28 | ## [7.5.0] 29 | 30 | ### Changed 31 | - Updated `Container` with `unloadSync`. 32 | - Updated `Container` with `loadSync`. 33 | 34 | ## [7.4.0] 35 | 36 | ### Changed 37 | - Updated `ContainerModuleLoadOptions` with `rebind`. 38 | - Updated `ContainerModuleLoadOptions` with `rebindSync`. 39 | - Updated `BindToFluentSyntax.toResolvedValue` with additional type constraints. 40 | 41 | ## [7.3.0] 42 | 43 | ### Changed 44 | - Updated `Container` with `rebindSync`. 45 | - Updated `Container` with `unbindSync`. 46 | - Updated `Container` with `rebind`. 47 | - Updated `ContainerModuleLoadOptions` with `unbindSync`. 48 | - Updated `ContainerModuleLoadOptions.unbind` to accept `BindingIdentifier`. 49 | 50 | ## [7.2.0] 51 | 52 | ### Added 53 | - Added `BindingIdentifier`. 54 | 55 | ### Changed 56 | - Updated `BindInFluentSyntax` with `getIdentifier`. 57 | - Updated `Container.unbind` to handle `BindingIdentifier`. 58 | - Updated `BindOnFluentSyntax` with `getIdentifier`. 59 | - Updated `BindWhenFluentSyntax` with `getIdentifier`. 60 | 61 | ## [7.1.0] 62 | 63 | ### Added 64 | - Added `BindingActivation`. 65 | - Added `BindingDeactivation`. 66 | 67 | ## [7.0.2] 68 | 69 | ### Changed 70 | - Updated `container.get` like methods to no longer initialize twice singleton scoped bindings. 71 | 72 | ## [7.0.1] 73 | 74 | ### Changed 75 | - Updated `Container.get` like methods to no longer initialize twice singleton scoped bindings. 76 | 77 | ## [7.0.0] 78 | 79 | Parity version with `7.0.0-alpha.5` 80 | 81 | ## [7.0.0-alpha.5] 82 | 83 | ### Changed 84 | - Renamed `BindingMetadata` to `BindingConstraints`. 85 | - Improved performance on `Container.get` like methods. 86 | 87 | ## [7.0.0-alpha.4] 88 | 89 | Parity version with `7.0.0-alpha.3`. 90 | 91 | ## [6.2.2] 92 | 93 | - Solved issue with npm registry. 94 | 95 | ## [7.0.0-alpha.3] 96 | 97 | ### Changed 98 | - Updated `BindToFluentSyntax` with `.toResolvedValue`. 99 | 100 | ## [7.0.0-alpha.2] 101 | 102 | ### Changed 103 | - Updated `Container` with a plan cache. `Container.get`, `Container.getAll`, `Container.getAllAsync` and `Container.getAsync` performance has been improved. 104 | 105 | ## [7.0.0-alpha.1] 106 | 107 | ### Changed 108 | - Updated `GetOptions` with `autobind`. 109 | - Updated `ContainerOptions` with `autobind`. 110 | 111 | ## [7.0.0-alpha.0] 112 | 113 | ### Added 114 | - Added `BindInFluentSyntax`. 115 | - Added `BindInWhenOnFluentSyntax`. 116 | - Added `BindOnFluentSyntax`. 117 | - Added `BindingScope`. 118 | - Added `BindToFluentSyntax`. 119 | - Added `BindWhenFluentSyntax`. 120 | - Added `BindWhenOnFluentSyntax`. 121 | - Added `ContainerModuleLoadOptions`. 122 | - Added `DynamicValueBuilder`. 123 | - Added `Factory`. 124 | - Added `GetOptions`. 125 | - Added `GetOptionsTagConstraint`. 126 | - Added `IsBoundOptions`. 127 | - Added `MetadataName`. 128 | - Added `MetadataTag`. 129 | - Added `MetadataTargetName`. 130 | - Added `OptionalGetOptions`. 131 | - Added `Provider`. 132 | - Added `ResolutionContext`. 133 | - Added `bindingScopeValues`. 134 | - Added `bindingTypeValues`. 135 | - Added `injectFromBase` decorator. 136 | 137 | ### Changed 138 | - Updated `injectable` with optional `scope`. 139 | - [Breaking] Updated `ContainerModule` constructor to receive a callback with `ContainerModuleLoadOptions` instead of `interfaces.ContainerModuleCallBack`. 140 | - [Breaking] Updated `ContainerModule`.load to return `Promise`. 141 | - Updated `ContainerOptions` with `parent`. 142 | - Updated `ContainerOptions` without `autoBindInjectable` and `skipBaseClassChecks`. 143 | - [Breaking] Updated `Container` to no longer expose `id`, `parent` nor `options`. 144 | - [Breaking] Updated `Container` with no `applyCustomMetadataReader`, `applyMiddleware`, `createChild`, `merge` and `rebind` methods. 145 | - [Breaking] Updated `Container` with no `isCurrentBound`, `isBoundNamed`, `isBoundTagged` methods in favor of using `Container.isBound` with `isBoundOptions`. 146 | - [Breaking] Updated `Container` with no `getNamed`, `getTagged`, `tryGet`, `tryGetNamed` and `tryGetTagged` methods in favor of `Container.get` with `OptionalGetOptions` options. 147 | - [Breaking] Updated `Container` with no `getNamedAsync`, `getTaggedAsync`, `tryGetAsync`, `tryGetNamedAsync` and `tryGetTaggedAsync` methods in favor of `Container.getAsync` with `OptionalGetOptions` options. 148 | - [Breaking] Updated `Container` with no `getAllNamed`, `getAllTagged`, `tryGetAll`, `tryGetAllNamed` and `tryGetAllTagged` methods in favor of `Container.getAll` with `GetOptions` options. 149 | - [Breaking] Updated `Container` with no `getAllNamedAsync`, `getAllTaggedAsync`, `tryGetAllAsync`, `tryGetAllNamedAsync` and `tryGetAllTaggedAsync` methods in favor of `Container.getAllAsync` with `GetOptions` options. 150 | - [Breaking] Updated `Container` with no `loadAsync` in favor of an async `Container.load`. 151 | - [Breaking] Updated `Container` with no `unbindAsync` in favor of an async `Container.unbind`. 152 | - [Breaking] Updated `Container` with no `unbindAllAsync` in favor of an async `Container.unbindAll`. 153 | - [Breaking] Updated `Container` with no `unloadAsync` in favor of an async `Container.unload`. 154 | 155 | 156 | ### Fixed 157 | - Updated `decorate` to no longer require a unexpected prototypes to decorate property nor methods. 158 | 159 | ### Removed 160 | - [Breaking] Removed deprecated `LazyServiceIdentifer`. Use `LazyServiceIdentifier` instead. 161 | - [Breaking] Removed `BindingScopeEnum`. Use `bindingScopeValues` instead. 162 | - [Breaking] Removed `BindingTypeEnum`. 163 | - [Breaking] Removed `TargetTypeEnum`. 164 | - [Breaking] Removed `METADATA_KEY`. 165 | - [Breaking] Removed `AsyncContainerModule`. Use `ContainerModule` instead. 166 | - [Breaking] Removed `createTaggedDecorator`. 167 | - [Breaking] Removed `MetadataReader`. 168 | - [Breaking] Removed `id`. 169 | - [Breaking] Removed `interfaces` types. Rely on new types instead. 170 | - [Breaking] Removed `traverseAncerstors`. 171 | - [Breaking] Removed `taggedConstraint`. 172 | - [Breaking] Removed `namedConstraint`. 173 | - [Breaking] Removed `typeConstraint`. 174 | - [Breaking] Removed `getServiceIdentifierAsString`. 175 | - [Breaking] Removed `multiBindToService`. 176 | 177 | 178 | ## [6.2.1] 179 | 180 | ### Fixed 181 | - Added missing `LazyServiceIdentifer`. 182 | 183 | ## [6.2.0] 184 | 185 | ### Added 186 | - Added `interfaces.GetAllOptions`. 187 | 188 | ### Changed 189 | - Updated `container.getAll` with `options` optional param. 190 | - Updated `container.getAllAsync` with `options` optional param. 191 | - Updated `interfaces.NextArgs` with optional `isOptional` param. 192 | - Updated `container` with `tryGet`. 193 | - Updated `container` with `tryGetAsync`. 194 | - Updated `container` with `tryGetTagged`. 195 | - Updated `container` with `tryGetTaggedAsync`. 196 | - Updated `container` with `tryGetNamed`. 197 | - Updated `container` with `tryGetNamedAsync`. 198 | - Updated `container` with `tryGetAll`. 199 | - Updated `container` with `tryGetAllAsync`. 200 | - Updated `container` with `tryGetAllTagged`. 201 | - Updated `container` with `tryGetAllTaggedAsync`. 202 | - Updated `container` with `tryGetAllNamed`. 203 | - Updated `container` with `tryGetAllNamedAsync`. 204 | 205 | ## [6.2.0-beta.1] 206 | 207 | ### Added 208 | 209 | ### Changed 210 | - Updated `interfaces.NextArgs` with optional `isOptional` param. 211 | - Updated `container` with `tryGet`. 212 | - Updated `container` with `tryGetAsync`. 213 | - Updated `container` with `tryGetTagged`. 214 | - Updated `container` with `tryGetTaggedAsync`. 215 | - Updated `container` with `tryGetNamed`. 216 | - Updated `container` with `tryGetNamedAsync`. 217 | - Updated `container` with `tryGetAll`. 218 | - Updated `container` with `tryGetAllAsync`. 219 | - Updated `container` with `tryGetAllTagged`. 220 | - Updated `container` with `tryGetAllTaggedAsync`. 221 | - Updated `container` with `tryGetAllNamed`. 222 | - Updated `container` with `tryGetAllNamedAsync`. 223 | 224 | ### Fixed 225 | 226 | ## [6.2.0-beta.0] 227 | 228 | ### Added 229 | - Added `interfaces.GetAllOptions`. 230 | 231 | ### Changed 232 | - Updated `container.getAll` with `options` optional param. 233 | - Updated `container.getAllAsync` with `options` optional param. 234 | 235 | ### Fixed 236 | 237 | ## [6.1.6] 238 | 239 | ### Fixed 240 | - Fixed unexpected property access while running planning checks on injected base types. 241 | - Updated ESM sourcemaps to refelct the right source code files. 242 | 243 | ## [6.1.5] 244 | 245 | ### Changed 246 | - Updated library to import `reflect-metadata`. Importing `reflect-metadata` before bootstraping a module in the userland is no longer required. 247 | 248 | ### Fixed 249 | - Updated ESM build to provide proper types regardless of the ts resolution module strategy in the userland. 250 | - Fixed container to properly resolve async `.toService` bindings. 251 | - Fixed `.toService` binding to properly disable caching any values. 252 | 253 | ## [6.1.5-beta.2] 254 | 255 | ### Fixed 256 | - Updated ESM bundled types to solve circularly referenced types. 257 | 258 | ## [6.1.5-beta.1] 259 | 260 | ### Fixed 261 | - Updated ESM build to provide proper types regardless of the ts resolution module strategy in the userland. 262 | 263 | ## [6.1.5-beta.0] 264 | 265 | ### Changed 266 | - Updated library to import `reflect-metadata`. Importing `reflect-metadata` before bootstraping a module in the userland is no longer required. 267 | 268 | ### Fixed 269 | - Fixed container to properly resolve async `.toService` bindings. 270 | - Fixed `.toService` binding to properly disable caching any values. 271 | 272 | ## [6.1.4] 273 | 274 | ### Changed 275 | - Updated planner with better error description when a binding can not be properly resolved. 276 | 277 | ### Fixed 278 | - Updated container to allow deactivating singleton undefined values. 279 | - Updated ESM build to be compatible with both bundler and NodeJS module resolution algorithms. 280 | 281 | ## [6.1.4-beta.1] 282 | 283 | ### Fixed 284 | - Updated ESM build to be compatible with both bundler and NodeJS module resolution algorithms. 285 | 286 | ## [6.1.4-beta.0] 287 | 288 | ### Changed 289 | - Updated planner with better error description when a binding can not be properly resolved. 290 | 291 | ## [6.1.3] 292 | 293 | ### Fixed 294 | - Updated ESM build with missing types. 295 | 296 | ## [6.1.2] 297 | 298 | ### Changed 299 | - Updated `package.json` to include the `exports` field for better bundler support. 300 | 301 | ### Fixed 302 | - Updated fetch metadata flows with better error descriptions. 303 | 304 | ## [6.1.2-beta.1] 305 | 306 | ### Changed 307 | - Updated `package.json` to include the `exports` field for better bundler support. 308 | 309 | ## [6.1.2-beta.0] 310 | 311 | ### Fixed 312 | - Updated fetch metadata flows with better error descriptions. 313 | 314 | ## [6.1.1] 315 | 316 | ### Fixed 317 | - Bumped `@inversifyjs/common` and `@inversifyjs/core` fixing wrong dev engines constraints. 318 | 319 | ## [6.1.0] 320 | 321 | ### Changed 322 | - Updated `ServiceIdentifier` to rely on `Function` instead of `Abstract`. 323 | 324 | ### Fixed 325 | - Fixed `Target.getNameTag` with the right type: `number | string | symbol`. 326 | - Fixed `interfaces.ModuleActivationStore.addDeactivation` to enforce `serviceIdentifier` and `onDeactivation` are consistent. 327 | - Fixed `interfaces.ModuleActivationStore.addActivation` to enforce `serviceIdentifier` and `onDeactivation` are consistent. 328 | 329 | ## [6.0.3] 330 | 331 | ### Fixed 332 | property injection tagged as @optional no longer overrides default values with `undefined`. 333 | Updated `targetName` to be a valid `typescript@5` decorator. 334 | 335 | ## [6.0.2] 336 | 337 | ### Added 338 | Brought tests up to 100% Code Coverage 339 | 340 | ### Changed 341 | LazyIdentfier Tests 342 | Removed browser test pipeline, browserify, karma (#1542) 343 | Update all dependencies except typescript (#1531) 344 | 345 | ### Fixed 346 | Less than 100% code coverage 347 | Use default class property for @optional injected properties (#1467) 348 | Remove circular import (#1516) 349 | Fix strict type checking on @unmanaged decorator (#1499) 350 | Fix typo (LazyServiceIdentifer -> LazyServiceIdentifier) (#1483) 351 | Fix typo (circular dependency error message) (#1485) 352 | 353 | ## [6.0.1] - 2021-10-14 354 | ### Added 355 | - add API method for check dependency only in current container 356 | - createTaggedDecorator #1343 357 | - Async bindings #1132 358 | - Async binding resolution (getAllAsync, getAllNamedAsync, getAllTaggedAsync, getAsync, getNamedAsync, getTaggedAsync, rebindAsync, unbindAsync, unbindAllAsync, unloadAsync) #1132 359 | - Global onActivation / onDeactivation #1132 360 | - Parent/Child onActivation / onDeactivation #1132 361 | - Module onActivation / onDeactivation #1132 362 | - Added @preDestroy decorator #1132 363 | 364 | ### Changed 365 | - @postConstruct can target an asyncronous function #1132 366 | - Singleton scoped services cache resolved values once the result promise is fulfilled #1320 367 | 368 | ### Fixed 369 | - only inject decorator can be applied to setters #1342 370 | - Container.resolve should resolve in that container #1338 371 | 372 | ## [5.1.1] - 2021-04-25 373 | -Fix pre-publish for build artifacts 374 | 375 | ## [5.1.0] - 2021-04-25 376 | ### Added 377 | - Upgrade information for v4.x to v5.x 378 | 379 | ### Changed 380 | - Update BindingToSyntax with `.toAutoNamedFactory()`. 381 | 382 | ### Fixed 383 | - Fix `Target.isTagged()` to exclude `optional` from tag injections #1190. 384 | - Update `toConstructor`, `toFactory`, `toFunction`, `toAutoFactory`, `toProvider` and `toConstantValue` to have singleton scope #1297. 385 | - Fix injection on optional properties when targeting ES6 #928 386 | 387 | ## [5.0.1] - 2018-10-17 388 | ### Added 389 | - Updating constructor injection wiki document with concrete injection example #922 390 | 391 | ### Changed 392 | - Change GUID to incremented counter for better performance #882 393 | 394 | ### Fixed 395 | - fix broken compilation by adding `.toString()` so symbols serialization #893 396 | - Fix problem with applying options on Container.resolve (fix #914) #915 397 | - Fixed documentation issues 398 | 399 | ## [4.14.0] - 2018-10-16 400 | Deprecated - Replaced by 5.0.1 401 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at remo.jansen@wolksoftware.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Inversify 2 | 3 | ## Setup 4 | 5 | 1 Clone your fork of the repository 6 | 7 | ```sh 8 | git clone https://github.com/YOUR_USERNAME/InversifyJS.git 9 | ``` 10 | 11 | 2 Install npm dependencies 12 | 13 | ```sh 14 | npm install 15 | ``` 16 | 17 | 3 Run build process 18 | 19 | ```sh 20 | npm test 21 | ``` 22 | 23 | ## Guidelines 24 | 25 | - Please try to [combine multiple commits before pushing](http://stackoverflow.com/questions/6934752/combining-multiple-commits-before-pushing-in-git) 26 | 27 | - Please use `TDD` when fixing bugs. This means that you should write a unit test that fails because it reproduces the issue, then fix the issue and finally run the test to ensure that the issue has been resolved. This helps us prevent fixed bugs from happening again in the future 28 | 29 | - Please keep the test coverage at 100%. Write additional unit tests if necessary 30 | 31 | - Please create an issue before sending a PR if it is going to change the public interface of InversifyJS or includes significant architecture changes 32 | 33 | - Feel free to ask for help from other members of the InversifyJS team via the chat / mailing list or github issues 34 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | ## Current Behavior 8 | 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | 15 | ## Steps to Reproduce (for bugs) 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Your Environment 28 | 29 | * Version used: 30 | * Environment name and version (e.g. Chrome 39, node.js 5.4): 31 | * Operating System and version (desktop or mobile): 32 | * Link to your project: 33 | 34 | # Stack trace 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Remo H. Jansen 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 | 23 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Types of changes 25 | 26 | 27 | 28 | - [ ] Updated docs / Refactor code / Added a tests case (non-breaking change) 29 | - [ ] Bug fix (non-breaking change which fixes an issue) 30 | - [ ] New feature (non-breaking change which adds functionality) 31 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 32 | 33 | ## Checklist: 34 | 35 | 36 | 37 | 38 | - [ ] My code follows the code style of this project. 39 | - [ ] My change requires a change to the documentation. 40 | - [ ] I have updated the documentation accordingly. 41 | - [ ] I have read the **CONTRIBUTING** document. 42 | - [ ] I have added tests to cover my changes. 43 | - [ ] All new and existing tests passed. 44 | - [ ] I have updated the changelog. 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | NPM version 3 | NPM Downloads 4 | Docs 5 | Codecov 6 |
7 |
8 | GitHub stars 9 | 10 | Discord Server 11 |

12 | 13 | # InversifyJS 14 | A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript. 15 | 16 | ## 📕 Documentation 17 | Documentation is available at https://inversify.io 18 | 19 | ## About 20 | InversifyJS is a lightweight inversion of control (IoC) container for TypeScript and JavaScript apps. 21 | An IoC container uses a class constructor to identify and inject its dependencies. 22 | InversifyJS has a friendly API and encourages the usage of the best OOP and IoC practices. 23 | 24 | ## Motivation 25 | JavaScript now supports object oriented (OO) programming with class based inheritance. These features are great but the truth is that they are also 26 | [dangerous](https://medium.com/@dan_abramov/how-to-use-classes-and-sleep-at-night-9af8de78ccb4). 27 | 28 | We need a good OO design ([SOLID](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)), [Composite Reuse](https://en.wikipedia.org/wiki/Composition_over_inheritance), etc.) to protect ourselves from these threats. The problem is that OO design is difficult and that is exactly why we created InversifyJS. 29 | 30 | InversifyJS is a tool that helps JavaScript developers write code with good OO design. 31 | 32 | ## Philosophy 33 | InversifyJS has been developed with 4 main goals: 34 | 35 | 1. Allow JavaScript developers to write code that adheres to the SOLID principles. 36 | 37 | 2. Facilitate and encourage the adherence to the best OOP and IoC practices. 38 | 39 | 3. Add as little runtime overhead as possible. 40 | 41 | 4. Provide a state of the art development experience. 42 | 43 | ## Testimonies 44 | 45 | **[Nate Kohari](https://twitter.com/nkohari)** - Author of [Ninject](https://github.com/ninject/Ninject) 46 | 47 | > *"Nice work! I've taken a couple shots at creating DI frameworks for JavaScript and TypeScript, but the lack of RTTI really hinders things.* 48 | > *The ES7 metadata gets us part of the way there (as you've discovered). Keep up the great work!"* 49 | 50 | **[Michel Weststrate](https://twitter.com/mweststrate)** - Author of [MobX](https://github.com/mobxjs/mobx) 51 | > *Dependency injection like InversifyJS works nicely* 52 | 53 | ## Some companies using InversifyJS 54 | 55 | [](https://opensource.microsoft.com/)[](https://code.facebook.com/projects/1021334114569758/nuclide/)[](https://aws.github.io/aws-amplify/)[](https://www.plainconcepts.com/)[](https://api.slack.com/)[](http://acia.aon.com/index.php/home/) [](https://www.lonelyplanet.com/) [](https://jincor.com/) [](https://www.web-computing.de/) [](https://dcos.io/) [](https://typefox.io/) [](https://code4.ro/) [](http://www.baidu.com/) [](https://www.imdada.cn/) [](https://www.ato.gov.au/) [](https://www.kaneoh.com/) [](https://particl.io/) [](https://slackmap.com/) [](https://www.go1.com/) [](http://www.stellwagengroup.com/stellwagen-technology/) [](https://www.edrlab.org/) [](https://www.goodgamestudios.com/) [](https://freshfox.at/) [](https://schubergphilis.com/) 56 | 57 | ## Acknowledgements 58 | 59 | Thanks a lot to all the [contributors](https://github.com/inversify/InversifyJS/graphs/contributors), all the developers out there using InversifyJS and all those that help us to spread the word by sharing content about InversifyJS online. Without your feedback and support this project would not be possible. 60 | 61 | ## License 62 | 63 | License under the MIT License (MIT) 64 | 65 | Copyright © 2015-2017 [Remo H. Jansen](http://www.remojansen.com) 66 | 67 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 68 | 69 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 70 | 71 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 72 | 73 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 74 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import eslintPrettierConfig from 'eslint-plugin-prettier/recommended'; 6 | import simpleImportSort from 'eslint-plugin-simple-import-sort'; 7 | 8 | /** 9 | * @returns {import('typescript-eslint').ConfigWithExtends} 10 | */ 11 | function buildBaseConfig() { 12 | return { 13 | extends: [ 14 | eslint.configs.recommended, 15 | ...tseslint.configs.strictTypeChecked, 16 | ], 17 | languageOptions: { 18 | parser: tseslint.parser, 19 | parserOptions: { 20 | project: './tsconfig.json', 21 | }, 22 | }, 23 | plugins: { 24 | '@typescript-eslint': tseslint.plugin, 25 | 'simple-import-sort': simpleImportSort, 26 | }, 27 | rules: { 28 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], 29 | '@typescript-eslint/explicit-member-accessibility': [ 30 | 'error', 31 | { 32 | overrides: { 33 | constructors: 'no-public', 34 | }, 35 | }, 36 | ], 37 | '@typescript-eslint/member-ordering': ['warn'], 38 | '@typescript-eslint/naming-convention': [ 39 | 'error', 40 | { 41 | selector: ['classProperty'], 42 | format: ['strictCamelCase', 'UPPER_CASE', 'snake_case'], 43 | leadingUnderscore: 'allow', 44 | }, 45 | { 46 | selector: 'typeParameter', 47 | format: ['StrictPascalCase'], 48 | prefix: ['T'], 49 | }, 50 | { 51 | selector: ['typeLike'], 52 | format: ['StrictPascalCase'], 53 | }, 54 | { 55 | selector: ['function', 'classMethod'], 56 | format: ['strictCamelCase'], 57 | leadingUnderscore: 'allow', 58 | }, 59 | { 60 | selector: ['parameter'], 61 | format: ['strictCamelCase'], 62 | leadingUnderscore: 'allow', 63 | }, 64 | { 65 | selector: ['variableLike'], 66 | format: ['strictCamelCase', 'UPPER_CASE', 'snake_case'], 67 | }, 68 | ], 69 | '@typescript-eslint/no-deprecated': 'error', 70 | '@typescript-eslint/no-duplicate-type-constituents': 'off', 71 | '@typescript-eslint/no-dynamic-delete': 'error', 72 | '@typescript-eslint/no-extraneous-class': 'off', 73 | '@typescript-eslint/no-inferrable-types': 'off', 74 | '@typescript-eslint/no-empty-interface': 'warn', 75 | '@typescript-eslint/no-explicit-any': 'error', 76 | '@typescript-eslint/no-floating-promises': ['error'], 77 | '@typescript-eslint/no-unsafe-enum-comparison': 'off', 78 | 'no-magic-numbers': 'off', 79 | '@typescript-eslint/no-magic-numbers': [ 80 | 'warn', 81 | { 82 | ignore: [0, 1], 83 | ignoreArrayIndexes: true, 84 | ignoreEnums: true, 85 | ignoreReadonlyClassProperties: true, 86 | }, 87 | ], 88 | '@typescript-eslint/no-require-imports': 'error', 89 | '@typescript-eslint/no-unnecessary-type-arguments': 'off', 90 | '@typescript-eslint/no-unused-expressions': ['error'], 91 | '@typescript-eslint/no-useless-constructor': 'error', 92 | '@typescript-eslint/prefer-for-of': 'error', 93 | '@typescript-eslint/prefer-nullish-coalescing': ['off'], 94 | '@typescript-eslint/prefer-optional-chain': 'off', 95 | '@typescript-eslint/prefer-readonly': ['warn'], 96 | '@typescript-eslint/promise-function-async': ['error'], 97 | '@typescript-eslint/require-await': 'off', 98 | '@typescript-eslint/restrict-plus-operands': [ 99 | 'error', 100 | { 101 | skipCompoundAssignments: false, 102 | }, 103 | ], 104 | '@typescript-eslint/typedef': [ 105 | 'error', 106 | { 107 | arrayDestructuring: true, 108 | arrowParameter: true, 109 | memberVariableDeclaration: true, 110 | objectDestructuring: true, 111 | parameter: true, 112 | propertyDeclaration: true, 113 | variableDeclaration: true, 114 | }, 115 | ], 116 | '@typescript-eslint/unified-signatures': 'error', 117 | '@typescript-eslint/strict-boolean-expressions': 'error', 118 | '@typescript-eslint/switch-exhaustiveness-check': [ 119 | 'error', 120 | { 121 | considerDefaultExhaustiveForUnions: true, 122 | }, 123 | ], 124 | '@typescript-eslint/no-unused-vars': [ 125 | 'warn', 126 | { 127 | args: 'all', 128 | argsIgnorePattern: '^_', 129 | caughtErrors: 'all', 130 | caughtErrorsIgnorePattern: '^_', 131 | destructuredArrayIgnorePattern: '^_', 132 | varsIgnorePattern: '^_', 133 | ignoreRestSiblings: true, 134 | }, 135 | ], 136 | 'simple-import-sort/imports': [ 137 | 'error', 138 | { 139 | groups: [['^\\u0000'], ['^node:'], ['^@?\\w'], ['^'], ['^\\.']], 140 | }, 141 | ], 142 | 'sort-keys': [ 143 | 'error', 144 | 'asc', 145 | { 146 | caseSensitive: false, 147 | natural: true, 148 | }, 149 | ], 150 | }, 151 | }; 152 | } 153 | 154 | const baseRules = buildBaseConfig(); 155 | 156 | const config = tseslint.config( 157 | { 158 | ...baseRules, 159 | files: ['**/*.ts'], 160 | ignores: ['**/*.test.ts'], 161 | }, 162 | { 163 | ...baseRules, 164 | files: ['**/*.test.ts'], 165 | rules: { 166 | ...(baseRules.rules ?? {}), 167 | '@typescript-eslint/no-confusing-void-expression': 'off', 168 | '@typescript-eslint/unbound-method': 'off', 169 | '@typescript-eslint/no-magic-numbers': 'off', 170 | }, 171 | }, 172 | /** @type {import('typescript-eslint').ConfigWithExtends} */ ( 173 | eslintPrettierConfig 174 | ), 175 | ); 176 | 177 | export default [...config]; 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Remo H. Jansen", 3 | "bugs": { 4 | "url": "https://github.com/inversify/InversifyJS/issues" 5 | }, 6 | "description": "A powerful and lightweight inversion of control container for JavaScript and Node.js apps powered by TypeScript.", 7 | "dependencies": { 8 | "@inversifyjs/common": "1.5.0", 9 | "@inversifyjs/container": "1.10.1", 10 | "@inversifyjs/core": "5.3.1" 11 | }, 12 | "devDependencies": { 13 | "@eslint/js": "9.28.0", 14 | "@rollup/plugin-terser": "0.4.4", 15 | "@rollup/plugin-typescript": "12.1.2", 16 | "@types/chai": "4.3.20", 17 | "@types/mocha": "10.0.10", 18 | "@types/sinon": "17.0.4", 19 | "@typescript-eslint/eslint-plugin": "8.33.0", 20 | "@typescript-eslint/parser": "8.33.0", 21 | "chai": "4.5.0", 22 | "eslint": "9.28.0", 23 | "eslint-config-prettier": "10.1.5", 24 | "eslint-plugin-prettier": "5.4.1", 25 | "eslint-plugin-simple-import-sort": "12.1.1", 26 | "mocha": "11.5.0", 27 | "nyc": "17.1.0", 28 | "prettier": "3.5.3", 29 | "rimraf": "6.0.1", 30 | "rollup-plugin-dts": "6.2.1", 31 | "sinon": "20.0.0", 32 | "ts-loader": "9.5.2", 33 | "ts-node": "10.9.2", 34 | "typescript": "5.8.3", 35 | "typescript-eslint": "8.33.0" 36 | }, 37 | "peerDependencies": { 38 | "reflect-metadata": "~0.2.2" 39 | }, 40 | "engines": {}, 41 | "homepage": "http://inversify.io", 42 | "jsnext:main": "es/inversify.js", 43 | "keywords": [ 44 | "dependency injection", 45 | "dependency inversion", 46 | "di", 47 | "inversion of control container", 48 | "ioc", 49 | "javascript", 50 | "node", 51 | "typescript" 52 | ], 53 | "license": "MIT", 54 | "main": "lib/cjs/index.js", 55 | "module": "lib/esm/index.js", 56 | "exports": { 57 | ".": { 58 | "import": "./lib/esm/index.js", 59 | "require": "./lib/cjs/index.js" 60 | } 61 | }, 62 | "name": "inversify", 63 | "repository": { 64 | "type": "git", 65 | "url": "https://github.com/inversify/InversifyJS.git" 66 | }, 67 | "scripts": { 68 | "build": "npm run build:cjs && npm run build:esm", 69 | "build:cjs": "tsc --build tsconfig.cjs.json && node ./scripts/writeCommonJsPackageJson.mjs ./lib/cjs", 70 | "build:esm": "rollup -c ./rollup.config.mjs && node ./scripts/writeEsmPackageJson.mjs ./lib/esm", 71 | "build:clean": "rimraf lib", 72 | "format": "prettier --write ./src/**/*.ts", 73 | "lint": "eslint ./src", 74 | "prebuild": "npm run build:clean", 75 | "prepublish": "npm run build", 76 | "test": "nyc --reporter=lcov --require ts-node/register mocha src/test/*.test.ts src/test/**/*.test.ts --reporter spec --exit", 77 | "test:cjs": "nyc --reporter=lcov mocha lib/cjs/test/*.test.js lib/cjs/test/**/*.test.js --reporter spec" 78 | }, 79 | "sideEffects": false, 80 | "version": "7.5.2" 81 | } 82 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | bracketSpacing: true, 8 | arrowParens: 'always', 9 | endOfLine: 'lf', 10 | trailingComma: 'all', 11 | }; 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "automerge": false, 4 | "extends": [ 5 | "config:recommended", 6 | ":disableRateLimiting", 7 | ":semanticCommitScopeDisabled" 8 | ], 9 | "ignoreDeps": [], 10 | "packageRules": [ 11 | { 12 | "groupName": "auto merge on patch or minor", 13 | "automerge": true, 14 | "matchUpdateTypes": ["patch", "minor"], 15 | "matchPackageNames": ["!turbo", "!typescript"] 16 | } 17 | ], 18 | "rangeStrategy": "bump", 19 | "rebaseWhen": "conflicted", 20 | "semanticCommits": "enabled", 21 | "schedule": ["at any time"] 22 | } 23 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | import terser from '@rollup/plugin-terser'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import { dts } from 'rollup-plugin-dts'; 6 | 7 | /** 8 | * @param {string} path 9 | * @returns {Promise} 10 | */ 11 | async function pathExists(path) { 12 | try { 13 | await fs.access(path); 14 | return true; 15 | } catch (_err) { 16 | return false; 17 | } 18 | } 19 | 20 | const PACKAGE_JSON_PATH = './package.json'; 21 | 22 | if (!pathExists(PACKAGE_JSON_PATH)) { 23 | throw new Error(`Expected "${PACKAGE_JSON_PATH}" path to exist`); 24 | } 25 | 26 | const packageJsonObject = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH)); 27 | const packageDependencies = Object.keys(packageJsonObject.dependencies ?? {}); 28 | const packagePeerDependencies = Object.keys( 29 | packageJsonObject.peerDependencies ?? {}, 30 | ); 31 | 32 | /** @type {!import("rollup").MergedRollupOptions[]} */ 33 | export default [ 34 | { 35 | input: './src/index.ts', 36 | external: [...packageDependencies, ...packagePeerDependencies], 37 | output: [ 38 | { 39 | dir: './lib/esm', 40 | format: 'esm', 41 | sourcemap: true, 42 | sourcemapPathTransform: (relativeSourcePath) => { 43 | // Rollup seems to generate source maps pointing to the wrong directory. Ugly patch to fix it 44 | if (relativeSourcePath.startsWith('../')) { 45 | return relativeSourcePath.slice(3); 46 | } else { 47 | return relativeSourcePath; 48 | } 49 | }, 50 | }, 51 | ], 52 | plugins: [ 53 | typescript({ 54 | tsconfig: './tsconfig.esm.json', 55 | }), 56 | terser(), 57 | ], 58 | }, 59 | { 60 | input: 'lib/esm/index.d.ts', 61 | output: [{ file: 'lib/esm/index.d.ts', format: 'es' }], 62 | plugins: [ 63 | dts({ 64 | tsconfig: './tsconfig.esm.json', 65 | }), 66 | ], 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /scripts/writeCommonJsPackageJson.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'node:fs/promises'; 4 | import { argv } from 'node:process'; 5 | import path from 'node:path'; 6 | import { writeFile } from 'node:fs/promises'; 7 | 8 | /** 9 | * @param {string} path 10 | * @returns {Promise} 11 | */ 12 | async function pathExists(path) { 13 | try { 14 | await fs.access(path); 15 | return true; 16 | } catch (_err) { 17 | return false; 18 | } 19 | } 20 | 21 | const directory = argv[2]; 22 | 23 | if (directory === undefined) { 24 | throw new Error('Expected a path'); 25 | } 26 | 27 | const directoryExists = await pathExists(directory); 28 | 29 | if (!directoryExists) { 30 | throw new Error(`Path ${directory} not found`); 31 | } 32 | 33 | const filePath = path.join(directory, 'package.json'); 34 | 35 | const packageJsonFileContent = JSON.stringify( 36 | { 37 | type: 'commonjs', 38 | }, 39 | undefined, 40 | 2, 41 | ); 42 | 43 | await writeFile(filePath, packageJsonFileContent); 44 | -------------------------------------------------------------------------------- /scripts/writeEsmPackageJson.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'node:fs/promises'; 4 | import { argv } from 'node:process'; 5 | import path from 'node:path'; 6 | import { writeFile } from 'node:fs/promises'; 7 | 8 | /** 9 | * @param {string} path 10 | * @returns {Promise} 11 | */ 12 | async function pathExists(path) { 13 | try { 14 | await fs.access(path); 15 | return true; 16 | } catch (_err) { 17 | return false; 18 | } 19 | } 20 | 21 | const directory = argv[2]; 22 | 23 | if (directory === undefined) { 24 | throw new Error('Expected a path'); 25 | } 26 | 27 | const directoryExists = await pathExists(directory); 28 | 29 | if (!directoryExists) { 30 | throw new Error(`Path ${directory} not found`); 31 | } 32 | 33 | const filePath = path.join(directory, 'package.json'); 34 | 35 | const packageJsonFileContent = JSON.stringify( 36 | { 37 | type: 'module', 38 | }, 39 | undefined, 40 | 2, 41 | ); 42 | 43 | await writeFile(filePath, packageJsonFileContent); 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | export { 4 | Newable, 5 | LazyServiceIdentifier, 6 | ServiceIdentifier, 7 | } from '@inversifyjs/common'; 8 | export { 9 | BindInFluentSyntax, 10 | BindingIdentifier, 11 | BindInWhenOnFluentSyntax, 12 | BindOnFluentSyntax, 13 | BindToFluentSyntax, 14 | BindWhenFluentSyntax, 15 | BindWhenOnFluentSyntax, 16 | BoundServiceSyntax, 17 | Container, 18 | ContainerModule, 19 | ContainerModuleLoadOptions, 20 | ContainerOptions, 21 | IsBoundOptions, 22 | } from '@inversifyjs/container'; 23 | export { 24 | BindingActivation, 25 | BindingConstraints, 26 | BindingDeactivation, 27 | BindingScope, 28 | DynamicValueBuilder, 29 | Factory, 30 | GetOptions, 31 | GetOptionsTagConstraint, 32 | MetadataName, 33 | MetadataTag, 34 | OptionalGetOptions, 35 | Provider, 36 | ResolutionContext, 37 | bindingScopeValues, 38 | bindingTypeValues, 39 | decorate, 40 | inject, 41 | injectFromBase, 42 | injectable, 43 | multiInject, 44 | named, 45 | optional, 46 | unmanaged, 47 | tagged, 48 | postConstruct, 49 | preDestroy, 50 | } from '@inversifyjs/core'; 51 | -------------------------------------------------------------------------------- /src/test/annotation/inject.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { LazyServiceIdentifier } from '@inversifyjs/common'; 4 | import { expect } from 'chai'; 5 | 6 | import { decorate, inject, ServiceIdentifier } from '../..'; 7 | 8 | class Katana {} 9 | class Shuriken {} 10 | 11 | const lazySwordId: LazyServiceIdentifier = new LazyServiceIdentifier( 12 | () => 'Sword', 13 | ); 14 | 15 | class InvalidDecoratorUsageWarrior { 16 | private readonly _primaryWeapon: Katana; 17 | private readonly _secondaryWeapon: Shuriken; 18 | 19 | constructor(primary: Katana, secondary: Shuriken) { 20 | this._primaryWeapon = primary; 21 | this._secondaryWeapon = secondary; 22 | } 23 | 24 | public test(_a: string) {} 25 | 26 | public debug() { 27 | return { 28 | primaryWeapon: this._primaryWeapon, 29 | secondaryWeapon: this._secondaryWeapon, 30 | }; 31 | } 32 | } 33 | 34 | describe('@inject', () => { 35 | it('Should throw when applied multiple times', () => { 36 | const useDecoratorMoreThanOnce: () => void = function () { 37 | decorate( 38 | [inject('Katana'), inject('Shuriken')], 39 | InvalidDecoratorUsageWarrior, 40 | 0, 41 | ); 42 | }; 43 | 44 | const msg: string = `Unexpected injection error. 45 | 46 | Cause: 47 | 48 | Unexpected injection found. Multiple @inject, @multiInject or @unmanaged decorators found 49 | 50 | Details 51 | 52 | [class: "InvalidDecoratorUsageWarrior", index: "0"]`; 53 | expect(useDecoratorMoreThanOnce).to.throw(msg); 54 | }); 55 | 56 | it('Should unwrap LazyServiceIdentifier', () => { 57 | const unwrapped: ServiceIdentifier = lazySwordId.unwrap(); 58 | 59 | expect(unwrapped).to.be.equal('Sword'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/test/annotation/injectable.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { decorate, injectable } from '../..'; 6 | 7 | describe('@injectable', () => { 8 | it('Should throw when applied multiple times', () => { 9 | @injectable() 10 | class Test {} 11 | 12 | const useDecoratorMoreThanOnce: () => void = function () { 13 | decorate([injectable(), injectable()], Test); 14 | }; 15 | 16 | expect(useDecoratorMoreThanOnce).to.throw( 17 | 'Cannot apply @injectable decorator multiple times', 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/test/annotation/multi_inject.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { decorate, multiInject } from '../..'; 6 | 7 | type Weapon = object; 8 | 9 | class InvalidDecoratorUsageWarrior { 10 | private readonly _primaryWeapon: Weapon; 11 | private readonly _secondaryWeapon: Weapon; 12 | 13 | constructor(weapons: [Weapon, Weapon]) { 14 | this._primaryWeapon = weapons[0]; 15 | this._secondaryWeapon = weapons[1]; 16 | } 17 | 18 | public test(_a: string) {} 19 | 20 | public debug() { 21 | return { 22 | primaryWeapon: this._primaryWeapon, 23 | secondaryWeapon: this._secondaryWeapon, 24 | }; 25 | } 26 | } 27 | 28 | describe('@multiInject', () => { 29 | it('Should throw when applied multiple times', () => { 30 | const useDecoratorMoreThanOnce: () => void = function () { 31 | decorate( 32 | [multiInject('Katana'), multiInject('Shuriken')], 33 | InvalidDecoratorUsageWarrior, 34 | 0, 35 | ); 36 | }; 37 | 38 | const msg: string = `Unexpected injection error. 39 | 40 | Cause: 41 | 42 | Unexpected injection found. Multiple @inject, @multiInject or @unmanaged decorators found 43 | 44 | Details 45 | 46 | [class: "InvalidDecoratorUsageWarrior", index: "0"]`; 47 | expect(useDecoratorMoreThanOnce).to.throw(msg); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/test/annotation/named.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { decorate, named } from '../..'; 6 | 7 | type Weapon = unknown; 8 | 9 | class InvalidDecoratorUsageWarrior { 10 | private readonly _primaryWeapon: Weapon; 11 | private readonly _secondaryWeapon: Weapon; 12 | 13 | constructor(primary: Weapon, secondary: Weapon) { 14 | this._primaryWeapon = primary; 15 | this._secondaryWeapon = secondary; 16 | } 17 | 18 | public test(_a: string) {} 19 | 20 | public debug() { 21 | return { 22 | primaryWeapon: this._primaryWeapon, 23 | secondaryWeapon: this._secondaryWeapon, 24 | }; 25 | } 26 | } 27 | 28 | describe('@named', () => { 29 | it('Should throw when applied multiple times', () => { 30 | const useDecoratorMoreThanOnce: () => void = function () { 31 | decorate( 32 | [named('Katana'), named('Shuriken')], 33 | InvalidDecoratorUsageWarrior, 34 | 0, 35 | ); 36 | }; 37 | 38 | const msg: string = `Unexpected injection error. 39 | 40 | Cause: 41 | 42 | Unexpected duplicated named decorator 43 | 44 | Details 45 | 46 | [class: "InvalidDecoratorUsageWarrior", index: "0"]`; 47 | 48 | expect(useDecoratorMoreThanOnce).to.throw(msg); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/test/annotation/optional.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable, optional } from '../..'; 6 | 7 | describe('@optional', () => { 8 | it('Should allow to flag dependencies as optional', () => { 9 | @injectable() 10 | class Katana { 11 | public name: string; 12 | constructor() { 13 | this.name = 'Katana'; 14 | } 15 | } 16 | 17 | @injectable() 18 | class Shuriken { 19 | public name: string; 20 | constructor() { 21 | this.name = 'Shuriken'; 22 | } 23 | } 24 | 25 | @injectable() 26 | class Ninja { 27 | public name: string; 28 | public katana: Katana; 29 | public shuriken: Shuriken; 30 | constructor( 31 | @inject('Katana') katana: Katana, 32 | @inject('Shuriken') @optional() shuriken: Shuriken, 33 | ) { 34 | this.name = 'Ninja'; 35 | this.katana = katana; 36 | this.shuriken = shuriken; 37 | } 38 | } 39 | 40 | const container: Container = new Container(); 41 | 42 | container.bind('Katana').to(Katana); 43 | container.bind('Ninja').to(Ninja); 44 | 45 | let ninja: Ninja = container.get('Ninja'); 46 | expect(ninja.name).to.eql('Ninja'); 47 | expect(ninja.katana.name).to.eql('Katana'); 48 | expect(ninja.shuriken).to.eql(undefined); 49 | 50 | container.bind('Shuriken').to(Shuriken); 51 | 52 | ninja = container.get('Ninja'); 53 | expect(ninja.name).to.eql('Ninja'); 54 | expect(ninja.katana.name).to.eql('Katana'); 55 | expect(ninja.shuriken.name).to.eql('Shuriken'); 56 | }); 57 | 58 | it('Should allow to set a default value for dependencies flagged as optional', () => { 59 | @injectable() 60 | class Katana { 61 | public name: string; 62 | constructor() { 63 | this.name = 'Katana'; 64 | } 65 | } 66 | 67 | @injectable() 68 | class Shuriken { 69 | public name: string; 70 | constructor() { 71 | this.name = 'Shuriken'; 72 | } 73 | } 74 | 75 | @injectable() 76 | class Ninja { 77 | public name: string; 78 | public katana: Katana; 79 | public shuriken: Shuriken; 80 | constructor( 81 | @inject('Katana') katana: Katana, 82 | @inject('Shuriken') 83 | @optional() 84 | shuriken: Shuriken = { name: 'DefaultShuriken' }, 85 | ) { 86 | this.name = 'Ninja'; 87 | this.katana = katana; 88 | this.shuriken = shuriken; 89 | } 90 | } 91 | 92 | const container: Container = new Container(); 93 | 94 | container.bind('Katana').to(Katana); 95 | container.bind('Ninja').to(Ninja); 96 | 97 | let ninja: Ninja = container.get('Ninja'); 98 | expect(ninja.name).to.eql('Ninja'); 99 | expect(ninja.katana.name).to.eql('Katana'); 100 | expect(ninja.shuriken.name).to.eql('DefaultShuriken'); 101 | 102 | container.bind('Shuriken').to(Shuriken); 103 | 104 | ninja = container.get('Ninja'); 105 | expect(ninja.name).to.eql('Ninja'); 106 | expect(ninja.katana.name).to.eql('Katana'); 107 | expect(ninja.shuriken.name).to.eql('Shuriken'); 108 | }); 109 | 110 | it('Should allow to set a default value for class property dependencies flagged as optional', () => { 111 | @injectable() 112 | class Katana { 113 | public name: string; 114 | constructor() { 115 | this.name = 'Katana'; 116 | } 117 | } 118 | 119 | @injectable() 120 | class Shuriken { 121 | public name: string; 122 | constructor() { 123 | this.name = 'Shuriken'; 124 | } 125 | } 126 | 127 | @injectable() 128 | class Ninja { 129 | @inject('Katana') public katana?: Katana; 130 | @inject('Shuriken') @optional() public shuriken: Shuriken = { 131 | name: 'DefaultShuriken', 132 | }; 133 | public name: string = 'Ninja'; 134 | } 135 | 136 | const container: Container = new Container(); 137 | 138 | container.bind('Katana').to(Katana); 139 | container.bind('Ninja').to(Ninja); 140 | 141 | let ninja: Ninja = container.get('Ninja'); 142 | 143 | expect(ninja.name).to.eql('Ninja'); 144 | expect(ninja.katana?.name).to.eql('Katana'); 145 | expect(ninja.shuriken.name).to.eql('DefaultShuriken'); 146 | 147 | container.bind('Shuriken').to(Shuriken); 148 | 149 | ninja = container.get('Ninja'); 150 | expect(ninja.name).to.eql('Ninja'); 151 | expect(ninja.katana?.name).to.eql('Katana'); 152 | expect(ninja.shuriken.name).to.eql('Shuriken'); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/test/annotation/post_construct.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { postConstruct } from '../..'; 6 | 7 | describe('@postConstruct', () => { 8 | it('Should throw when applied multiple times', () => { 9 | function setup() { 10 | class Katana { 11 | @postConstruct() 12 | public testMethod1() { 13 | /* ... */ 14 | } 15 | 16 | @postConstruct() 17 | public testMethod2() { 18 | /* ... */ 19 | } 20 | } 21 | Katana.toString(); 22 | } 23 | expect(setup).to.throw(''); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/test/bugs/bugs.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { 6 | BindingConstraints, 7 | Container, 8 | decorate, 9 | inject, 10 | injectable, 11 | injectFromBase, 12 | MetadataName, 13 | named, 14 | ServiceIdentifier, 15 | tagged, 16 | unmanaged, 17 | } from '../..'; 18 | 19 | describe('Bugs', () => { 20 | it('Should not throw when args length of base and derived class match (property setter)', () => { 21 | @injectable() 22 | class Warrior { 23 | public rank: string | null; 24 | constructor() { 25 | // length = 0 26 | this.rank = null; 27 | } 28 | } 29 | 30 | @injectable() 31 | class SamuraiMaster extends Warrior { 32 | constructor() { 33 | // length = 0 34 | super(); 35 | this.rank = 'master'; 36 | } 37 | } 38 | 39 | const container: Container = new Container(); 40 | container.bind(SamuraiMaster).to(SamuraiMaster); 41 | const master: SamuraiMaster = container.get(SamuraiMaster); 42 | 43 | expect(master.rank).eql('master'); 44 | }); 45 | 46 | it('Should not throw when args length of base and derived class match', () => { 47 | // Injecting into the derived class 48 | 49 | @injectable() 50 | class Warrior { 51 | protected rank: string; 52 | constructor(rank: string) { 53 | // length = 1 54 | this.rank = rank; 55 | } 56 | } 57 | 58 | // eslint-disable-next-line @typescript-eslint/typedef 59 | const TYPES = { Rank: 'Rank' }; 60 | 61 | @injectable() 62 | class SamuraiMaster extends Warrior { 63 | constructor( 64 | @inject(TYPES.Rank) @named('master') public override rank: string, // length = 1 65 | ) { 66 | super(rank); 67 | } 68 | } 69 | 70 | const container: Container = new Container(); 71 | container.bind(SamuraiMaster).to(SamuraiMaster); 72 | container 73 | .bind(TYPES.Rank) 74 | .toConstantValue('master') 75 | .whenNamed('master'); 76 | 77 | const master: SamuraiMaster = container.get(SamuraiMaster); 78 | expect(master.rank).eql('master'); 79 | }); 80 | 81 | it('Should not throw when args length of base and derived class match', () => { 82 | // Injecting into the derived class with multiple args 83 | 84 | @injectable() 85 | class Warrior { 86 | protected rank: string; 87 | constructor(rank: string) { 88 | // length = 1 89 | this.rank = rank; 90 | } 91 | } 92 | 93 | interface Weapon { 94 | name: string; 95 | } 96 | 97 | @injectable() 98 | class Katana implements Weapon { 99 | public name: string; 100 | constructor() { 101 | this.name = 'Katana'; 102 | } 103 | } 104 | 105 | // eslint-disable-next-line @typescript-eslint/typedef 106 | const TYPES = { 107 | Rank: 'Rank', 108 | Weapon: 'Weapon', 109 | }; 110 | 111 | @injectable() 112 | class SamuraiMaster extends Warrior { 113 | public weapon: Weapon; 114 | constructor( 115 | @inject(TYPES.Rank) @named('master') public override rank: string, 116 | @inject(TYPES.Weapon) weapon: Weapon, 117 | ) { 118 | // length = 2 119 | super(rank); 120 | this.weapon = weapon; 121 | } 122 | } 123 | 124 | const container: Container = new Container(); 125 | container.bind(TYPES.Weapon).to(Katana); 126 | container.bind(SamuraiMaster).to(SamuraiMaster); 127 | container 128 | .bind(TYPES.Rank) 129 | .toConstantValue('master') 130 | .whenNamed('master'); 131 | 132 | const master: SamuraiMaster = container.get(SamuraiMaster); 133 | expect(master.rank).eql('master'); 134 | expect(master.weapon.name).eql('Katana'); 135 | }); 136 | 137 | it('Should be able to convert a Symbol value to a string', () => { 138 | type Weapon = unknown; 139 | 140 | // eslint-disable-next-line @typescript-eslint/typedef 141 | const TYPES = { 142 | Weapon: Symbol.for('Weapon'), 143 | }; 144 | 145 | const container: Container = new Container(); 146 | const throwF: () => void = () => { 147 | container.get(TYPES.Weapon); 148 | }; 149 | 150 | expect(throwF).to.throw(''); 151 | }); 152 | 153 | it('Should be able to combine tagged injection and constant value bindings', () => { 154 | const container: Container = new Container(); 155 | 156 | type Intl = unknown; 157 | 158 | container 159 | .bind('Intl') 160 | .toConstantValue({ hello: 'bonjour' }) 161 | .whenTagged('lang', 'fr'); 162 | container 163 | .bind('Intl') 164 | .toConstantValue({ goodbye: 'au revoir' }) 165 | .whenTagged('lang', 'fr'); 166 | 167 | const f: () => void = function () { 168 | container.get('Intl', { 169 | tag: { 170 | key: 'lang', 171 | value: 'fr', 172 | }, 173 | }); 174 | }; 175 | expect(f).to.throw(); 176 | }); 177 | 178 | it('Should be able to combine dynamic value with singleton scope', () => { 179 | const container: Container = new Container(); 180 | 181 | container 182 | .bind('transient_random') 183 | .toDynamicValue(() => Math.random()) 184 | .inTransientScope(); 185 | 186 | container 187 | .bind('singleton_random') 188 | .toDynamicValue(() => Math.random()) 189 | .inSingletonScope(); 190 | 191 | const a: number = container.get('transient_random'); 192 | const b: number = container.get('transient_random'); 193 | 194 | expect(a).not.to.eql(b); 195 | 196 | const c: number = container.get('singleton_random'); 197 | const d: number = container.get('singleton_random'); 198 | 199 | expect(c).to.eql(d); 200 | }); 201 | 202 | it('Should be able to use an abstract class as the serviceIdentifier', () => { 203 | @injectable() 204 | abstract class Animal { 205 | protected name: string; 206 | constructor(@unmanaged() name: string) { 207 | this.name = name; 208 | } 209 | public move(meters: number) { 210 | return `${this.name} moved ${meters.toString()}m`; 211 | } 212 | public abstract makeSound(input: string): string; 213 | } 214 | 215 | @injectable() 216 | class Snake extends Animal { 217 | constructor() { 218 | super('Snake'); 219 | } 220 | public makeSound(input: string): string { 221 | return 'sssss' + input; 222 | } 223 | public override move() { 224 | return 'Slithering... ' + super.move(5); 225 | } 226 | } 227 | 228 | @injectable() 229 | class Jungle { 230 | public animal: Animal; 231 | constructor(@inject(Animal) animal: Animal) { 232 | this.animal = animal; 233 | } 234 | } 235 | 236 | const container: Container = new Container(); 237 | container.bind(Animal).to(Snake); 238 | container.bind(Jungle).to(Jungle); 239 | 240 | const jungle: Jungle = container.get(Jungle); 241 | expect(jungle.animal.makeSound('zzz')).to.eql('ssssszzz'); 242 | 243 | expect(jungle.animal.move(5)).to.eql('Slithering... Snake moved 5m'); 244 | }); 245 | 246 | it('Should not be able to get a named dependency if no named bindings are registered', () => { 247 | // eslint-disable-next-line @typescript-eslint/typedef 248 | const TYPES = { 249 | Weapon: 'Weapon', 250 | }; 251 | 252 | interface Weapon { 253 | name: string; 254 | } 255 | 256 | @injectable() 257 | class Katana implements Weapon { 258 | public name: string; 259 | constructor() { 260 | this.name = 'Katana'; 261 | } 262 | } 263 | 264 | const container: Container = new Container(); 265 | container.bind(TYPES.Weapon).to(Katana).whenNamed('sword'); 266 | 267 | const throws: () => void = () => { 268 | container.get(TYPES.Weapon, { 269 | name: 'bow', 270 | }); 271 | }; 272 | 273 | const error: string = `No bindings found for service: "Weapon". 274 | 275 | Trying to resolve bindings for "Weapon (Root service)"`; 276 | 277 | expect(throws).to.throw(error); 278 | }); 279 | 280 | it('Should throw a friendly error when binding a non-class using toSelf', () => { 281 | const container: Container = new Container(); 282 | const throws: () => void = () => { 283 | container.bind('testId').toSelf(); 284 | }; 285 | expect(throws).to.throw(''); 286 | }); 287 | 288 | it('Should be able to inject into an abstract class', () => { 289 | type Weapon = unknown; 290 | 291 | @injectable() 292 | abstract class BaseSoldier { 293 | public weapon: Weapon; 294 | constructor(@inject('Weapon') weapon: Weapon) { 295 | this.weapon = weapon; 296 | } 297 | } 298 | 299 | @injectable() 300 | @injectFromBase({ 301 | extendConstructorArguments: true, 302 | }) 303 | class Soldier extends BaseSoldier {} 304 | 305 | @injectable() 306 | @injectFromBase({ 307 | extendConstructorArguments: true, 308 | }) 309 | class Archer extends BaseSoldier {} 310 | 311 | @injectable() 312 | @injectFromBase({ 313 | extendConstructorArguments: true, 314 | }) 315 | class Knight extends BaseSoldier {} 316 | 317 | @injectable() 318 | class Sword {} 319 | 320 | @injectable() 321 | class Bow {} 322 | 323 | @injectable() 324 | class DefaultWeapon {} 325 | 326 | const container: Container = new Container(); 327 | 328 | function whenIsAndIsNamed( 329 | serviceIdentifier: ServiceIdentifier, 330 | name: MetadataName, 331 | ): (bindingConstraints: BindingConstraints) => boolean { 332 | return (bindingConstraints: BindingConstraints): boolean => 333 | bindingConstraints.serviceIdentifier === serviceIdentifier && 334 | bindingConstraints.name === name; 335 | } 336 | 337 | container 338 | .bind('Weapon') 339 | .to(DefaultWeapon) 340 | .whenParent(whenIsAndIsNamed('BaseSoldier', 'default')); 341 | container 342 | .bind('Weapon') 343 | .to(Sword) 344 | .whenParent(whenIsAndIsNamed('BaseSoldier', 'knight')); 345 | container 346 | .bind('Weapon') 347 | .to(Bow) 348 | .whenParent(whenIsAndIsNamed('BaseSoldier', 'archer')); 349 | container.bind('BaseSoldier').to(Soldier).whenNamed('default'); 350 | container.bind('BaseSoldier').to(Knight).whenNamed('knight'); 351 | container.bind('BaseSoldier').to(Archer).whenNamed('archer'); 352 | 353 | const soldier: BaseSoldier = container.get('BaseSoldier', { 354 | name: 'default', 355 | }); 356 | const knight: BaseSoldier = container.get('BaseSoldier', { 357 | name: 'knight', 358 | }); 359 | const archer: BaseSoldier = container.get('BaseSoldier', { 360 | name: 'archer', 361 | }); 362 | 363 | expect(soldier.weapon instanceof DefaultWeapon).to.eql(true); 364 | expect(knight.weapon instanceof Sword).to.eql(true); 365 | expect(archer.weapon instanceof Bow).to.eql(true); 366 | }); 367 | 368 | it('Should be able apply inject to property shortcut', () => { 369 | interface Weapon { 370 | use(): string; 371 | } 372 | 373 | @injectable() 374 | class Katana implements Weapon { 375 | public use() { 376 | return 'Used Katana!'; 377 | } 378 | } 379 | 380 | @injectable() 381 | class Ninja { 382 | constructor( 383 | @inject('Weapon') @named('sword') private readonly _weapon: Weapon, 384 | ) { 385 | // 386 | } 387 | public fight() { 388 | return this._weapon.use(); 389 | } 390 | } 391 | 392 | const container: Container = new Container(); 393 | container.bind('Weapon').to(Katana).whenNamed('sword'); 394 | container.bind(Ninja).toSelf(); 395 | 396 | const ninja: Ninja = container.get(Ninja); 397 | expect(ninja.fight()).eql('Used Katana!'); 398 | }); 399 | 400 | it('Should be able to inject into abstract base class without decorators', () => { 401 | // eslint-disable-next-line @typescript-eslint/typedef 402 | const TYPES = { 403 | Warrior: 'Warrior', 404 | Weapon: 'Weapon', 405 | }; 406 | 407 | // eslint-disable-next-line @typescript-eslint/typedef 408 | const TAGS = { 409 | Primary: 'Primary', 410 | Priority: 'Priority', 411 | Secondary: 'Secondary', 412 | }; 413 | 414 | interface Weapon { 415 | name: string; 416 | } 417 | 418 | @injectable() 419 | class Katana implements Weapon { 420 | public name: string; 421 | constructor() { 422 | this.name = 'Katana'; 423 | } 424 | } 425 | 426 | @injectable() 427 | class Shuriken implements Weapon { 428 | public name: string; 429 | constructor() { 430 | this.name = 'Shuriken'; 431 | } 432 | } 433 | 434 | interface Warrior { 435 | name: string; 436 | primaryWeapon: Weapon; 437 | } 438 | 439 | abstract class BaseWarrior implements Warrior { 440 | public name: string; 441 | public primaryWeapon!: Weapon; 442 | 443 | constructor(@unmanaged() name: string) { 444 | this.name = name; 445 | } 446 | } 447 | 448 | // @injectable() 449 | decorate([injectable()], BaseWarrior); 450 | 451 | // @inject(TYPES.Weapon) 452 | inject(TYPES.Weapon)(BaseWarrior.prototype, 'primaryWeapon'); 453 | 454 | // @tagged(TAGS.Priority, TAGS.Primary) 455 | tagged(TAGS.Priority, TAGS.Primary)(BaseWarrior.prototype, 'primaryWeapon'); 456 | 457 | @injectable() 458 | @injectFromBase({ 459 | extendProperties: true, 460 | }) 461 | class Samurai extends BaseWarrior { 462 | @inject(TYPES.Weapon) 463 | @tagged(TAGS.Priority, TAGS.Secondary) 464 | public secondaryWeapon!: Weapon; 465 | 466 | constructor() { 467 | super('Samurai'); 468 | } 469 | } 470 | 471 | const container: Container = new Container(); 472 | container.bind(TYPES.Warrior).to(Samurai); 473 | container 474 | .bind(TYPES.Weapon) 475 | .to(Katana) 476 | .whenTagged(TAGS.Priority, TAGS.Primary); 477 | container 478 | .bind(TYPES.Weapon) 479 | .to(Shuriken) 480 | .whenTagged(TAGS.Priority, TAGS.Secondary); 481 | 482 | const samurai: Samurai = container.get(TYPES.Warrior); 483 | 484 | expect(samurai.name).to.eql('Samurai'); 485 | expect(samurai.secondaryWeapon).not.to.eql(undefined); 486 | expect(samurai.secondaryWeapon.name).to.eql('Shuriken'); 487 | expect(samurai.primaryWeapon).not.to.eql(undefined); 488 | expect(samurai.primaryWeapon.name).to.eql('Katana'); 489 | }); 490 | 491 | it('Should be able to combine unmanaged and managed injections ', () => { 492 | interface Model { 493 | instance: T; 494 | } 495 | 496 | interface RepoBaseInterface { 497 | model: Model; 498 | } 499 | 500 | class Type { 501 | public name: string; 502 | constructor() { 503 | this.name = 'Type'; 504 | } 505 | } 506 | 507 | @injectable() 508 | class RepoBase implements RepoBaseInterface { 509 | public model: Model; 510 | 511 | constructor( 512 | // using @unmanaged() here is right 513 | // because entityType is NOT Injected by inversify 514 | @unmanaged() entityType: new () => T, 515 | ) { 516 | this.model = { instance: new entityType() }; 517 | } 518 | } 519 | 520 | @injectable() 521 | class TypedRepo extends RepoBase { 522 | constructor() { 523 | super(Type); // unmanaged injection (NOT Injected by inversify) 524 | } 525 | } 526 | 527 | @injectable() 528 | class BlBase { 529 | public repository: RepoBaseInterface; 530 | 531 | constructor( 532 | // using @unmanaged() here would wrong 533 | // because repository is injected by inversify 534 | repository: RepoBaseInterface, 535 | ) { 536 | this.repository = repository; 537 | } 538 | } 539 | 540 | @injectable() 541 | class TypedBl extends BlBase { 542 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 543 | constructor(repository: TypedRepo) { 544 | super(repository); 545 | } 546 | } 547 | 548 | const container: Container = new Container(); 549 | container.bind(TypedRepo).toSelf(); 550 | container.bind('TypedBL').to(TypedBl); 551 | 552 | const typedBl: TypedBl = container.get('TypedBL'); 553 | expect(typedBl.repository.model.instance.name).to.eq(new Type().name); 554 | }); 555 | 556 | it('Should allow missing annotations in base classes', () => { 557 | @injectable() 558 | class Katana implements Katana { 559 | public hit() { 560 | return 'cut!'; 561 | } 562 | } 563 | 564 | abstract class Warrior { 565 | private readonly _katana: Katana; 566 | 567 | constructor(@unmanaged() katana: Katana) { 568 | this._katana = katana; 569 | } 570 | 571 | public fight() { 572 | return this._katana.hit(); 573 | } 574 | } 575 | 576 | @injectable() 577 | class Ninja extends Warrior { 578 | constructor(@inject('Katana') katana: Katana) { 579 | super(katana); 580 | } 581 | } 582 | 583 | const container: Container = new Container(); 584 | container.bind('Ninja').to(Ninja); 585 | container.bind('Katana').to(Katana); 586 | 587 | const tryGet: () => void = () => { 588 | container.get('Ninja'); 589 | }; 590 | 591 | expect(tryGet).not.to.throw(); 592 | }); 593 | }); 594 | -------------------------------------------------------------------------------- /src/test/bugs/issue_1190.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable, named, optional } from '../../index'; 6 | 7 | describe('Issue 1190', () => { 8 | it('should inject a katana as default weapon to ninja', () => { 9 | // eslint-disable-next-line @typescript-eslint/typedef 10 | const TYPES = { 11 | Weapon: 'Weapon', 12 | }; 13 | 14 | // eslint-disable-next-line @typescript-eslint/typedef 15 | const TAG = { 16 | throwable: 'throwable', 17 | }; 18 | 19 | interface Weapon { 20 | name: string; 21 | } 22 | 23 | @injectable() 24 | class Katana implements Weapon { 25 | public name: string; 26 | constructor() { 27 | this.name = 'Katana'; 28 | } 29 | } 30 | 31 | @injectable() 32 | class Shuriken implements Weapon { 33 | public name: string; 34 | constructor() { 35 | this.name = 'Shuriken'; 36 | } 37 | } 38 | 39 | @injectable() 40 | class Ninja { 41 | public name: string; 42 | public katana: Katana; 43 | public shuriken: Shuriken; 44 | constructor( 45 | @inject(TYPES.Weapon) @optional() katana: Weapon, 46 | @inject(TYPES.Weapon) @named(TAG.throwable) shuriken: Weapon, 47 | ) { 48 | this.name = 'Ninja'; 49 | this.katana = katana; 50 | this.shuriken = shuriken; 51 | } 52 | } 53 | 54 | const container: Container = new Container(); 55 | 56 | container.bind(TYPES.Weapon).to(Katana).whenDefault(); 57 | container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAG.throwable); 58 | 59 | container.bind('Ninja').to(Ninja); 60 | 61 | const ninja: Ninja = container.get('Ninja'); 62 | 63 | expect(ninja.katana).to.deep.eq(new Katana()); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/test/bugs/issue_1297.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | import * as sinon from 'sinon'; 5 | 6 | import { 7 | Container, 8 | Factory, 9 | injectable, 10 | Provider, 11 | ResolutionContext, 12 | } from '../..'; 13 | 14 | describe('Issue 1297', () => { 15 | it('should call onActivation once if the service is a constant value binding', () => { 16 | const container: Container = new Container(); 17 | 18 | const onActivationHandlerSpy: sinon.SinonSpy< 19 | [ResolutionContext, string], 20 | string 21 | > = sinon.spy<(_: ResolutionContext, message: string) => string>( 22 | (_: ResolutionContext, message: string) => message, 23 | ); 24 | 25 | container 26 | .bind('message') 27 | .toConstantValue('Hello world') 28 | .onActivation(onActivationHandlerSpy); 29 | 30 | container.get('message'); 31 | container.get('message'); 32 | 33 | expect(onActivationHandlerSpy.callCount).to.eq(1); 34 | }); 35 | 36 | it('should call onActivation once if the service is a factory binding', () => { 37 | @injectable() 38 | class Katana { 39 | public hit() { 40 | return 'cut!'; 41 | } 42 | } 43 | 44 | const container: Container = new Container(); 45 | 46 | const onActivationHandlerSpy: sinon.SinonSpy< 47 | [ResolutionContext, Factory], 48 | Factory 49 | > = sinon.spy< 50 | (_: ResolutionContext, instance: Factory) => Factory 51 | >((_: ResolutionContext, instance: Factory) => instance); 52 | 53 | container.bind('Katana').to(Katana); 54 | 55 | container 56 | .bind>('Factory') 57 | .toFactory( 58 | (context: ResolutionContext) => () => context.get('Katana'), 59 | ) 60 | .onActivation(onActivationHandlerSpy); 61 | 62 | container.get('Factory'); 63 | container.get('Factory'); 64 | 65 | expect(onActivationHandlerSpy.callCount).to.eq(1); 66 | }); 67 | 68 | it('should call onActivation once if the service is a provider binding', () => { 69 | @injectable() 70 | class Katana { 71 | public hit() { 72 | return 'cut!'; 73 | } 74 | } 75 | 76 | const container: Container = new Container(); 77 | 78 | const onActivationHandlerSpy: sinon.SinonSpy< 79 | [ResolutionContext, Provider], 80 | Provider 81 | > = sinon.spy< 82 | ( 83 | _: ResolutionContext, 84 | injectableObj: Provider, 85 | ) => Provider 86 | >((_: ResolutionContext, injectableObj: Provider) => injectableObj); 87 | 88 | container 89 | .bind>('Provider') 90 | .toProvider( 91 | (_context: ResolutionContext) => async () => 92 | Promise.resolve(new Katana()), 93 | ) 94 | .onActivation(onActivationHandlerSpy); 95 | 96 | container.get('Provider'); 97 | container.get('Provider'); 98 | 99 | expect(onActivationHandlerSpy.callCount).to.eq(1); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/test/bugs/issue_1416.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { describe, it } from 'mocha'; 4 | import sinon from 'sinon'; 5 | 6 | import { Container, injectable, preDestroy } from '../..'; 7 | 8 | describe('Issue 1416', () => { 9 | it('should allow providing default values on optional bindings', async () => { 10 | @injectable() 11 | class Test1 { 12 | public stub: sinon.SinonStub = sinon.stub(); 13 | 14 | @preDestroy() 15 | public destroy() { 16 | this.stub(); 17 | } 18 | } 19 | 20 | @injectable() 21 | class Test2 { 22 | public destroy(): void {} 23 | } 24 | 25 | @injectable() 26 | class Test3 { 27 | public destroy(): void {} 28 | } 29 | 30 | const container: Container = new Container({ defaultScope: 'Singleton' }); 31 | 32 | container.bind(Test1).toSelf(); 33 | container.bind(Test2).toService(Test1); 34 | container.bind(Test3).toService(Test1); 35 | 36 | const test1: Test1 = container.get(Test1); 37 | container.get(Test2); 38 | container.get(Test3); 39 | 40 | await Promise.all([ 41 | container.unbind(Test1), 42 | container.unbind(Test2), 43 | container.unbind(Test3), 44 | ]); 45 | 46 | sinon.assert.calledOnce(test1.stub); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/test/bugs/issue_1515.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable, multiInject } from '../..'; 6 | 7 | describe('Issue 1515', () => { 8 | it('should properly throw on circular dependency', () => { 9 | @injectable() 10 | class Circle1 { 11 | constructor(@inject('circle-2') public readonly circle2: unknown) {} 12 | } 13 | 14 | @injectable() 15 | class Circle2 { 16 | constructor(@inject('circle-1') public circle1: unknown) {} 17 | } 18 | 19 | @injectable() 20 | class Multi1 {} 21 | @injectable() 22 | class Multi2 {} 23 | @injectable() 24 | class Multi3 {} 25 | 26 | @injectable() 27 | class Top { 28 | constructor( 29 | @multiInject('multi-inject') public readonly multis: unknown[], 30 | @inject('circle-1') public readonly circle1: unknown, 31 | ) {} 32 | } 33 | 34 | const container: Container = new Container(); 35 | 36 | container.bind('multi-inject').to(Multi1); 37 | container.bind('multi-inject').to(Multi2); 38 | container.bind('multi-inject').to(Multi3); 39 | container.bind('circle-1').to(Circle1); 40 | container.bind('circle-2').to(Circle2); 41 | container.bind(Top).toSelf(); 42 | 43 | expect(() => { 44 | container.get(Top); 45 | }).to.throw( 46 | 'Circular dependency found: Top -> circle-1 -> circle-2 -> circle-1', 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/test/bugs/issue_1518.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container } from '../..'; 6 | 7 | describe('Issue 1518', () => { 8 | it('should not throw on deactivating undefined singleton values', async () => { 9 | const container: Container = new Container(); 10 | const symbol: symbol = Symbol.for('foo'); 11 | container.bind(symbol).toConstantValue(undefined); 12 | 13 | console.log(container.get(symbol)); 14 | 15 | await container.unbind('foo'); 16 | 17 | expect(() => {}).not.to.throw(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/test/bugs/issue_1564.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | import { describe, it } from 'mocha'; 5 | 6 | import { Container, inject, injectable } from '../..'; 7 | 8 | describe('Issue 1564', () => { 9 | it('should not throw on getting async services bound using "toService"', async () => { 10 | @injectable() 11 | class Database { 12 | constructor() { 13 | console.log('new Database'); 14 | } 15 | } 16 | 17 | @injectable() 18 | class Service1 { 19 | constructor(@inject(Database) public database: Database) { 20 | console.log('new Service1'); 21 | } 22 | } 23 | 24 | @injectable() 25 | class Service2 { 26 | constructor(@inject(Service1) public service1: Service1) { 27 | console.log('new Service2'); 28 | } 29 | } 30 | 31 | const container: Container = new Container({ defaultScope: 'Request' }); 32 | 33 | container.bind(Database).toDynamicValue(async () => { 34 | console.log('connecting to db...'); 35 | return new Database(); 36 | }); 37 | 38 | container.bind(Service1).toSelf(); 39 | container.bind(Service2).toSelf(); 40 | 41 | container.bind('services').toService(Service1); 42 | container.bind('services').toService(Service2); 43 | 44 | const result: unknown[] = await container.getAllAsync('services'); 45 | 46 | expect(result).to.have.length(2); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/test/bugs/issue_543.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable } from '../../index'; 6 | 7 | describe('Issue 543', () => { 8 | it('Should throw correct circular dependency path', () => { 9 | // eslint-disable-next-line @typescript-eslint/typedef 10 | const TYPE = { 11 | Child: Symbol.for('Child'), 12 | Child2: Symbol.for('Child2'), 13 | Circular: Symbol.for('Circular'), 14 | Irrelevant: Symbol.for('Irrelevant1'), 15 | Root: Symbol.for('Root'), 16 | }; 17 | 18 | @injectable() 19 | class Irrelevant {} 20 | 21 | @injectable() 22 | class Child2 { 23 | public circ: unknown; 24 | constructor(@inject(TYPE.Circular) circ: unknown) { 25 | this.circ = circ; 26 | } 27 | } 28 | 29 | @injectable() 30 | class Child { 31 | public irrelevant: Irrelevant; 32 | public child2: Child2; 33 | constructor( 34 | @inject(TYPE.Irrelevant) irrelevant: Irrelevant, 35 | @inject(TYPE.Child2) child2: Child2, 36 | ) { 37 | this.irrelevant = irrelevant; 38 | this.child2 = child2; 39 | } 40 | } 41 | 42 | @injectable() 43 | class Circular { 44 | public irrelevant: Irrelevant; 45 | public child: Child; 46 | constructor( 47 | @inject(TYPE.Irrelevant) irrelevant: Irrelevant, 48 | @inject(TYPE.Child) child: Child, 49 | ) { 50 | this.irrelevant = irrelevant; 51 | this.child = child; 52 | } 53 | } 54 | 55 | @injectable() 56 | class Root { 57 | public irrelevant: Irrelevant; 58 | public circ: Circular; 59 | constructor( 60 | @inject(TYPE.Irrelevant) irrelevant1: Irrelevant, 61 | @inject(TYPE.Circular) circ: Circular, 62 | ) { 63 | this.irrelevant = irrelevant1; 64 | this.circ = circ; 65 | } 66 | } 67 | 68 | const container: Container = new Container(); 69 | container.bind(TYPE.Root).to(Root); 70 | container.bind(TYPE.Irrelevant).to(Irrelevant); 71 | container.bind(TYPE.Circular).to(Circular); 72 | container.bind(TYPE.Child).to(Child); 73 | container.bind(TYPE.Child2).to(Child2); 74 | 75 | function throws() { 76 | return container.get(TYPE.Root); 77 | } 78 | 79 | expect(throws).to.throw( 80 | 'Circular dependency found: Symbol(Root) -> Symbol(Circular) -> Symbol(Child) -> Symbol(Child2) -> Symbol(Circular)', 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/test/bugs/issue_549.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Container, inject, injectable, ResolutionContext } from '../..'; 4 | 5 | describe('Issue 549', () => { 6 | it('Should throw if circular dependencies found with dynamics', () => { 7 | // eslint-disable-next-line @typescript-eslint/typedef 8 | const TYPE = { 9 | ADynamicValue: Symbol.for('ADynamicValue'), 10 | BDynamicValue: Symbol.for('BDynamicValue'), 11 | }; 12 | 13 | type InterfaceA = unknown; 14 | type InterfaceB = unknown; 15 | 16 | @injectable() 17 | class A { 18 | public b: InterfaceB; 19 | constructor(@inject(TYPE.BDynamicValue) b: InterfaceB) { 20 | this.b = b; 21 | } 22 | } 23 | 24 | @injectable() 25 | class B { 26 | public a: InterfaceA; 27 | constructor(@inject(TYPE.ADynamicValue) a: InterfaceA) { 28 | this.a = a; 29 | } 30 | } 31 | 32 | const container: Container = new Container({ defaultScope: 'Singleton' }); 33 | container.bind(A).toSelf(); 34 | container.bind(B).toSelf(); 35 | 36 | container 37 | .bind(TYPE.ADynamicValue) 38 | .toDynamicValue((ctx: ResolutionContext) => ctx.get(A)); 39 | 40 | container 41 | .bind(TYPE.BDynamicValue) 42 | .toDynamicValue((ctx: ResolutionContext) => ctx.get(B)); 43 | 44 | function willThrow() { 45 | return container.get(A); 46 | } 47 | 48 | try { 49 | const result: A = willThrow(); 50 | throw new Error( 51 | `This line should never be executed. Expected \`willThrow\` to throw! ${JSON.stringify(result)}`, 52 | ); 53 | } catch (e) { 54 | const localError: Error = e as Error; 55 | const expectedErrorA: string = ''; 56 | const expectedErrorB: string = ''; 57 | const matchesErrorA: boolean = 58 | localError.message.indexOf(expectedErrorA) !== -1; 59 | const matchesErrorB: boolean = 60 | localError.message.indexOf(expectedErrorB) !== -1; 61 | 62 | if (!matchesErrorA && !matchesErrorB) { 63 | throw new Error( 64 | 'Expected `willThrow` to throw:\n' + 65 | `- ${expectedErrorA}\n` + 66 | 'or\n' + 67 | `- ${expectedErrorB}\n` + 68 | 'but got\n' + 69 | `- ${localError.message}`, 70 | ); 71 | } 72 | } 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/test/bugs/issue_706.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { bindingScopeValues, Container, injectable } from '../..'; 6 | 7 | describe('Issue 706', () => { 8 | it('Should expose BindingScopeEnum as part of the public API', () => { 9 | @injectable() 10 | class SomeClass { 11 | public time: number; 12 | constructor() { 13 | this.time = new Date().getTime(); 14 | } 15 | } 16 | 17 | const container: Container = new Container({ 18 | defaultScope: bindingScopeValues.Singleton, 19 | }); 20 | 21 | // eslint-disable-next-line @typescript-eslint/typedef 22 | const TYPE = { 23 | SomeClass: Symbol.for('SomeClass'), 24 | }; 25 | 26 | container.bind(TYPE.SomeClass).to(SomeClass); 27 | 28 | const instanceOne: SomeClass = container.get(TYPE.SomeClass); 29 | const instanceTwo: SomeClass = container.get(TYPE.SomeClass); 30 | 31 | expect(instanceOne.time).to.eq(instanceTwo.time); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/bugs/issue_928.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable, optional } from '../..'; 6 | 7 | describe('Issue 928', () => { 8 | it('should inject the right instances', () => { 9 | let injectedA: unknown; 10 | let injectedB: unknown; 11 | let injectedC: unknown; 12 | 13 | // some dependencies 14 | @injectable() 15 | class DepA { 16 | public a: number = 1; 17 | } 18 | @injectable() 19 | class DepB { 20 | public b: number = 1; 21 | } 22 | @injectable() 23 | class DepC { 24 | public c: number = 1; 25 | } 26 | 27 | @injectable() 28 | abstract class AbstractCls { 29 | constructor( 30 | @inject(DepA) a: DepA, 31 | @inject(DepB) @optional() b: DepB = { b: 0 }, 32 | ) { 33 | injectedA = a; 34 | injectedB = b; 35 | } 36 | } 37 | 38 | @injectable() 39 | class Cls extends AbstractCls { 40 | constructor( 41 | @inject(DepC) c: DepC, 42 | @inject(DepB) @optional() b: DepB = { b: 0 }, 43 | @inject(DepA) a: DepA, 44 | ) { 45 | super(a, b); 46 | 47 | injectedC = c; 48 | } 49 | } 50 | 51 | const container: Container = new Container(); 52 | [DepA, DepB, DepC, Cls].forEach((i: NewableFunction) => 53 | container.bind(i).toSelf().inSingletonScope(), 54 | ); 55 | 56 | container.get(Cls); 57 | 58 | expect(injectedA).to.deep.eq(new DepA()); 59 | expect(injectedB).to.deep.eq(new DepB()); 60 | expect(injectedC).to.deep.eq(new DepC()); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/test/container/container.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | import * as sinon from 'sinon'; 5 | 6 | import { 7 | bindingScopeValues, 8 | Container, 9 | inject, 10 | injectable, 11 | postConstruct, 12 | ResolutionContext, 13 | } from '../..'; 14 | 15 | describe('Container', () => { 16 | let sandbox: sinon.SinonSandbox; 17 | 18 | beforeEach(() => { 19 | sandbox = sinon.createSandbox(); 20 | }); 21 | 22 | afterEach(() => { 23 | sandbox.restore(); 24 | }); 25 | 26 | it('Should unbind a binding when requested', async () => { 27 | @injectable() 28 | class Ninja {} 29 | const ninjaId: string = 'Ninja'; 30 | 31 | const container: Container = new Container(); 32 | container.bind(ninjaId).to(Ninja); 33 | 34 | await container.unbind(ninjaId); 35 | 36 | expect(container.isBound(ninjaId)).equal(false); 37 | }); 38 | 39 | it('Should unbind a binding when requested', async () => { 40 | @injectable() 41 | class Ninja {} 42 | 43 | @injectable() 44 | class Samurai {} 45 | 46 | const ninjaId: string = 'Ninja'; 47 | const samuraiId: string = 'Samurai'; 48 | 49 | const container: Container = new Container(); 50 | container.bind(ninjaId).to(Ninja); 51 | container.bind(samuraiId).to(Samurai); 52 | 53 | expect(container.isBound(ninjaId)).equal(true); 54 | expect(container.isBound(samuraiId)).equal(true); 55 | 56 | await container.unbind(ninjaId); 57 | 58 | expect(container.isBound(ninjaId)).equal(false); 59 | expect(container.isBound(samuraiId)).equal(true); 60 | }); 61 | 62 | it('Should be able unbound all dependencies', async () => { 63 | @injectable() 64 | class Ninja {} 65 | 66 | @injectable() 67 | class Samurai {} 68 | 69 | const ninjaId: string = 'Ninja'; 70 | const samuraiId: string = 'Samurai'; 71 | 72 | const container: Container = new Container(); 73 | container.bind(ninjaId).to(Ninja); 74 | container.bind(samuraiId).to(Samurai); 75 | 76 | expect(container.isBound(ninjaId)).equal(true); 77 | expect(container.isBound(samuraiId)).equal(true); 78 | 79 | await container.unbindAll(); 80 | 81 | expect(container.isBound(ninjaId)).equal(false); 82 | expect(container.isBound(samuraiId)).equal(false); 83 | }); 84 | 85 | it('Should NOT be able to get unregistered services', () => { 86 | @injectable() 87 | class Ninja {} 88 | const ninjaId: string = 'Ninja'; 89 | 90 | const container: Container = new Container(); 91 | const throwFunction: () => void = () => { 92 | container.get(ninjaId); 93 | }; 94 | 95 | expect(throwFunction).to.throw(''); 96 | }); 97 | 98 | it('Should NOT be able to get ambiguous match', () => { 99 | type Warrior = unknown; 100 | 101 | @injectable() 102 | class Ninja {} 103 | 104 | @injectable() 105 | class Samurai {} 106 | 107 | const warriorId: string = 'Warrior'; 108 | 109 | const container: Container = new Container(); 110 | container.bind(warriorId).to(Ninja); 111 | container.bind(warriorId).to(Samurai); 112 | 113 | const throwFunction: () => void = () => { 114 | container.get(warriorId); 115 | }; 116 | expect(throwFunction).to.throw(''); 117 | }); 118 | 119 | it('Should be able to getAll of unregistered services', () => { 120 | @injectable() 121 | class Ninja {} 122 | const ninjaId: string = 'Ninja'; 123 | 124 | const container: Container = new Container(); 125 | 126 | expect(container.getAll(ninjaId)).to.deep.equal([]); 127 | }); 128 | 129 | it('Should be able to snapshot and restore container', async () => { 130 | @injectable() 131 | class Ninja {} 132 | 133 | @injectable() 134 | class Samurai {} 135 | 136 | const container: Container = new Container(); 137 | container.bind(Ninja).to(Ninja); 138 | container.bind(Samurai).to(Samurai); 139 | 140 | expect(container.get(Samurai)).to.be.instanceOf(Samurai); 141 | expect(container.get(Ninja)).to.be.instanceOf(Ninja); 142 | 143 | container.snapshot(); // snapshot container = v1 144 | 145 | await container.unbind(Ninja); 146 | expect(container.get(Samurai)).to.be.instanceOf(Samurai); 147 | expect(() => container.get(Ninja)).to.throw(); 148 | 149 | container.snapshot(); // snapshot container = v2 150 | expect(() => container.get(Ninja)).to.throw(); 151 | 152 | container.bind(Ninja).to(Ninja); 153 | expect(container.get(Samurai)).to.be.instanceOf(Samurai); 154 | expect(container.get(Ninja)).to.be.instanceOf(Ninja); 155 | 156 | container.restore(); // restore container to v2 157 | expect(container.get(Samurai)).to.be.instanceOf(Samurai); 158 | expect(() => container.get(Ninja)).to.throw(); 159 | 160 | container.restore(); // restore container to v1 161 | expect(container.get(Samurai)).to.be.instanceOf(Samurai); 162 | expect(container.get(Ninja)).to.be.instanceOf(Ninja); 163 | 164 | expect(() => { 165 | container.restore(); 166 | }).to.throw(''); 167 | }); 168 | 169 | it('Should maintain the activation state of a singleton when doing a snapshot of a container', () => { 170 | let timesCalled: number = 0; 171 | 172 | @injectable() 173 | class Ninja { 174 | @postConstruct() 175 | public postConstruct() { 176 | timesCalled++; 177 | } 178 | } 179 | 180 | const container: Container = new Container(); 181 | 182 | container.bind(Ninja).to(Ninja).inSingletonScope(); 183 | 184 | container.get(Ninja); 185 | container.snapshot(); 186 | container.restore(); 187 | container.get(Ninja); 188 | 189 | expect(timesCalled).to.be.equal(1); 190 | }); 191 | 192 | it('Should save and restore the container activations and deactivations when snapshot and restore', async () => { 193 | const sid: string = 'sid'; 194 | const container: Container = new Container(); 195 | container.bind(sid).toConstantValue('Value'); 196 | 197 | let activated: boolean = false; 198 | let deactivated: boolean = false; 199 | 200 | container.snapshot(); 201 | 202 | container.onActivation(sid, (_c: ResolutionContext, i: string) => { 203 | activated = true; 204 | return i; 205 | }); 206 | container.onDeactivation(sid, (_i: unknown) => { 207 | deactivated = true; 208 | }); 209 | 210 | container.restore(); 211 | 212 | container.get(sid); 213 | await container.unbind(sid); 214 | 215 | expect(activated).to.equal(false); 216 | expect(deactivated).to.equal(false); 217 | }); 218 | 219 | it('Should be able to check is there are bindings available for a given identifier', () => { 220 | const warriorId: string = 'Warrior'; 221 | const warriorSymbol: symbol = Symbol.for('Warrior'); 222 | 223 | @injectable() 224 | class Ninja {} 225 | 226 | const container: Container = new Container(); 227 | container.bind(Ninja).to(Ninja); 228 | container.bind(warriorId).to(Ninja); 229 | container.bind(warriorSymbol).to(Ninja); 230 | 231 | expect(container.isBound(Ninja)).equal(true); 232 | expect(container.isBound(warriorId)).equal(true); 233 | expect(container.isBound(warriorSymbol)).equal(true); 234 | 235 | const katanaId: string = 'Katana'; 236 | const katanaSymbol: symbol = Symbol.for('Katana'); 237 | 238 | @injectable() 239 | class Katana {} 240 | 241 | expect(container.isBound(Katana)).equal(false); 242 | expect(container.isBound(katanaId)).equal(false); 243 | expect(container.isBound(katanaSymbol)).equal(false); 244 | }); 245 | 246 | it('Should be able to check is there are bindings available for a given identifier only in current container', () => { 247 | @injectable() 248 | class Ninja {} 249 | 250 | const containerParent: Container = new Container(); 251 | const containerChild: Container = new Container({ 252 | parent: containerParent, 253 | }); 254 | 255 | containerParent.bind(Ninja).to(Ninja); 256 | 257 | expect(containerParent.isBound(Ninja)).to.eql(true); 258 | expect(containerChild.isBound(Ninja)).to.eql(true); 259 | expect(containerChild.isCurrentBound(Ninja)).to.eql(false); 260 | }); 261 | 262 | it('Should be able to get services from parent container', () => { 263 | const weaponIdentifier: string = 'Weapon'; 264 | 265 | @injectable() 266 | class Katana {} 267 | 268 | const container: Container = new Container(); 269 | container.bind(weaponIdentifier).to(Katana); 270 | 271 | const childContainer: Container = new Container({ parent: container }); 272 | 273 | const secondChildContainer: Container = new Container({ 274 | parent: childContainer, 275 | }); 276 | 277 | expect(secondChildContainer.get(weaponIdentifier)).to.be.instanceOf(Katana); 278 | }); 279 | 280 | it('Should be able to check if services are bound from parent container', () => { 281 | const weaponIdentifier: string = 'Weapon'; 282 | 283 | @injectable() 284 | class Katana {} 285 | 286 | const container: Container = new Container(); 287 | container.bind(weaponIdentifier).to(Katana); 288 | 289 | const childContainer: Container = new Container({ parent: container }); 290 | 291 | const secondChildContainer: Container = new Container({ 292 | parent: childContainer, 293 | }); 294 | 295 | expect(secondChildContainer.isBound(weaponIdentifier)).to.be.equal(true); 296 | }); 297 | 298 | it('Should prioritize requested container to resolve a service identifier', () => { 299 | const weaponIdentifier: string = 'Weapon'; 300 | 301 | @injectable() 302 | class Katana {} 303 | 304 | @injectable() 305 | class DivineRapier {} 306 | 307 | const container: Container = new Container(); 308 | container.bind(weaponIdentifier).to(Katana); 309 | 310 | const childContainer: Container = new Container({ parent: container }); 311 | 312 | const secondChildContainer: Container = new Container({ 313 | parent: childContainer, 314 | }); 315 | secondChildContainer.bind(weaponIdentifier).to(DivineRapier); 316 | 317 | expect(secondChildContainer.get(weaponIdentifier)).to.be.instanceOf( 318 | DivineRapier, 319 | ); 320 | }); 321 | 322 | it('Should be able to resolve named multi-injection', () => { 323 | interface Intl { 324 | hello?: string; 325 | goodbye?: string; 326 | } 327 | 328 | const container: Container = new Container(); 329 | container 330 | .bind('Intl') 331 | .toConstantValue({ hello: 'bonjour' }) 332 | .whenNamed('fr'); 333 | container 334 | .bind('Intl') 335 | .toConstantValue({ goodbye: 'au revoir' }) 336 | .whenNamed('fr'); 337 | container 338 | .bind('Intl') 339 | .toConstantValue({ hello: 'hola' }) 340 | .whenNamed('es'); 341 | container 342 | .bind('Intl') 343 | .toConstantValue({ goodbye: 'adios' }) 344 | .whenNamed('es'); 345 | 346 | const fr: Intl[] = container.getAll('Intl', { name: 'fr' }); 347 | 348 | expect(fr.length).to.equal(2); 349 | expect(fr[0]?.hello).to.equal('bonjour'); 350 | expect(fr[1]?.goodbye).to.equal('au revoir'); 351 | 352 | const es: Intl[] = container.getAll('Intl', { name: 'es' }); 353 | 354 | expect(es.length).to.equal(2); 355 | expect(es[0]?.hello).to.equal('hola'); 356 | expect(es[1]?.goodbye).to.equal('adios'); 357 | }); 358 | 359 | it('Should be able to resolve tagged multi-injection', () => { 360 | interface Intl { 361 | hello?: string; 362 | goodbye?: string; 363 | } 364 | 365 | const container: Container = new Container(); 366 | container 367 | .bind('Intl') 368 | .toConstantValue({ hello: 'bonjour' }) 369 | .whenTagged('lang', 'fr'); 370 | container 371 | .bind('Intl') 372 | .toConstantValue({ goodbye: 'au revoir' }) 373 | .whenTagged('lang', 'fr'); 374 | container 375 | .bind('Intl') 376 | .toConstantValue({ hello: 'hola' }) 377 | .whenTagged('lang', 'es'); 378 | container 379 | .bind('Intl') 380 | .toConstantValue({ goodbye: 'adios' }) 381 | .whenTagged('lang', 'es'); 382 | 383 | const fr: Intl[] = container.getAll('Intl', { 384 | tag: { key: 'lang', value: 'fr' }, 385 | }); 386 | 387 | expect(fr.length).to.equal(2); 388 | expect(fr[0]?.hello).to.equal('bonjour'); 389 | expect(fr[1]?.goodbye).to.equal('au revoir'); 390 | 391 | const es: Intl[] = container.getAll('Intl', { 392 | tag: { key: 'lang', value: 'es' }, 393 | }); 394 | 395 | expect(es.length).to.equal(2); 396 | expect(es[0]?.hello).to.equal('hola'); 397 | expect(es[1]?.goodbye).to.equal('adios'); 398 | }); 399 | 400 | it('Should be able to resolve optional injection', async () => { 401 | const container: Container = new Container(); 402 | 403 | const serviceIdentifier: string = 'service-id'; 404 | 405 | expect(container.get(serviceIdentifier, { optional: true })).to.eq( 406 | undefined, 407 | ); 408 | expect(container.getAll(serviceIdentifier, { optional: true })).to.deep.eq( 409 | [], 410 | ); 411 | expect( 412 | await container.getAllAsync(serviceIdentifier, { optional: true }), 413 | ).to.deep.eq([]); 414 | expect( 415 | container.getAll(serviceIdentifier, { 416 | name: 'name', 417 | optional: true, 418 | }), 419 | ).to.deep.eq([]); 420 | expect( 421 | await container.getAllAsync(serviceIdentifier, { 422 | name: 'name', 423 | optional: true, 424 | }), 425 | ).to.deep.eq([]); 426 | expect( 427 | container.getAll(serviceIdentifier, { 428 | optional: true, 429 | tag: { key: 'tag', value: 'value' }, 430 | }), 431 | ).to.deep.eq([]); 432 | expect( 433 | await container.getAllAsync(serviceIdentifier, { 434 | optional: true, 435 | tag: { key: 'tag', value: 'value' }, 436 | }), 437 | ).to.deep.eq([]); 438 | expect( 439 | await container.getAsync(serviceIdentifier, { 440 | optional: true, 441 | }), 442 | ).to.eq(undefined); 443 | expect( 444 | container.get(serviceIdentifier, { 445 | name: 'name', 446 | optional: true, 447 | }), 448 | ).to.eq(undefined); 449 | expect( 450 | await container.getAsync(serviceIdentifier, { 451 | name: 'name', 452 | optional: true, 453 | }), 454 | ).to.eq(undefined); 455 | expect( 456 | container.get(serviceIdentifier, { 457 | optional: true, 458 | tag: { key: 'tag', value: 'value' }, 459 | }), 460 | ).to.eq(undefined); 461 | expect( 462 | await container.getAsync(serviceIdentifier, { 463 | optional: true, 464 | tag: { key: 'tag', value: 'value' }, 465 | }), 466 | ).to.eq(undefined); 467 | }); 468 | 469 | it('Should be able configure the default scope at a global level', () => { 470 | interface Warrior { 471 | health: number; 472 | takeHit(damage: number): void; 473 | } 474 | 475 | @injectable() 476 | class Ninja implements Warrior { 477 | public health: number; 478 | constructor() { 479 | this.health = 100; 480 | } 481 | public takeHit(damage: number) { 482 | this.health = this.health - damage; 483 | } 484 | } 485 | 486 | // eslint-disable-next-line @typescript-eslint/typedef 487 | const TYPES = { 488 | Warrior: 'Warrior', 489 | }; 490 | 491 | const container1: Container = new Container(); 492 | container1.bind(TYPES.Warrior).to(Ninja); 493 | 494 | const transientNinja1: Warrior = container1.get(TYPES.Warrior); 495 | 496 | expect(transientNinja1.health).to.equal(100); 497 | 498 | transientNinja1.takeHit(10); 499 | 500 | expect(transientNinja1.health).to.equal(90); 501 | 502 | const transientNinja2: Warrior = container1.get(TYPES.Warrior); 503 | 504 | expect(transientNinja2.health).to.equal(100); 505 | 506 | transientNinja2.takeHit(10); 507 | 508 | expect(transientNinja2.health).to.equal(90); 509 | 510 | const container2: Container = new Container({ 511 | defaultScope: bindingScopeValues.Singleton, 512 | }); 513 | container2.bind(TYPES.Warrior).to(Ninja); 514 | 515 | const singletonNinja1: Warrior = container2.get(TYPES.Warrior); 516 | 517 | expect(singletonNinja1.health).to.equal(100); 518 | 519 | singletonNinja1.takeHit(10); 520 | 521 | expect(singletonNinja1.health).to.equal(90); 522 | 523 | const singletonNinja2: Warrior = container2.get(TYPES.Warrior); 524 | 525 | expect(singletonNinja2.health).to.equal(90); 526 | 527 | singletonNinja2.takeHit(10); 528 | 529 | expect(singletonNinja2.health).to.equal(80); 530 | }); 531 | 532 | it('Should be able to override options to child containers', () => { 533 | @injectable() 534 | class Warrior {} 535 | 536 | const parent: Container = new Container({ 537 | defaultScope: bindingScopeValues.Request, 538 | }); 539 | 540 | const child: Container = new Container({ 541 | defaultScope: 'Singleton', 542 | parent, 543 | }); 544 | 545 | child.bind(Warrior).toSelf(); 546 | 547 | const singletonWarrior1: Warrior = child.get(Warrior); 548 | const singletonWarrior2: Warrior = child.get(Warrior); 549 | expect(singletonWarrior1).to.equal(singletonWarrior2); 550 | }); 551 | 552 | it('Should be able check if a named binding is bound', async () => { 553 | const zero: string = 'Zero'; 554 | const invalidDivisor: string = 'InvalidDivisor'; 555 | const validDivisor: string = 'ValidDivisor'; 556 | const container: Container = new Container(); 557 | 558 | expect(container.isBound(zero)).to.equal(false); 559 | container.bind(zero).toConstantValue(0); 560 | expect(container.isBound(zero)).to.equal(true); 561 | 562 | await container.unbindAll(); 563 | 564 | expect(container.isBound(zero)).to.equal(false); 565 | 566 | container.bind(zero).toConstantValue(0).whenNamed(invalidDivisor); 567 | 568 | expect(container.isBound(zero, { name: invalidDivisor })).to.equal(true); 569 | expect(container.isBound(zero, { name: validDivisor })).to.equal(false); 570 | 571 | container.bind(zero).toConstantValue(1).whenNamed(validDivisor); 572 | expect(container.isBound(zero, { name: invalidDivisor })).to.equal(true); 573 | expect(container.isBound(zero, { name: validDivisor })).to.equal(true); 574 | }); 575 | 576 | it('Should be able to check if a named binding is bound from parent container', () => { 577 | const zero: string = 'Zero'; 578 | const invalidDivisor: string = 'InvalidDivisor'; 579 | const validDivisor: string = 'ValidDivisor'; 580 | const container: Container = new Container(); 581 | const childContainer: Container = new Container({ parent: container }); 582 | const secondChildContainer: Container = new Container({ 583 | parent: childContainer, 584 | }); 585 | 586 | container.bind(zero).toConstantValue(0).whenNamed(invalidDivisor); 587 | 588 | expect( 589 | secondChildContainer.isBound(zero, { name: invalidDivisor }), 590 | ).to.equal(true); 591 | expect(secondChildContainer.isBound(zero, { name: validDivisor })).to.equal( 592 | false, 593 | ); 594 | 595 | container.bind(zero).toConstantValue(1).whenNamed(validDivisor); 596 | 597 | expect( 598 | secondChildContainer.isBound(zero, { name: invalidDivisor }), 599 | ).to.equal(true); 600 | expect(secondChildContainer.isBound(zero, { name: validDivisor })).to.equal( 601 | true, 602 | ); 603 | }); 604 | 605 | it('Should be able to get a tagged binding', () => { 606 | const zero: string = 'Zero'; 607 | const isValidDivisor: string = 'IsValidDivisor'; 608 | const container: Container = new Container(); 609 | 610 | container 611 | .bind(zero) 612 | .toConstantValue(0) 613 | .whenTagged(isValidDivisor, false); 614 | 615 | expect( 616 | container.get(zero, { 617 | tag: { key: isValidDivisor, value: false }, 618 | }), 619 | ).to.equal(0); 620 | 621 | container 622 | .bind(zero) 623 | .toConstantValue(1) 624 | .whenTagged(isValidDivisor, true); 625 | expect( 626 | container.get(zero, { 627 | tag: { key: isValidDivisor, value: false }, 628 | }), 629 | ).to.equal(0); 630 | expect( 631 | container.get(zero, { 632 | tag: { key: isValidDivisor, value: true }, 633 | }), 634 | ).to.equal(1); 635 | }); 636 | 637 | it('Should be able to get a tagged binding from parent container', () => { 638 | const zero: string = 'Zero'; 639 | const isValidDivisor: string = 'IsValidDivisor'; 640 | const container: Container = new Container(); 641 | const childContainer: Container = new Container({ parent: container }); 642 | const secondChildContainer: Container = new Container({ 643 | parent: childContainer, 644 | }); 645 | 646 | container 647 | .bind(zero) 648 | .toConstantValue(0) 649 | .whenTagged(isValidDivisor, false); 650 | container 651 | .bind(zero) 652 | .toConstantValue(1) 653 | .whenTagged(isValidDivisor, true); 654 | expect( 655 | secondChildContainer.get(zero, { 656 | tag: { key: isValidDivisor, value: false }, 657 | }), 658 | ).to.equal(0); 659 | expect( 660 | secondChildContainer.get(zero, { 661 | tag: { key: isValidDivisor, value: true }, 662 | }), 663 | ).to.equal(1); 664 | }); 665 | 666 | it('Should be able check if a tagged binding is bound', async () => { 667 | const zero: string = 'Zero'; 668 | const isValidDivisor: string = 'IsValidDivisor'; 669 | const container: Container = new Container(); 670 | 671 | expect(container.isBound(zero)).to.equal(false); 672 | container.bind(zero).toConstantValue(0); 673 | expect(container.isBound(zero)).to.equal(true); 674 | 675 | await container.unbindAll(); 676 | expect(container.isBound(zero)).to.equal(false); 677 | container 678 | .bind(zero) 679 | .toConstantValue(0) 680 | .whenTagged(isValidDivisor, false); 681 | expect( 682 | container.isBound(zero, { 683 | tag: { key: isValidDivisor, value: false }, 684 | }), 685 | ).to.equal(true); 686 | expect( 687 | container.isBound(zero, { 688 | tag: { key: isValidDivisor, value: true }, 689 | }), 690 | ).to.equal(false); 691 | 692 | container 693 | .bind(zero) 694 | .toConstantValue(1) 695 | .whenTagged(isValidDivisor, true); 696 | expect( 697 | container.isBound(zero, { 698 | tag: { key: isValidDivisor, value: false }, 699 | }), 700 | ).to.equal(true); 701 | expect( 702 | container.isBound(zero, { 703 | tag: { key: isValidDivisor, value: true }, 704 | }), 705 | ).to.equal(true); 706 | }); 707 | 708 | it('Should be able to check if a tagged binding is bound from parent container', () => { 709 | const zero: string = 'Zero'; 710 | const isValidDivisor: string = 'IsValidDivisor'; 711 | const container: Container = new Container(); 712 | const childContainer: Container = new Container({ parent: container }); 713 | const secondChildContainer: Container = new Container({ 714 | parent: childContainer, 715 | }); 716 | 717 | container 718 | .bind(zero) 719 | .toConstantValue(0) 720 | .whenTagged(isValidDivisor, false); 721 | expect( 722 | secondChildContainer.isBound(zero, { 723 | tag: { key: isValidDivisor, value: false }, 724 | }), 725 | ).to.equal(true); 726 | expect( 727 | secondChildContainer.isBound(zero, { 728 | tag: { key: isValidDivisor, value: true }, 729 | }), 730 | ).to.equal(false); 731 | 732 | container 733 | .bind(zero) 734 | .toConstantValue(1) 735 | .whenTagged(isValidDivisor, true); 736 | expect( 737 | secondChildContainer.isBound(zero, { 738 | tag: { key: isValidDivisor, value: false }, 739 | }), 740 | ).to.equal(true); 741 | expect( 742 | secondChildContainer.isBound(zero, { 743 | tag: { key: isValidDivisor, value: true }, 744 | }), 745 | ).to.equal(true); 746 | }); 747 | 748 | it('Should be able to override a binding using rebind', async () => { 749 | // eslint-disable-next-line @typescript-eslint/typedef 750 | const TYPES = { 751 | someType: 'someType', 752 | }; 753 | 754 | const container: Container = new Container(); 755 | container.bind(TYPES.someType).toConstantValue(1); 756 | 757 | container.bind(TYPES.someType).toConstantValue(2); 758 | 759 | const values1: unknown[] = container.getAll(TYPES.someType); 760 | expect(values1[0]).to.eq(1); 761 | 762 | expect(values1[1]).to.eq(2); 763 | 764 | await container.unbind(TYPES.someType); 765 | 766 | container.bind(TYPES.someType).toConstantValue(3); 767 | const values2: unknown[] = container.getAll(TYPES.someType); 768 | 769 | expect(values2[0]).to.eq(3); 770 | expect(values2[1]).to.eq(undefined); 771 | }); 772 | 773 | it('Should be able to resolve named multi-injection (async)', async () => { 774 | interface Intl { 775 | hello?: string; 776 | goodbye?: string; 777 | } 778 | 779 | const container: Container = new Container(); 780 | container 781 | .bind('Intl') 782 | .toDynamicValue(async () => Promise.resolve({ hello: 'bonjour' })) 783 | .whenNamed('fr'); 784 | container 785 | .bind('Intl') 786 | .toDynamicValue(async () => Promise.resolve({ goodbye: 'au revoir' })) 787 | .whenNamed('fr'); 788 | container 789 | .bind('Intl') 790 | .toDynamicValue(async () => Promise.resolve({ hello: 'hola' })) 791 | .whenNamed('es'); 792 | container 793 | .bind('Intl') 794 | .toDynamicValue(async () => Promise.resolve({ goodbye: 'adios' })) 795 | .whenNamed('es'); 796 | 797 | const fr: Intl[] = await container.getAllAsync('Intl', { 798 | name: 'fr', 799 | }); 800 | 801 | expect(fr.length).to.equal(2); 802 | expect(fr[0]?.hello).to.equal('bonjour'); 803 | expect(fr[1]?.goodbye).to.equal('au revoir'); 804 | 805 | const es: Intl[] = await container.getAllAsync('Intl', { 806 | name: 'es', 807 | }); 808 | 809 | expect(es.length).to.equal(2); 810 | expect(es[0]?.hello).to.equal('hola'); 811 | expect(es[1]?.goodbye).to.equal('adios'); 812 | }); 813 | 814 | it('Should be able to resolve named (async)', async () => { 815 | interface Intl { 816 | hello?: string; 817 | goodbye?: string; 818 | } 819 | 820 | const container: Container = new Container(); 821 | container 822 | .bind('Intl') 823 | .toDynamicValue(async () => Promise.resolve({ hello: 'bonjour' })) 824 | .whenNamed('fr'); 825 | container 826 | .bind('Intl') 827 | .toDynamicValue(async () => Promise.resolve({ hello: 'hola' })) 828 | .whenNamed('es'); 829 | 830 | const fr: Intl = await container.getAsync('Intl', { name: 'fr' }); 831 | expect(fr.hello).to.equal('bonjour'); 832 | 833 | const es: Intl = await container.getAsync('Intl', { name: 'es' }); 834 | expect(es.hello).to.equal('hola'); 835 | }); 836 | 837 | it('Should be able to resolve tagged multi-injection (async)', async () => { 838 | interface Intl { 839 | hello?: string; 840 | goodbye?: string; 841 | } 842 | 843 | const container: Container = new Container(); 844 | container 845 | .bind('Intl') 846 | .toDynamicValue(async () => Promise.resolve({ hello: 'bonjour' })) 847 | .whenTagged('lang', 'fr'); 848 | container 849 | .bind('Intl') 850 | .toDynamicValue(async () => Promise.resolve({ goodbye: 'au revoir' })) 851 | .whenTagged('lang', 'fr'); 852 | container 853 | .bind('Intl') 854 | .toDynamicValue(async () => Promise.resolve({ hello: 'hola' })) 855 | .whenTagged('lang', 'es'); 856 | container 857 | .bind('Intl') 858 | .toDynamicValue(async () => Promise.resolve({ goodbye: 'adios' })) 859 | .whenTagged('lang', 'es'); 860 | 861 | const fr: Intl[] = await container.getAllAsync('Intl', { 862 | tag: { key: 'lang', value: 'fr' }, 863 | }); 864 | 865 | expect(fr.length).to.equal(2); 866 | expect(fr[0]?.hello).to.equal('bonjour'); 867 | expect(fr[1]?.goodbye).to.equal('au revoir'); 868 | 869 | const es: Intl[] = await container.getAllAsync('Intl', { 870 | tag: { key: 'lang', value: 'es' }, 871 | }); 872 | 873 | expect(es.length).to.equal(2); 874 | expect(es[0]?.hello).to.equal('hola'); 875 | expect(es[1]?.goodbye).to.equal('adios'); 876 | }); 877 | 878 | it('Should be able to get a tagged binding (async)', async () => { 879 | const zero: string = 'Zero'; 880 | const isValidDivisor: string = 'IsValidDivisor'; 881 | const container: Container = new Container(); 882 | 883 | container 884 | .bind(zero) 885 | .toDynamicValue(async () => Promise.resolve(0)) 886 | .whenTagged(isValidDivisor, false); 887 | expect( 888 | await container.getAsync(zero, { 889 | tag: { key: isValidDivisor, value: false }, 890 | }), 891 | ).to.equal(0); 892 | 893 | container 894 | .bind(zero) 895 | .toDynamicValue(async () => Promise.resolve(1)) 896 | .whenTagged(isValidDivisor, true); 897 | expect( 898 | await container.getAsync(zero, { 899 | tag: { key: isValidDivisor, value: false }, 900 | }), 901 | ).to.equal(0); 902 | expect( 903 | await container.getAsync(zero, { 904 | tag: { key: isValidDivisor, value: true }, 905 | }), 906 | ).to.equal(1); 907 | }); 908 | 909 | it('should be able to get all the services binded (async)', async () => { 910 | const serviceIdentifier: string = 'service-identifier'; 911 | 912 | const container: Container = new Container(); 913 | 914 | const firstValueBinded: string = 'value-one'; 915 | const secondValueBinded: string = 'value-two'; 916 | const thirdValueBinded: string = 'value-three'; 917 | 918 | container.bind(serviceIdentifier).toConstantValue(firstValueBinded); 919 | container.bind(serviceIdentifier).toConstantValue(secondValueBinded); 920 | container 921 | .bind(serviceIdentifier) 922 | .toDynamicValue(async (_: ResolutionContext) => 923 | Promise.resolve(thirdValueBinded), 924 | ); 925 | const services: string[] = 926 | await container.getAllAsync(serviceIdentifier); 927 | 928 | expect(services).to.deep.eq([ 929 | firstValueBinded, 930 | secondValueBinded, 931 | thirdValueBinded, 932 | ]); 933 | }); 934 | 935 | it('Should be able to inject when symbol property key ', () => { 936 | const weaponProperty: unique symbol = Symbol(); 937 | type Weapon = unknown; 938 | @injectable() 939 | class Shuriken {} 940 | @injectable() 941 | class Ninja { 942 | @inject('Weapon') 943 | public [weaponProperty]!: Weapon; 944 | } 945 | const container: Container = new Container(); 946 | container.bind('Weapon').to(Shuriken); 947 | 948 | container.bind(Ninja).toSelf(); 949 | 950 | const myNinja: Ninja = container.get(Ninja); 951 | const weapon: Weapon = myNinja[weaponProperty]; 952 | expect(weapon).to.be.instanceOf(Shuriken); 953 | }); 954 | }); 955 | -------------------------------------------------------------------------------- /src/test/container/container_module.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | import * as sinon from 'sinon'; 5 | 6 | import { 7 | Container, 8 | ContainerModule, 9 | ContainerModuleLoadOptions, 10 | ResolutionContext, 11 | } from '../..'; 12 | 13 | describe('ContainerModule', () => { 14 | it('Should be able to set the registry of a container module', () => { 15 | const registry: ( 16 | options: ContainerModuleLoadOptions, 17 | ) => Promise = async (_options: ContainerModuleLoadOptions) => 18 | undefined; 19 | 20 | const warriors: ContainerModule = new ContainerModule(registry); 21 | 22 | expect(warriors.id).to.be.a('number'); 23 | }); 24 | 25 | it('Should be able to remove some bindings from within a container module', async () => { 26 | const container: Container = new Container(); 27 | 28 | container.bind('A').toConstantValue('1'); 29 | expect(container.get('A')).to.eql('1'); 30 | 31 | const warriors: ContainerModule = new ContainerModule( 32 | async (options: ContainerModuleLoadOptions) => { 33 | await options.unbind('A'); 34 | 35 | expect(() => { 36 | container.get('A'); 37 | }).to.throw(); 38 | 39 | options.bind('A').toConstantValue('2'); 40 | expect(container.get('A')).to.eql('2'); 41 | 42 | options.bind('B').toConstantValue('3'); 43 | expect(container.get('B')).to.eql('3'); 44 | }, 45 | ); 46 | 47 | await container.load(warriors); 48 | 49 | expect(container.get('A')).to.eql('2'); 50 | expect(container.get('B')).to.eql('3'); 51 | }); 52 | 53 | it('Should be able to check for existence of bindings within a container module', async () => { 54 | const container: Container = new Container(); 55 | container.bind('A').toConstantValue('1'); 56 | expect(container.get('A')).to.eql('1'); 57 | 58 | const warriors: ContainerModule = new ContainerModule( 59 | async (options: ContainerModuleLoadOptions) => { 60 | expect(options.isBound('A')).to.eql(true); 61 | await options.unbind('A'); 62 | expect(options.isBound('A')).to.eql(false); 63 | }, 64 | ); 65 | 66 | await container.load(warriors); 67 | }); 68 | 69 | it('Should be able to override a binding using rebind within a container module', async () => { 70 | // eslint-disable-next-line @typescript-eslint/typedef 71 | const TYPES = { 72 | someType: 'someType', 73 | }; 74 | 75 | const container: Container = new Container(); 76 | 77 | const module1: ContainerModule = new ContainerModule( 78 | async (options: ContainerModuleLoadOptions) => { 79 | options.bind(TYPES.someType).toConstantValue(1); 80 | options.bind(TYPES.someType).toConstantValue(2); 81 | }, 82 | ); 83 | 84 | const module2: ContainerModule = new ContainerModule( 85 | async (options: ContainerModuleLoadOptions) => { 86 | await options.unbind(TYPES.someType); 87 | options.bind(TYPES.someType).toConstantValue(3); 88 | }, 89 | ); 90 | 91 | await container.load(module1); 92 | 93 | const values1: unknown[] = container.getAll(TYPES.someType); 94 | expect(values1[0]).to.eq(1); 95 | 96 | expect(values1[1]).to.eq(2); 97 | 98 | await container.load(module2); 99 | 100 | const values2: unknown[] = container.getAll(TYPES.someType); 101 | 102 | expect(values2[0]).to.eq(3); 103 | expect(values2[1]).to.eq(undefined); 104 | }); 105 | 106 | it('Should be able use await async functions in container modules', async () => { 107 | const container: Container = new Container(); 108 | 109 | const someAsyncFactory: () => Promise = async () => 110 | new Promise( 111 | (res: (value: number | PromiseLike) => void) => 112 | setTimeout(() => { 113 | res(1); 114 | }, 100), 115 | ); 116 | const A: unique symbol = Symbol.for('A'); 117 | const B: unique symbol = Symbol.for('B'); 118 | 119 | const moduleOne: ContainerModule = new ContainerModule( 120 | async (options: ContainerModuleLoadOptions) => { 121 | const val: number = await someAsyncFactory(); 122 | options.bind(A).toConstantValue(val); 123 | }, 124 | ); 125 | 126 | const moduleTwo: ContainerModule = new ContainerModule( 127 | async (options: ContainerModuleLoadOptions) => { 128 | options.bind(B).toConstantValue(2); 129 | }, 130 | ); 131 | 132 | await container.load(moduleOne, moduleTwo); 133 | 134 | const aIsBound: boolean = container.isBound(A); 135 | expect(aIsBound).to.eq(true); 136 | const a: unknown = container.get(A); 137 | expect(a).to.eq(1); 138 | }); 139 | 140 | it('Should be able to add an activation hook through a container module', async () => { 141 | const container: Container = new Container(); 142 | 143 | const module: ContainerModule = new ContainerModule( 144 | async (options: ContainerModuleLoadOptions) => { 145 | options 146 | .bind('B') 147 | .toConstantValue('2') 148 | .onActivation(() => 'C'); 149 | 150 | options.onActivation('A', () => 'B'); 151 | 152 | container.bind('A').toConstantValue('1'); 153 | }, 154 | ); 155 | 156 | await container.load(module); 157 | 158 | expect(container.get('A')).to.eql('B'); 159 | expect(container.get('B')).to.eql('C'); 160 | }); 161 | 162 | it('Should be able to add a deactivation hook through a container module', async () => { 163 | const container: Container = new Container(); 164 | container.bind('A').toConstantValue('1'); 165 | 166 | let deact: boolean = false; 167 | const warriors: ContainerModule = new ContainerModule( 168 | async (options: ContainerModuleLoadOptions) => { 169 | options.onDeactivation('A', () => { 170 | deact = true; 171 | }); 172 | }, 173 | ); 174 | 175 | await container.load(warriors); 176 | container.get('A'); 177 | await container.unbind('A'); 178 | 179 | expect(deact).eql(true); 180 | }); 181 | 182 | it('Should be able to add an async deactivation hook through a container module (async)', async () => { 183 | const container: Container = new Container(); 184 | container.bind('A').toConstantValue('1'); 185 | 186 | let deact: boolean = false; 187 | 188 | const warriors: ContainerModule = new ContainerModule( 189 | async (options: ContainerModuleLoadOptions) => { 190 | options.onDeactivation('A', async () => { 191 | deact = true; 192 | }); 193 | }, 194 | ); 195 | 196 | await container.load(warriors); 197 | 198 | container.get('A'); 199 | 200 | await container.unbind('A'); 201 | 202 | expect(deact).eql(true); 203 | }); 204 | 205 | it('Should be able to add multiple async deactivation hook through a container module (async)', async () => { 206 | const onActivationHandlerSpy: sinon.SinonSpy<[], Promise> = sinon.spy< 207 | () => Promise 208 | >(async () => undefined); 209 | 210 | const serviceIdentifier: string = 'destroyable'; 211 | const container: Container = new Container(); 212 | 213 | const containerModule: ContainerModule = new ContainerModule( 214 | async (options: ContainerModuleLoadOptions) => { 215 | options.onDeactivation(serviceIdentifier, onActivationHandlerSpy); 216 | options.onDeactivation(serviceIdentifier, onActivationHandlerSpy); 217 | }, 218 | ); 219 | 220 | container.bind(serviceIdentifier).toConstantValue(serviceIdentifier); 221 | 222 | container.get(serviceIdentifier); 223 | 224 | await container.load(containerModule); 225 | 226 | await container.unbind(serviceIdentifier); 227 | 228 | expect(onActivationHandlerSpy.callCount).to.eq(2); 229 | }); 230 | 231 | it('Should remove module bindings when unload', async () => { 232 | const sid: string = 'sid'; 233 | const container: Container = new Container(); 234 | container.bind(sid).toConstantValue('Not module'); 235 | const module: ContainerModule = new ContainerModule( 236 | async (options: ContainerModuleLoadOptions) => { 237 | options.bind(sid).toConstantValue('Module'); 238 | }, 239 | ); 240 | 241 | await container.load(module); 242 | 243 | expect(container.getAll(sid)).to.deep.equal(['Not module', 'Module']); 244 | 245 | await container.unload(module); 246 | 247 | expect(container.getAll(sid)).to.deep.equal(['Not module']); 248 | }); 249 | 250 | it('Should deactivate singletons from module bindings when unload', async () => { 251 | const sid: string = 'sid'; 252 | const container: Container = new Container(); 253 | let moduleBindingDeactivated: string | undefined; 254 | let containerDeactivated: string | undefined; 255 | const module: ContainerModule = new ContainerModule( 256 | async (options: ContainerModuleLoadOptions) => { 257 | options 258 | .bind(sid) 259 | .toConstantValue('Module') 260 | .onDeactivation((injectable: string) => { 261 | moduleBindingDeactivated = injectable; 262 | }); 263 | options.onDeactivation(sid, (injectable: string) => { 264 | containerDeactivated = injectable; 265 | }); 266 | }, 267 | ); 268 | 269 | await container.load(module); 270 | 271 | container.get(sid); 272 | 273 | await container.unload(module); 274 | 275 | expect(moduleBindingDeactivated).to.equal('Module'); 276 | expect(containerDeactivated).to.equal('Module'); 277 | }); 278 | 279 | it('Should remove container handlers from module when unload', async () => { 280 | const sid: string = 'sid'; 281 | const container: Container = new Container(); 282 | let activatedNotModule: string | undefined; 283 | let deactivatedNotModule: string | undefined; 284 | container.onActivation( 285 | sid, 286 | (_: ResolutionContext, injected: string) => { 287 | activatedNotModule = injected; 288 | return injected; 289 | }, 290 | ); 291 | container.onDeactivation(sid, (injected: string) => { 292 | deactivatedNotModule = injected; 293 | }); 294 | container.bind(sid).toConstantValue('Value'); 295 | let activationCount: number = 0; 296 | let deactivationCount: number = 0; 297 | const module: ContainerModule = new ContainerModule( 298 | async (options: ContainerModuleLoadOptions) => { 299 | options.onDeactivation(sid, (_: string) => { 300 | deactivationCount++; 301 | }); 302 | options.onActivation( 303 | sid, 304 | (_: ResolutionContext, injected: string) => { 305 | activationCount++; 306 | return injected; 307 | }, 308 | ); 309 | }, 310 | ); 311 | 312 | await container.load(module); 313 | await container.unload(module); 314 | 315 | container.get(sid); 316 | await container.unbind(sid); 317 | 318 | expect(activationCount).to.equal(0); 319 | expect(deactivationCount).to.equal(0); 320 | 321 | expect(activatedNotModule).to.equal('Value'); 322 | expect(deactivatedNotModule).to.equal('Value'); 323 | }); 324 | 325 | it('Should remove module bindings when unloadAsync', async () => { 326 | const sid: string = 'sid'; 327 | const container: Container = new Container(); 328 | container.onDeactivation(sid, async (_injected: unknown) => 329 | Promise.resolve(), 330 | ); 331 | container.bind(sid).toConstantValue('Not module'); 332 | const module: ContainerModule = new ContainerModule( 333 | async (options: ContainerModuleLoadOptions) => { 334 | options.bind(sid).toConstantValue('Module'); 335 | }, 336 | ); 337 | 338 | await container.load(module); 339 | let values: unknown[] = container.getAll(sid); 340 | expect(values).to.deep.equal(['Not module', 'Module']); 341 | 342 | await container.unload(module); 343 | values = container.getAll(sid); 344 | expect(values).to.deep.equal(['Not module']); 345 | }); 346 | 347 | it('Should deactivate singletons from module bindings when unloadAsync', async () => { 348 | const sid: string = 'sid'; 349 | const container: Container = new Container(); 350 | let moduleBindingDeactivated: string | undefined; 351 | let containerDeactivated: string | undefined; 352 | const module: ContainerModule = new ContainerModule( 353 | async (options: ContainerModuleLoadOptions) => { 354 | options 355 | .bind(sid) 356 | .toConstantValue('Module') 357 | .onDeactivation((injectable: string) => { 358 | moduleBindingDeactivated = injectable; 359 | }); 360 | options.onDeactivation(sid, async (injectable: string) => { 361 | containerDeactivated = injectable; 362 | return Promise.resolve(); 363 | }); 364 | }, 365 | ); 366 | 367 | await container.load(module); 368 | container.get(sid); 369 | 370 | await container.unload(module); 371 | expect(moduleBindingDeactivated).to.equal('Module'); 372 | expect(containerDeactivated).to.equal('Module'); 373 | }); 374 | 375 | it('Should remove container handlers from module when unloadAsync', async () => { 376 | const sid: string = 'sid'; 377 | const container: Container = new Container(); 378 | let activatedNotModule: string | undefined; 379 | let deactivatedNotModule: string | undefined; 380 | container.onActivation( 381 | sid, 382 | (_: ResolutionContext, injected: string) => { 383 | activatedNotModule = injected; 384 | return injected; 385 | }, 386 | ); 387 | container.onDeactivation(sid, (injected: string) => { 388 | deactivatedNotModule = injected; 389 | }); 390 | container.bind(sid).toConstantValue('Value'); 391 | let activationCount: number = 0; 392 | let deactivationCount: number = 0; 393 | const module: ContainerModule = new ContainerModule( 394 | async (options: ContainerModuleLoadOptions) => { 395 | options.onDeactivation(sid, async (_: string) => { 396 | deactivationCount++; 397 | return Promise.resolve(); 398 | }); 399 | options.onActivation( 400 | sid, 401 | (_: ResolutionContext, injected: string) => { 402 | activationCount++; 403 | return injected; 404 | }, 405 | ); 406 | }, 407 | ); 408 | 409 | await container.load(module); 410 | await container.unload(module); 411 | 412 | container.get(sid); 413 | await container.unbind(sid); 414 | 415 | expect(activationCount).to.equal(0); 416 | expect(deactivationCount).to.equal(0); 417 | 418 | expect(activatedNotModule).to.equal('Value'); 419 | expect(deactivatedNotModule).to.equal('Value'); 420 | }); 421 | 422 | it('should be able to unbindAsync from a module', async () => { 423 | const sid: string = 'sid'; 424 | 425 | const container: Container = new Container(); 426 | const module: ContainerModule = new ContainerModule( 427 | async (options: ContainerModuleLoadOptions) => { 428 | await options.unbind(sid); 429 | }, 430 | ); 431 | 432 | container.bind(sid).toConstantValue('Value'); 433 | container.bind(sid).toConstantValue('Value2'); 434 | const deactivated: string[] = []; 435 | container.onDeactivation(sid, async (injected: string) => { 436 | deactivated.push(injected); 437 | return Promise.resolve(); 438 | }); 439 | 440 | container.getAll(sid); 441 | await container.load(module); 442 | 443 | expect(deactivated).to.deep.equal(['Value', 'Value2']); 444 | //bindings removed 445 | expect(container.getAll(sid)).to.deep.equal([]); 446 | }); 447 | }); 448 | -------------------------------------------------------------------------------- /src/test/features/named_default.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable, named } from '../..'; 6 | 7 | describe('Named default', () => { 8 | it('Should be able to inject a default to avoid ambiguous binding exceptions', () => { 9 | // eslint-disable-next-line @typescript-eslint/typedef 10 | const TYPES = { 11 | Warrior: 'Warrior', 12 | Weapon: 'Weapon', 13 | }; 14 | 15 | // eslint-disable-next-line @typescript-eslint/typedef 16 | const TAG = { 17 | chinese: 'chinese', 18 | japanese: 'japanese', 19 | throwable: 'throwable', 20 | }; 21 | 22 | interface Weapon { 23 | name: string; 24 | } 25 | 26 | interface Warrior { 27 | name: string; 28 | weapon: Weapon; 29 | } 30 | 31 | @injectable() 32 | class Katana implements Weapon { 33 | public name: string; 34 | constructor() { 35 | this.name = 'Katana'; 36 | } 37 | } 38 | 39 | @injectable() 40 | class Shuriken implements Weapon { 41 | public name: string; 42 | constructor() { 43 | this.name = 'Shuriken'; 44 | } 45 | } 46 | 47 | @injectable() 48 | class Samurai implements Warrior { 49 | public name: string; 50 | public weapon: Weapon; 51 | constructor(@inject(TYPES.Weapon) weapon: Weapon) { 52 | this.name = 'Samurai'; 53 | this.weapon = weapon; 54 | } 55 | } 56 | 57 | @injectable() 58 | class Ninja implements Warrior { 59 | public name: string; 60 | public weapon: Weapon; 61 | constructor(@inject(TYPES.Weapon) @named(TAG.throwable) weapon: Weapon) { 62 | this.name = 'Ninja'; 63 | this.weapon = weapon; 64 | } 65 | } 66 | 67 | const container: Container = new Container(); 68 | container.bind(TYPES.Warrior).to(Ninja).whenNamed(TAG.chinese); 69 | container.bind(TYPES.Warrior).to(Samurai).whenNamed(TAG.japanese); 70 | container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAG.throwable); 71 | container.bind(TYPES.Weapon).to(Katana).whenDefault(); 72 | 73 | const ninja: Warrior = container.get(TYPES.Warrior, { 74 | name: TAG.chinese, 75 | }); 76 | const samurai: Warrior = container.get(TYPES.Warrior, { 77 | name: TAG.japanese, 78 | }); 79 | 80 | expect(ninja.name).to.eql('Ninja'); 81 | expect(ninja.weapon.name).to.eql('Shuriken'); 82 | expect(samurai.name).to.eql('Samurai'); 83 | expect(samurai.weapon.name).to.eql('Katana'); 84 | }); 85 | 86 | it('Should be able to select a default to avoid ambiguous binding exceptions', () => { 87 | // eslint-disable-next-line @typescript-eslint/typedef 88 | const TYPES = { 89 | Weapon: 'Weapon', 90 | }; 91 | 92 | // eslint-disable-next-line @typescript-eslint/typedef 93 | const TAG = { 94 | throwable: 'throwable', 95 | }; 96 | 97 | interface Weapon { 98 | name: string; 99 | } 100 | 101 | @injectable() 102 | class Katana implements Weapon { 103 | public name: string; 104 | constructor() { 105 | this.name = 'Katana'; 106 | } 107 | } 108 | 109 | @injectable() 110 | class Shuriken implements Weapon { 111 | public name: string; 112 | constructor() { 113 | this.name = 'Shuriken'; 114 | } 115 | } 116 | 117 | const container: Container = new Container(); 118 | container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAG.throwable); 119 | container 120 | .bind(TYPES.Weapon) 121 | .to(Katana) 122 | .inSingletonScope() 123 | .whenDefault(); 124 | 125 | const defaultWeapon: Weapon = container.get(TYPES.Weapon); 126 | const throwableWeapon: Weapon = container.get(TYPES.Weapon, { 127 | name: TAG.throwable, 128 | }); 129 | 130 | expect(defaultWeapon.name).eql('Katana'); 131 | expect(throwableWeapon.name).eql('Shuriken'); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /src/test/features/property_injection.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { 6 | Container, 7 | inject, 8 | injectable, 9 | injectFromBase, 10 | multiInject, 11 | named, 12 | optional, 13 | tagged, 14 | unmanaged, 15 | } from '../..'; 16 | 17 | describe('Property Injection', () => { 18 | it('Should be able to inject a property', () => { 19 | // eslint-disable-next-line @typescript-eslint/typedef 20 | const TYPES = { 21 | Warrior: 'Warrior', 22 | Weapon: 'Weapon', 23 | }; 24 | 25 | interface Weapon { 26 | name: string; 27 | } 28 | 29 | @injectable() 30 | class Katana implements Weapon { 31 | public name: string; 32 | constructor() { 33 | this.name = 'Katana'; 34 | } 35 | } 36 | 37 | interface Warrior { 38 | name: string; 39 | weapon: Weapon; 40 | } 41 | 42 | @injectable() 43 | class Samurai implements Warrior { 44 | @inject(TYPES.Weapon) 45 | public weapon!: Weapon; 46 | public name: string; 47 | 48 | constructor() { 49 | this.name = 'Samurai'; 50 | } 51 | } 52 | 53 | const container: Container = new Container(); 54 | container.bind(TYPES.Warrior).to(Samurai); 55 | container.bind(TYPES.Weapon).to(Katana); 56 | 57 | const warrior: Warrior = container.get(TYPES.Warrior); 58 | expect(warrior.name).to.eql('Samurai'); 59 | expect(warrior.weapon).not.to.eql(undefined); 60 | expect(warrior.weapon.name).to.eql('Katana'); 61 | }); 62 | 63 | it('Should be able to inject a property combined with constructor injection', () => { 64 | // eslint-disable-next-line @typescript-eslint/typedef 65 | const TYPES = { 66 | Warrior: 'Warrior', 67 | Weapon: 'Weapon', 68 | }; 69 | 70 | // eslint-disable-next-line @typescript-eslint/typedef 71 | const TAGS = { 72 | Primary: 'Primary', 73 | Secondary: 'Secondary', 74 | }; 75 | 76 | interface Weapon { 77 | name: string; 78 | } 79 | 80 | @injectable() 81 | class Katana implements Weapon { 82 | public name: string; 83 | constructor() { 84 | this.name = 'Katana'; 85 | } 86 | } 87 | 88 | @injectable() 89 | class Shuriken implements Weapon { 90 | public name: string; 91 | constructor() { 92 | this.name = 'Shuriken'; 93 | } 94 | } 95 | 96 | interface Warrior { 97 | name: string; 98 | primaryWeapon: Weapon; 99 | secondaryWeapon: Weapon; 100 | } 101 | 102 | @injectable() 103 | class Samurai implements Warrior { 104 | @inject(TYPES.Weapon) 105 | @named(TAGS.Secondary) 106 | public secondaryWeapon!: Weapon; 107 | public name: string; 108 | public primaryWeapon: Weapon; 109 | 110 | constructor(@inject(TYPES.Weapon) @named(TAGS.Primary) weapon: Weapon) { 111 | this.name = 'Samurai'; 112 | this.primaryWeapon = weapon; 113 | } 114 | } 115 | 116 | const container: Container = new Container(); 117 | container.bind(TYPES.Warrior).to(Samurai); 118 | container.bind(TYPES.Weapon).to(Katana).whenNamed(TAGS.Primary); 119 | container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAGS.Secondary); 120 | 121 | const warrior: Warrior = container.get(TYPES.Warrior); 122 | expect(warrior.name).to.eql('Samurai'); 123 | expect(warrior.primaryWeapon).not.to.eql(undefined); 124 | expect(warrior.primaryWeapon.name).to.eql('Katana'); 125 | expect(warrior.secondaryWeapon).not.to.eql(undefined); 126 | expect(warrior.secondaryWeapon.name).to.eql('Shuriken'); 127 | }); 128 | 129 | it('Should be able to inject a named property', () => { 130 | // eslint-disable-next-line @typescript-eslint/typedef 131 | const TYPES = { 132 | Warrior: 'Warrior', 133 | Weapon: 'Weapon', 134 | }; 135 | 136 | // eslint-disable-next-line @typescript-eslint/typedef 137 | const TAGS = { 138 | Primary: 'Primary', 139 | Secondary: 'Secondary', 140 | }; 141 | 142 | interface Weapon { 143 | name: string; 144 | } 145 | 146 | @injectable() 147 | class Katana implements Weapon { 148 | public name: string; 149 | constructor() { 150 | this.name = 'Katana'; 151 | } 152 | } 153 | 154 | @injectable() 155 | class Shuriken implements Weapon { 156 | public name: string; 157 | constructor() { 158 | this.name = 'Shuriken'; 159 | } 160 | } 161 | 162 | interface Warrior { 163 | name: string; 164 | primaryWeapon: Weapon; 165 | secondaryWeapon: Weapon; 166 | } 167 | 168 | @injectable() 169 | class Samurai implements Warrior { 170 | @inject(TYPES.Weapon) 171 | @named(TAGS.Primary) 172 | public primaryWeapon!: Weapon; 173 | 174 | @inject(TYPES.Weapon) 175 | @named(TAGS.Secondary) 176 | public secondaryWeapon!: Weapon; 177 | 178 | public name: string; 179 | 180 | constructor() { 181 | this.name = 'Samurai'; 182 | } 183 | } 184 | 185 | const container: Container = new Container(); 186 | container.bind(TYPES.Warrior).to(Samurai); 187 | container.bind(TYPES.Weapon).to(Katana).whenNamed(TAGS.Primary); 188 | container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAGS.Secondary); 189 | 190 | const warrior: Warrior = container.get(TYPES.Warrior); 191 | expect(warrior.name).to.eql('Samurai'); 192 | expect(warrior.primaryWeapon).not.to.eql(undefined); 193 | expect(warrior.primaryWeapon.name).to.eql('Katana'); 194 | expect(warrior.secondaryWeapon).not.to.eql(undefined); 195 | expect(warrior.secondaryWeapon.name).to.eql('Shuriken'); 196 | }); 197 | 198 | it('Should be able to inject a tagged property', () => { 199 | // eslint-disable-next-line @typescript-eslint/typedef 200 | const TYPES = { 201 | Warrior: 'Warrior', 202 | Weapon: 'Weapon', 203 | }; 204 | 205 | // eslint-disable-next-line @typescript-eslint/typedef 206 | const TAGS = { 207 | Primary: 'Primary', 208 | Priority: 'Priority', 209 | Secondary: 'Secondary', 210 | }; 211 | 212 | interface Weapon { 213 | name: string; 214 | } 215 | 216 | @injectable() 217 | class Katana implements Weapon { 218 | public name: string; 219 | constructor() { 220 | this.name = 'Katana'; 221 | } 222 | } 223 | 224 | @injectable() 225 | class Shuriken implements Weapon { 226 | public name: string; 227 | constructor() { 228 | this.name = 'Shuriken'; 229 | } 230 | } 231 | 232 | interface Warrior { 233 | name: string; 234 | primaryWeapon: Weapon; 235 | secondaryWeapon: Weapon; 236 | } 237 | 238 | @injectable() 239 | class Samurai implements Warrior { 240 | @inject(TYPES.Weapon) 241 | @tagged(TAGS.Priority, TAGS.Primary) 242 | public primaryWeapon!: Weapon; 243 | 244 | @inject(TYPES.Weapon) 245 | @tagged(TAGS.Priority, TAGS.Secondary) 246 | public secondaryWeapon!: Weapon; 247 | 248 | public name: string; 249 | 250 | constructor() { 251 | this.name = 'Samurai'; 252 | } 253 | } 254 | 255 | const container: Container = new Container(); 256 | container.bind(TYPES.Warrior).to(Samurai); 257 | container 258 | .bind(TYPES.Weapon) 259 | .to(Katana) 260 | .whenTagged(TAGS.Priority, TAGS.Primary); 261 | container 262 | .bind(TYPES.Weapon) 263 | .to(Shuriken) 264 | .whenTagged(TAGS.Priority, TAGS.Secondary); 265 | 266 | const warrior: Warrior = container.get(TYPES.Warrior); 267 | expect(warrior.name).to.eql('Samurai'); 268 | expect(warrior.primaryWeapon).not.to.eql(undefined); 269 | expect(warrior.primaryWeapon.name).to.eql('Katana'); 270 | expect(warrior.secondaryWeapon).not.to.eql(undefined); 271 | expect(warrior.secondaryWeapon.name).to.eql('Shuriken'); 272 | }); 273 | 274 | it('Should be able to multi-inject a property', () => { 275 | // eslint-disable-next-line @typescript-eslint/typedef 276 | const TYPES = { 277 | Warrior: 'Warrior', 278 | Weapon: 'Weapon', 279 | }; 280 | 281 | interface Weapon { 282 | name: string; 283 | } 284 | 285 | @injectable() 286 | class Katana implements Weapon { 287 | public name: string; 288 | constructor() { 289 | this.name = 'Katana'; 290 | } 291 | } 292 | 293 | @injectable() 294 | class Shuriken implements Weapon { 295 | public name: string; 296 | constructor() { 297 | this.name = 'Shuriken'; 298 | } 299 | } 300 | 301 | interface Warrior { 302 | name: string; 303 | weapons: Weapon[]; 304 | } 305 | 306 | @injectable() 307 | class Samurai implements Warrior { 308 | @multiInject(TYPES.Weapon) 309 | public weapons!: Weapon[]; 310 | public name: string; 311 | 312 | constructor() { 313 | this.name = 'Samurai'; 314 | } 315 | } 316 | 317 | const container: Container = new Container(); 318 | container.bind(TYPES.Warrior).to(Samurai); 319 | container.bind(TYPES.Weapon).to(Katana); 320 | container.bind(TYPES.Weapon).to(Shuriken); 321 | 322 | const warrior: Warrior = container.get(TYPES.Warrior); 323 | expect(warrior.name).to.eql('Samurai'); 324 | expect(warrior.weapons[0]).not.to.eql(undefined); 325 | expect(warrior.weapons[0]?.name).to.eql('Katana'); 326 | expect(warrior.weapons[1]).not.to.eql(undefined); 327 | expect(warrior.weapons[1]?.name).to.eql('Shuriken'); 328 | }); 329 | 330 | it('Should be able to inject a property in a base class', () => { 331 | // eslint-disable-next-line @typescript-eslint/typedef 332 | const TYPES = { 333 | Warrior: 'Warrior', 334 | Weapon: 'Weapon', 335 | }; 336 | 337 | // eslint-disable-next-line @typescript-eslint/typedef 338 | const TAGS = { 339 | Primary: 'Primary', 340 | Priority: 'Priority', 341 | Secondary: 'Secondary', 342 | }; 343 | 344 | interface Weapon { 345 | name: string; 346 | } 347 | 348 | @injectable() 349 | class Katana implements Weapon { 350 | public name: string; 351 | constructor() { 352 | this.name = 'Katana'; 353 | } 354 | } 355 | 356 | @injectable() 357 | class Shuriken implements Weapon { 358 | public name: string; 359 | constructor() { 360 | this.name = 'Shuriken'; 361 | } 362 | } 363 | 364 | interface Warrior { 365 | name: string; 366 | primaryWeapon: Weapon; 367 | } 368 | 369 | @injectable() 370 | class BaseWarrior implements Warrior { 371 | @inject(TYPES.Weapon) 372 | @tagged(TAGS.Priority, TAGS.Primary) 373 | public primaryWeapon!: Weapon; 374 | public name: string; 375 | 376 | constructor(@unmanaged() name: string) { 377 | this.name = name; 378 | } 379 | } 380 | 381 | @injectable() 382 | @injectFromBase({ 383 | extendConstructorArguments: false, 384 | extendProperties: true, 385 | }) 386 | class Samurai extends BaseWarrior { 387 | @inject(TYPES.Weapon) 388 | @tagged(TAGS.Priority, TAGS.Secondary) 389 | public secondaryWeapon!: Weapon; 390 | 391 | constructor() { 392 | super('Samurai'); 393 | } 394 | } 395 | 396 | const container: Container = new Container(); 397 | container.bind(TYPES.Warrior).to(Samurai); 398 | container 399 | .bind(TYPES.Weapon) 400 | .to(Katana) 401 | .whenTagged(TAGS.Priority, TAGS.Primary); 402 | container 403 | .bind(TYPES.Weapon) 404 | .to(Shuriken) 405 | .whenTagged(TAGS.Priority, TAGS.Secondary); 406 | 407 | const samurai: Samurai = container.get(TYPES.Warrior); 408 | expect(samurai.name).to.eql('Samurai'); 409 | expect(samurai.secondaryWeapon).not.to.eql(undefined); 410 | expect(samurai.secondaryWeapon.name).to.eql('Shuriken'); 411 | expect(samurai.primaryWeapon).not.to.eql(undefined); 412 | expect(samurai.primaryWeapon.name).to.eql('Katana'); 413 | }); 414 | 415 | it('Should be able to flag a property injection as optional', () => { 416 | // eslint-disable-next-line @typescript-eslint/typedef 417 | const TYPES = { 418 | Route: 'Route', 419 | Router: 'Router', 420 | }; 421 | 422 | interface Route { 423 | name: string; 424 | } 425 | 426 | @injectable() 427 | class Router { 428 | @inject(TYPES.Route) 429 | @optional() 430 | private readonly route!: Route; 431 | 432 | public getRoute(): Route { 433 | return this.route; 434 | } 435 | } 436 | 437 | const container: Container = new Container(); 438 | 439 | container.bind(TYPES.Router).to(Router); 440 | 441 | const router1: Router = container.get(TYPES.Router); 442 | expect(router1.getRoute()).to.eql(undefined); 443 | 444 | container.bind(TYPES.Route).toConstantValue({ name: 'route1' }); 445 | 446 | const router2: Router = container.get(TYPES.Router); 447 | expect(router2.getRoute().name).to.eql('route1'); 448 | }); 449 | }); 450 | -------------------------------------------------------------------------------- /src/test/features/provider.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, injectable, Provider, ResolutionContext } from '../..'; 6 | 7 | describe('Provider', () => { 8 | it('Should support complex asynchronous initialization processes', (done: Mocha.Done) => { 9 | @injectable() 10 | class Ninja { 11 | public level: number; 12 | public rank: string; 13 | constructor() { 14 | this.level = 0; 15 | this.rank = 'Ninja'; 16 | } 17 | public async train(): Promise { 18 | return new Promise( 19 | (resolve: (value: number | PromiseLike) => void) => { 20 | setTimeout(() => { 21 | this.level += 10; 22 | resolve(this.level); 23 | }, 10); 24 | }, 25 | ); 26 | } 27 | } 28 | 29 | @injectable() 30 | class NinjaMaster { 31 | public rank: string; 32 | constructor() { 33 | this.rank = 'NinjaMaster'; 34 | } 35 | } 36 | 37 | type NinjaMasterProvider = () => Promise; 38 | 39 | const container: Container = new Container(); 40 | 41 | container.bind('Ninja').to(Ninja).inSingletonScope(); 42 | container.bind('Provider').toProvider( 43 | (context: ResolutionContext) => async () => 44 | new Promise( 45 | ( 46 | resolve: (value: NinjaMaster | PromiseLike) => void, 47 | reject: (reason?: unknown) => void, 48 | ) => { 49 | const ninja: Ninja = context.get('Ninja'); 50 | 51 | void ninja.train().then((level: number) => { 52 | if (level >= 20) { 53 | resolve(new NinjaMaster()); 54 | } else { 55 | // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors 56 | reject('Not enough training'); 57 | } 58 | }); 59 | }, 60 | ), 61 | ); 62 | 63 | const ninjaMasterProvider: NinjaMasterProvider = 64 | container.get('Provider'); 65 | 66 | // helper 67 | async function valueOrDefault( 68 | provider: () => Promise, 69 | defaultValue: T, 70 | ) { 71 | return new Promise((resolve: (value: T | PromiseLike) => void) => { 72 | provider() 73 | .then((value: T) => { 74 | resolve(value); 75 | }) 76 | .catch(() => { 77 | resolve(defaultValue); 78 | }); 79 | }); 80 | } 81 | 82 | void valueOrDefault(ninjaMasterProvider, { 83 | rank: 'DefaultNinjaMaster', 84 | }).then((ninjaMaster: NinjaMaster) => { 85 | expect(ninjaMaster.rank).to.eql('DefaultNinjaMaster'); 86 | }); 87 | 88 | void valueOrDefault(ninjaMasterProvider, { 89 | rank: 'DefaultNinjaMaster', 90 | }).then((ninjaMaster: NinjaMaster) => { 91 | expect(ninjaMaster.rank).to.eql('NinjaMaster'); 92 | done(); 93 | }); 94 | }); 95 | 96 | it('Should support custom arguments', (done: Mocha.Done) => { 97 | const container: Container = new Container(); 98 | 99 | interface Sword { 100 | material: string; 101 | damage: number; 102 | } 103 | 104 | @injectable() 105 | class Katana implements Sword { 106 | public material!: string; 107 | public damage!: number; 108 | } 109 | 110 | container.bind('Sword').to(Katana); 111 | 112 | type SwordProvider = Provider; 113 | 114 | container.bind('SwordProvider').toProvider( 115 | (context: ResolutionContext): SwordProvider => 116 | async (material: string, damage: number) => 117 | new Promise( 118 | (resolve: (value: Sword | PromiseLike) => void) => { 119 | setTimeout(() => { 120 | const katana: Sword = context.get('Sword'); 121 | katana.material = material; 122 | katana.damage = damage; 123 | resolve(katana); 124 | }, 10); 125 | }, 126 | ), 127 | ); 128 | 129 | const katanaProvider: SwordProvider = 130 | container.get('SwordProvider'); 131 | 132 | void katanaProvider('gold', 100).then((powerfulGoldKatana: Sword) => { 133 | expect(powerfulGoldKatana.material).to.eql('gold'); 134 | 135 | expect(powerfulGoldKatana.damage).to.eql(100); 136 | 137 | void katanaProvider('gold', 10).then((notSoPowerfulGoldKatana: Sword) => { 138 | expect(notSoPowerfulGoldKatana.material).to.eql('gold'); 139 | 140 | expect(notSoPowerfulGoldKatana.damage).to.eql(10); 141 | done(); 142 | }); 143 | }); 144 | }); 145 | 146 | it('Should support the declaration of singletons', (done: Mocha.Done) => { 147 | const container: Container = new Container(); 148 | 149 | interface Warrior { 150 | level: number; 151 | } 152 | 153 | @injectable() 154 | class Ninja implements Warrior { 155 | public level: number; 156 | constructor() { 157 | this.level = 0; 158 | } 159 | } 160 | 161 | type WarriorProvider = (level: number) => Promise; 162 | 163 | container.bind('Warrior').to(Ninja).inSingletonScope(); // Value is singleton! 164 | 165 | container.bind('WarriorProvider').toProvider( 166 | (context: ResolutionContext) => async (increaseLevel: number) => 167 | new Promise((resolve: (value: Warrior) => void) => { 168 | setTimeout(() => { 169 | const warrior: Warrior = context.get('Warrior'); 170 | warrior.level += increaseLevel; 171 | resolve(warrior); 172 | }, 100); 173 | }), 174 | ); 175 | 176 | const warriorProvider: WarriorProvider = container.get('WarriorProvider'); 177 | 178 | void warriorProvider(10).then((warrior: Warrior) => { 179 | expect(warrior.level).to.eql(10); 180 | 181 | void warriorProvider(10).then((warrior2: Warrior) => { 182 | expect(warrior2.level).to.eql(20); 183 | done(); 184 | }); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /src/test/features/request_scope.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | import { performance } from 'perf_hooks'; 5 | 6 | import { Container, inject, injectable, named } from '../..'; 7 | 8 | describe('inRequestScope', () => { 9 | it('Should support request scope in basic bindings', () => { 10 | // eslint-disable-next-line @typescript-eslint/typedef 11 | const TYPE = { 12 | Warrior: Symbol('Warrior'), 13 | Weapon: Symbol('Weapon'), 14 | }; 15 | 16 | interface Weapon { 17 | use(): string; 18 | } 19 | 20 | interface Warrior { 21 | primaryWeapon: Weapon; 22 | secondaryWeapon: Weapon; 23 | } 24 | 25 | @injectable() 26 | class Katana implements Weapon { 27 | private readonly _madeOn: number; 28 | constructor() { 29 | this._madeOn = performance.now(); 30 | } 31 | public use() { 32 | return `Used Katana made on ${this._madeOn.toString()}!`; 33 | } 34 | } 35 | 36 | @injectable() 37 | class Samurai implements Warrior { 38 | public primaryWeapon: Weapon; 39 | public secondaryWeapon: Weapon; 40 | constructor( 41 | @inject(TYPE.Weapon) primaryWeapon: Weapon, 42 | @inject(TYPE.Weapon) secondaryWeapon: Weapon, 43 | ) { 44 | this.primaryWeapon = primaryWeapon; 45 | this.secondaryWeapon = secondaryWeapon; 46 | } 47 | } 48 | 49 | // Without request scope 50 | const container: Container = new Container(); 51 | container.bind(TYPE.Weapon).to(Katana); 52 | container.bind(TYPE.Warrior).to(Samurai); 53 | const samurai: Warrior = container.get(TYPE.Warrior); 54 | const samurai2: Warrior = container.get(TYPE.Warrior); 55 | 56 | // One requests should use two instances because scope is transient 57 | expect(samurai.primaryWeapon.use()).not.to.eql( 58 | samurai.secondaryWeapon.use(), 59 | ); 60 | 61 | // One requests should use two instances because scope is transient 62 | expect(samurai2.primaryWeapon.use()).not.to.eql( 63 | samurai2.secondaryWeapon.use(), 64 | ); 65 | 66 | // Two request should use two Katana instances 67 | // for each instance of Samuari because scope is transient 68 | expect(samurai.primaryWeapon.use()).not.to.eql( 69 | samurai2.primaryWeapon.use(), 70 | ); 71 | expect(samurai.secondaryWeapon.use()).not.to.eql( 72 | samurai2.secondaryWeapon.use(), 73 | ); 74 | 75 | // With request scope 76 | const container1: Container = new Container(); 77 | container1.bind(TYPE.Weapon).to(Katana).inRequestScope(); // Important 78 | container1.bind(TYPE.Warrior).to(Samurai); 79 | const samurai3: Warrior = container1.get(TYPE.Warrior); 80 | const samurai4: Warrior = container1.get(TYPE.Warrior); 81 | 82 | // One requests should use one instance because scope is request scope 83 | expect(samurai3.primaryWeapon.use()).to.eql(samurai3.secondaryWeapon.use()); 84 | 85 | // One requests should use one instance because scope is request scope 86 | expect(samurai4.primaryWeapon.use()).to.eql(samurai4.secondaryWeapon.use()); 87 | 88 | // Two request should use one instances of Katana 89 | // for each instance of Samurai because scope is request scope 90 | expect(samurai3.primaryWeapon.use()).not.to.eql( 91 | samurai4.primaryWeapon.use(), 92 | ); 93 | expect(samurai3.secondaryWeapon.use()).not.to.eql( 94 | samurai4.secondaryWeapon.use(), 95 | ); 96 | }); 97 | 98 | it('Should support request scope when using contraints', () => { 99 | // eslint-disable-next-line @typescript-eslint/typedef 100 | const TYPE = { 101 | Warrior: Symbol('Warrior'), 102 | Weapon: Symbol('Weapon'), 103 | }; 104 | 105 | interface Weapon { 106 | use(): string; 107 | } 108 | 109 | interface Warrior { 110 | primaryWeapon: Weapon; 111 | secondaryWeapon: Weapon; 112 | tertiaryWeapon: Weapon; 113 | } 114 | 115 | @injectable() 116 | class Katana implements Weapon { 117 | private readonly _madeOn: number; 118 | constructor() { 119 | this._madeOn = performance.now(); 120 | } 121 | public use() { 122 | return `Used Katana made on ${this._madeOn.toString()}!`; 123 | } 124 | } 125 | 126 | @injectable() 127 | class Shuriken implements Weapon { 128 | private readonly _madeOn: number; 129 | constructor() { 130 | this._madeOn = performance.now(); 131 | } 132 | public use() { 133 | return `Used Shuriken made on ${this._madeOn.toString()}!`; 134 | } 135 | } 136 | 137 | @injectable() 138 | class Samurai implements Warrior { 139 | public primaryWeapon: Weapon; 140 | public secondaryWeapon: Weapon; 141 | public tertiaryWeapon: Weapon; 142 | constructor( 143 | @inject(TYPE.Weapon) @named('sword') primaryWeapon: Weapon, 144 | @inject(TYPE.Weapon) @named('throwable') secondaryWeapon: Weapon, 145 | @inject(TYPE.Weapon) @named('throwable') tertiaryWeapon: Weapon, 146 | ) { 147 | this.primaryWeapon = primaryWeapon; 148 | this.secondaryWeapon = secondaryWeapon; 149 | this.tertiaryWeapon = tertiaryWeapon; 150 | } 151 | } 152 | 153 | const container: Container = new Container(); 154 | 155 | container 156 | .bind(TYPE.Weapon) 157 | .to(Katana) 158 | .inRequestScope() 159 | .whenNamed('sword'); 160 | 161 | container 162 | .bind(TYPE.Weapon) 163 | .to(Shuriken) 164 | .inRequestScope() 165 | .whenNamed('throwable'); 166 | 167 | container.bind(TYPE.Warrior).to(Samurai); 168 | 169 | const samurai1: Warrior = container.get(TYPE.Warrior); 170 | const samurai2: Warrior = container.get(TYPE.Warrior); 171 | 172 | // Katana and Shuriken are two instances 173 | expect(samurai1.primaryWeapon.use()).not.to.eql( 174 | samurai1.secondaryWeapon.use(), 175 | ); 176 | 177 | // Shuriken should be one shared instance because scope is request scope 178 | expect(samurai1.secondaryWeapon.use()).to.eql( 179 | samurai1.tertiaryWeapon.use(), 180 | ); 181 | 182 | // Katana and Shuriken are two instances 183 | expect(samurai2.primaryWeapon.use()).not.to.eql( 184 | samurai2.secondaryWeapon.use(), 185 | ); 186 | 187 | // Shuriken should be one shared instance because scope is request scope 188 | expect(samurai2.secondaryWeapon.use()).to.eql( 189 | samurai2.tertiaryWeapon.use(), 190 | ); 191 | 192 | // Two request should use one instances of Katana 193 | // for each instance of Samurai because scope is request scope 194 | expect(samurai1.secondaryWeapon.use()).not.to.eql( 195 | samurai2.secondaryWeapon.use(), 196 | ); 197 | expect(samurai1.tertiaryWeapon.use()).not.to.eql( 198 | samurai2.tertiaryWeapon.use(), 199 | ); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /src/test/features/transitive_bindings.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, injectable } from '../..'; 6 | 7 | describe('Transitive bindings', () => { 8 | it('Should be able to bind to a service', () => { 9 | @injectable() 10 | class MySqlDatabaseTransactionLog { 11 | public time: number; 12 | public name: string; 13 | constructor() { 14 | this.time = new Date().getTime(); 15 | this.name = 'MySqlDatabaseTransactionLog'; 16 | } 17 | } 18 | 19 | @injectable() 20 | class DatabaseTransactionLog { 21 | public time!: number; 22 | public name!: string; 23 | } 24 | 25 | @injectable() 26 | class TransactionLog { 27 | public time!: number; 28 | public name!: string; 29 | } 30 | 31 | const container: Container = new Container(); 32 | container.bind(MySqlDatabaseTransactionLog).toSelf().inSingletonScope(); 33 | container 34 | .bind(DatabaseTransactionLog) 35 | .toService(MySqlDatabaseTransactionLog); 36 | container.bind(TransactionLog).toService(DatabaseTransactionLog); 37 | 38 | const mySqlDatabaseTransactionLog: MySqlDatabaseTransactionLog = 39 | container.get(MySqlDatabaseTransactionLog); 40 | const databaseTransactionLog: DatabaseTransactionLog = container.get( 41 | DatabaseTransactionLog, 42 | ); 43 | const transactionLog: TransactionLog = container.get(TransactionLog); 44 | 45 | expect(mySqlDatabaseTransactionLog.name).to.eq( 46 | 'MySqlDatabaseTransactionLog', 47 | ); 48 | expect(databaseTransactionLog.name).to.eq('MySqlDatabaseTransactionLog'); 49 | expect(transactionLog.name).to.eq('MySqlDatabaseTransactionLog'); 50 | expect(mySqlDatabaseTransactionLog.time).to.eq(databaseTransactionLog.time); 51 | expect(databaseTransactionLog.time).to.eq(transactionLog.time); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/test/node/error_messages.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, injectable } from '../..'; 6 | 7 | describe('Error message when resolving fails', () => { 8 | @injectable() 9 | class Katana {} 10 | 11 | @injectable() 12 | class Shuriken {} 13 | 14 | @injectable() 15 | class Bokken {} 16 | 17 | it('Should contain correct message and the serviceIdentifier in error message', () => { 18 | const container: Container = new Container(); 19 | 20 | container.bind('Weapon').to(Katana); 21 | 22 | const tryWeapon: () => void = () => { 23 | container.get('Ninja'); 24 | }; 25 | 26 | expect(tryWeapon).to.throw(''); 27 | }); 28 | 29 | it('Should contain the provided name in error message when target is named', () => { 30 | const container: Container = new Container(); 31 | const tryGetNamedWeapon: (name: string | number | symbol) => void = ( 32 | name: string | number | symbol, 33 | ) => { 34 | container.get('Weapon', { name }); 35 | }; 36 | 37 | expect(() => { 38 | tryGetNamedWeapon('superior'); 39 | }).to.throw(`No bindings found for service: "Weapon". 40 | 41 | Trying to resolve bindings for "Weapon (Root service)". 42 | 43 | Binding constraints: 44 | - service identifier: Weapon 45 | - name: superior`); 46 | expect(() => { 47 | tryGetNamedWeapon(Symbol.for('Superior')); 48 | }).to.throw(`No bindings found for service: "Weapon". 49 | 50 | Trying to resolve bindings for "Weapon (Root service)". 51 | 52 | Binding constraints: 53 | - service identifier: Weapon 54 | - name: Symbol(Superior)`); 55 | expect(() => { 56 | tryGetNamedWeapon(0); 57 | }).to.throw(`No bindings found for service: "Weapon". 58 | 59 | Trying to resolve bindings for "Weapon (Root service)". 60 | 61 | Binding constraints: 62 | - service identifier: Weapon 63 | - name: 0`); 64 | }); 65 | 66 | it('Should contain the provided tag in error message when target is tagged', () => { 67 | const container: Container = new Container(); 68 | const tryGetTaggedWeapon: (tag: string | number | symbol) => void = ( 69 | tag: string | number | symbol, 70 | ) => { 71 | container.get('Weapon', { 72 | tag: { 73 | key: tag, 74 | value: true, 75 | }, 76 | }); 77 | }; 78 | 79 | expect(() => { 80 | tryGetTaggedWeapon('canShoot'); 81 | }).to.throw(`No bindings found for service: "Weapon". 82 | 83 | Trying to resolve bindings for "Weapon (Root service)". 84 | 85 | Binding constraints: 86 | - service identifier: Weapon 87 | - name: - 88 | - tags: 89 | - canShoot`); 90 | expect(() => { 91 | tryGetTaggedWeapon(Symbol.for('Can shoot')); 92 | }).to.throw(`No bindings found for service: "Weapon". 93 | 94 | Trying to resolve bindings for "Weapon (Root service)". 95 | 96 | Binding constraints: 97 | - service identifier: Weapon 98 | - name: - 99 | - tags: 100 | - Symbol(Can shoot)`); 101 | expect(() => { 102 | tryGetTaggedWeapon(0); 103 | }).to.throw(`No bindings found for service: "Weapon". 104 | 105 | Trying to resolve bindings for "Weapon (Root service)". 106 | 107 | Binding constraints: 108 | - service identifier: Weapon 109 | - name: - 110 | - tags: 111 | - 0`); 112 | }); 113 | 114 | it('Should list all possible bindings in error message if ambiguous matching binding found', () => { 115 | const container: Container = new Container(); 116 | container.bind('Weapon').to(Katana); 117 | container.bind('Weapon').to(Shuriken); 118 | container.bind('Weapon').to(Bokken); 119 | 120 | try { 121 | container.get('Weapon'); 122 | } catch (error) { 123 | expect((error as Error).message).to 124 | .equal(`Ambiguous bindings found for service: "Weapon". 125 | 126 | Registered bindings: 127 | 128 | [ type: "Instance", serviceIdentifier: "Weapon", scope: "Transient", implementationType: "Katana" ] 129 | [ type: "Instance", serviceIdentifier: "Weapon", scope: "Transient", implementationType: "Shuriken" ] 130 | [ type: "Instance", serviceIdentifier: "Weapon", scope: "Transient", implementationType: "Bokken" ] 131 | 132 | Trying to resolve bindings for "Weapon (Root service)". 133 | 134 | Binding constraints: 135 | - service identifier: Weapon 136 | - name: -`); 137 | } 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /src/test/node/exceptions.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable } from '../..'; 6 | 7 | describe('Node', () => { 8 | it('Should throw if circular dependencies found', () => { 9 | @injectable() 10 | class A { 11 | public b: unknown; 12 | public c: unknown; 13 | constructor(@inject('B') b: unknown, @inject('C') c: unknown) { 14 | this.b = b; 15 | this.c = c; 16 | } 17 | } 18 | 19 | @injectable() 20 | class B {} 21 | 22 | @injectable() 23 | class C { 24 | public d: unknown; 25 | constructor(@inject('D') d: unknown) { 26 | this.d = d; 27 | } 28 | } 29 | 30 | @injectable() 31 | class D { 32 | public a: unknown; 33 | constructor(@inject('A') a: unknown) { 34 | this.a = a; 35 | } 36 | } 37 | 38 | const container: Container = new Container(); 39 | container.bind('A').to(A); 40 | container.bind('B').to(B); 41 | container.bind('C').to(C); 42 | container.bind('D').to(D); 43 | 44 | function willThrow() { 45 | const a: A = container.get('A'); 46 | return a; 47 | } 48 | 49 | expect(willThrow).to.throw(''); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/test/node/performance.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | import { performance } from 'perf_hooks'; 5 | 6 | import { Container } from '../../index'; 7 | 8 | describe('Performance', () => { 9 | function registerN(times: number) { 10 | const result: { 11 | container: Container; 12 | register: number; 13 | } = { 14 | container: new Container(), 15 | register: -1, 16 | }; 17 | 18 | for (let i: number = 0; i < times; i++) { 19 | const start: number = performance.now(); 20 | result.container 21 | .bind(`SOME_ID_${i.toString()}`) 22 | .toConstantValue({ test: i }); 23 | const end: number = performance.now(); 24 | result.register = end - start; 25 | } 26 | 27 | return result; 28 | } 29 | 30 | function resolveN(container: Container, times: number) { 31 | const result: { 32 | avg: number; 33 | max: number; 34 | min: number; 35 | } = { 36 | avg: -1, 37 | max: -1, 38 | min: Number.MAX_SAFE_INTEGER, 39 | }; 40 | 41 | const items: number[] = []; 42 | 43 | for (let i: number = 0; i < times; i++) { 44 | const start: number = performance.now(); 45 | container.get(`SOME_ID_${times.toString()}`); 46 | const end: number = performance.now(); 47 | const total: number = end - start; 48 | 49 | if (total < result.min) { 50 | result.min = total; 51 | } 52 | if (total > result.max) { 53 | result.max = total; 54 | } 55 | 56 | items.push(total); 57 | } 58 | 59 | result.avg = 60 | items.reduce((p: number, c: number) => p + c, 0) / items.length; 61 | 62 | return result; 63 | } 64 | 65 | it('Should be able to register 1 binding in less than 1 ms', () => { 66 | const result1: { 67 | container: Container; 68 | register: number; 69 | } = registerN(1); 70 | expect(result1.register).to.below(1); 71 | expect(result1.register).to.below(1); 72 | }); 73 | 74 | it('Should be able to register 5 bindings in less than 1 ms', () => { 75 | const result5: { 76 | container: Container; 77 | register: number; 78 | } = registerN(5); 79 | expect(result5.register).to.below(1); 80 | }); 81 | 82 | it('Should be able to register 1K bindings in less than 1 ms', () => { 83 | const result1K: { 84 | container: Container; 85 | register: number; 86 | } = registerN(1000); 87 | expect(result1K.register).to.below(1); 88 | }); 89 | 90 | it('Should be able to register 5K bindings in less than 1 ms', () => { 91 | const result5K: { 92 | container: Container; 93 | register: number; 94 | } = registerN(5000); 95 | expect(result5K.register).to.below(1); 96 | }); 97 | 98 | it('Should be able to register 1 bindings in less than 1 ms', () => { 99 | const container1: Container = registerN(1000).container; 100 | const result1: { 101 | avg: number; 102 | max: number; 103 | min: number; 104 | } = resolveN(container1, 5); 105 | expect(result1.avg).to.below(1); 106 | }); 107 | 108 | it('Should be able to register 5 bindings in less than 1 ms', () => { 109 | const container5: Container = registerN(1000).container; 110 | const result5: { 111 | avg: number; 112 | max: number; 113 | min: number; 114 | } = resolveN(container5, 5); 115 | expect(result5.avg).to.below(1); 116 | }); 117 | 118 | it('Should be able to register 1K bindings in less than 1 ms', () => { 119 | const container1K: Container = registerN(1000).container; 120 | const result1K: { 121 | avg: number; 122 | max: number; 123 | min: number; 124 | } = resolveN(container1K, 5); 125 | expect(result1K.avg).to.below(1); 126 | }); 127 | 128 | it('Should be able to register 5K bindings in less than 1 ms', () => { 129 | const container5K: Container = registerN(5000).container; 130 | const result5K: { 131 | avg: number; 132 | max: number; 133 | min: number; 134 | } = resolveN(container5K, 5); 135 | expect(result5K.avg).to.below(1); 136 | }); 137 | 138 | it('Should be able to register 10K bindings in less than 1 ms', () => { 139 | const container10K: Container = registerN(10000).container; 140 | const result10K: { 141 | avg: number; 142 | max: number; 143 | min: number; 144 | } = resolveN(container10K, 5); 145 | expect(result10K.avg).to.below(1); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /src/test/node/proxies.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { Container, inject, injectable, ResolutionContext } from '../..'; 6 | 7 | describe('InversifyJS', () => { 8 | it('Should support the injection of proxied objects', () => { 9 | const weaponId: string = 'Weapon'; 10 | const warriorId: string = 'Warrior'; 11 | 12 | interface Weapon { 13 | use(): void; 14 | } 15 | 16 | @injectable() 17 | class Katana implements Weapon { 18 | public use() { 19 | return 'Used Katana!'; 20 | } 21 | } 22 | 23 | interface Warrior { 24 | weapon: Weapon; 25 | } 26 | 27 | @injectable() 28 | class Ninja implements Warrior { 29 | public weapon: Weapon; 30 | constructor(@inject(weaponId) weapon: Weapon) { 31 | this.weapon = weapon; 32 | } 33 | } 34 | 35 | const container: Container = new Container(); 36 | container.bind(warriorId).to(Ninja); 37 | const log: string[] = []; 38 | 39 | container 40 | .bind(weaponId) 41 | .to(Katana) 42 | .onActivation((_context: ResolutionContext, weapon: Weapon) => { 43 | const handler: ProxyHandler<() => void> = { 44 | apply( 45 | target: () => void, 46 | thisArgument: unknown, 47 | argumentsList: [], 48 | ): void { 49 | log.push(`Starting: ${new Date().getTime().toString()}`); 50 | target.apply(thisArgument, argumentsList); 51 | log.push(`Finished: ${new Date().getTime().toString()}`); 52 | }, 53 | }; 54 | weapon.use = new Proxy(weapon.use, handler); 55 | return weapon; 56 | }); 57 | 58 | const ninja: Warrior = container.get(warriorId); 59 | ninja.weapon.use(); 60 | 61 | expect(log.length).eql(2); 62 | expect(log[0]?.indexOf('Starting: ')).not.to.eql(-1); 63 | expect(log[1]?.indexOf('Finished: ')).not.to.eql(-1); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tsconfig.base.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.base.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "module": "ES2022", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "esModuleInterop": true, 9 | "exactOptionalPropertyTypes": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "lib": ["ES2022"], 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noPropertyAccessFromIndexSignature": true, 16 | "noUncheckedIndexedAccess": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "sourceMap": true, 20 | "strict": true, 21 | "target": "ES2022" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.base.cjs.json", 4 | "compilerOptions": { 5 | "outDir": "./lib/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "tsconfig.cjs.tsbuildinfo" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.base.esm.json", 4 | "compilerOptions": { 5 | "outDir": "./lib/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "tsconfig.esm.tsbuildinfo" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.cjs.json" 4 | } 5 | --------------------------------------------------------------------------------