Repository: inversify/InversifyJS Branch: master Commit: fdd9186891e7 Files: 57 Total size: 234.2 KB Directory structure: gitextract_m2iu_kne/ ├── .github/ │ ├── FUNDING.yml │ ├── 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.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: inversify open_collective: inversifyjs ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: "\U0001F41B Bug Report" description: "If something isn't working as expected \U0001F914" labels: ["needs triage"] body: - type: markdown attributes: value: | Please, submit the issue in the [monorepo](https://github.com/inversify/monorepo/issues/new?template=bug.yml) Please do not create the issue here, it will be closed. --- - type: checkboxes id: no-post attributes: label: | Please do not submit this issue. description: | :bangbang:   This issue will be closed. :bangbang: options: - label: I understand required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: "\U0001F680 Community Ideas" description: "I have an idea or proposal \U0001F4A1!" labels: ["needs triage"] assignees: - "notaphplover" body: - type: markdown attributes: value: | ## :heart: We would love to hear your ideas and proposals Please, submit your idea or proposal in the [monorepo](https://github.com/inversify/monorepo/issues/new?template=feature.yml) Please do not create the issue here, it will be closed. --- - type: checkboxes id: no-post attributes: label: | Please do not submit this issue. description: | :bangbang:   This issue will be closed. :bangbang: options: - label: I understand required: true ================================================ FILE: .github/workflows/ci.yml ================================================ name: Continuous Integration on: push: branches: - master pull_request: jobs: Testing: name: Compile source code and run tests runs-on: ${{ matrix.os }} strategy: matrix: node-version: [20.x, 22.x] os: [ubuntu-latest, windows-latest] ts-project: [tsconfig.json] exclude: - node-version: 22.x os: ubuntu-latest ts-project: tsconfig.json env: TS_NODE_PROJECT: ${{ matrix.ts-project }} steps: - name: Checkout Project uses: actions/checkout@v5 - name: Install Node.js uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} package-manager-cache: false - uses: pnpm/action-setup@v3 name: Install pnpm id: pnpm-install with: version: 10.17.1 run_install: false - name: Get pnpm store directory id: pnpm-cache run: | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT shell: bash - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/package.json') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Compile source code run: pnpm run build - name: Run tests run: pnpm test Upload_Coverage_Report: name: Upload coverage report to codecov environment: CI env: TS_NODE_PROJECT: tsconfig.json needs: [Testing] runs-on: ubuntu-latest steps: - name: Checkout Project uses: actions/checkout@v5 - name: Install Node.js uses: actions/setup-node@v5 with: node-version: 22.20.0 - uses: pnpm/action-setup@v3 name: Install pnpm id: pnpm-install with: version: 10.17.1 run_install: false - name: Get pnpm store directory id: pnpm-cache run: | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT shell: bash - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/package.json') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Compile source code run: pnpm run build - name: Run linter run: pnpm run lint - name: Run tests run: pnpm test --coverage - name: Codecov Upload uses: codecov/codecov-action@v5 with: directory: coverage/ fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Coverage directory used by tools like istanbul coverage # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules dist dts lib temp es type_definitions/inversify/*.js src/*.js src/**/*.js src/*.js.map src/**/*.js.map *.d.ts *.tsbuildinfo src/annotation/*.d.ts src/bindings/*.d.ts src/constants/*.d.ts src/container/*.d.ts src/planning/*.d.ts src/resolution/*.d.ts src/syntax/*.d.ts test/*.js test/**/*.js test/*.js.map test/**/*.js.map src/**/*.js.map src/*.js.map type_definitions/**/*.js .DS_store .idea .nyc_output ================================================ FILE: .npmignore ================================================ .github .gitignore .nyc_output .vscode build CODE_OF_CONDUCT.md CONTRIBUTING.md coverage eslint.config.mjs ISSUE_TEMPLATE.md **/*.ts !lib/cjs/**/*.d.ts lib/esm/**/*.d.ts.map !lib/esm/index.d.ts !lib/esm/index.d.ts.map lib/*/test/** mocha.opts prettier.config.mjs PULL_REQUEST_TEMPLATE.md renovate.json rollup.config.mjs scripts src temp tsconfig.json tsconfig.base.cjs.json tsconfig.base.esm.json tsconfig.base.json tsconfig.cjs.json tsconfig.cjs.tsbuildinfo tsconfig.esm.json wiki ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project from 5.0.0 forward will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added ### Changed ## [7.10.0] ### Added - Added `InjectFromBaseOptionsLifecycle`. - Added `InjectFromHierarchyOptionsLifecycle`. ### Changed - Updated `injectFromBase` with `lifecycle` property. - Updated `injectFromHierarchy` with `lifecycle` property. ## [7.9.1] ### Changed - Updated `Container` to trigger autobind options on autobound parent container related binding requests. ## [7.9.0] ### Changed - Updated `decorate` to allow method parameter decoration. ## [7.8.1] ### Changed - Fixed `injectFromHierarchy` avoiding crash when traversing `Object` metadata. ## [7.8.0] ### Added - Added `injectFromHierarchy`. ## [7.7.1] ### Changed - Fixed a bug involving circular dependencies when resolving services in some edge cases. - Fixed a bug involving circular dependencies when bindings are bound in some edge cases. ## [7.7.1-beta.0] ### Changed - Fixed a bug causing `Container.bind` and `Container.unbind` to avoid throwing planning errors. - Fixed a bug causing wrong cached plans involving bindings with ancestor related constraints. ## [7.7.0] ### Added - Added `Bind` type. - Added `IsBound` type. - Added `OnActivation` type. - Added `OnDeactivation` type. - Added `Rebind` type. - Added `RebindSync` type. - Added `Unbind` type. - Added `UnbindSync` type. ### Changed - Updated `BindOnFluentSyntaxImplementation.onDeactivation` to throw an error on non singleton scoped bindings. - Updated `ServiceResolutionManager` to provide right `getChained` operation after computed properties are reset. - Updated `Container` to properly manage bindings bindings on child containers after a parent container restores a snapshot. ## [7.6.1] ### Changed - Updated `getAll` and `getAllAsync` options with missing `chained` property. ## [7.6.0] ### Added - Added `MultiInjectOptions`. ### Changed - Updated `multiInject` decorator with optional `MultiInjectOptions`. - Updated `getAll` and `getAllAsync` options with optional`chained` property. ## [7.5.4] ### Changed - Updated `ServiceIdentifier` to rely on `Function` again. This solves an issue affecting classes with protected constructors. ## [7.5.3] ### Changed - Updated `ServiceIdentifier` removing `Function` in favor of a new `AbstractNewable` type. ServiceIdentifier now uses AbstractNewable instead of Function to better represent abstract classes. This provides better type safety and semantics. ## [7.5.2] ### Changed - Fixed `Container.snapshot` so snapshot bindings are not updated after a snapshot is taken. - Fixed a memory leak affecting child containers. ## [7.5.1] ### Changed - Updated `Container.get` like methods to properly set bindings when autobind mode is set: - `@injectable` scopes properly handled. - Autobind mode no longer creates duplicated bindings. ## [7.5.0] ### Changed - Updated `Container` with `unloadSync`. - Updated `Container` with `loadSync`. ## [7.4.0] ### Changed - Updated `ContainerModuleLoadOptions` with `rebind`. - Updated `ContainerModuleLoadOptions` with `rebindSync`. - Updated `BindToFluentSyntax.toResolvedValue` with additional type constraints. ## [7.3.0] ### Changed - Updated `Container` with `rebindSync`. - Updated `Container` with `unbindSync`. - Updated `Container` with `rebind`. - Updated `ContainerModuleLoadOptions` with `unbindSync`. - Updated `ContainerModuleLoadOptions.unbind` to accept `BindingIdentifier`. ## [7.2.0] ### Added - Added `BindingIdentifier`. ### Changed - Updated `BindInFluentSyntax` with `getIdentifier`. - Updated `Container.unbind` to handle `BindingIdentifier`. - Updated `BindOnFluentSyntax` with `getIdentifier`. - Updated `BindWhenFluentSyntax` with `getIdentifier`. ## [7.1.0] ### Added - Added `BindingActivation`. - Added `BindingDeactivation`. ## [7.0.2] ### Changed - Updated `container.get` like methods to no longer initialize twice singleton scoped bindings. ## [7.0.1] ### Changed - Updated `Container.get` like methods to no longer initialize twice singleton scoped bindings. ## [7.0.0] Parity version with `7.0.0-alpha.5` ## [7.0.0-alpha.5] ### Changed - Renamed `BindingMetadata` to `BindingConstraints`. - Improved performance on `Container.get` like methods. ## [7.0.0-alpha.4] Parity version with `7.0.0-alpha.3`. ## [6.2.2] - Solved issue with npm registry. ## [7.0.0-alpha.3] ### Changed - Updated `BindToFluentSyntax` with `.toResolvedValue`. ## [7.0.0-alpha.2] ### Changed - Updated `Container` with a plan cache. `Container.get`, `Container.getAll`, `Container.getAllAsync` and `Container.getAsync` performance has been improved. ## [7.0.0-alpha.1] ### Changed - Updated `GetOptions` with `autobind`. - Updated `ContainerOptions` with `autobind`. ## [7.0.0-alpha.0] ### Added - Added `BindInFluentSyntax`. - Added `BindInWhenOnFluentSyntax`. - Added `BindOnFluentSyntax`. - Added `BindingScope`. - Added `BindToFluentSyntax`. - Added `BindWhenFluentSyntax`. - Added `BindWhenOnFluentSyntax`. - Added `ContainerModuleLoadOptions`. - Added `DynamicValueBuilder`. - Added `Factory`. - Added `GetOptions`. - Added `GetOptionsTagConstraint`. - Added `IsBoundOptions`. - Added `MetadataName`. - Added `MetadataTag`. - Added `MetadataTargetName`. - Added `OptionalGetOptions`. - Added `Provider`. - Added `ResolutionContext`. - Added `bindingScopeValues`. - Added `bindingTypeValues`. - Added `injectFromBase` decorator. ### Changed - Updated `injectable` with optional `scope`. - [Breaking] Updated `ContainerModule` constructor to receive a callback with `ContainerModuleLoadOptions` instead of `interfaces.ContainerModuleCallBack`. - [Breaking] Updated `ContainerModule`.load to return `Promise`. - Updated `ContainerOptions` with `parent`. - Updated `ContainerOptions` without `autoBindInjectable` and `skipBaseClassChecks`. - [Breaking] Updated `Container` to no longer expose `id`, `parent` nor `options`. - [Breaking] Updated `Container` with no `applyCustomMetadataReader`, `applyMiddleware`, `createChild`, `merge` and `rebind` methods. - [Breaking] Updated `Container` with no `isCurrentBound`, `isBoundNamed`, `isBoundTagged` methods in favor of using `Container.isBound` with `isBoundOptions`. - [Breaking] Updated `Container` with no `getNamed`, `getTagged`, `tryGet`, `tryGetNamed` and `tryGetTagged` methods in favor of `Container.get` with `OptionalGetOptions` options. - [Breaking] Updated `Container` with no `getNamedAsync`, `getTaggedAsync`, `tryGetAsync`, `tryGetNamedAsync` and `tryGetTaggedAsync` methods in favor of `Container.getAsync` with `OptionalGetOptions` options. - [Breaking] Updated `Container` with no `getAllNamed`, `getAllTagged`, `tryGetAll`, `tryGetAllNamed` and `tryGetAllTagged` methods in favor of `Container.getAll` with `GetOptions` options. - [Breaking] Updated `Container` with no `getAllNamedAsync`, `getAllTaggedAsync`, `tryGetAllAsync`, `tryGetAllNamedAsync` and `tryGetAllTaggedAsync` methods in favor of `Container.getAllAsync` with `GetOptions` options. - [Breaking] Updated `Container` with no `loadAsync` in favor of an async `Container.load`. - [Breaking] Updated `Container` with no `unbindAsync` in favor of an async `Container.unbind`. - [Breaking] Updated `Container` with no `unbindAllAsync` in favor of an async `Container.unbindAll`. - [Breaking] Updated `Container` with no `unloadAsync` in favor of an async `Container.unload`. ### Fixed - Updated `decorate` to no longer require a unexpected prototypes to decorate property nor methods. ### Removed - [Breaking] Removed deprecated `LazyServiceIdentifer`. Use `LazyServiceIdentifier` instead. - [Breaking] Removed `BindingScopeEnum`. Use `bindingScopeValues` instead. - [Breaking] Removed `BindingTypeEnum`. - [Breaking] Removed `TargetTypeEnum`. - [Breaking] Removed `METADATA_KEY`. - [Breaking] Removed `AsyncContainerModule`. Use `ContainerModule` instead. - [Breaking] Removed `createTaggedDecorator`. - [Breaking] Removed `MetadataReader`. - [Breaking] Removed `id`. - [Breaking] Removed `interfaces` types. Rely on new types instead. - [Breaking] Removed `traverseAncerstors`. - [Breaking] Removed `taggedConstraint`. - [Breaking] Removed `namedConstraint`. - [Breaking] Removed `typeConstraint`. - [Breaking] Removed `getServiceIdentifierAsString`. - [Breaking] Removed `multiBindToService`. ## [6.2.1] ### Fixed - Added missing `LazyServiceIdentifer`. ## [6.2.0] ### Added - Added `interfaces.GetAllOptions`. ### Changed - Updated `container.getAll` with `options` optional param. - Updated `container.getAllAsync` with `options` optional param. - Updated `interfaces.NextArgs` with optional `isOptional` param. - Updated `container` with `tryGet`. - Updated `container` with `tryGetAsync`. - Updated `container` with `tryGetTagged`. - Updated `container` with `tryGetTaggedAsync`. - Updated `container` with `tryGetNamed`. - Updated `container` with `tryGetNamedAsync`. - Updated `container` with `tryGetAll`. - Updated `container` with `tryGetAllAsync`. - Updated `container` with `tryGetAllTagged`. - Updated `container` with `tryGetAllTaggedAsync`. - Updated `container` with `tryGetAllNamed`. - Updated `container` with `tryGetAllNamedAsync`. ## [6.2.0-beta.1] ### Added ### Changed - Updated `interfaces.NextArgs` with optional `isOptional` param. - Updated `container` with `tryGet`. - Updated `container` with `tryGetAsync`. - Updated `container` with `tryGetTagged`. - Updated `container` with `tryGetTaggedAsync`. - Updated `container` with `tryGetNamed`. - Updated `container` with `tryGetNamedAsync`. - Updated `container` with `tryGetAll`. - Updated `container` with `tryGetAllAsync`. - Updated `container` with `tryGetAllTagged`. - Updated `container` with `tryGetAllTaggedAsync`. - Updated `container` with `tryGetAllNamed`. - Updated `container` with `tryGetAllNamedAsync`. ### Fixed ## [6.2.0-beta.0] ### Added - Added `interfaces.GetAllOptions`. ### Changed - Updated `container.getAll` with `options` optional param. - Updated `container.getAllAsync` with `options` optional param. ### Fixed ## [6.1.6] ### Fixed - Fixed unexpected property access while running planning checks on injected base types. - Updated ESM sourcemaps to refelct the right source code files. ## [6.1.5] ### Changed - Updated library to import `reflect-metadata`. Importing `reflect-metadata` before bootstraping a module in the userland is no longer required. ### Fixed - Updated ESM build to provide proper types regardless of the ts resolution module strategy in the userland. - Fixed container to properly resolve async `.toService` bindings. - Fixed `.toService` binding to properly disable caching any values. ## [6.1.5-beta.2] ### Fixed - Updated ESM bundled types to solve circularly referenced types. ## [6.1.5-beta.1] ### Fixed - Updated ESM build to provide proper types regardless of the ts resolution module strategy in the userland. ## [6.1.5-beta.0] ### Changed - Updated library to import `reflect-metadata`. Importing `reflect-metadata` before bootstraping a module in the userland is no longer required. ### Fixed - Fixed container to properly resolve async `.toService` bindings. - Fixed `.toService` binding to properly disable caching any values. ## [6.1.4] ### Changed - Updated planner with better error description when a binding can not be properly resolved. ### Fixed - Updated container to allow deactivating singleton undefined values. - Updated ESM build to be compatible with both bundler and NodeJS module resolution algorithms. ## [6.1.4-beta.1] ### Fixed - Updated ESM build to be compatible with both bundler and NodeJS module resolution algorithms. ## [6.1.4-beta.0] ### Changed - Updated planner with better error description when a binding can not be properly resolved. ## [6.1.3] ### Fixed - Updated ESM build with missing types. ## [6.1.2] ### Changed - Updated `package.json` to include the `exports` field for better bundler support. ### Fixed - Updated fetch metadata flows with better error descriptions. ## [6.1.2-beta.1] ### Changed - Updated `package.json` to include the `exports` field for better bundler support. ## [6.1.2-beta.0] ### Fixed - Updated fetch metadata flows with better error descriptions. ## [6.1.1] ### Fixed - Bumped `@inversifyjs/common` and `@inversifyjs/core` fixing wrong dev engines constraints. ## [6.1.0] ### Changed - Updated `ServiceIdentifier` to rely on `Function` instead of `Abstract`. ### Fixed - Fixed `Target.getNameTag` with the right type: `number | string | symbol`. - Fixed `interfaces.ModuleActivationStore.addDeactivation` to enforce `serviceIdentifier` and `onDeactivation` are consistent. - Fixed `interfaces.ModuleActivationStore.addActivation` to enforce `serviceIdentifier` and `onDeactivation` are consistent. ## [6.0.3] ### Fixed property injection tagged as @optional no longer overrides default values with `undefined`. Updated `targetName` to be a valid `typescript@5` decorator. ## [6.0.2] ### Added Brought tests up to 100% Code Coverage ### Changed LazyIdentfier Tests Removed browser test pipeline, browserify, karma (#1542) Update all dependencies except typescript (#1531) ### Fixed Less than 100% code coverage Use default class property for @optional injected properties (#1467) Remove circular import (#1516) Fix strict type checking on @unmanaged decorator (#1499) Fix typo (LazyServiceIdentifer -> LazyServiceIdentifier) (#1483) Fix typo (circular dependency error message) (#1485) ## [6.0.1] - 2021-10-14 ### Added - add API method for check dependency only in current container - createTaggedDecorator #1343 - Async bindings #1132 - Async binding resolution (getAllAsync, getAllNamedAsync, getAllTaggedAsync, getAsync, getNamedAsync, getTaggedAsync, rebindAsync, unbindAsync, unbindAllAsync, unloadAsync) #1132 - Global onActivation / onDeactivation #1132 - Parent/Child onActivation / onDeactivation #1132 - Module onActivation / onDeactivation #1132 - Added @preDestroy decorator #1132 ### Changed - @postConstruct can target an asyncronous function #1132 - Singleton scoped services cache resolved values once the result promise is fulfilled #1320 ### Fixed - only inject decorator can be applied to setters #1342 - Container.resolve should resolve in that container #1338 ## [5.1.1] - 2021-04-25 -Fix pre-publish for build artifacts ## [5.1.0] - 2021-04-25 ### Added - Upgrade information for v4.x to v5.x ### Changed - Update BindingToSyntax with `.toAutoNamedFactory()`. ### Fixed - Fix `Target.isTagged()` to exclude `optional` from tag injections #1190. - Update `toConstructor`, `toFactory`, `toFunction`, `toAutoFactory`, `toProvider` and `toConstantValue` to have singleton scope #1297. - Fix injection on optional properties when targeting ES6 #928 ## [5.0.1] - 2018-10-17 ### Added - Updating constructor injection wiki document with concrete injection example #922 ### Changed - Change GUID to incremented counter for better performance #882 ### Fixed - fix broken compilation by adding `.toString()` so symbols serialization #893 - Fix problem with applying options on Container.resolve (fix #914) #915 - Fixed documentation issues ## [4.14.0] - 2018-10-16 Deprecated - Replaced by 5.0.1 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge 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. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities 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. 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. ## Scope 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. ## Enforcement 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. 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. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Inversify ## Setup 1 Clone your fork of the repository ```sh git clone https://github.com/YOUR_USERNAME/InversifyJS.git ``` 2 Install npm dependencies ```sh pnpm install ``` 3 Run build process ```sh pnpm test ``` ## Guidelines - Please try to [combine multiple commits before pushing](http://stackoverflow.com/questions/6934752/combining-multiple-commits-before-pushing-in-git) - 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 - Please keep the test coverage at 100%. Write additional unit tests if necessary - Please create an issue before sending a PR if it is going to change the public interface of InversifyJS or includes significant architecture changes - Feel free to ask for help from other members of the InversifyJS team via the chat / mailing list or github issues ================================================ FILE: ISSUE_TEMPLATE.md ================================================ ## Expected Behavior ## Current Behavior ## Possible Solution ## Steps to Reproduce (for bugs) 1. 2. 3. 4. ## Context ## Your Environment * Version used: * Environment name and version (e.g. Chrome 39, node.js 5.4): * Operating System and version (desktop or mobile): * Link to your project: # Stack trace ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-2017 Remo H. Jansen 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. 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. ================================================ FILE: PULL_REQUEST_TEMPLATE.md ================================================ ## Description ## Related Issue ## Motivation and Context ## How Has This Been Tested? ## Types of changes - [ ] Updated docs / Refactor code / Added a tests case (non-breaking change) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [ ] My code follows the code style of this project. - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. - [ ] I have read the **CONTRIBUTING** document. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. - [ ] I have updated the changelog. ================================================ FILE: README.md ================================================ > [!NOTE] > InversifyJS will be moving to the [monorepo](https://github.com/inversify/monorepo). Please, consider creating issues and pull requests there. > > Next releases will be published in the monorepo, so consider starring it to be notified.

NPM version NPM Downloads Docs Codecov

GitHub stars Discord Server

# InversifyJS A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript. ## 📕 Documentation Documentation is available at https://inversify.io ## About InversifyJS is a lightweight inversion of control (IoC) container for TypeScript and JavaScript apps. An IoC container uses a class constructor to identify and inject its dependencies. InversifyJS has a friendly API and encourages the usage of the best OOP and IoC practices. ## Motivation JavaScript now supports object oriented (OO) programming with class based inheritance. These features are great but the truth is that they are also [dangerous](https://medium.com/@dan_abramov/how-to-use-classes-and-sleep-at-night-9af8de78ccb4). 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. InversifyJS is a tool that helps JavaScript developers write code with good OO design. ## Philosophy InversifyJS has been developed with 4 main goals: 1. Allow JavaScript developers to write code that adheres to the SOLID principles. 2. Facilitate and encourage the adherence to the best OOP and IoC practices. 3. Add as little runtime overhead as possible. 4. Provide a state of the art development experience. ## Testimonies **[Nate Kohari](https://twitter.com/nkohari)** - Author of [Ninject](https://github.com/ninject/Ninject) > *"Nice work! I've taken a couple shots at creating DI frameworks for JavaScript and TypeScript, but the lack of RTTI really hinders things.* > *The ES7 metadata gets us part of the way there (as you've discovered). Keep up the great work!"* **[Michel Weststrate](https://twitter.com/mweststrate)** - Author of [MobX](https://github.com/mobxjs/mobx) > *Dependency injection like InversifyJS works nicely* ## Some companies using InversifyJS [](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/) ## Acknowledgements 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. ## License License under the MIT License (MIT) Copyright © 2015-2017 [Remo H. Jansen](http://www.remojansen.com) 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. 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. ================================================ FILE: eslint.config.mjs ================================================ // @ts-check import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; import eslintPrettierConfig from 'eslint-plugin-prettier/recommended'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; /** * @returns {import('typescript-eslint').ConfigWithExtends} */ function buildBaseConfig() { return { extends: [ eslint.configs.recommended, ...tseslint.configs.strictTypeChecked, ], languageOptions: { parser: tseslint.parser, parserOptions: { project: './tsconfig.json', }, }, plugins: { '@typescript-eslint': tseslint.plugin, 'simple-import-sort': simpleImportSort, }, rules: { '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], '@typescript-eslint/explicit-member-accessibility': [ 'error', { overrides: { constructors: 'no-public', }, }, ], '@typescript-eslint/member-ordering': ['warn'], '@typescript-eslint/naming-convention': [ 'error', { selector: ['classProperty'], format: ['strictCamelCase', 'UPPER_CASE', 'snake_case'], leadingUnderscore: 'allow', }, { selector: 'typeParameter', format: ['StrictPascalCase'], prefix: ['T'], }, { selector: ['typeLike'], format: ['StrictPascalCase'], }, { selector: ['function', 'classMethod'], format: ['strictCamelCase'], leadingUnderscore: 'allow', }, { selector: ['parameter'], format: ['strictCamelCase'], leadingUnderscore: 'allow', }, { selector: ['variableLike'], format: ['strictCamelCase', 'UPPER_CASE', 'snake_case'], }, ], '@typescript-eslint/no-deprecated': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'off', '@typescript-eslint/no-dynamic-delete': 'error', '@typescript-eslint/no-extraneous-class': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-empty-interface': 'warn', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-floating-promises': ['error'], '@typescript-eslint/no-unsafe-enum-comparison': 'off', 'no-magic-numbers': 'off', '@typescript-eslint/no-magic-numbers': [ 'warn', { ignore: [0, 1], ignoreArrayIndexes: true, ignoreEnums: true, ignoreReadonlyClassProperties: true, }, ], '@typescript-eslint/no-require-imports': 'error', '@typescript-eslint/no-unnecessary-type-arguments': 'off', '@typescript-eslint/no-unused-expressions': ['error'], '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-nullish-coalescing': ['off'], '@typescript-eslint/prefer-optional-chain': 'off', '@typescript-eslint/prefer-readonly': ['warn'], '@typescript-eslint/promise-function-async': ['error'], '@typescript-eslint/require-await': 'off', '@typescript-eslint/restrict-plus-operands': [ 'error', { skipCompoundAssignments: false, }, ], '@typescript-eslint/typedef': [ 'error', { arrayDestructuring: true, arrowParameter: true, memberVariableDeclaration: true, objectDestructuring: true, parameter: true, propertyDeclaration: true, variableDeclaration: true, }, ], '@typescript-eslint/unified-signatures': 'error', '@typescript-eslint/strict-boolean-expressions': 'error', '@typescript-eslint/switch-exhaustiveness-check': [ 'error', { considerDefaultExhaustiveForUnions: true, }, ], '@typescript-eslint/no-unused-vars': [ 'warn', { args: 'all', argsIgnorePattern: '^_', caughtErrors: 'all', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true, }, ], 'simple-import-sort/imports': [ 'error', { groups: [['^\\u0000'], ['^node:'], ['^@?\\w'], ['^'], ['^\\.']], }, ], 'sort-keys': [ 'error', 'asc', { caseSensitive: false, natural: true, }, ], }, }; } const baseRules = buildBaseConfig(); const config = tseslint.config( { ...baseRules, files: ['**/*.ts'], ignores: ['**/*.test.ts'], }, { ...baseRules, files: ['**/*.test.ts'], rules: { ...(baseRules.rules ?? {}), '@typescript-eslint/no-confusing-void-expression': 'off', '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/no-magic-numbers': 'off', }, }, /** @type {import('typescript-eslint').ConfigWithExtends} */ ( eslintPrettierConfig ), ); export default [...config]; ================================================ FILE: package.json ================================================ { "author": "Remo H. Jansen", "bugs": { "url": "https://github.com/inversify/InversifyJS/issues" }, "description": "A powerful and lightweight inversion of control container for JavaScript and Node.js apps powered by TypeScript.", "dependencies": { "@inversifyjs/common": "1.5.2", "@inversifyjs/container": "1.13.0", "@inversifyjs/core": "9.0.0" }, "devEngines": { "packageManager": { "name": "pnpm", "version": "^10.13.1", "onFail": "warn" }, "runtime": { "name": "node", "version": "^22.10.2", "onFail": "warn" } }, "devDependencies": { "@eslint/js": "9.36.0", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "12.1.4", "@types/chai": "5.2.2", "@types/mocha": "10.0.10", "@types/node": "22.18.6", "@types/sinon": "17.0.4", "@typescript-eslint/eslint-plugin": "8.47.0", "@typescript-eslint/parser": "8.47.0", "chai": "6.0.1", "eslint": "9.36.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.4", "eslint-plugin-simple-import-sort": "12.1.1", "mocha": "11.7.2", "nyc": "17.1.0", "prettier": "3.6.2", "rimraf": "6.0.1", "rollup": "4.52.2", "rollup-plugin-dts": "6.2.3", "sinon": "21.0.0", "ts-loader": "9.5.4", "tslib": "2.8.1", "ts-node": "10.9.2", "typescript": "5.9.2", "typescript-eslint": "8.44.1" }, "peerDependencies": { "reflect-metadata": "~0.2.2" }, "homepage": "http://inversify.io", "keywords": [ "dependency injection", "dependency inversion", "di", "inversion of control container", "ioc", "javascript", "node", "typescript" ], "license": "MIT", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", "exports": { ".": { "import": "./lib/esm/index.js", "require": "./lib/cjs/index.js" } }, "name": "inversify", "repository": { "type": "git", "url": "https://github.com/inversify/InversifyJS.git" }, "scripts": { "build": "pnpm run build:cjs && pnpm run build:esm", "build:cjs": "tsc --build tsconfig.cjs.json && node ./scripts/writeCommonJsPackageJson.mjs ./lib/cjs", "build:esm": "rollup -c ./rollup.config.mjs && node ./scripts/writeEsmPackageJson.mjs ./lib/esm", "build:clean": "rimraf lib", "format": "prettier --write ./src/**/*.ts", "lint": "eslint ./src", "prebuild": "pnpm run build:clean", "test": "nyc --reporter=lcov --require ts-node/register mocha src/test/*.test.ts src/test/**/*.test.ts --reporter spec --exit", "test:cjs": "nyc --reporter=lcov mocha lib/cjs/test/*.test.js lib/cjs/test/**/*.test.js --reporter spec" }, "sideEffects": false, "version": "7.10.0" } ================================================ FILE: prettier.config.mjs ================================================ export default { printWidth: 80, tabWidth: 2, useTabs: false, semi: true, singleQuote: true, bracketSpacing: true, arrowParens: 'always', endOfLine: 'lf', trailingComma: 'all', }; ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "automerge": false, "extends": [ "config:recommended", ":disableRateLimiting", ":semanticCommitScopeDisabled" ], "ignoreDeps": [], "packageRules": [ { "groupName": "auto merge on patch or minor", "automerge": true, "matchUpdateTypes": ["patch", "minor"], "matchPackageNames": ["!turbo", "!typescript"] } ], "rangeStrategy": "bump", "rebaseWhen": "conflicted", "semanticCommits": "enabled", "schedule": ["at any time"] } ================================================ FILE: rollup.config.mjs ================================================ import fs from 'node:fs/promises'; import terser from '@rollup/plugin-terser'; import typescript from '@rollup/plugin-typescript'; import { dts } from 'rollup-plugin-dts'; /** * @param {string} path * @returns {Promise} */ async function pathExists(path) { try { await fs.access(path); return true; } catch (_err) { return false; } } const PACKAGE_JSON_PATH = './package.json'; if (!pathExists(PACKAGE_JSON_PATH)) { throw new Error(`Expected "${PACKAGE_JSON_PATH}" path to exist`); } const packageJsonObject = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH)); const packageDependencies = Object.keys(packageJsonObject.dependencies ?? {}); const packagePeerDependencies = Object.keys( packageJsonObject.peerDependencies ?? {}, ); /** @type {!import("rollup").MergedRollupOptions[]} */ export default [ { input: './src/index.ts', external: [...packageDependencies, ...packagePeerDependencies], output: [ { dir: './lib/esm', format: 'esm', sourcemap: true, sourcemapPathTransform: (relativeSourcePath) => { // Rollup seems to generate source maps pointing to the wrong directory. Ugly patch to fix it if (relativeSourcePath.startsWith('../')) { return relativeSourcePath.slice(3); } else { return relativeSourcePath; } }, }, ], plugins: [ typescript({ tsconfig: './tsconfig.esm.json', }), terser(), ], }, { input: 'lib/esm/index.d.ts', output: [{ file: 'lib/esm/index.d.ts', format: 'es' }], plugins: [ dts({ tsconfig: './tsconfig.esm.json', }), ], }, ]; ================================================ FILE: scripts/writeCommonJsPackageJson.mjs ================================================ #!/usr/bin/env node import fs from 'node:fs/promises'; import { argv } from 'node:process'; import path from 'node:path'; import { writeFile } from 'node:fs/promises'; /** * @param {string} path * @returns {Promise} */ async function pathExists(path) { try { await fs.access(path); return true; } catch (_err) { return false; } } const directory = argv[2]; if (directory === undefined) { throw new Error('Expected a path'); } const directoryExists = await pathExists(directory); if (!directoryExists) { throw new Error(`Path ${directory} not found`); } const filePath = path.join(directory, 'package.json'); const packageJsonFileContent = JSON.stringify( { type: 'commonjs', }, undefined, 2, ); await writeFile(filePath, packageJsonFileContent); ================================================ FILE: scripts/writeEsmPackageJson.mjs ================================================ #!/usr/bin/env node import fs from 'node:fs/promises'; import { argv } from 'node:process'; import path from 'node:path'; import { writeFile } from 'node:fs/promises'; /** * @param {string} path * @returns {Promise} */ async function pathExists(path) { try { await fs.access(path); return true; } catch (_err) { return false; } } const directory = argv[2]; if (directory === undefined) { throw new Error('Expected a path'); } const directoryExists = await pathExists(directory); if (!directoryExists) { throw new Error(`Path ${directory} not found`); } const filePath = path.join(directory, 'package.json'); const packageJsonFileContent = JSON.stringify( { type: 'module', }, undefined, 2, ); await writeFile(filePath, packageJsonFileContent); ================================================ FILE: src/index.ts ================================================ import 'reflect-metadata'; export { Newable, LazyServiceIdentifier, ServiceIdentifier, } from '@inversifyjs/common'; export { Bind, BindInFluentSyntax, BindingIdentifier, BindInWhenOnFluentSyntax, BindOnFluentSyntax, BindToFluentSyntax, BindWhenFluentSyntax, BindWhenOnFluentSyntax, BoundServiceSyntax, Container, ContainerModule, ContainerModuleLoadOptions, ContainerOptions, IsBound, IsBoundOptions, OnActivation, OnDeactivation, Rebind, RebindSync, ResolvedValueInjectOptions, ResolvedValueMetadataInjectOptions, ResolvedValueMetadataInjectTagOptions, Unbind, UnbindSync, } from '@inversifyjs/container'; export { BindingActivation, BindingConstraints, BindingDeactivation, BindingScope, DynamicValueBuilder, Factory, GetAllOptions, GetOptions, GetOptionsTagConstraint, InjectFromBaseOptions, InjectFromBaseOptionsLifecycle, InjectFromHierarchyOptions, InjectFromHierarchyOptionsLifecycle, MetadataName, MetadataTag, MultiInjectOptions, OptionalGetOptions, // eslint-disable-next-line @typescript-eslint/no-deprecated Provider, ResolutionContext, bindingScopeValues, bindingTypeValues, decorate, inject, injectFromBase, injectFromHierarchy, injectable, multiInject, named, optional, unmanaged, tagged, postConstruct, preDestroy, } from '@inversifyjs/core'; ================================================ FILE: src/test/annotation/inject.test.ts ================================================ import 'reflect-metadata'; import { LazyServiceIdentifier } from '@inversifyjs/common'; import { expect } from 'chai'; import { decorate, inject, ServiceIdentifier } from '../..'; class Katana {} class Shuriken {} const lazySwordId: LazyServiceIdentifier = new LazyServiceIdentifier( () => 'Sword', ); class InvalidDecoratorUsageWarrior { private readonly _primaryWeapon: Katana; private readonly _secondaryWeapon: Shuriken; constructor(primary: Katana, secondary: Shuriken) { this._primaryWeapon = primary; this._secondaryWeapon = secondary; } public test(_a: string) {} public debug() { return { primaryWeapon: this._primaryWeapon, secondaryWeapon: this._secondaryWeapon, }; } } describe('@inject', () => { it('Should throw when applied multiple times', () => { const useDecoratorMoreThanOnce: () => void = function () { decorate( [inject('Katana'), inject('Shuriken')], InvalidDecoratorUsageWarrior, 0, ); }; const msg: string = `Unexpected injection error. Cause: Unexpected injection found. Multiple @inject, @multiInject or @unmanaged decorators found Details [class: "InvalidDecoratorUsageWarrior", index: "0"]`; expect(useDecoratorMoreThanOnce).to.throw(msg); }); it('Should unwrap LazyServiceIdentifier', () => { const unwrapped: ServiceIdentifier = lazySwordId.unwrap(); expect(unwrapped).to.be.equal('Sword'); }); }); ================================================ FILE: src/test/annotation/injectable.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { decorate, injectable } from '../..'; describe('@injectable', () => { it('Should throw when applied multiple times', () => { @injectable() class Test {} const useDecoratorMoreThanOnce: () => void = function () { decorate([injectable(), injectable()], Test); }; expect(useDecoratorMoreThanOnce).to.throw( 'Cannot apply @injectable decorator multiple times', ); }); }); ================================================ FILE: src/test/annotation/multi_inject.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { decorate, multiInject } from '../..'; type Weapon = object; class InvalidDecoratorUsageWarrior { private readonly _primaryWeapon: Weapon; private readonly _secondaryWeapon: Weapon; constructor(weapons: [Weapon, Weapon]) { this._primaryWeapon = weapons[0]; this._secondaryWeapon = weapons[1]; } public test(_a: string) {} public debug() { return { primaryWeapon: this._primaryWeapon, secondaryWeapon: this._secondaryWeapon, }; } } describe('@multiInject', () => { it('Should throw when applied multiple times', () => { const useDecoratorMoreThanOnce: () => void = function () { decorate( [multiInject('Katana'), multiInject('Shuriken')], InvalidDecoratorUsageWarrior, 0, ); }; const msg: string = `Unexpected injection error. Cause: Unexpected injection found. Multiple @inject, @multiInject or @unmanaged decorators found Details [class: "InvalidDecoratorUsageWarrior", index: "0"]`; expect(useDecoratorMoreThanOnce).to.throw(msg); }); }); ================================================ FILE: src/test/annotation/named.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { decorate, named } from '../..'; type Weapon = unknown; class InvalidDecoratorUsageWarrior { private readonly _primaryWeapon: Weapon; private readonly _secondaryWeapon: Weapon; constructor(primary: Weapon, secondary: Weapon) { this._primaryWeapon = primary; this._secondaryWeapon = secondary; } public test(_a: string) {} public debug() { return { primaryWeapon: this._primaryWeapon, secondaryWeapon: this._secondaryWeapon, }; } } describe('@named', () => { it('Should throw when applied multiple times', () => { const useDecoratorMoreThanOnce: () => void = function () { decorate( [named('Katana'), named('Shuriken')], InvalidDecoratorUsageWarrior, 0, ); }; const msg: string = `Unexpected injection error. Cause: Unexpected duplicated named decorator Details [class: "InvalidDecoratorUsageWarrior", index: "0"]`; expect(useDecoratorMoreThanOnce).to.throw(msg); }); }); ================================================ FILE: src/test/annotation/optional.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable, optional } from '../..'; describe('@optional', () => { it('Should allow to flag dependencies as optional', () => { @injectable() class Katana { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken { public name: string; constructor() { this.name = 'Shuriken'; } } @injectable() class Ninja { public name: string; public katana: Katana; public shuriken: Shuriken; constructor( @inject('Katana') katana: Katana, @inject('Shuriken') @optional() shuriken: Shuriken, ) { this.name = 'Ninja'; this.katana = katana; this.shuriken = shuriken; } } const container: Container = new Container(); container.bind('Katana').to(Katana); container.bind('Ninja').to(Ninja); let ninja: Ninja = container.get('Ninja'); expect(ninja.name).to.eql('Ninja'); expect(ninja.katana.name).to.eql('Katana'); expect(ninja.shuriken).to.eql(undefined); container.bind('Shuriken').to(Shuriken); ninja = container.get('Ninja'); expect(ninja.name).to.eql('Ninja'); expect(ninja.katana.name).to.eql('Katana'); expect(ninja.shuriken.name).to.eql('Shuriken'); }); it('Should allow to set a default value for dependencies flagged as optional', () => { @injectable() class Katana { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken { public name: string; constructor() { this.name = 'Shuriken'; } } @injectable() class Ninja { public name: string; public katana: Katana; public shuriken: Shuriken; constructor( @inject('Katana') katana: Katana, @inject('Shuriken') @optional() shuriken: Shuriken = { name: 'DefaultShuriken' }, ) { this.name = 'Ninja'; this.katana = katana; this.shuriken = shuriken; } } const container: Container = new Container(); container.bind('Katana').to(Katana); container.bind('Ninja').to(Ninja); let ninja: Ninja = container.get('Ninja'); expect(ninja.name).to.eql('Ninja'); expect(ninja.katana.name).to.eql('Katana'); expect(ninja.shuriken.name).to.eql('DefaultShuriken'); container.bind('Shuriken').to(Shuriken); ninja = container.get('Ninja'); expect(ninja.name).to.eql('Ninja'); expect(ninja.katana.name).to.eql('Katana'); expect(ninja.shuriken.name).to.eql('Shuriken'); }); it('Should allow to set a default value for class property dependencies flagged as optional', () => { @injectable() class Katana { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken { public name: string; constructor() { this.name = 'Shuriken'; } } @injectable() class Ninja { @inject('Katana') public katana?: Katana; @inject('Shuriken') @optional() public shuriken: Shuriken = { name: 'DefaultShuriken', }; public name: string = 'Ninja'; } const container: Container = new Container(); container.bind('Katana').to(Katana); container.bind('Ninja').to(Ninja); let ninja: Ninja = container.get('Ninja'); expect(ninja.name).to.eql('Ninja'); expect(ninja.katana?.name).to.eql('Katana'); expect(ninja.shuriken.name).to.eql('DefaultShuriken'); container.bind('Shuriken').to(Shuriken); ninja = container.get('Ninja'); expect(ninja.name).to.eql('Ninja'); expect(ninja.katana?.name).to.eql('Katana'); expect(ninja.shuriken.name).to.eql('Shuriken'); }); }); ================================================ FILE: src/test/annotation/post_construct.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { postConstruct } from '../..'; describe('@postConstruct', () => { it('Should throw when applied multiple times', () => { function setup() { class Katana { @postConstruct() @postConstruct() public testMethod1() { /* ... */ } } Katana.toString(); } expect(setup).to.throw(''); }); }); ================================================ FILE: src/test/bugs/bugs.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { BindingConstraints, Container, decorate, inject, injectable, injectFromBase, MetadataName, named, ServiceIdentifier, tagged, unmanaged, } from '../..'; describe('Bugs', () => { it('Should not throw when args length of base and derived class match (property setter)', () => { @injectable() class Warrior { public rank: string | null; constructor() { // length = 0 this.rank = null; } } @injectable() class SamuraiMaster extends Warrior { constructor() { // length = 0 super(); this.rank = 'master'; } } const container: Container = new Container(); container.bind(SamuraiMaster).to(SamuraiMaster); const master: SamuraiMaster = container.get(SamuraiMaster); expect(master.rank).eql('master'); }); it('Should not throw when args length of base and derived class match', () => { // Injecting into the derived class @injectable() class Warrior { protected rank: string; constructor(rank: string) { // length = 1 this.rank = rank; } } // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Rank: 'Rank' }; @injectable() class SamuraiMaster extends Warrior { constructor( @inject(TYPES.Rank) @named('master') public override rank: string, // length = 1 ) { super(rank); } } const container: Container = new Container(); container.bind(SamuraiMaster).to(SamuraiMaster); container .bind(TYPES.Rank) .toConstantValue('master') .whenNamed('master'); const master: SamuraiMaster = container.get(SamuraiMaster); expect(master.rank).eql('master'); }); it('Should not throw when args length of base and derived class match', () => { // Injecting into the derived class with multiple args @injectable() class Warrior { protected rank: string; constructor(rank: string) { // length = 1 this.rank = rank; } } interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Rank: 'Rank', Weapon: 'Weapon', }; @injectable() class SamuraiMaster extends Warrior { public weapon: Weapon; constructor( @inject(TYPES.Rank) @named('master') public override rank: string, @inject(TYPES.Weapon) weapon: Weapon, ) { // length = 2 super(rank); this.weapon = weapon; } } const container: Container = new Container(); container.bind(TYPES.Weapon).to(Katana); container.bind(SamuraiMaster).to(SamuraiMaster); container .bind(TYPES.Rank) .toConstantValue('master') .whenNamed('master'); const master: SamuraiMaster = container.get(SamuraiMaster); expect(master.rank).eql('master'); expect(master.weapon.name).eql('Katana'); }); it('Should be able to convert a Symbol value to a string', () => { type Weapon = unknown; // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Weapon: Symbol.for('Weapon'), }; const container: Container = new Container(); const throwF: () => void = () => { container.get(TYPES.Weapon); }; expect(throwF).to.throw(''); }); it('Should be able to combine tagged injection and constant value bindings', () => { const container: Container = new Container(); type Intl = unknown; container .bind('Intl') .toConstantValue({ hello: 'bonjour' }) .whenTagged('lang', 'fr'); container .bind('Intl') .toConstantValue({ goodbye: 'au revoir' }) .whenTagged('lang', 'fr'); const f: () => void = function () { container.get('Intl', { tag: { key: 'lang', value: 'fr', }, }); }; expect(f).to.throw(); }); it('Should be able to combine dynamic value with singleton scope', () => { const container: Container = new Container(); container .bind('transient_random') .toDynamicValue(() => Math.random()) .inTransientScope(); container .bind('singleton_random') .toDynamicValue(() => Math.random()) .inSingletonScope(); const a: number = container.get('transient_random'); const b: number = container.get('transient_random'); expect(a).not.to.eql(b); const c: number = container.get('singleton_random'); const d: number = container.get('singleton_random'); expect(c).to.eql(d); }); it('Should be able to use an abstract class as the serviceIdentifier', () => { @injectable() abstract class Animal { protected name: string; constructor(@unmanaged() name: string) { this.name = name; } public move(meters: number) { return `${this.name} moved ${meters.toString()}m`; } public abstract makeSound(input: string): string; } @injectable() class Snake extends Animal { constructor() { super('Snake'); } public makeSound(input: string): string { return 'sssss' + input; } public override move() { return 'Slithering... ' + super.move(5); } } @injectable() class Jungle { public animal: Animal; constructor(@inject(Animal) animal: Animal) { this.animal = animal; } } const container: Container = new Container(); container.bind(Animal).to(Snake); container.bind(Jungle).to(Jungle); const jungle: Jungle = container.get(Jungle); expect(jungle.animal.makeSound('zzz')).to.eql('ssssszzz'); expect(jungle.animal.move(5)).to.eql('Slithering... Snake moved 5m'); }); it('Should not be able to get a named dependency if no named bindings are registered', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Weapon: 'Weapon', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } const container: Container = new Container(); container.bind(TYPES.Weapon).to(Katana).whenNamed('sword'); const throws: () => void = () => { container.get(TYPES.Weapon, { name: 'bow', }); }; const error: string = `No bindings found for service: "Weapon". Trying to resolve bindings for "Weapon (Root service)"`; expect(throws).to.throw(error); }); it('Should throw a friendly error when binding a non-class using toSelf', () => { const container: Container = new Container(); const throws: () => void = () => { container.bind('testId').toSelf(); }; expect(throws).to.throw(''); }); it('Should be able to inject into an abstract class', () => { type Weapon = unknown; @injectable() abstract class BaseSoldier { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } @injectable() @injectFromBase({ extendConstructorArguments: true, }) class Soldier extends BaseSoldier {} @injectable() @injectFromBase({ extendConstructorArguments: true, }) class Archer extends BaseSoldier {} @injectable() @injectFromBase({ extendConstructorArguments: true, }) class Knight extends BaseSoldier {} @injectable() class Sword {} @injectable() class Bow {} @injectable() class DefaultWeapon {} const container: Container = new Container(); function whenIsAndIsNamed( serviceIdentifier: ServiceIdentifier, name: MetadataName, ): (bindingConstraints: BindingConstraints) => boolean { return (bindingConstraints: BindingConstraints): boolean => bindingConstraints.serviceIdentifier === serviceIdentifier && bindingConstraints.name === name; } container .bind('Weapon') .to(DefaultWeapon) .whenParent(whenIsAndIsNamed('BaseSoldier', 'default')); container .bind('Weapon') .to(Sword) .whenParent(whenIsAndIsNamed('BaseSoldier', 'knight')); container .bind('Weapon') .to(Bow) .whenParent(whenIsAndIsNamed('BaseSoldier', 'archer')); container.bind('BaseSoldier').to(Soldier).whenNamed('default'); container.bind('BaseSoldier').to(Knight).whenNamed('knight'); container.bind('BaseSoldier').to(Archer).whenNamed('archer'); const soldier: BaseSoldier = container.get('BaseSoldier', { name: 'default', }); const knight: BaseSoldier = container.get('BaseSoldier', { name: 'knight', }); const archer: BaseSoldier = container.get('BaseSoldier', { name: 'archer', }); expect(soldier.weapon instanceof DefaultWeapon).to.eql(true); expect(knight.weapon instanceof Sword).to.eql(true); expect(archer.weapon instanceof Bow).to.eql(true); }); it('Should be able apply inject to property shortcut', () => { interface Weapon { use(): string; } @injectable() class Katana implements Weapon { public use() { return 'Used Katana!'; } } @injectable() class Ninja { constructor( @inject('Weapon') @named('sword') private readonly _weapon: Weapon, ) { // } public fight() { return this._weapon.use(); } } const container: Container = new Container(); container.bind('Weapon').to(Katana).whenNamed('sword'); container.bind(Ninja).toSelf(); const ninja: Ninja = container.get(Ninja); expect(ninja.fight()).eql('Used Katana!'); }); it('Should be able to inject into abstract base class without decorators', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAGS = { Primary: 'Primary', Priority: 'Priority', Secondary: 'Secondary', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } interface Warrior { name: string; primaryWeapon: Weapon; } abstract class BaseWarrior implements Warrior { public name: string; public primaryWeapon!: Weapon; constructor(@unmanaged() name: string) { this.name = name; } } // @injectable() decorate([injectable()], BaseWarrior); // @inject(TYPES.Weapon) inject(TYPES.Weapon)(BaseWarrior.prototype, 'primaryWeapon'); // @tagged(TAGS.Priority, TAGS.Primary) tagged(TAGS.Priority, TAGS.Primary)(BaseWarrior.prototype, 'primaryWeapon'); @injectable() @injectFromBase({ extendProperties: true, }) class Samurai extends BaseWarrior { @inject(TYPES.Weapon) @tagged(TAGS.Priority, TAGS.Secondary) public secondaryWeapon!: Weapon; constructor() { super('Samurai'); } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Samurai); container .bind(TYPES.Weapon) .to(Katana) .whenTagged(TAGS.Priority, TAGS.Primary); container .bind(TYPES.Weapon) .to(Shuriken) .whenTagged(TAGS.Priority, TAGS.Secondary); const samurai: Samurai = container.get(TYPES.Warrior); expect(samurai.name).to.eql('Samurai'); expect(samurai.secondaryWeapon).not.to.eql(undefined); expect(samurai.secondaryWeapon.name).to.eql('Shuriken'); expect(samurai.primaryWeapon).not.to.eql(undefined); expect(samurai.primaryWeapon.name).to.eql('Katana'); }); it('Should be able to combine unmanaged and managed injections ', () => { interface Model { instance: T; } interface RepoBaseInterface { model: Model; } class Type { public name: string; constructor() { this.name = 'Type'; } } @injectable() class RepoBase implements RepoBaseInterface { public model: Model; constructor( // using @unmanaged() here is right // because entityType is NOT Injected by inversify @unmanaged() entityType: new () => T, ) { this.model = { instance: new entityType() }; } } @injectable() class TypedRepo extends RepoBase { constructor() { super(Type); // unmanaged injection (NOT Injected by inversify) } } @injectable() class BlBase { public repository: RepoBaseInterface; constructor( // using @unmanaged() here would wrong // because repository is injected by inversify repository: RepoBaseInterface, ) { this.repository = repository; } } @injectable() class TypedBl extends BlBase { // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor(repository: TypedRepo) { super(repository); } } const container: Container = new Container(); container.bind(TypedRepo).toSelf(); container.bind('TypedBL').to(TypedBl); const typedBl: TypedBl = container.get('TypedBL'); expect(typedBl.repository.model.instance.name).to.eq(new Type().name); }); it('Should allow missing annotations in base classes', () => { @injectable() class Katana implements Katana { public hit() { return 'cut!'; } } abstract class Warrior { private readonly _katana: Katana; constructor(@unmanaged() katana: Katana) { this._katana = katana; } public fight() { return this._katana.hit(); } } @injectable() class Ninja extends Warrior { constructor(@inject('Katana') katana: Katana) { super(katana); } } const container: Container = new Container(); container.bind('Ninja').to(Ninja); container.bind('Katana').to(Katana); const tryGet: () => void = () => { container.get('Ninja'); }; expect(tryGet).not.to.throw(); }); }); ================================================ FILE: src/test/bugs/issue_1190.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable, named, optional } from '../../index'; describe('Issue 1190', () => { it('should inject a katana as default weapon to ninja', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAG = { throwable: 'throwable', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } @injectable() class Ninja { public name: string; public katana: Katana; public shuriken: Shuriken; constructor( @inject(TYPES.Weapon) @optional() katana: Weapon, @inject(TYPES.Weapon) @named(TAG.throwable) shuriken: Weapon, ) { this.name = 'Ninja'; this.katana = katana; this.shuriken = shuriken; } } const container: Container = new Container(); container.bind(TYPES.Weapon).to(Katana).whenDefault(); container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAG.throwable); container.bind('Ninja').to(Ninja); const ninja: Ninja = container.get('Ninja'); expect(ninja.katana).to.deep.eq(new Katana()); }); }); ================================================ FILE: src/test/bugs/issue_1297.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { Container, Factory, injectable, Provider, ResolutionContext, } from '../..'; describe('Issue 1297', () => { it('should call onActivation once if the service is a constant value binding', () => { const container: Container = new Container(); const onActivationHandlerSpy: sinon.SinonSpy< [ResolutionContext, string], string > = sinon.spy<(_: ResolutionContext, message: string) => string>( (_: ResolutionContext, message: string) => message, ); container .bind('message') .toConstantValue('Hello world') .onActivation(onActivationHandlerSpy); container.get('message'); container.get('message'); expect(onActivationHandlerSpy.callCount).to.eq(1); }); it('should call onActivation once if the service is a factory binding', () => { @injectable() class Katana { public hit() { return 'cut!'; } } const container: Container = new Container(); const onActivationHandlerSpy: sinon.SinonSpy< [ResolutionContext, Factory], Factory > = sinon.spy< (_: ResolutionContext, instance: Factory) => Factory >((_: ResolutionContext, instance: Factory) => instance); container.bind('Katana').to(Katana); container .bind>('Factory') .toFactory( (context: ResolutionContext) => () => context.get('Katana'), ) .onActivation(onActivationHandlerSpy); container.get('Factory'); container.get('Factory'); expect(onActivationHandlerSpy.callCount).to.eq(1); }); it('should call onActivation once if the service is a provider binding', () => { @injectable() class Katana { public hit() { return 'cut!'; } } const container: Container = new Container(); const onActivationHandlerSpy: sinon.SinonSpy< // eslint-disable-next-line @typescript-eslint/no-deprecated [ResolutionContext, Provider], // eslint-disable-next-line @typescript-eslint/no-deprecated Provider > = sinon.spy< ( _: ResolutionContext, // eslint-disable-next-line @typescript-eslint/no-deprecated injectableObj: Provider, // eslint-disable-next-line @typescript-eslint/no-deprecated ) => Provider // eslint-disable-next-line @typescript-eslint/no-deprecated >((_: ResolutionContext, injectableObj: Provider) => injectableObj); container // eslint-disable-next-line @typescript-eslint/no-deprecated .bind>('Provider') // eslint-disable-next-line @typescript-eslint/no-deprecated .toProvider( (_context: ResolutionContext) => async () => Promise.resolve(new Katana()), ) .onActivation(onActivationHandlerSpy); container.get('Provider'); container.get('Provider'); expect(onActivationHandlerSpy.callCount).to.eq(1); }); }); ================================================ FILE: src/test/bugs/issue_1416.test.ts ================================================ import 'reflect-metadata'; import { describe, it } from 'mocha'; import sinon from 'sinon'; import { Container, injectable, preDestroy } from '../..'; describe('Issue 1416', () => { it('should allow providing default values on optional bindings', async () => { @injectable() class Test1 { public stub: sinon.SinonStub = sinon.stub(); @preDestroy() public destroy() { this.stub(); } } @injectable() class Test2 { public destroy(): void {} } @injectable() class Test3 { public destroy(): void {} } const container: Container = new Container({ defaultScope: 'Singleton' }); container.bind(Test1).toSelf(); container.bind(Test2).toService(Test1); container.bind(Test3).toService(Test1); const test1: Test1 = container.get(Test1); container.get(Test2); container.get(Test3); await Promise.all([ container.unbind(Test1), container.unbind(Test2), container.unbind(Test3), ]); sinon.assert.calledOnce(test1.stub); }); }); ================================================ FILE: src/test/bugs/issue_1515.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable, multiInject } from '../..'; describe('Issue 1515', () => { it('should properly throw on circular dependency', () => { @injectable() class Circle1 { constructor(@inject('circle-2') public readonly circle2: unknown) {} } @injectable() class Circle2 { constructor(@inject('circle-1') public circle1: unknown) {} } @injectable() class Multi1 {} @injectable() class Multi2 {} @injectable() class Multi3 {} @injectable() class Top { constructor( @multiInject('multi-inject') public readonly multis: unknown[], @inject('circle-1') public readonly circle1: unknown, ) {} } const container: Container = new Container(); container.bind('multi-inject').to(Multi1); container.bind('multi-inject').to(Multi2); container.bind('multi-inject').to(Multi3); container.bind('circle-1').to(Circle1); container.bind('circle-2').to(Circle2); container.bind(Top).toSelf(); expect(() => { container.get(Top); }).to.throw( 'Circular dependency found: Top -> circle-1 -> circle-2 -> circle-1', ); }); }); ================================================ FILE: src/test/bugs/issue_1518.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container } from '../..'; describe('Issue 1518', () => { it('should not throw on deactivating undefined singleton values', async () => { const container: Container = new Container(); const symbol: symbol = Symbol.for('foo'); container.bind(symbol).toConstantValue(undefined); console.log(container.get(symbol)); await container.unbind('foo'); expect(() => {}).not.to.throw(); }); }); ================================================ FILE: src/test/bugs/issue_1564.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { describe, it } from 'mocha'; import { Container, inject, injectable } from '../..'; describe('Issue 1564', () => { it('should not throw on getting async services bound using "toService"', async () => { @injectable() class Database { constructor() { console.log('new Database'); } } @injectable() class Service1 { constructor(@inject(Database) public database: Database) { console.log('new Service1'); } } @injectable() class Service2 { constructor(@inject(Service1) public service1: Service1) { console.log('new Service2'); } } const container: Container = new Container({ defaultScope: 'Request' }); container.bind(Database).toDynamicValue(async () => { console.log('connecting to db...'); return new Database(); }); container.bind(Service1).toSelf(); container.bind(Service2).toSelf(); container.bind('services').toService(Service1); container.bind('services').toService(Service2); const result: unknown[] = await container.getAllAsync('services'); expect(result).to.have.length(2); }); }); ================================================ FILE: src/test/bugs/issue_543.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable } from '../../index'; describe('Issue 543', () => { it('Should throw correct circular dependency path', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPE = { Child: Symbol.for('Child'), Child2: Symbol.for('Child2'), Circular: Symbol.for('Circular'), Irrelevant: Symbol.for('Irrelevant1'), Root: Symbol.for('Root'), }; @injectable() class Irrelevant {} @injectable() class Child2 { public circ: unknown; constructor(@inject(TYPE.Circular) circ: unknown) { this.circ = circ; } } @injectable() class Child { public irrelevant: Irrelevant; public child2: Child2; constructor( @inject(TYPE.Irrelevant) irrelevant: Irrelevant, @inject(TYPE.Child2) child2: Child2, ) { this.irrelevant = irrelevant; this.child2 = child2; } } @injectable() class Circular { public irrelevant: Irrelevant; public child: Child; constructor( @inject(TYPE.Irrelevant) irrelevant: Irrelevant, @inject(TYPE.Child) child: Child, ) { this.irrelevant = irrelevant; this.child = child; } } @injectable() class Root { public irrelevant: Irrelevant; public circ: Circular; constructor( @inject(TYPE.Irrelevant) irrelevant1: Irrelevant, @inject(TYPE.Circular) circ: Circular, ) { this.irrelevant = irrelevant1; this.circ = circ; } } const container: Container = new Container(); container.bind(TYPE.Root).to(Root); container.bind(TYPE.Irrelevant).to(Irrelevant); container.bind(TYPE.Circular).to(Circular); container.bind(TYPE.Child).to(Child); container.bind(TYPE.Child2).to(Child2); function throws() { return container.get(TYPE.Root); } expect(throws).to.throw( 'Circular dependency found: Symbol(Root) -> Symbol(Circular) -> Symbol(Child) -> Symbol(Child2) -> Symbol(Circular)', ); }); }); ================================================ FILE: src/test/bugs/issue_549.test.ts ================================================ import 'reflect-metadata'; import { Container, inject, injectable, ResolutionContext } from '../..'; describe('Issue 549', () => { it('Should throw if circular dependencies found with dynamics', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPE = { ADynamicValue: Symbol.for('ADynamicValue'), BDynamicValue: Symbol.for('BDynamicValue'), }; type InterfaceA = unknown; type InterfaceB = unknown; @injectable() class A { public b: InterfaceB; constructor(@inject(TYPE.BDynamicValue) b: InterfaceB) { this.b = b; } } @injectable() class B { public a: InterfaceA; constructor(@inject(TYPE.ADynamicValue) a: InterfaceA) { this.a = a; } } const container: Container = new Container({ defaultScope: 'Singleton' }); container.bind(A).toSelf(); container.bind(B).toSelf(); container .bind(TYPE.ADynamicValue) .toDynamicValue((ctx: ResolutionContext) => ctx.get(A)); container .bind(TYPE.BDynamicValue) .toDynamicValue((ctx: ResolutionContext) => ctx.get(B)); function willThrow() { return container.get(A); } try { const result: A = willThrow(); throw new Error( `This line should never be executed. Expected \`willThrow\` to throw! ${JSON.stringify(result)}`, ); } catch (e) { const localError: Error = e as Error; const expectedErrorA: string = ''; const expectedErrorB: string = ''; const matchesErrorA: boolean = localError.message.indexOf(expectedErrorA) !== -1; const matchesErrorB: boolean = localError.message.indexOf(expectedErrorB) !== -1; if (!matchesErrorA && !matchesErrorB) { throw new Error( 'Expected `willThrow` to throw:\n' + `- ${expectedErrorA}\n` + 'or\n' + `- ${expectedErrorB}\n` + 'but got\n' + `- ${localError.message}`, ); } } }); }); ================================================ FILE: src/test/bugs/issue_706.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { bindingScopeValues, Container, injectable } from '../..'; describe('Issue 706', () => { it('Should expose BindingScopeEnum as part of the public API', () => { @injectable() class SomeClass { public time: number; constructor() { this.time = new Date().getTime(); } } const container: Container = new Container({ defaultScope: bindingScopeValues.Singleton, }); // eslint-disable-next-line @typescript-eslint/typedef const TYPE = { SomeClass: Symbol.for('SomeClass'), }; container.bind(TYPE.SomeClass).to(SomeClass); const instanceOne: SomeClass = container.get(TYPE.SomeClass); const instanceTwo: SomeClass = container.get(TYPE.SomeClass); expect(instanceOne.time).to.eq(instanceTwo.time); }); }); ================================================ FILE: src/test/bugs/issue_928.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable, Newable, optional } from '../..'; describe('Issue 928', () => { it('should inject the right instances', () => { let injectedA: unknown; let injectedB: unknown; let injectedC: unknown; // some dependencies @injectable() class DepA { public a: number = 1; } @injectable() class DepB { public b: number = 1; } @injectable() class DepC { public c: number = 1; } @injectable() abstract class AbstractCls { constructor( @inject(DepA) a: DepA, @inject(DepB) @optional() b: DepB = { b: 0 }, ) { injectedA = a; injectedB = b; } } @injectable() class Cls extends AbstractCls { constructor( @inject(DepC) c: DepC, @inject(DepB) @optional() b: DepB = { b: 0 }, @inject(DepA) a: DepA, ) { super(a, b); injectedC = c; } } const container: Container = new Container(); [DepA, DepB, DepC, Cls].forEach((i: Newable) => container.bind(i).toSelf().inSingletonScope(), ); container.get(Cls); expect(injectedA).to.deep.eq(new DepA()); expect(injectedB).to.deep.eq(new DepB()); expect(injectedC).to.deep.eq(new DepC()); }); }); ================================================ FILE: src/test/container/container.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { bindingScopeValues, Container, inject, injectable, postConstruct, ResolutionContext, } from '../..'; describe('Container', () => { let sandbox: sinon.SinonSandbox; beforeEach(() => { sandbox = sinon.createSandbox(); }); afterEach(() => { sandbox.restore(); }); it('Should unbind a binding when requested', async () => { @injectable() class Ninja {} const ninjaId: string = 'Ninja'; const container: Container = new Container(); container.bind(ninjaId).to(Ninja); await container.unbind(ninjaId); expect(container.isBound(ninjaId)).equal(false); }); it('Should unbind a binding when requested', async () => { @injectable() class Ninja {} @injectable() class Samurai {} const ninjaId: string = 'Ninja'; const samuraiId: string = 'Samurai'; const container: Container = new Container(); container.bind(ninjaId).to(Ninja); container.bind(samuraiId).to(Samurai); expect(container.isBound(ninjaId)).equal(true); expect(container.isBound(samuraiId)).equal(true); await container.unbind(ninjaId); expect(container.isBound(ninjaId)).equal(false); expect(container.isBound(samuraiId)).equal(true); }); it('Should be able unbound all dependencies', async () => { @injectable() class Ninja {} @injectable() class Samurai {} const ninjaId: string = 'Ninja'; const samuraiId: string = 'Samurai'; const container: Container = new Container(); container.bind(ninjaId).to(Ninja); container.bind(samuraiId).to(Samurai); expect(container.isBound(ninjaId)).equal(true); expect(container.isBound(samuraiId)).equal(true); await container.unbindAll(); expect(container.isBound(ninjaId)).equal(false); expect(container.isBound(samuraiId)).equal(false); }); it('Should NOT be able to get unregistered services', () => { @injectable() class Ninja {} const ninjaId: string = 'Ninja'; const container: Container = new Container(); const throwFunction: () => void = () => { container.get(ninjaId); }; expect(throwFunction).to.throw(''); }); it('Should NOT be able to get ambiguous match', () => { type Warrior = unknown; @injectable() class Ninja {} @injectable() class Samurai {} const warriorId: string = 'Warrior'; const container: Container = new Container(); container.bind(warriorId).to(Ninja); container.bind(warriorId).to(Samurai); const throwFunction: () => void = () => { container.get(warriorId); }; expect(throwFunction).to.throw(''); }); it('Should be able to getAll of unregistered services', () => { @injectable() class Ninja {} const ninjaId: string = 'Ninja'; const container: Container = new Container(); expect(container.getAll(ninjaId)).to.deep.equal([]); }); it('Should be able to snapshot and restore container', async () => { @injectable() class Ninja {} @injectable() class Samurai {} const container: Container = new Container(); container.bind(Ninja).to(Ninja); container.bind(Samurai).to(Samurai); expect(container.get(Samurai)).to.be.instanceOf(Samurai); expect(container.get(Ninja)).to.be.instanceOf(Ninja); container.snapshot(); // snapshot container = v1 await container.unbind(Ninja); expect(container.get(Samurai)).to.be.instanceOf(Samurai); expect(() => container.get(Ninja)).to.throw(); container.snapshot(); // snapshot container = v2 expect(() => container.get(Ninja)).to.throw(); container.bind(Ninja).to(Ninja); expect(container.get(Samurai)).to.be.instanceOf(Samurai); expect(container.get(Ninja)).to.be.instanceOf(Ninja); container.restore(); // restore container to v2 expect(container.get(Samurai)).to.be.instanceOf(Samurai); expect(() => container.get(Ninja)).to.throw(); container.restore(); // restore container to v1 expect(container.get(Samurai)).to.be.instanceOf(Samurai); expect(container.get(Ninja)).to.be.instanceOf(Ninja); expect(() => { container.restore(); }).to.throw(''); }); it('Should maintain the activation state of a singleton when doing a snapshot of a container', () => { let timesCalled: number = 0; @injectable() class Ninja { @postConstruct() public postConstruct() { timesCalled++; } } const container: Container = new Container(); container.bind(Ninja).to(Ninja).inSingletonScope(); container.get(Ninja); container.snapshot(); container.restore(); container.get(Ninja); expect(timesCalled).to.be.equal(1); }); it('Should save and restore the container activations and deactivations when snapshot and restore', async () => { const sid: string = 'sid'; const container: Container = new Container(); container.bind(sid).toConstantValue('Value'); let activated: boolean = false; let deactivated: boolean = false; container.snapshot(); container.onActivation(sid, (_c: ResolutionContext, i: string) => { activated = true; return i; }); container.onDeactivation(sid, (_i: unknown) => { deactivated = true; }); container.restore(); container.get(sid); await container.unbind(sid); expect(activated).to.equal(false); expect(deactivated).to.equal(false); }); it('Should be able to check is there are bindings available for a given identifier', () => { const warriorId: string = 'Warrior'; const warriorSymbol: symbol = Symbol.for('Warrior'); @injectable() class Ninja {} const container: Container = new Container(); container.bind(Ninja).to(Ninja); container.bind(warriorId).to(Ninja); container.bind(warriorSymbol).to(Ninja); expect(container.isBound(Ninja)).equal(true); expect(container.isBound(warriorId)).equal(true); expect(container.isBound(warriorSymbol)).equal(true); const katanaId: string = 'Katana'; const katanaSymbol: symbol = Symbol.for('Katana'); @injectable() class Katana {} expect(container.isBound(Katana)).equal(false); expect(container.isBound(katanaId)).equal(false); expect(container.isBound(katanaSymbol)).equal(false); }); it('Should be able to check is there are bindings available for a given identifier only in current container', () => { @injectable() class Ninja {} const containerParent: Container = new Container(); const containerChild: Container = new Container({ parent: containerParent, }); containerParent.bind(Ninja).to(Ninja); expect(containerParent.isBound(Ninja)).to.eql(true); expect(containerChild.isBound(Ninja)).to.eql(true); expect(containerChild.isCurrentBound(Ninja)).to.eql(false); }); it('Should be able to get services from parent container', () => { const weaponIdentifier: string = 'Weapon'; @injectable() class Katana {} const container: Container = new Container(); container.bind(weaponIdentifier).to(Katana); const childContainer: Container = new Container({ parent: container }); const secondChildContainer: Container = new Container({ parent: childContainer, }); expect(secondChildContainer.get(weaponIdentifier)).to.be.instanceOf(Katana); }); it('Should be able to check if services are bound from parent container', () => { const weaponIdentifier: string = 'Weapon'; @injectable() class Katana {} const container: Container = new Container(); container.bind(weaponIdentifier).to(Katana); const childContainer: Container = new Container({ parent: container }); const secondChildContainer: Container = new Container({ parent: childContainer, }); expect(secondChildContainer.isBound(weaponIdentifier)).to.be.equal(true); }); it('Should prioritize requested container to resolve a service identifier', () => { const weaponIdentifier: string = 'Weapon'; @injectable() class Katana {} @injectable() class DivineRapier {} const container: Container = new Container(); container.bind(weaponIdentifier).to(Katana); const childContainer: Container = new Container({ parent: container }); const secondChildContainer: Container = new Container({ parent: childContainer, }); secondChildContainer.bind(weaponIdentifier).to(DivineRapier); expect(secondChildContainer.get(weaponIdentifier)).to.be.instanceOf( DivineRapier, ); }); it('Should be able to resolve named multi-injection', () => { interface Intl { hello?: string; goodbye?: string; } const container: Container = new Container(); container .bind('Intl') .toConstantValue({ hello: 'bonjour' }) .whenNamed('fr'); container .bind('Intl') .toConstantValue({ goodbye: 'au revoir' }) .whenNamed('fr'); container .bind('Intl') .toConstantValue({ hello: 'hola' }) .whenNamed('es'); container .bind('Intl') .toConstantValue({ goodbye: 'adios' }) .whenNamed('es'); const fr: Intl[] = container.getAll('Intl', { name: 'fr' }); expect(fr.length).to.equal(2); expect(fr[0]?.hello).to.equal('bonjour'); expect(fr[1]?.goodbye).to.equal('au revoir'); const es: Intl[] = container.getAll('Intl', { name: 'es' }); expect(es.length).to.equal(2); expect(es[0]?.hello).to.equal('hola'); expect(es[1]?.goodbye).to.equal('adios'); }); it('Should be able to resolve tagged multi-injection', () => { interface Intl { hello?: string; goodbye?: string; } const container: Container = new Container(); container .bind('Intl') .toConstantValue({ hello: 'bonjour' }) .whenTagged('lang', 'fr'); container .bind('Intl') .toConstantValue({ goodbye: 'au revoir' }) .whenTagged('lang', 'fr'); container .bind('Intl') .toConstantValue({ hello: 'hola' }) .whenTagged('lang', 'es'); container .bind('Intl') .toConstantValue({ goodbye: 'adios' }) .whenTagged('lang', 'es'); const fr: Intl[] = container.getAll('Intl', { tag: { key: 'lang', value: 'fr' }, }); expect(fr.length).to.equal(2); expect(fr[0]?.hello).to.equal('bonjour'); expect(fr[1]?.goodbye).to.equal('au revoir'); const es: Intl[] = container.getAll('Intl', { tag: { key: 'lang', value: 'es' }, }); expect(es.length).to.equal(2); expect(es[0]?.hello).to.equal('hola'); expect(es[1]?.goodbye).to.equal('adios'); }); it('Should be able to resolve optional injection', async () => { const container: Container = new Container(); const serviceIdentifier: string = 'service-id'; expect(container.get(serviceIdentifier, { optional: true })).to.eq( undefined, ); expect(container.getAll(serviceIdentifier, { optional: true })).to.deep.eq( [], ); expect( await container.getAllAsync(serviceIdentifier, { optional: true }), ).to.deep.eq([]); expect( container.getAll(serviceIdentifier, { name: 'name', optional: true, }), ).to.deep.eq([]); expect( await container.getAllAsync(serviceIdentifier, { name: 'name', optional: true, }), ).to.deep.eq([]); expect( container.getAll(serviceIdentifier, { optional: true, tag: { key: 'tag', value: 'value' }, }), ).to.deep.eq([]); expect( await container.getAllAsync(serviceIdentifier, { optional: true, tag: { key: 'tag', value: 'value' }, }), ).to.deep.eq([]); expect( await container.getAsync(serviceIdentifier, { optional: true, }), ).to.eq(undefined); expect( container.get(serviceIdentifier, { name: 'name', optional: true, }), ).to.eq(undefined); expect( await container.getAsync(serviceIdentifier, { name: 'name', optional: true, }), ).to.eq(undefined); expect( container.get(serviceIdentifier, { optional: true, tag: { key: 'tag', value: 'value' }, }), ).to.eq(undefined); expect( await container.getAsync(serviceIdentifier, { optional: true, tag: { key: 'tag', value: 'value' }, }), ).to.eq(undefined); }); it('Should be able configure the default scope at a global level', () => { interface Warrior { health: number; takeHit(damage: number): void; } @injectable() class Ninja implements Warrior { public health: number; constructor() { this.health = 100; } public takeHit(damage: number) { this.health = this.health - damage; } } // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', }; const container1: Container = new Container(); container1.bind(TYPES.Warrior).to(Ninja); const transientNinja1: Warrior = container1.get(TYPES.Warrior); expect(transientNinja1.health).to.equal(100); transientNinja1.takeHit(10); expect(transientNinja1.health).to.equal(90); const transientNinja2: Warrior = container1.get(TYPES.Warrior); expect(transientNinja2.health).to.equal(100); transientNinja2.takeHit(10); expect(transientNinja2.health).to.equal(90); const container2: Container = new Container({ defaultScope: bindingScopeValues.Singleton, }); container2.bind(TYPES.Warrior).to(Ninja); const singletonNinja1: Warrior = container2.get(TYPES.Warrior); expect(singletonNinja1.health).to.equal(100); singletonNinja1.takeHit(10); expect(singletonNinja1.health).to.equal(90); const singletonNinja2: Warrior = container2.get(TYPES.Warrior); expect(singletonNinja2.health).to.equal(90); singletonNinja2.takeHit(10); expect(singletonNinja2.health).to.equal(80); }); it('Should be able to override options to child containers', () => { @injectable() class Warrior {} const parent: Container = new Container({ defaultScope: bindingScopeValues.Request, }); const child: Container = new Container({ defaultScope: 'Singleton', parent, }); child.bind(Warrior).toSelf(); const singletonWarrior1: Warrior = child.get(Warrior); const singletonWarrior2: Warrior = child.get(Warrior); expect(singletonWarrior1).to.equal(singletonWarrior2); }); it('Should be able check if a named binding is bound', async () => { const zero: string = 'Zero'; const invalidDivisor: string = 'InvalidDivisor'; const validDivisor: string = 'ValidDivisor'; const container: Container = new Container(); expect(container.isBound(zero)).to.equal(false); container.bind(zero).toConstantValue(0); expect(container.isBound(zero)).to.equal(true); await container.unbindAll(); expect(container.isBound(zero)).to.equal(false); container.bind(zero).toConstantValue(0).whenNamed(invalidDivisor); expect(container.isBound(zero, { name: invalidDivisor })).to.equal(true); expect(container.isBound(zero, { name: validDivisor })).to.equal(false); container.bind(zero).toConstantValue(1).whenNamed(validDivisor); expect(container.isBound(zero, { name: invalidDivisor })).to.equal(true); expect(container.isBound(zero, { name: validDivisor })).to.equal(true); }); it('Should be able to check if a named binding is bound from parent container', () => { const zero: string = 'Zero'; const invalidDivisor: string = 'InvalidDivisor'; const validDivisor: string = 'ValidDivisor'; const container: Container = new Container(); const childContainer: Container = new Container({ parent: container }); const secondChildContainer: Container = new Container({ parent: childContainer, }); container.bind(zero).toConstantValue(0).whenNamed(invalidDivisor); expect( secondChildContainer.isBound(zero, { name: invalidDivisor }), ).to.equal(true); expect(secondChildContainer.isBound(zero, { name: validDivisor })).to.equal( false, ); container.bind(zero).toConstantValue(1).whenNamed(validDivisor); expect( secondChildContainer.isBound(zero, { name: invalidDivisor }), ).to.equal(true); expect(secondChildContainer.isBound(zero, { name: validDivisor })).to.equal( true, ); }); it('Should be able to get a tagged binding', () => { const zero: string = 'Zero'; const isValidDivisor: string = 'IsValidDivisor'; const container: Container = new Container(); container .bind(zero) .toConstantValue(0) .whenTagged(isValidDivisor, false); expect( container.get(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(0); container .bind(zero) .toConstantValue(1) .whenTagged(isValidDivisor, true); expect( container.get(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(0); expect( container.get(zero, { tag: { key: isValidDivisor, value: true }, }), ).to.equal(1); }); it('Should be able to get a tagged binding from parent container', () => { const zero: string = 'Zero'; const isValidDivisor: string = 'IsValidDivisor'; const container: Container = new Container(); const childContainer: Container = new Container({ parent: container }); const secondChildContainer: Container = new Container({ parent: childContainer, }); container .bind(zero) .toConstantValue(0) .whenTagged(isValidDivisor, false); container .bind(zero) .toConstantValue(1) .whenTagged(isValidDivisor, true); expect( secondChildContainer.get(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(0); expect( secondChildContainer.get(zero, { tag: { key: isValidDivisor, value: true }, }), ).to.equal(1); }); it('Should be able check if a tagged binding is bound', async () => { const zero: string = 'Zero'; const isValidDivisor: string = 'IsValidDivisor'; const container: Container = new Container(); expect(container.isBound(zero)).to.equal(false); container.bind(zero).toConstantValue(0); expect(container.isBound(zero)).to.equal(true); await container.unbindAll(); expect(container.isBound(zero)).to.equal(false); container .bind(zero) .toConstantValue(0) .whenTagged(isValidDivisor, false); expect( container.isBound(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(true); expect( container.isBound(zero, { tag: { key: isValidDivisor, value: true }, }), ).to.equal(false); container .bind(zero) .toConstantValue(1) .whenTagged(isValidDivisor, true); expect( container.isBound(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(true); expect( container.isBound(zero, { tag: { key: isValidDivisor, value: true }, }), ).to.equal(true); }); it('Should be able to check if a tagged binding is bound from parent container', () => { const zero: string = 'Zero'; const isValidDivisor: string = 'IsValidDivisor'; const container: Container = new Container(); const childContainer: Container = new Container({ parent: container }); const secondChildContainer: Container = new Container({ parent: childContainer, }); container .bind(zero) .toConstantValue(0) .whenTagged(isValidDivisor, false); expect( secondChildContainer.isBound(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(true); expect( secondChildContainer.isBound(zero, { tag: { key: isValidDivisor, value: true }, }), ).to.equal(false); container .bind(zero) .toConstantValue(1) .whenTagged(isValidDivisor, true); expect( secondChildContainer.isBound(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(true); expect( secondChildContainer.isBound(zero, { tag: { key: isValidDivisor, value: true }, }), ).to.equal(true); }); it('Should be able to override a binding using rebind', async () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { someType: 'someType', }; const container: Container = new Container(); container.bind(TYPES.someType).toConstantValue(1); container.bind(TYPES.someType).toConstantValue(2); const values1: unknown[] = container.getAll(TYPES.someType); expect(values1[0]).to.eq(1); expect(values1[1]).to.eq(2); await container.unbind(TYPES.someType); container.bind(TYPES.someType).toConstantValue(3); const values2: unknown[] = container.getAll(TYPES.someType); expect(values2[0]).to.eq(3); expect(values2[1]).to.eq(undefined); }); it('Should be able to resolve named multi-injection (async)', async () => { interface Intl { hello?: string; goodbye?: string; } const container: Container = new Container(); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ hello: 'bonjour' })) .whenNamed('fr'); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ goodbye: 'au revoir' })) .whenNamed('fr'); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ hello: 'hola' })) .whenNamed('es'); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ goodbye: 'adios' })) .whenNamed('es'); const fr: Intl[] = await container.getAllAsync('Intl', { name: 'fr', }); expect(fr.length).to.equal(2); expect(fr[0]?.hello).to.equal('bonjour'); expect(fr[1]?.goodbye).to.equal('au revoir'); const es: Intl[] = await container.getAllAsync('Intl', { name: 'es', }); expect(es.length).to.equal(2); expect(es[0]?.hello).to.equal('hola'); expect(es[1]?.goodbye).to.equal('adios'); }); it('Should be able to resolve named (async)', async () => { interface Intl { hello?: string; goodbye?: string; } const container: Container = new Container(); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ hello: 'bonjour' })) .whenNamed('fr'); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ hello: 'hola' })) .whenNamed('es'); const fr: Intl = await container.getAsync('Intl', { name: 'fr' }); expect(fr.hello).to.equal('bonjour'); const es: Intl = await container.getAsync('Intl', { name: 'es' }); expect(es.hello).to.equal('hola'); }); it('Should be able to resolve tagged multi-injection (async)', async () => { interface Intl { hello?: string; goodbye?: string; } const container: Container = new Container(); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ hello: 'bonjour' })) .whenTagged('lang', 'fr'); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ goodbye: 'au revoir' })) .whenTagged('lang', 'fr'); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ hello: 'hola' })) .whenTagged('lang', 'es'); container .bind('Intl') .toDynamicValue(async () => Promise.resolve({ goodbye: 'adios' })) .whenTagged('lang', 'es'); const fr: Intl[] = await container.getAllAsync('Intl', { tag: { key: 'lang', value: 'fr' }, }); expect(fr.length).to.equal(2); expect(fr[0]?.hello).to.equal('bonjour'); expect(fr[1]?.goodbye).to.equal('au revoir'); const es: Intl[] = await container.getAllAsync('Intl', { tag: { key: 'lang', value: 'es' }, }); expect(es.length).to.equal(2); expect(es[0]?.hello).to.equal('hola'); expect(es[1]?.goodbye).to.equal('adios'); }); it('Should be able to get a tagged binding (async)', async () => { const zero: string = 'Zero'; const isValidDivisor: string = 'IsValidDivisor'; const container: Container = new Container(); container .bind(zero) .toDynamicValue(async () => Promise.resolve(0)) .whenTagged(isValidDivisor, false); expect( await container.getAsync(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(0); container .bind(zero) .toDynamicValue(async () => Promise.resolve(1)) .whenTagged(isValidDivisor, true); expect( await container.getAsync(zero, { tag: { key: isValidDivisor, value: false }, }), ).to.equal(0); expect( await container.getAsync(zero, { tag: { key: isValidDivisor, value: true }, }), ).to.equal(1); }); it('should be able to get all the services binded (async)', async () => { const serviceIdentifier: string = 'service-identifier'; const container: Container = new Container(); const firstValueBinded: string = 'value-one'; const secondValueBinded: string = 'value-two'; const thirdValueBinded: string = 'value-three'; container.bind(serviceIdentifier).toConstantValue(firstValueBinded); container.bind(serviceIdentifier).toConstantValue(secondValueBinded); container .bind(serviceIdentifier) .toDynamicValue(async (_: ResolutionContext) => Promise.resolve(thirdValueBinded), ); const services: string[] = await container.getAllAsync(serviceIdentifier); expect(services).to.deep.eq([ firstValueBinded, secondValueBinded, thirdValueBinded, ]); }); it('Should be able to inject when symbol property key ', () => { const weaponProperty: unique symbol = Symbol(); type Weapon = unknown; @injectable() class Shuriken {} @injectable() class Ninja { @inject('Weapon') public [weaponProperty]!: Weapon; } const container: Container = new Container(); container.bind('Weapon').to(Shuriken); container.bind(Ninja).toSelf(); const myNinja: Ninja = container.get(Ninja); const weapon: Weapon = myNinja[weaponProperty]; expect(weapon).to.be.instanceOf(Shuriken); }); }); ================================================ FILE: src/test/container/container_module.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { Container, ContainerModule, ContainerModuleLoadOptions, ResolutionContext, } from '../..'; describe('ContainerModule', () => { it('Should be able to set the registry of a container module', () => { const registry: ( options: ContainerModuleLoadOptions, ) => Promise = async (_options: ContainerModuleLoadOptions) => undefined; const warriors: ContainerModule = new ContainerModule(registry); expect(warriors.id).to.be.a('number'); }); it('Should be able to remove some bindings from within a container module', async () => { const container: Container = new Container(); container.bind('A').toConstantValue('1'); expect(container.get('A')).to.eql('1'); const warriors: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { await options.unbind('A'); expect(() => { container.get('A'); }).to.throw(); options.bind('A').toConstantValue('2'); expect(container.get('A')).to.eql('2'); options.bind('B').toConstantValue('3'); expect(container.get('B')).to.eql('3'); }, ); await container.load(warriors); expect(container.get('A')).to.eql('2'); expect(container.get('B')).to.eql('3'); }); it('Should be able to check for existence of bindings within a container module', async () => { const container: Container = new Container(); container.bind('A').toConstantValue('1'); expect(container.get('A')).to.eql('1'); const warriors: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { expect(options.isBound('A')).to.eql(true); await options.unbind('A'); expect(options.isBound('A')).to.eql(false); }, ); await container.load(warriors); }); it('Should be able to override a binding using rebind within a container module', async () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { someType: 'someType', }; const container: Container = new Container(); const module1: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.bind(TYPES.someType).toConstantValue(1); options.bind(TYPES.someType).toConstantValue(2); }, ); const module2: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { await options.unbind(TYPES.someType); options.bind(TYPES.someType).toConstantValue(3); }, ); await container.load(module1); const values1: unknown[] = container.getAll(TYPES.someType); expect(values1[0]).to.eq(1); expect(values1[1]).to.eq(2); await container.load(module2); const values2: unknown[] = container.getAll(TYPES.someType); expect(values2[0]).to.eq(3); expect(values2[1]).to.eq(undefined); }); it('Should be able use await async functions in container modules', async () => { const container: Container = new Container(); const someAsyncFactory: () => Promise = async () => new Promise( (res: (value: number | PromiseLike) => void) => setTimeout(() => { res(1); }, 100), ); const A: unique symbol = Symbol.for('A'); const B: unique symbol = Symbol.for('B'); const moduleOne: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { const val: number = await someAsyncFactory(); options.bind(A).toConstantValue(val); }, ); const moduleTwo: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.bind(B).toConstantValue(2); }, ); await container.load(moduleOne, moduleTwo); const aIsBound: boolean = container.isBound(A); expect(aIsBound).to.eq(true); const a: unknown = container.get(A); expect(a).to.eq(1); }); it('Should be able to add an activation hook through a container module', async () => { const container: Container = new Container(); const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options .bind('B') .toConstantValue('2') .onActivation(() => 'C'); options.onActivation('A', () => 'B'); container.bind('A').toConstantValue('1'); }, ); await container.load(module); expect(container.get('A')).to.eql('B'); expect(container.get('B')).to.eql('C'); }); it('Should be able to add a deactivation hook through a container module', async () => { const container: Container = new Container(); container.bind('A').toConstantValue('1'); let deact: boolean = false; const warriors: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.onDeactivation('A', () => { deact = true; }); }, ); await container.load(warriors); container.get('A'); await container.unbind('A'); expect(deact).eql(true); }); it('Should be able to add an async deactivation hook through a container module (async)', async () => { const container: Container = new Container(); container.bind('A').toConstantValue('1'); let deact: boolean = false; const warriors: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.onDeactivation('A', async () => { deact = true; }); }, ); await container.load(warriors); container.get('A'); await container.unbind('A'); expect(deact).eql(true); }); it('Should be able to add multiple async deactivation hook through a container module (async)', async () => { const onActivationHandlerSpy: sinon.SinonSpy<[], Promise> = sinon.spy< () => Promise >(async () => undefined); const serviceIdentifier: string = 'destroyable'; const container: Container = new Container(); const containerModule: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.onDeactivation(serviceIdentifier, onActivationHandlerSpy); options.onDeactivation(serviceIdentifier, onActivationHandlerSpy); }, ); container.bind(serviceIdentifier).toConstantValue(serviceIdentifier); container.get(serviceIdentifier); await container.load(containerModule); await container.unbind(serviceIdentifier); expect(onActivationHandlerSpy.callCount).to.eq(2); }); it('Should remove module bindings when unload', async () => { const sid: string = 'sid'; const container: Container = new Container(); container.bind(sid).toConstantValue('Not module'); const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.bind(sid).toConstantValue('Module'); }, ); await container.load(module); expect(container.getAll(sid)).to.deep.equal(['Not module', 'Module']); await container.unload(module); expect(container.getAll(sid)).to.deep.equal(['Not module']); }); it('Should deactivate singletons from module bindings when unload', async () => { const sid: string = 'sid'; const container: Container = new Container(); let moduleBindingDeactivated: string | undefined; let containerDeactivated: string | undefined; const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options .bind(sid) .toConstantValue('Module') .onDeactivation((injectable: string) => { moduleBindingDeactivated = injectable; }); options.onDeactivation(sid, (injectable: string) => { containerDeactivated = injectable; }); }, ); await container.load(module); container.get(sid); await container.unload(module); expect(moduleBindingDeactivated).to.equal('Module'); expect(containerDeactivated).to.equal('Module'); }); it('Should remove container handlers from module when unload', async () => { const sid: string = 'sid'; const container: Container = new Container(); let activatedNotModule: string | undefined; let deactivatedNotModule: string | undefined; container.onActivation( sid, (_: ResolutionContext, injected: string) => { activatedNotModule = injected; return injected; }, ); container.onDeactivation(sid, (injected: string) => { deactivatedNotModule = injected; }); container.bind(sid).toConstantValue('Value'); let activationCount: number = 0; let deactivationCount: number = 0; const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.onDeactivation(sid, (_: string) => { deactivationCount++; }); options.onActivation( sid, (_: ResolutionContext, injected: string) => { activationCount++; return injected; }, ); }, ); await container.load(module); await container.unload(module); container.get(sid); await container.unbind(sid); expect(activationCount).to.equal(0); expect(deactivationCount).to.equal(0); expect(activatedNotModule).to.equal('Value'); expect(deactivatedNotModule).to.equal('Value'); }); it('Should remove module bindings when unloadAsync', async () => { const sid: string = 'sid'; const container: Container = new Container(); container.onDeactivation(sid, async (_injected: unknown) => Promise.resolve(), ); container.bind(sid).toConstantValue('Not module'); const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.bind(sid).toConstantValue('Module'); }, ); await container.load(module); let values: unknown[] = container.getAll(sid); expect(values).to.deep.equal(['Not module', 'Module']); await container.unload(module); values = container.getAll(sid); expect(values).to.deep.equal(['Not module']); }); it('Should deactivate singletons from module bindings when unloadAsync', async () => { const sid: string = 'sid'; const container: Container = new Container(); let moduleBindingDeactivated: string | undefined; let containerDeactivated: string | undefined; const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options .bind(sid) .toConstantValue('Module') .onDeactivation((injectable: string) => { moduleBindingDeactivated = injectable; }); options.onDeactivation(sid, async (injectable: string) => { containerDeactivated = injectable; return Promise.resolve(); }); }, ); await container.load(module); container.get(sid); await container.unload(module); expect(moduleBindingDeactivated).to.equal('Module'); expect(containerDeactivated).to.equal('Module'); }); it('Should remove container handlers from module when unloadAsync', async () => { const sid: string = 'sid'; const container: Container = new Container(); let activatedNotModule: string | undefined; let deactivatedNotModule: string | undefined; container.onActivation( sid, (_: ResolutionContext, injected: string) => { activatedNotModule = injected; return injected; }, ); container.onDeactivation(sid, (injected: string) => { deactivatedNotModule = injected; }); container.bind(sid).toConstantValue('Value'); let activationCount: number = 0; let deactivationCount: number = 0; const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { options.onDeactivation(sid, async (_: string) => { deactivationCount++; return Promise.resolve(); }); options.onActivation( sid, (_: ResolutionContext, injected: string) => { activationCount++; return injected; }, ); }, ); await container.load(module); await container.unload(module); container.get(sid); await container.unbind(sid); expect(activationCount).to.equal(0); expect(deactivationCount).to.equal(0); expect(activatedNotModule).to.equal('Value'); expect(deactivatedNotModule).to.equal('Value'); }); it('should be able to unbindAsync from a module', async () => { const sid: string = 'sid'; const container: Container = new Container(); const module: ContainerModule = new ContainerModule( async (options: ContainerModuleLoadOptions) => { await options.unbind(sid); }, ); container.bind(sid).toConstantValue('Value'); container.bind(sid).toConstantValue('Value2'); const deactivated: string[] = []; container.onDeactivation(sid, async (injected: string) => { deactivated.push(injected); return Promise.resolve(); }); container.getAll(sid); await container.load(module); expect(deactivated).to.deep.equal(['Value', 'Value2']); //bindings removed expect(container.getAll(sid)).to.deep.equal([]); }); }); ================================================ FILE: src/test/features/named_default.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable, named } from '../..'; describe('Named default', () => { it('Should be able to inject a default to avoid ambiguous binding exceptions', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAG = { chinese: 'chinese', japanese: 'japanese', throwable: 'throwable', }; interface Weapon { name: string; } interface Warrior { name: string; weapon: Weapon; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } @injectable() class Samurai implements Warrior { public name: string; public weapon: Weapon; constructor(@inject(TYPES.Weapon) weapon: Weapon) { this.name = 'Samurai'; this.weapon = weapon; } } @injectable() class Ninja implements Warrior { public name: string; public weapon: Weapon; constructor(@inject(TYPES.Weapon) @named(TAG.throwable) weapon: Weapon) { this.name = 'Ninja'; this.weapon = weapon; } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Ninja).whenNamed(TAG.chinese); container.bind(TYPES.Warrior).to(Samurai).whenNamed(TAG.japanese); container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAG.throwable); container.bind(TYPES.Weapon).to(Katana).whenDefault(); const ninja: Warrior = container.get(TYPES.Warrior, { name: TAG.chinese, }); const samurai: Warrior = container.get(TYPES.Warrior, { name: TAG.japanese, }); expect(ninja.name).to.eql('Ninja'); expect(ninja.weapon.name).to.eql('Shuriken'); expect(samurai.name).to.eql('Samurai'); expect(samurai.weapon.name).to.eql('Katana'); }); it('Should be able to select a default to avoid ambiguous binding exceptions', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAG = { throwable: 'throwable', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } const container: Container = new Container(); container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAG.throwable); container .bind(TYPES.Weapon) .to(Katana) .inSingletonScope() .whenDefault(); const defaultWeapon: Weapon = container.get(TYPES.Weapon); const throwableWeapon: Weapon = container.get(TYPES.Weapon, { name: TAG.throwable, }); expect(defaultWeapon.name).eql('Katana'); expect(throwableWeapon.name).eql('Shuriken'); }); }); ================================================ FILE: src/test/features/property_injection.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable, injectFromBase, multiInject, named, optional, tagged, unmanaged, } from '../..'; describe('Property Injection', () => { it('Should be able to inject a property', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } interface Warrior { name: string; weapon: Weapon; } @injectable() class Samurai implements Warrior { @inject(TYPES.Weapon) public weapon!: Weapon; public name: string; constructor() { this.name = 'Samurai'; } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Samurai); container.bind(TYPES.Weapon).to(Katana); const warrior: Warrior = container.get(TYPES.Warrior); expect(warrior.name).to.eql('Samurai'); expect(warrior.weapon).not.to.eql(undefined); expect(warrior.weapon.name).to.eql('Katana'); }); it('Should be able to inject a property combined with constructor injection', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAGS = { Primary: 'Primary', Secondary: 'Secondary', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } interface Warrior { name: string; primaryWeapon: Weapon; secondaryWeapon: Weapon; } @injectable() class Samurai implements Warrior { @inject(TYPES.Weapon) @named(TAGS.Secondary) public secondaryWeapon!: Weapon; public name: string; public primaryWeapon: Weapon; constructor(@inject(TYPES.Weapon) @named(TAGS.Primary) weapon: Weapon) { this.name = 'Samurai'; this.primaryWeapon = weapon; } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Samurai); container.bind(TYPES.Weapon).to(Katana).whenNamed(TAGS.Primary); container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAGS.Secondary); const warrior: Warrior = container.get(TYPES.Warrior); expect(warrior.name).to.eql('Samurai'); expect(warrior.primaryWeapon).not.to.eql(undefined); expect(warrior.primaryWeapon.name).to.eql('Katana'); expect(warrior.secondaryWeapon).not.to.eql(undefined); expect(warrior.secondaryWeapon.name).to.eql('Shuriken'); }); it('Should be able to inject a named property', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAGS = { Primary: 'Primary', Secondary: 'Secondary', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } interface Warrior { name: string; primaryWeapon: Weapon; secondaryWeapon: Weapon; } @injectable() class Samurai implements Warrior { @inject(TYPES.Weapon) @named(TAGS.Primary) public primaryWeapon!: Weapon; @inject(TYPES.Weapon) @named(TAGS.Secondary) public secondaryWeapon!: Weapon; public name: string; constructor() { this.name = 'Samurai'; } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Samurai); container.bind(TYPES.Weapon).to(Katana).whenNamed(TAGS.Primary); container.bind(TYPES.Weapon).to(Shuriken).whenNamed(TAGS.Secondary); const warrior: Warrior = container.get(TYPES.Warrior); expect(warrior.name).to.eql('Samurai'); expect(warrior.primaryWeapon).not.to.eql(undefined); expect(warrior.primaryWeapon.name).to.eql('Katana'); expect(warrior.secondaryWeapon).not.to.eql(undefined); expect(warrior.secondaryWeapon.name).to.eql('Shuriken'); }); it('Should be able to inject a tagged property', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAGS = { Primary: 'Primary', Priority: 'Priority', Secondary: 'Secondary', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } interface Warrior { name: string; primaryWeapon: Weapon; secondaryWeapon: Weapon; } @injectable() class Samurai implements Warrior { @inject(TYPES.Weapon) @tagged(TAGS.Priority, TAGS.Primary) public primaryWeapon!: Weapon; @inject(TYPES.Weapon) @tagged(TAGS.Priority, TAGS.Secondary) public secondaryWeapon!: Weapon; public name: string; constructor() { this.name = 'Samurai'; } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Samurai); container .bind(TYPES.Weapon) .to(Katana) .whenTagged(TAGS.Priority, TAGS.Primary); container .bind(TYPES.Weapon) .to(Shuriken) .whenTagged(TAGS.Priority, TAGS.Secondary); const warrior: Warrior = container.get(TYPES.Warrior); expect(warrior.name).to.eql('Samurai'); expect(warrior.primaryWeapon).not.to.eql(undefined); expect(warrior.primaryWeapon.name).to.eql('Katana'); expect(warrior.secondaryWeapon).not.to.eql(undefined); expect(warrior.secondaryWeapon.name).to.eql('Shuriken'); }); it('Should be able to multi-inject a property', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } interface Warrior { name: string; weapons: Weapon[]; } @injectable() class Samurai implements Warrior { @multiInject(TYPES.Weapon) public weapons!: Weapon[]; public name: string; constructor() { this.name = 'Samurai'; } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Samurai); container.bind(TYPES.Weapon).to(Katana); container.bind(TYPES.Weapon).to(Shuriken); const warrior: Warrior = container.get(TYPES.Warrior); expect(warrior.name).to.eql('Samurai'); expect(warrior.weapons[0]).not.to.eql(undefined); expect(warrior.weapons[0]?.name).to.eql('Katana'); expect(warrior.weapons[1]).not.to.eql(undefined); expect(warrior.weapons[1]?.name).to.eql('Shuriken'); }); it('Should be able to inject a property in a base class', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', Weapon: 'Weapon', }; // eslint-disable-next-line @typescript-eslint/typedef const TAGS = { Primary: 'Primary', Priority: 'Priority', Secondary: 'Secondary', }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'Katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'Shuriken'; } } interface Warrior { name: string; primaryWeapon: Weapon; } @injectable() class BaseWarrior implements Warrior { @inject(TYPES.Weapon) @tagged(TAGS.Priority, TAGS.Primary) public primaryWeapon!: Weapon; public name: string; constructor(@unmanaged() name: string) { this.name = name; } } @injectable() @injectFromBase({ extendConstructorArguments: false, extendProperties: true, }) class Samurai extends BaseWarrior { @inject(TYPES.Weapon) @tagged(TAGS.Priority, TAGS.Secondary) public secondaryWeapon!: Weapon; constructor() { super('Samurai'); } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Samurai); container .bind(TYPES.Weapon) .to(Katana) .whenTagged(TAGS.Priority, TAGS.Primary); container .bind(TYPES.Weapon) .to(Shuriken) .whenTagged(TAGS.Priority, TAGS.Secondary); const samurai: Samurai = container.get(TYPES.Warrior); expect(samurai.name).to.eql('Samurai'); expect(samurai.secondaryWeapon).not.to.eql(undefined); expect(samurai.secondaryWeapon.name).to.eql('Shuriken'); expect(samurai.primaryWeapon).not.to.eql(undefined); expect(samurai.primaryWeapon.name).to.eql('Katana'); }); it('Should be able to flag a property injection as optional', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Route: 'Route', Router: 'Router', }; interface Route { name: string; } @injectable() class Router { @inject(TYPES.Route) @optional() private readonly route!: Route; public getRoute(): Route { return this.route; } } const container: Container = new Container(); container.bind(TYPES.Router).to(Router); const router1: Router = container.get(TYPES.Router); expect(router1.getRoute()).to.eql(undefined); container.bind(TYPES.Route).toConstantValue({ name: 'route1' }); const router2: Router = container.get(TYPES.Router); expect(router2.getRoute().name).to.eql('route1'); }); }); ================================================ FILE: src/test/features/provider.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, injectable, Provider, ResolutionContext } from '../..'; describe('Provider', () => { it('Should support complex asynchronous initialization processes', (done: Mocha.Done) => { @injectable() class Ninja { public level: number; public rank: string; constructor() { this.level = 0; this.rank = 'Ninja'; } public async train(): Promise { return new Promise( (resolve: (value: number | PromiseLike) => void) => { setTimeout(() => { this.level += 10; resolve(this.level); }, 10); }, ); } } @injectable() class NinjaMaster { public rank: string; constructor() { this.rank = 'NinjaMaster'; } } type NinjaMasterProvider = () => Promise; const container: Container = new Container(); container.bind('Ninja').to(Ninja).inSingletonScope(); // eslint-disable-next-line @typescript-eslint/no-deprecated container.bind('Provider').toProvider( (context: ResolutionContext) => async () => new Promise( ( resolve: (value: NinjaMaster | PromiseLike) => void, reject: (reason?: unknown) => void, ) => { const ninja: Ninja = context.get('Ninja'); void ninja.train().then((level: number) => { if (level >= 20) { resolve(new NinjaMaster()); } else { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject('Not enough training'); } }); }, ), ); const ninjaMasterProvider: NinjaMasterProvider = container.get('Provider'); // helper async function valueOrDefault( provider: () => Promise, defaultValue: T, ) { return new Promise((resolve: (value: T | PromiseLike) => void) => { provider() .then((value: T) => { resolve(value); }) .catch(() => { resolve(defaultValue); }); }); } void valueOrDefault(ninjaMasterProvider, { rank: 'DefaultNinjaMaster', }).then((ninjaMaster: NinjaMaster) => { expect(ninjaMaster.rank).to.eql('DefaultNinjaMaster'); }); void valueOrDefault(ninjaMasterProvider, { rank: 'DefaultNinjaMaster', }).then((ninjaMaster: NinjaMaster) => { expect(ninjaMaster.rank).to.eql('NinjaMaster'); done(); }); }); it('Should support custom arguments', (done: Mocha.Done) => { const container: Container = new Container(); interface Sword { material: string; damage: number; } @injectable() class Katana implements Sword { public material!: string; public damage!: number; } container.bind('Sword').to(Katana); // eslint-disable-next-line @typescript-eslint/no-deprecated type SwordProvider = Provider; // eslint-disable-next-line @typescript-eslint/no-deprecated container.bind('SwordProvider').toProvider( (context: ResolutionContext): SwordProvider => async (material: string, damage: number) => new Promise( (resolve: (value: Sword | PromiseLike) => void) => { setTimeout(() => { const katana: Sword = context.get('Sword'); katana.material = material; katana.damage = damage; resolve(katana); }, 10); }, ), ); const katanaProvider: SwordProvider = container.get('SwordProvider'); void katanaProvider('gold', 100).then((powerfulGoldKatana: Sword) => { expect(powerfulGoldKatana.material).to.eql('gold'); expect(powerfulGoldKatana.damage).to.eql(100); void katanaProvider('gold', 10).then((notSoPowerfulGoldKatana: Sword) => { expect(notSoPowerfulGoldKatana.material).to.eql('gold'); expect(notSoPowerfulGoldKatana.damage).to.eql(10); done(); }); }); }); it('Should support the declaration of singletons', (done: Mocha.Done) => { const container: Container = new Container(); interface Warrior { level: number; } @injectable() class Ninja implements Warrior { public level: number; constructor() { this.level = 0; } } type WarriorProvider = (level: number) => Promise; container.bind('Warrior').to(Ninja).inSingletonScope(); // Value is singleton! // eslint-disable-next-line @typescript-eslint/no-deprecated container.bind('WarriorProvider').toProvider( (context: ResolutionContext) => async (increaseLevel: number) => new Promise((resolve: (value: Warrior) => void) => { setTimeout(() => { const warrior: Warrior = context.get('Warrior'); warrior.level += increaseLevel; resolve(warrior); }, 100); }), ); const warriorProvider: WarriorProvider = container.get('WarriorProvider'); void warriorProvider(10).then((warrior: Warrior) => { expect(warrior.level).to.eql(10); void warriorProvider(10).then((warrior2: Warrior) => { expect(warrior2.level).to.eql(20); done(); }); }); }); }); ================================================ FILE: src/test/features/request_scope.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { performance } from 'perf_hooks'; import { Container, inject, injectable, named } from '../..'; describe('inRequestScope', () => { it('Should support request scope in basic bindings', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPE = { Warrior: Symbol('Warrior'), Weapon: Symbol('Weapon'), }; interface Weapon { use(): string; } interface Warrior { primaryWeapon: Weapon; secondaryWeapon: Weapon; } @injectable() class Katana implements Weapon { private readonly _madeOn: number; constructor() { this._madeOn = performance.now(); } public use() { return `Used Katana made on ${this._madeOn.toString()}!`; } } @injectable() class Samurai implements Warrior { public primaryWeapon: Weapon; public secondaryWeapon: Weapon; constructor( @inject(TYPE.Weapon) primaryWeapon: Weapon, @inject(TYPE.Weapon) secondaryWeapon: Weapon, ) { this.primaryWeapon = primaryWeapon; this.secondaryWeapon = secondaryWeapon; } } // Without request scope const container: Container = new Container(); container.bind(TYPE.Weapon).to(Katana); container.bind(TYPE.Warrior).to(Samurai); const samurai: Warrior = container.get(TYPE.Warrior); const samurai2: Warrior = container.get(TYPE.Warrior); // One requests should use two instances because scope is transient expect(samurai.primaryWeapon.use()).not.to.eql( samurai.secondaryWeapon.use(), ); // One requests should use two instances because scope is transient expect(samurai2.primaryWeapon.use()).not.to.eql( samurai2.secondaryWeapon.use(), ); // Two request should use two Katana instances // for each instance of Samuari because scope is transient expect(samurai.primaryWeapon.use()).not.to.eql( samurai2.primaryWeapon.use(), ); expect(samurai.secondaryWeapon.use()).not.to.eql( samurai2.secondaryWeapon.use(), ); // With request scope const container1: Container = new Container(); container1.bind(TYPE.Weapon).to(Katana).inRequestScope(); // Important container1.bind(TYPE.Warrior).to(Samurai); const samurai3: Warrior = container1.get(TYPE.Warrior); const samurai4: Warrior = container1.get(TYPE.Warrior); // One requests should use one instance because scope is request scope expect(samurai3.primaryWeapon.use()).to.eql(samurai3.secondaryWeapon.use()); // One requests should use one instance because scope is request scope expect(samurai4.primaryWeapon.use()).to.eql(samurai4.secondaryWeapon.use()); // Two request should use one instances of Katana // for each instance of Samurai because scope is request scope expect(samurai3.primaryWeapon.use()).not.to.eql( samurai4.primaryWeapon.use(), ); expect(samurai3.secondaryWeapon.use()).not.to.eql( samurai4.secondaryWeapon.use(), ); }); it('Should support request scope when using contraints', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPE = { Warrior: Symbol('Warrior'), Weapon: Symbol('Weapon'), }; interface Weapon { use(): string; } interface Warrior { primaryWeapon: Weapon; secondaryWeapon: Weapon; tertiaryWeapon: Weapon; } @injectable() class Katana implements Weapon { private readonly _madeOn: number; constructor() { this._madeOn = performance.now(); } public use() { return `Used Katana made on ${this._madeOn.toString()}!`; } } @injectable() class Shuriken implements Weapon { private readonly _madeOn: number; constructor() { this._madeOn = performance.now(); } public use() { return `Used Shuriken made on ${this._madeOn.toString()}!`; } } @injectable() class Samurai implements Warrior { public primaryWeapon: Weapon; public secondaryWeapon: Weapon; public tertiaryWeapon: Weapon; constructor( @inject(TYPE.Weapon) @named('sword') primaryWeapon: Weapon, @inject(TYPE.Weapon) @named('throwable') secondaryWeapon: Weapon, @inject(TYPE.Weapon) @named('throwable') tertiaryWeapon: Weapon, ) { this.primaryWeapon = primaryWeapon; this.secondaryWeapon = secondaryWeapon; this.tertiaryWeapon = tertiaryWeapon; } } const container: Container = new Container(); container .bind(TYPE.Weapon) .to(Katana) .inRequestScope() .whenNamed('sword'); container .bind(TYPE.Weapon) .to(Shuriken) .inRequestScope() .whenNamed('throwable'); container.bind(TYPE.Warrior).to(Samurai); const samurai1: Warrior = container.get(TYPE.Warrior); const samurai2: Warrior = container.get(TYPE.Warrior); // Katana and Shuriken are two instances expect(samurai1.primaryWeapon.use()).not.to.eql( samurai1.secondaryWeapon.use(), ); // Shuriken should be one shared instance because scope is request scope expect(samurai1.secondaryWeapon.use()).to.eql( samurai1.tertiaryWeapon.use(), ); // Katana and Shuriken are two instances expect(samurai2.primaryWeapon.use()).not.to.eql( samurai2.secondaryWeapon.use(), ); // Shuriken should be one shared instance because scope is request scope expect(samurai2.secondaryWeapon.use()).to.eql( samurai2.tertiaryWeapon.use(), ); // Two request should use one instances of Katana // for each instance of Samurai because scope is request scope expect(samurai1.secondaryWeapon.use()).not.to.eql( samurai2.secondaryWeapon.use(), ); expect(samurai1.tertiaryWeapon.use()).not.to.eql( samurai2.tertiaryWeapon.use(), ); }); }); ================================================ FILE: src/test/features/transitive_bindings.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, injectable } from '../..'; describe('Transitive bindings', () => { it('Should be able to bind to a service', () => { @injectable() class MySqlDatabaseTransactionLog { public time: number; public name: string; constructor() { this.time = new Date().getTime(); this.name = 'MySqlDatabaseTransactionLog'; } } @injectable() class DatabaseTransactionLog { public time!: number; public name!: string; } @injectable() class TransactionLog { public time!: number; public name!: string; } const container: Container = new Container(); container.bind(MySqlDatabaseTransactionLog).toSelf().inSingletonScope(); container .bind(DatabaseTransactionLog) .toService(MySqlDatabaseTransactionLog); container.bind(TransactionLog).toService(DatabaseTransactionLog); const mySqlDatabaseTransactionLog: MySqlDatabaseTransactionLog = container.get(MySqlDatabaseTransactionLog); const databaseTransactionLog: DatabaseTransactionLog = container.get( DatabaseTransactionLog, ); const transactionLog: TransactionLog = container.get(TransactionLog); expect(mySqlDatabaseTransactionLog.name).to.eq( 'MySqlDatabaseTransactionLog', ); expect(databaseTransactionLog.name).to.eq('MySqlDatabaseTransactionLog'); expect(transactionLog.name).to.eq('MySqlDatabaseTransactionLog'); expect(mySqlDatabaseTransactionLog.time).to.eq(databaseTransactionLog.time); expect(databaseTransactionLog.time).to.eq(transactionLog.time); }); }); ================================================ FILE: src/test/inversify.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { BindingConstraints, Container, ContainerModule, ContainerModuleLoadOptions, decorate, Factory, inject, injectable, LazyServiceIdentifier, multiInject, named, Newable, ResolutionContext, tagged, } from '..'; describe('InversifyJS', () => { it('Should be able to resolve and inject dependencies', () => { interface NinjaInterface { fight(): string; sneak(): string; } interface KatanaInterface { hit(): string; } interface ShurikenInterface { throw(): string; } @injectable() class Katana implements KatanaInterface { public hit() { return 'cut!'; } } @injectable() class Shuriken implements ShurikenInterface { public throw() { return 'hit!'; } } @injectable() class Ninja implements NinjaInterface { private readonly _katana: KatanaInterface; private readonly _shuriken: ShurikenInterface; constructor( @inject('Katana') katana: KatanaInterface, @inject('Shuriken') shuriken: ShurikenInterface, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } const container: Container = new Container(); container.bind('Ninja').to(Ninja); container.bind('Katana').to(Katana); container.bind('Shuriken').to(Shuriken); const ninja: NinjaInterface = container.get('Ninja'); expect(ninja.fight()).eql('cut!'); expect(ninja.sneak()).eql('hit!'); }); it('Should be able to do setter injection and property injection', () => { @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Katana implements Katana { public hit() { return 'cut!'; } } @injectable() class Ninja { @inject(Katana) public katana!: Katana; private _shuriken!: Shuriken; @inject(Shuriken) public set Shuriken(shuriken: Shuriken) { this._shuriken = shuriken; } public sneak() { return this._shuriken.throw(); } public fight() { return this.katana.hit(); } } const container: Container = new Container(); container.bind('Ninja').to(Ninja); container.bind(Shuriken).toSelf(); container.bind(Katana).toSelf(); const ninja: Ninja = container.get('Ninja'); expect(ninja.sneak()).to.eql('hit!'); expect(ninja.fight()).to.eql('cut!'); }); it('Should be able to resolve and inject dependencies in VanillaJS', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Blowgun: 'Blowgun', Katana: 'Katana', Ninja: 'Ninja', Shuriken: 'Shuriken', }; class Blowgun { public blow() { return 'poison!'; } } class Katana { public hit() { return 'cut!'; } } class Shuriken { public throw() { return 'hit!'; } } class Ninja { public _katana: Katana; public _shuriken: Shuriken; public _blowgun!: Blowgun; constructor(katana: Katana, shuriken: Shuriken) { this._katana = katana; this._shuriken = shuriken; } public set blowgun(blowgun: Blowgun) { this._blowgun = blowgun; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } public poisonDart() { return this._blowgun.blow(); } } decorate([injectable()], Katana); decorate([injectable()], Shuriken); decorate([injectable()], Ninja); decorate([injectable()], Blowgun); decorate([inject(TYPES.Katana)], Ninja, 0); decorate([inject(TYPES.Shuriken)], Ninja, 1); decorate([inject(TYPES.Blowgun)], Ninja, 'blowgun'); const container: Container = new Container(); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.Katana).to(Katana); container.bind(TYPES.Shuriken).to(Shuriken); container.bind(TYPES.Blowgun).to(Blowgun); const ninja: Ninja = container.get(TYPES.Ninja); expect(ninja.fight()).eql('cut!'); expect(ninja.sneak()).eql('hit!'); expect(ninja.poisonDart()).eql('poison!'); }); it('Should be able to use classes as runtime identifiers', () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor(katana: Katana, shuriken: Shuriken) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } const container: Container = new Container(); container.bind(Ninja).to(Ninja); container.bind(Katana).to(Katana); container.bind(Shuriken).to(Shuriken); const ninja: Ninja = container.get(Ninja); expect(ninja.fight()).eql('cut!'); expect(ninja.sneak()).eql('hit!'); }); it('Should be able to use Symbols as runtime identifiers', () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Katana: Symbol.for('Katana'), Ninja: Symbol.for('Ninja'), Shuriken: Symbol.for('Shuriken'), }; @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject(TYPES.Katana) katana: Katana, @inject(TYPES.Shuriken) shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } const container: Container = new Container(); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.Katana).to(Katana); container.bind(TYPES.Shuriken).to(Shuriken); const ninja: Ninja = container.get(TYPES.Ninja); expect(ninja.fight()).eql('cut!'); expect(ninja.sneak()).eql('hit!'); }); it('Should be able to wrap Symbols with LazyServiceIdentifier', () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Katana: Symbol.for('Katana'), Ninja: Symbol.for('Ninja'), Shuriken: Symbol.for('Shuriken'), }; @injectable() class Ninja implements Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject(new LazyServiceIdentifier(() => TYPES.Katana)) katana: Katana, @inject(new LazyServiceIdentifier(() => TYPES.Shuriken)) shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } const container: Container = new Container(); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.Katana).to(Katana); container.bind(TYPES.Shuriken).to(Shuriken); const ninja: Ninja = container.get(TYPES.Ninja); expect(ninja.fight()).eql('cut!'); expect(ninja.sneak()).eql('hit!'); }); it('Should support Container modules', async () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject('Katana') katana: Katana, @inject('Shuriken') shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } const warriors: ContainerModule = new ContainerModule( (options: ContainerModuleLoadOptions) => { options.bind('Ninja').to(Ninja); }, ); const weapons: ContainerModule = new ContainerModule( (options: ContainerModuleLoadOptions) => { options.bind('Katana').to(Katana); options.bind('Shuriken').to(Shuriken); }, ); const container: Container = new Container(); // load await container.load(warriors, weapons); const ninja: Ninja = container.get('Ninja'); expect(ninja.fight()).eql('cut!'); expect(ninja.sneak()).eql('hit!'); const tryGetNinja: () => void = () => { container.get('Ninja'); }; const tryGetKatana: () => void = () => { container.get('Katana'); }; const tryGetShuruken: () => void = () => { container.get('Shuriken'); }; // unload await container.unload(warriors); expect(tryGetNinja).to.throw(''); expect(tryGetKatana).not.to.throw(); expect(tryGetShuruken).not.to.throw(); await container.unload(weapons); expect(tryGetNinja).to.throw(''); expect(tryGetKatana).to.throw(''); expect(tryGetShuruken).to.throw(''); }); it('Should support control over the scope of the dependencies', () => { @injectable() class Katana { private _usageCount: number; constructor() { this._usageCount = 0; } public hit() { this._usageCount = this._usageCount + 1; return `This katana was used ${this._usageCount.toString()} times!`; } } @injectable() class Shuriken { private _shurikenCount: number; constructor() { this._shurikenCount = 10; } public throw() { this._shurikenCount = this._shurikenCount - 1; return `Only ${this._shurikenCount.toString()} items left!`; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject('Katana') katana: Katana, @inject('Shuriken') shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } const container: Container = new Container(); container.bind('Ninja').to(Ninja); container.bind('Katana').to(Katana).inSingletonScope(); container.bind('Shuriken').to(Shuriken); const ninja1: Ninja = container.get('Ninja'); expect(ninja1.fight()).eql('This katana was used 1 times!'); expect(ninja1.fight()).eql('This katana was used 2 times!'); expect(ninja1.sneak()).eql('Only 9 items left!'); expect(ninja1.sneak()).eql('Only 8 items left!'); const ninja2: Ninja = container.get('Ninja'); expect(ninja2.fight()).eql('This katana was used 3 times!'); expect(ninja2.sneak()).eql('Only 9 items left!'); }); it('Should support the injection of classes to itself', () => { const heroName: string = 'superman'; @injectable() class Hero { public name: string; constructor() { this.name = heroName; } } const container: Container = new Container(); container.bind(Hero).toSelf(); const hero: Hero = container.get(Hero); expect(hero.name).eql(heroName); }); it('Should support the injection of constant values', () => { interface Warrior { name: string; } // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: 'Warrior', }; const heroName: string = 'superman'; @injectable() class Hero implements Warrior { public name: string; constructor() { this.name = heroName; } } const container: Container = new Container(); container.bind(TYPES.Warrior).toConstantValue(new Hero()); const hero: Warrior = container.get(TYPES.Warrior); expect(hero.name).eql(heroName); }); it('Should support the injection of dynamic values', async () => { @injectable() class UseSymbol { public currentSymbol: symbol; constructor(@inject('Symbol') currentDate: symbol) { this.currentSymbol = currentDate; } public doSomething() { return this.currentSymbol; } } const container: Container = new Container(); container.bind('UseSymbol').to(UseSymbol); container .bind('Symbol') .toDynamicValue((_context: ResolutionContext) => Symbol()); const subject1: UseSymbol = container.get('UseSymbol'); const subject2: UseSymbol = container.get('UseSymbol'); expect(subject1.doSomething() === subject2.doSomething()).eql(false); await container.unbind('Symbol'); container.bind('Symbol').toConstantValue(Symbol()); const subject3: UseSymbol = container.get('UseSymbol'); const subject4: UseSymbol = container.get('UseSymbol'); expect(subject3.doSomething() === subject4.doSomething()).eql(true); }); it('Should support the injection of Functions', () => { const ninjaId: string = 'Ninja'; const longDistanceWeaponId: string = 'LongDistanceWeapon'; const shortDistanceWeaponFactoryId: string = 'ShortDistanceWeaponFactory'; type ShortDistanceWeaponFactory = () => ShortDistanceWeapon; @injectable() class KatanaBlade {} @injectable() class KatanaHandler {} interface ShortDistanceWeapon { handler: KatanaHandler; blade: KatanaBlade; } @injectable() class Katana implements ShortDistanceWeapon { public handler: KatanaHandler; public blade: KatanaBlade; constructor(handler: KatanaHandler, blade: KatanaBlade) { this.handler = handler; this.blade = blade; } } @injectable() class Shuriken {} interface Warrior { shortDistanceWeaponFactory: ShortDistanceWeaponFactory; longDistanceWeapon: Shuriken; } @injectable() class Ninja implements Warrior { public shortDistanceWeaponFactory: ShortDistanceWeaponFactory; public longDistanceWeapon: Shuriken; constructor( @inject(shortDistanceWeaponFactoryId) shortDistanceWeaponFactory: ShortDistanceWeaponFactory, @inject(longDistanceWeaponId) longDistanceWeapon: Shuriken, ) { this.shortDistanceWeaponFactory = shortDistanceWeaponFactory; this.longDistanceWeapon = longDistanceWeapon; } } const container: Container = new Container(); container.bind(ninjaId).to(Ninja); container.bind(longDistanceWeaponId).to(Shuriken); const katanaFactory: () => Katana = function () { return new Katana(new KatanaHandler(), new KatanaBlade()); }; container .bind(shortDistanceWeaponFactoryId) .toConstantValue(katanaFactory); const ninja: Ninja = container.get(ninjaId); expect(ninja instanceof Ninja).eql(true); expect(typeof ninja.shortDistanceWeaponFactory === 'function').eql(true); expect(ninja.shortDistanceWeaponFactory() instanceof Katana).eql(true); expect( ninja.shortDistanceWeaponFactory().handler instanceof KatanaHandler, ).eql(true); expect(ninja.shortDistanceWeaponFactory().blade instanceof KatanaBlade).eql( true, ); expect(ninja.longDistanceWeapon instanceof Shuriken).eql(true); }); it('Should support the injection of class constructors', () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Ninja { private readonly _katana: Katana; constructor(@inject('Newable') katana: Newable) { this._katana = new katana(); } public fight() { return this._katana.hit(); } } const container: Container = new Container(); container.bind('Ninja').to(Ninja); container.bind>('Newable').toConstantValue(Katana); const ninja: Ninja = container.get('Ninja'); expect(ninja.fight()).eql('cut!'); }); it('Should support the injection of user defined factories', () => { interface Ninja { fight(): string; sneak(): string; } @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken implements Shuriken { public throw() { return 'hit!'; } } @injectable() class NinjaWithUserDefinedFactory implements Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject('Factory') katanaFactory: () => Katana, @inject('Shuriken') shuriken: Shuriken, ) { this._katana = katanaFactory(); this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } const container: Container = new Container(); container.bind('Ninja').to(NinjaWithUserDefinedFactory); container.bind('Shuriken').to(Shuriken); container.bind('Katana').to(Katana); container .bind>('Factory') .toFactory( (context: ResolutionContext) => () => context.get('Katana'), ); const ninja: Ninja = container.get('Ninja'); expect(ninja.fight()).eql('cut!'); expect(ninja.sneak()).eql('hit!'); }); it('Should support the injection of user defined factories with args', () => { interface Ninja { fight(): string; sneak(): string; } interface Weapon { use(): string; } @injectable() class Katana implements Weapon { public use() { return 'katana!'; } } @injectable() class Shuriken implements Weapon { public use() { return 'shuriken!'; } } @injectable() class NinjaWithUserDefinedFactory implements Ninja { private readonly _katana: Weapon; private readonly _shuriken: Weapon; constructor( @inject('Factory') weaponFactory: (throwable: boolean) => Weapon, ) { this._katana = weaponFactory(false); this._shuriken = weaponFactory(true); } public fight() { return this._katana.use(); } public sneak() { return this._shuriken.use(); } } const container: Container = new Container(); container.bind('Ninja').to(NinjaWithUserDefinedFactory); container.bind('Weapon').to(Shuriken).whenTagged('throwable', true); container.bind('Weapon').to(Katana).whenTagged('throwable', false); container.bind>('Factory').toFactory( (context: ResolutionContext) => (throwable: boolean) => context.get('Weapon', { tag: { key: 'throwable', value: throwable, }, }), ); const ninja: Ninja = container.get('Ninja'); expect(ninja.fight()).eql('katana!'); expect(ninja.sneak()).eql('shuriken!'); }); it('Should support the injection of user defined factories with partial application', () => { @injectable() class InjectorPump {} @injectable() class SparkPlugs {} class Engine { public displacement!: number | null; } @injectable() class DieselEngine implements Engine { public displacement: number | null; private readonly _injectorPump: InjectorPump; constructor(@inject('InjectorPump') injectorPump: InjectorPump) { this._injectorPump = injectorPump; this.displacement = null; } public debug() { return this._injectorPump; } } @injectable() class PetrolEngine implements Engine { public displacement: number | null; private readonly _sparkPlugs: SparkPlugs; constructor(@inject('SparkPlugs') sparkPlugs: SparkPlugs) { this._sparkPlugs = sparkPlugs; this.displacement = null; } public debug() { return this._sparkPlugs; } } interface CarFactory { createEngine(displacement: number): Engine; } @injectable() class DieselCarFactory implements CarFactory { private readonly _dieselFactory: (displacement: number) => Engine; constructor( @inject('Factory') factory: (category: string) => (displacement: number) => Engine, ) { this._dieselFactory = factory('diesel'); } public createEngine(displacement: number): Engine { return this._dieselFactory(displacement); } } const container: Container = new Container(); container.bind('SparkPlugs').to(SparkPlugs); container.bind('InjectorPump').to(InjectorPump); container.bind('Engine').to(PetrolEngine).whenNamed('petrol'); container.bind('Engine').to(DieselEngine).whenNamed('diesel'); container .bind Engine>>('Factory') .toFactory( (context: ResolutionContext) => (theNamed: string) => (displacement: number) => { const theEngine: Engine = context.get('Engine', { name: theNamed, }); theEngine.displacement = displacement; return theEngine; }, ); container.bind('DieselCarFactory').to(DieselCarFactory); const dieselCarFactory: CarFactory = container.get('DieselCarFactory'); const engine: Engine = dieselCarFactory.createEngine(300); expect(engine.displacement).eql(300); expect(engine instanceof DieselEngine).eql(true); }); it('Should support the injection of providers', (done: Mocha.Done) => { type KatanaProvider = () => Promise; interface Ninja { katana: Katana | null; katanaProvider: KatanaProvider; } @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class NinjaWithProvider implements Ninja { public katana: Katana | null; public katanaProvider: KatanaProvider; constructor(@inject('Provider') katanaProvider: KatanaProvider) { this.katanaProvider = katanaProvider; this.katana = null; } } const container: Container = new Container(); container.bind('Ninja').to(NinjaWithProvider); container.bind('Katana').to(Katana); // eslint-disable-next-line @typescript-eslint/no-deprecated container.bind('Provider').toProvider( (context: ResolutionContext) => async () => new Promise((resolve: (value: Katana) => void) => { const katana: Katana = context.get('Katana'); resolve(katana); }), ); const ninja: Ninja = container.get('Ninja'); ninja .katanaProvider() .then((katana: Katana) => { ninja.katana = katana; expect(ninja.katana.hit()).eql('cut!'); done(); }) .catch((_e: unknown) => { /* do nothing */ }); }); describe('Injection of multiple values with string as keys', () => { it('Should support the injection of multiple values', () => { const warriorId: string = 'Warrior'; const weaponId: string = 'Weapon'; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string = 'Katana'; } @injectable() class Shuriken implements Weapon { public name: string = 'Shuriken'; } interface Warrior { katana: Weapon; shuriken: Weapon; } @injectable() class Ninja implements Warrior { public katana: Weapon; public shuriken: Weapon; constructor(@multiInject(weaponId) weapons: Weapon[]) { this.katana = weapons[0] as Weapon; this.shuriken = weapons[1] as Weapon; } } const container: Container = new Container(); container.bind(warriorId).to(Ninja); container.bind(weaponId).to(Katana); container.bind(weaponId).to(Shuriken); const ninja: Warrior = container.get(warriorId); expect(ninja.katana.name).eql('Katana'); expect(ninja.shuriken.name).eql('Shuriken'); // if only one value is bound to Weapon const container2: Container = new Container(); container2.bind(warriorId).to(Ninja); container2.bind(weaponId).to(Katana); const ninja2: Ninja = container2.get(warriorId); expect(ninja2.katana.name).eql('Katana'); }); it('Should support the injection of multiple values with nested inject', () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject('Katana') katana: Katana, @inject('Shuriken') shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } interface School { ninjaMaster: Ninja; student: Ninja; } @injectable() class NinjaSchool implements School { public ninjaMaster: Ninja; public student: Ninja; constructor(@multiInject('Ninja') ninja: Ninja[]) { this.ninjaMaster = ninja[0] as Ninja; this.student = ninja[1] as Ninja; } } const container: Container = new Container(); container.bind('Katana').to(Katana); container.bind('Shuriken').to(Shuriken); container.bind('Ninja').to(Ninja); container.bind('Ninja').to(Ninja); container.bind('School').to(NinjaSchool); const ninjaSchool: School = container.get('School'); expect(ninjaSchool.ninjaMaster.fight()).eql('cut!'); expect(ninjaSchool.ninjaMaster.sneak()).eql('hit!'); expect(ninjaSchool.student.fight()).eql('cut!'); expect(ninjaSchool.student.sneak()).eql('hit!'); }); it('Should support the injection of multiple values with nested multiInject', () => { const warriorId: string = 'Warrior'; const swordId: string = 'Sword'; const shurikenId: string = 'Shuriken'; const schoolId: string = 'School'; const organisationId: string = 'Organisation'; interface Warrior { fight(): string; sneak(): string; } interface Sword { hit(): string; } @injectable() class Katana implements Sword { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja implements Warrior { private readonly _katana: Sword; private readonly _shuriken: Shuriken; constructor( @inject(swordId) katana: Sword, @inject(shurikenId) shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } interface School { ninjaMaster: Warrior; student: Warrior; } @injectable() class NinjaSchool implements School { public ninjaMaster: Warrior; public student: Warrior; constructor(@multiInject(warriorId) ninjas: Ninja[]) { this.ninjaMaster = ninjas[0] as Ninja; this.student = ninjas[1] as Ninja; } } interface Organisation { schools: School[]; } @injectable() class NinjaOrganisation implements Organisation { public schools: School[]; constructor(@multiInject(schoolId) schools: School[]) { this.schools = schools; } } const container: Container = new Container(); container.bind(swordId).to(Katana); container.bind(shurikenId).to(Shuriken); container.bind(warriorId).to(Ninja); container.bind(warriorId).to(Ninja); container.bind(schoolId).to(NinjaSchool); container.bind(schoolId).to(NinjaSchool); container.bind(organisationId).to(NinjaOrganisation); const ninjaOrganisation: Organisation = container.get(organisationId); for (let i: number = 0; i < 2; i++) { const ithNinjaOrganizationSchool: School = ninjaOrganisation.schools[ i ] as School; expect(ithNinjaOrganizationSchool.ninjaMaster.fight()).eql('cut!'); expect(ithNinjaOrganizationSchool.ninjaMaster.sneak()).eql('hit!'); expect(ithNinjaOrganizationSchool.student.fight()).eql('cut!'); expect(ithNinjaOrganizationSchool.student.sneak()).eql('hit!'); } }); }); describe('Injection of multiple values with class as keys', () => { it('Should support the injection of multiple values when using classes as keys', () => { @injectable() class Weapon { public name!: string; } @injectable() class Katana extends Weapon { constructor() { super(); this.name = 'Katana'; } } @injectable() class Shuriken extends Weapon { constructor() { super(); this.name = 'Shuriken'; } } @injectable() class Ninja { public katana: Weapon; public shuriken: Weapon; constructor(@multiInject(Weapon) weapons: Weapon[]) { this.katana = weapons[0] as Weapon; this.shuriken = weapons[1] as Weapon; } } const container: Container = new Container(); container.bind(Ninja).to(Ninja); container.bind(Weapon).to(Katana); container.bind(Weapon).to(Shuriken); const ninja: Ninja = container.get(Ninja); expect(ninja.katana.name).eql('Katana'); expect(ninja.shuriken.name).eql('Shuriken'); // if only one value is bound to Weapon const container2: Container = new Container(); container2.bind(Ninja).to(Ninja); container2.bind(Weapon).to(Katana); const ninja2: Ninja = container2.get(Ninja); expect(ninja2.katana.name).eql('Katana'); }); it('Should support the injection of multiple values with nested inject', () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor(katana: Katana, shuriken: Shuriken) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } @injectable() class NinjaSchool { public ninjaMaster: Ninja; public student: Ninja; constructor(@multiInject(Ninja) ninja: Ninja[]) { this.ninjaMaster = ninja[0] as Ninja; this.student = ninja[1] as Ninja; } } const container: Container = new Container(); container.bind(Katana).to(Katana); container.bind(Shuriken).to(Shuriken); container.bind(Ninja).to(Ninja); container.bind(Ninja).to(Ninja); container.bind(NinjaSchool).to(NinjaSchool); const ninjaSchool: NinjaSchool = container.get(NinjaSchool); expect(ninjaSchool.ninjaMaster.fight()).eql('cut!'); expect(ninjaSchool.ninjaMaster.sneak()).eql('hit!'); expect(ninjaSchool.student.fight()).eql('cut!'); expect(ninjaSchool.student.sneak()).eql('hit!'); }); it('Should support the injection of multiple values with nested multiInject', () => { @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor(katana: Katana, shuriken: Shuriken) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } @injectable() class NinjaSchool { public ninjaMaster: Ninja; public student: Ninja; constructor(@multiInject(Ninja) ninjas: Ninja[]) { this.ninjaMaster = ninjas[0] as Ninja; this.student = ninjas[1] as Ninja; } } @injectable() class NinjaOrganisation { public schools: NinjaSchool[]; constructor(@multiInject(NinjaSchool) schools: NinjaSchool[]) { this.schools = schools; } } const container: Container = new Container(); container.bind(Katana).to(Katana); container.bind(Shuriken).to(Shuriken); container.bind(Ninja).to(Ninja); container.bind(Ninja).to(Ninja); container.bind(NinjaSchool).to(NinjaSchool); container.bind(NinjaSchool).to(NinjaSchool); container .bind(NinjaOrganisation) .to(NinjaOrganisation); const ninjaOrganisation: NinjaOrganisation = container.get(NinjaOrganisation); for (let i: number = 0; i < 2; i++) { const ithNinjaOrganizationSchool: NinjaSchool = ninjaOrganisation .schools[i] as NinjaSchool; expect(ithNinjaOrganizationSchool.ninjaMaster.fight()).eql('cut!'); expect(ithNinjaOrganizationSchool.ninjaMaster.sneak()).eql('hit!'); expect(ithNinjaOrganizationSchool.student.fight()).eql('cut!'); expect(ithNinjaOrganizationSchool.student.sneak()).eql('hit!'); } }); }); describe('Injection of multiple values with Symbol as keys', () => { it('Should support the injection of multiple values when using Symbols as keys', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Warrior: Symbol.for('Warrior'), Weapon: Symbol.for('Weapon'), }; interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string = 'Katana'; } @injectable() class Shuriken implements Weapon { public name: string = 'Shuriken'; } interface Warrior { katana: Weapon; shuriken: Weapon; } @injectable() class Ninja implements Warrior { public katana: Weapon; public shuriken: Weapon; constructor(@multiInject(TYPES.Weapon) weapons: Weapon[]) { this.katana = weapons[0] as Weapon; this.shuriken = weapons[1] as Weapon; } } const container: Container = new Container(); container.bind(TYPES.Warrior).to(Ninja); container.bind(TYPES.Weapon).to(Katana); container.bind(TYPES.Weapon).to(Shuriken); const ninja: Ninja = container.get(TYPES.Warrior); expect(ninja.katana.name).eql('Katana'); expect(ninja.shuriken.name).eql('Shuriken'); // if only one value is bound to Weapon const container2: Container = new Container(); container2.bind(TYPES.Warrior).to(Ninja); container2.bind(TYPES.Weapon).to(Katana); const ninja2: Ninja = container2.get(TYPES.Warrior); expect(ninja2.katana.name).eql('Katana'); }); it('Should support the injection of multiple values with nested inject', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Katana: Symbol.for('Katana'), Ninja: Symbol.for('Ninja'), School: Symbol.for('School'), Shuriken: Symbol.for('Shuriken'), }; @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject(TYPES.Katana) katana: Katana, @inject(TYPES.Shuriken) shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } interface School { ninjaMaster: Ninja; student: Ninja; } @injectable() class NinjaSchool implements School { public ninjaMaster: Ninja; public student: Ninja; constructor(@multiInject(TYPES.Ninja) ninja: Ninja[]) { this.ninjaMaster = ninja[0] as Ninja; this.student = ninja[1] as Ninja; } } const container: Container = new Container(); container.bind(TYPES.Katana).to(Katana); container.bind(TYPES.Shuriken).to(Shuriken); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.School).to(NinjaSchool); const ninjaSchool: School = container.get(TYPES.School); expect(ninjaSchool.ninjaMaster.fight()).eql('cut!'); expect(ninjaSchool.ninjaMaster.sneak()).eql('hit!'); expect(ninjaSchool.student.fight()).eql('cut!'); expect(ninjaSchool.student.sneak()).eql('hit!'); }); it('Should support the injection of multiple values with nested multiInject', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Katana: Symbol.for('Katana'), Ninja: Symbol.for('Ninja'), Organisation: Symbol.for('Organisation'), School: Symbol.for('School'), Shuriken: Symbol.for('Shuriken'), }; @injectable() class Katana { public hit() { return 'cut!'; } } @injectable() class Shuriken { public throw() { return 'hit!'; } } @injectable() class Ninja { private readonly _katana: Katana; private readonly _shuriken: Shuriken; constructor( @inject(TYPES.Katana) katana: Katana, @inject(TYPES.Shuriken) shuriken: Shuriken, ) { this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } interface School { ninjaMaster: Ninja; student: Ninja; } @injectable() class NinjaSchool implements School { public ninjaMaster: Ninja; public student: Ninja; constructor(@multiInject(TYPES.Ninja) ninjas: Ninja[]) { this.ninjaMaster = ninjas[0] as Ninja; this.student = ninjas[1] as Ninja; } } interface Organisation { schools: NinjaSchool[]; } @injectable() class NinjaOrganisation implements Organisation { public schools: NinjaSchool[]; constructor(@multiInject(TYPES.School) schools: School[]) { this.schools = schools; } } const container: Container = new Container(); container.bind(TYPES.Katana).to(Katana); container.bind(TYPES.Shuriken).to(Shuriken); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.School).to(NinjaSchool); container.bind(TYPES.School).to(NinjaSchool); container.bind(TYPES.Organisation).to(NinjaOrganisation); const ninjaOrganisation: Organisation = container.get( TYPES.Organisation, ); for (let i: number = 0; i < 2; i++) { const ithNinjaOrganizationSchool: School = ninjaOrganisation.schools[ i ] as School; expect(ithNinjaOrganizationSchool.ninjaMaster.fight()).eql('cut!'); expect(ithNinjaOrganizationSchool.ninjaMaster.sneak()).eql('hit!'); expect(ithNinjaOrganizationSchool.student.fight()).eql('cut!'); expect(ithNinjaOrganizationSchool.student.sneak()).eql('hit!'); } }); }); it('Should support tagged bindings', () => { enum Tag { CanThrow, } @injectable() class Katana {} @injectable() class Shuriken {} interface Warrior { katana: unknown; shuriken: unknown; } @injectable() class Ninja implements Warrior { public katana: unknown; public shuriken: unknown; constructor( @inject('Weapon') @tagged('canThrow', false) katana: unknown, @inject('Weapon') @tagged(Tag.CanThrow, true) shuriken: unknown, ) { this.katana = katana; this.shuriken = shuriken; } } const container: Container = new Container(); container.bind('Warrior').to(Ninja); container.bind('Weapon').to(Katana).whenTagged('canThrow', false); container.bind('Weapon').to(Shuriken).whenTagged(Tag.CanThrow, true); const ninja: Ninja = container.get('Warrior'); expect(ninja.katana instanceof Katana).eql(true); expect(ninja.shuriken instanceof Shuriken).eql(true); }); it('Should support custom tag decorators', () => { @injectable() class Katana {} @injectable() class Shuriken {} interface Warrior { katana: unknown; shuriken: unknown; } const throwable: ParameterDecorator & PropertyDecorator = tagged( 'canThrow', true, ); const notThrowable: ParameterDecorator & PropertyDecorator = tagged( 'canThrow', false, ); @injectable() class Ninja implements Warrior { public katana: unknown; public shuriken: unknown; constructor( @inject('Weapon') @notThrowable katana: unknown, @inject('Weapon') @throwable shuriken: unknown, ) { this.katana = katana; this.shuriken = shuriken; } } const container: Container = new Container(); container.bind('Warrior').to(Ninja); container.bind('Weapon').to(Katana).whenTagged('canThrow', false); container.bind('Weapon').to(Shuriken).whenTagged('canThrow', true); const ninja: Warrior = container.get('Warrior'); expect(ninja.katana instanceof Katana).eql(true); expect(ninja.shuriken instanceof Shuriken).eql(true); }); it('Should support named bindings', () => { const name: symbol = Symbol.for('Weak'); @injectable() class Katana {} @injectable() class Shuriken {} interface Warrior { katana: unknown; shuriken: unknown; } @injectable() class Ninja implements Warrior { public katana: unknown; public shuriken: unknown; constructor( @inject('Weapon') @named('strong') katana: unknown, @inject('Weapon') @named(name) shuriken: unknown, ) { this.katana = katana; this.shuriken = shuriken; } } const container: Container = new Container(); container.bind('Warrior').to(Ninja); container.bind('Weapon').to(Katana).whenNamed('strong'); container.bind('Weapon').to(Shuriken).whenNamed(name); const ninja: Warrior = container.get('Warrior'); expect(ninja.katana instanceof Katana).eql(true); expect(ninja.shuriken instanceof Shuriken).eql(true); }); it('Should be able to resolve a ambiguous binding by providing a named tag', () => { interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'shuriken'; } } const container: Container = new Container(); container.bind('Weapon').to(Katana).whenNamed('japanese'); container.bind('Weapon').to(Shuriken).whenNamed('chinese'); const katana: Weapon = container.get('Weapon', { name: 'japanese', }); const shuriken: Weapon = container.get('Weapon', { name: 'chinese', }); expect(katana.name).eql('katana'); expect(shuriken.name).eql('shuriken'); }); it('Should be able to resolve a ambiguous binding by providing a custom tag', () => { interface Weapon { name: string; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'katana'; } } @injectable() class Shuriken implements Weapon { public name: string; constructor() { this.name = 'shuriken'; } } const container: Container = new Container(); container .bind('Weapon') .to(Katana) .whenTagged('faction', 'samurai'); container .bind('Weapon') .to(Shuriken) .whenTagged('faction', 'ninja'); const katana: Weapon = container.get('Weapon', { tag: { key: 'faction', value: 'samurai', }, }); const shuriken: Weapon = container.get('Weapon', { tag: { key: 'faction', value: 'ninja', }, }); expect(katana.name).eql('katana'); expect(shuriken.name).eql('shuriken'); }); it('Should be able to inject into a super constructor', () => { // eslint-disable-next-line @typescript-eslint/typedef const SYMBOLS = { Samurai: Symbol.for('Samurai'), SamuraiMaster: Symbol.for('SamuraiMaster'), SamuraiMaster2: Symbol.for('SamuraiMaster2'), Weapon: Symbol.for('Weapon'), }; interface Weapon { name: string; } interface Warrior { weapon: Weapon; } @injectable() class Katana implements Weapon { public name: string; constructor() { this.name = 'katana'; } } @injectable() class Samurai implements Warrior { public weapon: Weapon; constructor(weapon: Weapon) { this.weapon = weapon; } } @injectable() class SamuraiMaster extends Samurai implements Warrior { public isMaster: boolean; constructor(@inject(SYMBOLS.Weapon) weapon: Weapon) { super(weapon); this.isMaster = true; } } const container: Container = new Container(); container.bind(SYMBOLS.Weapon).to(Katana); container.bind(SYMBOLS.SamuraiMaster2).to(SamuraiMaster); const samuraiMaster2: SamuraiMaster = container.get( SYMBOLS.SamuraiMaster2, ); expect(samuraiMaster2.weapon.name).eql('katana'); expect(typeof samuraiMaster2.isMaster).eql('boolean'); }); it('Should support a whenParentNamed contextual bindings constraint', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Material: 'Material', Ninja: 'Ninja', Weapon: 'Weapon', }; interface Material { name: string; } @injectable() class Wood implements Material { public name: string; constructor() { this.name = 'wood'; } } @injectable() class Iron implements Material { public name: string; constructor() { this.name = 'iron'; } } interface Weapon { material: Material; } @injectable() class Sword implements Weapon { public material: Material; constructor(@inject('Material') material: Material) { this.material = material; } } interface Ninja { weapon: Weapon; } @injectable() class NinjaStudent implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') @named('non-lethal') weapon: Weapon) { this.weapon = weapon; } } @injectable() class NinjaMaster implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') @named('lethal') weapon: Weapon) { this.weapon = weapon; } } const container: Container = new Container(); container .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('master', false); container .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('master', true); container.bind(TYPES.Weapon).to(Sword); container.bind(TYPES.Material).to(Iron).whenParentNamed('lethal'); container .bind(TYPES.Material) .to(Wood) .whenParentNamed('non-lethal'); const master: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: true, }, }); const student: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: false, }, }); expect(master.weapon.material.name).eql('iron'); expect(student.weapon.material.name).eql('wood'); }); it('Should support a whenParentTagged contextual bindings constraint', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Material: 'Material', Ninja: 'Ninja', Weapon: 'Weapon', }; interface Material { name: string; } @injectable() class Wood implements Material { public name: string; constructor() { this.name = 'wood'; } } @injectable() class Iron implements Material { public name: string; constructor() { this.name = 'iron'; } } interface Weapon { material: Material; } @injectable() class Sword implements Weapon { public material: Material; constructor(@inject('Material') material: Material) { this.material = material; } } interface Ninja { weapon: Weapon; } @injectable() class NinjaStudent implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') @tagged('lethal', false) weapon: Weapon) { this.weapon = weapon; } } @injectable() class NinjaMaster implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') @tagged('lethal', true) weapon: Weapon) { this.weapon = weapon; } } const container: Container = new Container(); container .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('master', false); container .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('master', true); container.bind(TYPES.Weapon).to(Sword); container .bind(TYPES.Material) .to(Iron) .whenParentTagged('lethal', true); container .bind(TYPES.Material) .to(Wood) .whenParentTagged('lethal', false); const master: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: true, }, }); const student: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: false, }, }); expect(master.weapon.material.name).eql('iron'); expect(student.weapon.material.name).eql('wood'); }); it('Should support a whenAnyAncestorIs and whenNoAncestorIs contextual bindings constraint', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Material: 'Material', Ninja: 'Ninja', Weapon: 'Weapon', }; interface Material { name: string; } @injectable() class Wood implements Material { public name: string; constructor() { this.name = 'wood'; } } @injectable() class Iron implements Material { public name: string; constructor() { this.name = 'iron'; } } interface Weapon { material: Material; } @injectable() class Sword implements Weapon { public material: Material; constructor(@inject('Material') material: Material) { this.material = material; } } interface Ninja { weapon: Weapon; } @injectable() class NinjaStudent implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } @injectable() class NinjaMaster implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } function isNinjaStudentConstraint( bindingConstraints: BindingConstraints, ): boolean { return ( bindingConstraints.serviceIdentifier === TYPES.Ninja && bindingConstraints.tags.get('master') === false ); } function isNinjaMasterConstraint( bindingConstraints: BindingConstraints, ): boolean { return ( bindingConstraints.serviceIdentifier === TYPES.Ninja && bindingConstraints.tags.get('master') === true ); } // whenAnyAncestorIs const container: Container = new Container(); container .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('master', false); container .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('master', true); container.bind(TYPES.Weapon).to(Sword); container .bind(TYPES.Material) .to(Iron) .whenAnyAncestor(isNinjaMasterConstraint); container .bind(TYPES.Material) .to(Wood) .whenAnyAncestor(isNinjaStudentConstraint); const master: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: true, }, }); const student: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: false, }, }); expect(master.weapon.material.name).eql('iron'); expect(student.weapon.material.name).eql('wood'); // whenNoAncestorIs const container2: Container = new Container(); container2 .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('master', false); container2 .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('master', true); container2.bind(TYPES.Weapon).to(Sword); container2 .bind(TYPES.Material) .to(Iron) .whenNoAncestor(isNinjaStudentConstraint); container2 .bind(TYPES.Material) .to(Wood) .whenNoAncestor(isNinjaMasterConstraint); const master2: Ninja = container2.get(TYPES.Ninja, { tag: { key: 'master', value: true, }, }); const student2: Ninja = container2.get(TYPES.Ninja, { tag: { key: 'master', value: false, }, }); expect(master2.weapon.material.name).eql('iron'); expect(student2.weapon.material.name).eql('wood'); }); it('Should support a whenAnyAncestorNamed and whenNoAncestorNamed contextual bindings constraint', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Material: 'Material', Ninja: 'Ninja', Weapon: 'Weapon', }; interface Material { name: string; } @injectable() class Wood implements Material { public name: string; constructor() { this.name = 'wood'; } } @injectable() class Iron implements Material { public name: string; constructor() { this.name = 'iron'; } } interface Weapon { material: Material; } @injectable() class Sword implements Weapon { public material: Material; constructor(@inject('Material') material: Material) { this.material = material; } } interface Ninja { weapon: Weapon; } @injectable() class NinjaStudent implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } @injectable() class NinjaMaster implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } // whenAnyAncestorNamed const container: Container = new Container(); container.bind(TYPES.Ninja).to(NinjaStudent).whenNamed('non-lethal'); container.bind(TYPES.Ninja).to(NinjaMaster).whenNamed('lethal'); container.bind(TYPES.Weapon).to(Sword); container .bind(TYPES.Material) .to(Iron) .whenAnyAncestorNamed('lethal'); container .bind(TYPES.Material) .to(Wood) .whenAnyAncestorNamed('non-lethal'); const master: Ninja = container.get(TYPES.Ninja, { name: 'lethal', }); const student: Ninja = container.get(TYPES.Ninja, { name: 'non-lethal', }); expect(master.weapon.material.name).eql('iron'); expect(student.weapon.material.name).eql('wood'); // whenNoAncestorNamed const container2: Container = new Container(); container2 .bind(TYPES.Ninja) .to(NinjaStudent) .whenNamed('non-lethal'); container2.bind(TYPES.Ninja).to(NinjaMaster).whenNamed('lethal'); container2.bind(TYPES.Weapon).to(Sword); container2 .bind(TYPES.Material) .to(Iron) .whenNoAncestorNamed('non-lethal'); container2 .bind(TYPES.Material) .to(Wood) .whenNoAncestorNamed('lethal'); const master2: Ninja = container.get(TYPES.Ninja, { name: 'lethal', }); const student2: Ninja = container.get(TYPES.Ninja, { name: 'non-lethal', }); expect(master2.weapon.material.name).eql('iron'); expect(student2.weapon.material.name).eql('wood'); }); it('Should support a whenAnyAncestorTagged and whenNoAncestorTaggedcontextual bindings constraint', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Material: 'Material', Ninja: 'Ninja', Weapon: 'Weapon', }; interface Material { name: string; } @injectable() class Wood implements Material { public name: string; constructor() { this.name = 'wood'; } } @injectable() class Iron implements Material { public name: string; constructor() { this.name = 'iron'; } } interface Weapon { material: Material; } @injectable() class Sword implements Weapon { public material: Material; constructor(@inject('Material') material: Material) { this.material = material; } } interface Ninja { weapon: Weapon; } @injectable() class NinjaStudent implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } @injectable() class NinjaMaster implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } // whenAnyAncestorTagged const container: Container = new Container(); container .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('lethal', false); container .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('lethal', true); container.bind(TYPES.Weapon).to(Sword); container .bind(TYPES.Material) .to(Iron) .whenAnyAncestorTagged('lethal', true); container .bind(TYPES.Material) .to(Wood) .whenAnyAncestorTagged('lethal', false); const master: Ninja = container.get(TYPES.Ninja, { tag: { key: 'lethal', value: true, }, }); const student: Ninja = container.get(TYPES.Ninja, { tag: { key: 'lethal', value: false, }, }); expect(master.weapon.material.name).eql('iron'); expect(student.weapon.material.name).eql('wood'); // whenNoAncestorTagged const container2: Container = new Container(); container2 .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('lethal', false); container2 .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('lethal', true); container2.bind(TYPES.Weapon).to(Sword); container2 .bind(TYPES.Material) .to(Iron) .whenNoAncestorTagged('lethal', false); container2 .bind(TYPES.Material) .to(Wood) .whenNoAncestorTagged('lethal', true); const master2: Ninja = container.get(TYPES.Ninja, { tag: { key: 'lethal', value: true, }, }); const student2: Ninja = container.get(TYPES.Ninja, { tag: { key: 'lethal', value: false, }, }); expect(master2.weapon.material.name).eql('iron'); expect(student2.weapon.material.name).eql('wood'); }); it('Should support a whenAnyAncestorMatches and whenNoAncestorMatches contextual bindings constraint', () => { // eslint-disable-next-line @typescript-eslint/typedef const TYPES = { Material: 'Material', Ninja: 'Ninja', Weapon: 'Weapon', }; interface Material { name: string; } @injectable() class Wood implements Material { public name: string; constructor() { this.name = 'wood'; } } @injectable() class Iron implements Material { public name: string; constructor() { this.name = 'iron'; } } interface Weapon { material: Material; } @injectable() class Sword implements Weapon { public material: Material; constructor(@inject('Material') material: Material) { this.material = material; } } interface Ninja { weapon: Weapon; } @injectable() class NinjaStudent implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } @injectable() class NinjaMaster implements Ninja { public weapon: Weapon; constructor(@inject('Weapon') weapon: Weapon) { this.weapon = weapon; } } // custom constraints function isNinjaStudentConstraint( bindingConstraints: BindingConstraints, ): boolean { return ( bindingConstraints.serviceIdentifier === TYPES.Ninja && bindingConstraints.tags.get('master') === false ); } function isNinjaMasterConstraint( bindingConstraints: BindingConstraints, ): boolean { return ( bindingConstraints.serviceIdentifier === TYPES.Ninja && bindingConstraints.tags.get('master') === true ); } // whenAnyAncestorMatches const container: Container = new Container(); container .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('master', false); container .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('master', true); container.bind(TYPES.Weapon).to(Sword); container .bind(TYPES.Material) .to(Iron) .whenAnyAncestor(isNinjaMasterConstraint); container .bind(TYPES.Material) .to(Wood) .whenAnyAncestor(isNinjaStudentConstraint); const master: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: true, }, }); const student: Ninja = container.get(TYPES.Ninja, { tag: { key: 'master', value: false, }, }); expect(master.weapon.material.name).eql('iron'); expect(student.weapon.material.name).eql('wood'); // whenNoAncestorMatches const container2: Container = new Container(); container2 .bind(TYPES.Ninja) .to(NinjaStudent) .whenTagged('master', false); container2 .bind(TYPES.Ninja) .to(NinjaMaster) .whenTagged('master', true); container2.bind(TYPES.Weapon).to(Sword); container2 .bind(TYPES.Material) .to(Iron) .whenNoAncestor(isNinjaStudentConstraint); container2 .bind(TYPES.Material) .to(Wood) .whenNoAncestor(isNinjaMasterConstraint); const master2: Ninja = container2.get(TYPES.Ninja, { tag: { key: 'master', value: true, }, }); const student2: Ninja = container2.get(TYPES.Ninja, { tag: { key: 'master', value: false, }, }); expect(master2.weapon.material.name).eql('iron'); expect(student2.weapon.material.name).eql('wood'); }); it('Should be able to inject a regular derived class', () => { // eslint-disable-next-line @typescript-eslint/typedef const SYMBOLS = { RANK: Symbol.for('RANK'), SamuraiMaster: Symbol.for('SamuraiMaster'), }; interface Warrior { rank: string; } @injectable() class Samurai implements Warrior { public rank: string; constructor(rank: string) { this.rank = rank; } } @injectable() class SamuraiMaster extends Samurai implements Warrior { constructor(@inject(SYMBOLS.RANK) rank: string) { super(rank); } } const container: Container = new Container(); container.bind(SYMBOLS.SamuraiMaster).to(SamuraiMaster); container.bind(SYMBOLS.RANK).toConstantValue('Master'); const samurai: SamuraiMaster = container.get( SYMBOLS.SamuraiMaster, ); expect(samurai.rank).eql('Master'); }); it('Should not throw due to a missing @injectable in a base class', () => { // eslint-disable-next-line @typescript-eslint/typedef const SYMBOLS = { SamuraiMaster: Symbol.for('SamuraiMaster'), }; interface Warrior { rank: string; } // IMPORTANT: Missing @injectable() class Samurai implements Warrior { public rank: string; constructor(rank: string) { this.rank = rank; } } @injectable() class SamuraiMaster extends Samurai implements Warrior { constructor() { super('master'); } } const container: Container = new Container(); container.bind(SYMBOLS.SamuraiMaster).to(SamuraiMaster); function notThrows() { return container.get(SYMBOLS.SamuraiMaster); } expect(notThrows).not.to.throw(); }); }); ================================================ FILE: src/test/node/error_messages.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, injectable } from '../..'; describe('Error message when resolving fails', () => { @injectable() class Katana {} @injectable() class Shuriken {} @injectable() class Bokken {} it('Should contain correct message and the serviceIdentifier in error message', () => { const container: Container = new Container(); container.bind('Weapon').to(Katana); const tryWeapon: () => void = () => { container.get('Ninja'); }; expect(tryWeapon).to.throw(''); }); it('Should contain the provided name in error message when target is named', () => { const container: Container = new Container(); const tryGetNamedWeapon: (name: string | number | symbol) => void = ( name: string | number | symbol, ) => { container.get('Weapon', { name }); }; expect(() => { tryGetNamedWeapon('superior'); }).to.throw(`No bindings found for service: "Weapon". Trying to resolve bindings for "Weapon (Root service)". Binding constraints: - service identifier: Weapon - name: superior`); expect(() => { tryGetNamedWeapon(Symbol.for('Superior')); }).to.throw(`No bindings found for service: "Weapon". Trying to resolve bindings for "Weapon (Root service)". Binding constraints: - service identifier: Weapon - name: Symbol(Superior)`); expect(() => { tryGetNamedWeapon(0); }).to.throw(`No bindings found for service: "Weapon". Trying to resolve bindings for "Weapon (Root service)". Binding constraints: - service identifier: Weapon - name: 0`); }); it('Should contain the provided tag in error message when target is tagged', () => { const container: Container = new Container(); const tryGetTaggedWeapon: (tag: string | number | symbol) => void = ( tag: string | number | symbol, ) => { container.get('Weapon', { tag: { key: tag, value: true, }, }); }; expect(() => { tryGetTaggedWeapon('canShoot'); }).to.throw(`No bindings found for service: "Weapon". Trying to resolve bindings for "Weapon (Root service)". Binding constraints: - service identifier: Weapon - name: - - tags: - canShoot`); expect(() => { tryGetTaggedWeapon(Symbol.for('Can shoot')); }).to.throw(`No bindings found for service: "Weapon". Trying to resolve bindings for "Weapon (Root service)". Binding constraints: - service identifier: Weapon - name: - - tags: - Symbol(Can shoot)`); expect(() => { tryGetTaggedWeapon(0); }).to.throw(`No bindings found for service: "Weapon". Trying to resolve bindings for "Weapon (Root service)". Binding constraints: - service identifier: Weapon - name: - - tags: - 0`); }); it('Should list all possible bindings in error message if ambiguous matching binding found', () => { const container: Container = new Container(); container.bind('Weapon').to(Katana); container.bind('Weapon').to(Shuriken); container.bind('Weapon').to(Bokken); try { container.get('Weapon'); } catch (error) { expect((error as Error).message).to .equal(`Ambiguous bindings found for service: "Weapon". Registered bindings: [ type: "Instance", serviceIdentifier: "Weapon", scope: "Transient", implementationType: "Katana" ] [ type: "Instance", serviceIdentifier: "Weapon", scope: "Transient", implementationType: "Shuriken" ] [ type: "Instance", serviceIdentifier: "Weapon", scope: "Transient", implementationType: "Bokken" ] Trying to resolve bindings for "Weapon (Root service)". Binding constraints: - service identifier: Weapon - name: -`); } }); }); ================================================ FILE: src/test/node/exceptions.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable } from '../..'; describe('Node', () => { it('Should throw if circular dependencies found', () => { @injectable() class A { public b: unknown; public c: unknown; constructor(@inject('B') b: unknown, @inject('C') c: unknown) { this.b = b; this.c = c; } } @injectable() class B {} @injectable() class C { public d: unknown; constructor(@inject('D') d: unknown) { this.d = d; } } @injectable() class D { public a: unknown; constructor(@inject('A') a: unknown) { this.a = a; } } const container: Container = new Container(); container.bind('A').to(A); container.bind('B').to(B); container.bind('C').to(C); container.bind('D').to(D); function willThrow() { const a: A = container.get('A'); return a; } expect(willThrow).to.throw(''); }); }); ================================================ FILE: src/test/node/performance.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { performance } from 'perf_hooks'; import { Container } from '../../index'; describe('Performance', () => { function registerN(times: number) { const result: { container: Container; register: number; } = { container: new Container(), register: -1, }; for (let i: number = 0; i < times; i++) { const start: number = performance.now(); result.container .bind(`SOME_ID_${i.toString()}`) .toConstantValue({ test: i }); const end: number = performance.now(); result.register = end - start; } return result; } function resolveN(container: Container, times: number) { const result: { avg: number; max: number; min: number; } = { avg: -1, max: -1, min: Number.MAX_SAFE_INTEGER, }; const items: number[] = []; for (let i: number = 0; i < times; i++) { const start: number = performance.now(); container.get(`SOME_ID_${times.toString()}`); const end: number = performance.now(); const total: number = end - start; if (total < result.min) { result.min = total; } if (total > result.max) { result.max = total; } items.push(total); } result.avg = items.reduce((p: number, c: number) => p + c, 0) / items.length; return result; } it('Should be able to register 1 binding in less than 1 ms', () => { const result1: { container: Container; register: number; } = registerN(1); expect(result1.register).to.below(1); expect(result1.register).to.below(1); }); it('Should be able to register 5 bindings in less than 1 ms', () => { const result5: { container: Container; register: number; } = registerN(5); expect(result5.register).to.below(1); }); it('Should be able to register 1K bindings in less than 1 ms', () => { const result1K: { container: Container; register: number; } = registerN(1000); expect(result1K.register).to.below(1); }); it('Should be able to register 5K bindings in less than 1 ms', () => { const result5K: { container: Container; register: number; } = registerN(5000); expect(result5K.register).to.below(1); }); it('Should be able to register 1 bindings in less than 1 ms', () => { const container1: Container = registerN(1000).container; const result1: { avg: number; max: number; min: number; } = resolveN(container1, 5); expect(result1.avg).to.below(1); }); it('Should be able to register 5 bindings in less than 1 ms', () => { const container5: Container = registerN(1000).container; const result5: { avg: number; max: number; min: number; } = resolveN(container5, 5); expect(result5.avg).to.below(1); }); it('Should be able to register 1K bindings in less than 1 ms', () => { const container1K: Container = registerN(1000).container; const result1K: { avg: number; max: number; min: number; } = resolveN(container1K, 5); expect(result1K.avg).to.below(1); }); it('Should be able to register 5K bindings in less than 1 ms', () => { const container5K: Container = registerN(5000).container; const result5K: { avg: number; max: number; min: number; } = resolveN(container5K, 5); expect(result5K.avg).to.below(1); }); it('Should be able to register 10K bindings in less than 1 ms', () => { const container10K: Container = registerN(10000).container; const result10K: { avg: number; max: number; min: number; } = resolveN(container10K, 5); expect(result10K.avg).to.below(1); }); }); ================================================ FILE: src/test/node/proxies.test.ts ================================================ import 'reflect-metadata'; import { expect } from 'chai'; import { Container, inject, injectable, ResolutionContext } from '../..'; describe('InversifyJS', () => { it('Should support the injection of proxied objects', () => { const weaponId: string = 'Weapon'; const warriorId: string = 'Warrior'; interface Weapon { use(): void; } @injectable() class Katana implements Weapon { public use() { return 'Used Katana!'; } } interface Warrior { weapon: Weapon; } @injectable() class Ninja implements Warrior { public weapon: Weapon; constructor(@inject(weaponId) weapon: Weapon) { this.weapon = weapon; } } const container: Container = new Container(); container.bind(warriorId).to(Ninja); const log: string[] = []; container .bind(weaponId) .to(Katana) .onActivation((_context: ResolutionContext, weapon: Weapon) => { const handler: ProxyHandler<() => void> = { apply( target: () => void, thisArgument: unknown, argumentsList: [], ): void { log.push(`Starting: ${new Date().getTime().toString()}`); target.apply(thisArgument, argumentsList); log.push(`Finished: ${new Date().getTime().toString()}`); }, }; weapon.use = new Proxy(weapon.use, handler); return weapon; }); const ninja: Warrior = container.get(warriorId); ninja.weapon.use(); expect(log.length).eql(2); expect(log[0]?.indexOf('Starting: ')).not.to.eql(-1); expect(log[1]?.indexOf('Finished: ')).not.to.eql(-1); }); }); ================================================ FILE: tsconfig.base.cjs.json ================================================ { "$schema": "http://json.schemastore.org/tsconfig", "extends": "./tsconfig.base.json", "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } } ================================================ FILE: tsconfig.base.esm.json ================================================ { "$schema": "http://json.schemastore.org/tsconfig", "extends": "./tsconfig.base.json", "compilerOptions": { "module": "ES2022", "moduleResolution": "Bundler", "resolveJsonModule": false } } ================================================ FILE: tsconfig.base.json ================================================ { "$schema": "http://json.schemastore.org/tsconfig", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, "lib": ["ES2022"], "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "target": "ES2022" } } ================================================ FILE: tsconfig.cjs.json ================================================ { "$schema": "http://json.schemastore.org/tsconfig", "extends": "./tsconfig.base.cjs.json", "compilerOptions": { "outDir": "./lib/cjs", "rootDir": "./src", "tsBuildInfoFile": "tsconfig.cjs.tsbuildinfo" }, "include": ["src"] } ================================================ FILE: tsconfig.esm.json ================================================ { "$schema": "http://json.schemastore.org/tsconfig", "extends": "./tsconfig.base.esm.json", "compilerOptions": { "outDir": "./lib/esm", "rootDir": "./src", "tsBuildInfoFile": "tsconfig.esm.tsbuildinfo" }, "include": ["src"] } ================================================ FILE: tsconfig.json ================================================ { "$schema": "http://json.schemastore.org/tsconfig", "extends": "./tsconfig.cjs.json" }