├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTION.md ├── LICENSE ├── README.md ├── examples ├── javascript │ ├── basic-example.js │ ├── hierarchical-containers.js │ ├── life-time-control.js │ ├── resolving-container.js │ ├── use-factory-with-injections.js │ ├── use-factory.js │ └── use-value.js └── typescript │ ├── alternative-syntax.ts │ ├── basic-example.ts │ ├── hierarchical-containers.ts │ ├── life-time-control.ts │ ├── resolving-container.ts │ ├── use-factory-with-injections.ts │ ├── use-factory.ts │ └── use-value.ts ├── gulpfile.js ├── package.json ├── src ├── lib │ ├── container.interface.ts │ ├── container.ts │ ├── decorators.ts │ ├── exceptions.ts │ ├── index.ts │ ├── injection-token.ts │ ├── interfaces.ts │ ├── metadata │ │ ├── annotator-provider.ts │ │ ├── index.ts │ │ ├── keys.ts │ │ ├── metadata-annotator.interface.ts │ │ └── static-metadata-annotator.ts │ └── registry-data.ts └── tests │ ├── container.spec.ts │ └── decorators.spec.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | src/**/*.js* 4 | *.d.ts 5 | dist 6 | usage.js 7 | CURRENT-CHANGELOG.txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .npmignore 3 | .gitignore 4 | 5 | node_modules 6 | tsconfig.json 7 | tslint.json 8 | README.md 9 | CURRENT-CHANGELOG.txt 10 | 11 | examples 12 | gulpfile.js 13 | src 14 | dist/tests 15 | usage.js 16 | .travis.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | install: 6 | - npm install 7 | script: 8 | - npm run dist -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [1.7.16]() (2017-11-30) 3 | 4 | ### Added 5 | * **Component** decorator - functions the same as Injectable. 6 | 7 | 8 | # [1.7.15]() (2017-10-18) 9 | 10 | ### Fixed 11 | * **Wrong hierarchical dependency resolution**. 12 | 13 | 14 | # [1.7.14]() (2017-10-14) 15 | 16 | ### Fixed 17 | * **Wrong injections order when using Inject decorator**. 18 | 19 | 20 | # [1.7.3]() (2017-10-07) 21 | 22 | ### API changes 23 | #### Container 24 | * **Added option parameter to constructor**. - options object has two attributes: (parent and defaultLifeTime). 25 | 26 | ### Fixed 27 | * **Factories always resolve instances**. 28 | 29 | 30 | # [1.7.0]() (2017-10-07) 31 | 32 | ### API changes 33 | #### Container 34 | * **Container.createScope() is marked as deprecated, use Container.createChild() instead+**. 35 | * **Added method Container.setParent()**. 36 | 37 | ### Fixed 38 | * **Symbol values for types are now properly handled in error messages**. 39 | 40 | 41 | # [1.6.8]() (2017-10-05) 42 | 43 | ### Features 44 | 45 | * **Added support for ES6+** - Added new syntax for declaring injectioins of a class, now it's possible via Injectable decorator. see [examples/javascript](examples/javascript). 46 | 47 | 48 | # [1.6.0]() (2017-10-04) 49 | 50 | ### Features 51 | 52 | * **Life Time control** - added LifeTime managment configuration, see [example](examples/typescript/life-time-control.ts) [5ba097a9cb](https://github.com/thohoh/container-ioc/commit/5ba097a9cb41277e0e9013d4ef5e694f3595de36) 53 | 54 | 55 | # [1.5.0]() (2017-10-02) 56 | 57 | ### Features 58 | 59 | * **Plugable MetadataAnnotator via AnnotatorProvider** examples on [README.md](README.md) [551cbc9c](https://github.com/thohoh/container-ioc/commit/551cbc9cfc9316ce72ad9572ac500089b011ca12) 60 | 61 | ### Removed 62 | * **reflect-metadata as a dependency** 63 | 64 | 65 | # [1.4.0]() (2017-10-01) 66 | 67 | ### Features 68 | 69 | * **@Injectable() decorator:** - now to make a class available for injection you have to mark it with @Injectable() decorator: [86fede13](https://github.com/thohoh/container-ioc/commit/86fede13be7147079c36bc77e204ac21deb360bc) 70 | 71 | ### Breaking Changes 72 | * **Class registration:** - now it's necessary to mark the class you want to make available for injections with **Injectable** decorator. 73 | ```Typescript 74 | // old version 75 | class A {} 76 | container.register({ token: 'IA', useClass: A }); 77 | 78 | // new version 79 | @Injectable() 80 | class A {} 81 | container.register({ token: 'IA', useClass: A }); 82 | ``` 83 | 84 | 85 | 86 | # [1.3.1]() (2017-09-30) 87 | 88 | ### Features 89 | 90 | * **Value and Factory:** - container now can resolve values or anything returned from a factory. commit 91 | [50ebb63](https://github.com/thohoh/container-ioc/commit/50ebb63451878b262626446828f7b7ac5ce6afe5) 92 | * **Injection Token** - added InjectionToken class to facilitate working with abstractions. [3c380c878](https://github.com/thohoh/container-ioc/commit/3c380c878abef883b293007f97299d5053eafe5b) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at itrefak@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | ## Contribution guidelines 2 | 3 | ##### Fork the project, and then go: 4 | ``` 5 | git clone https://github.com//container-ioc 6 | 7 | cd container-ioc 8 | ``` 9 | ##### Install packages: 10 | ``` 11 | npm install 12 | ``` 13 | ##### Npm scripts: 14 | ``` 15 | npm start // lints, compiles and tests source code 16 | npm run dev // runs dev workflow, lints, compiles, tests source code and watches source files. 17 | npm run dist // compiles src code and puts everything into dist folder 18 | ``` 19 | 20 | ##### Set up your fork to point to original repository: 21 | ``` 22 | git remote add upstream https://github.com/thohoh/container-ioc 23 | ``` 24 | 25 | ##### Update your master branch with the latest upstream version: 26 | ``` 27 | git pull --ff upstream master 28 | ``` 29 | 30 | #### Create a new branch: 31 | ``` 32 | git checkout -b '#_some_info' 33 | ``` 34 | #### Commit your changes: 35 | ``` 36 | git commit -m '# changes message' 37 | ``` 38 | 39 | #### Push your changes: 40 | ``` 41 | git push origin 42 | ``` 43 | #### Hit "submit pull request" button on your fork's page -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander Kozlov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](http://abcselfstorageperth.com.au/wp-content/uploads/2014/08/icon-container-storage1.png) 2 | 3 | 4 | ## **container-ioc** 5 | is a [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) / [Inversion of Control (IoC) container](http://martinfowler.com/articles/injection.html) package for Javascript and Node.js applications powered by Typescript . It manages the dependencies between classes, so that applications stay easy to change and maintain as they grow. 6 | 7 | [![npm version](https://badge.fury.io/js/container-ioc.svg)](https://badge.fury.io/js/container-ioc) 8 | [![Build Status](https://travis-ci.org/typesoft/container-ioc.svg?branch=master)](https://travis-ci.org/typesoft/container-ioc) 9 | [![npm](https://img.shields.io/npm/dt/container-ioc.svg)](https://www.npmjs.com/package/container-ioc) 10 | [![Gitter chat](https://badges.gitter.im/typesoft-org/container-ioc.png)](https://gitter.im/typesoft-org/container-ioc) 11 | [![license](https://img.shields.io/github/license/thohoh/container-ioc.svg)](https://github.com/thohoh/container-ioc/blob/master/LICENSE) 12 | 13 | ### Features: 14 | * Well-known Angular DI API. 15 | * No external dependencies. 16 | * [Life Time control](#life-time-control). 17 | * [Hierarchical containers](#hierarchical-containers). 18 | * Resolves values using Classes, [Factories](#using-factories) and [Values](#using-values). 19 | * Descriptive error messages. 20 | * 97% test coverage. 21 | 22 | ### Examples: 23 | * [examples/javascript](examples/javascript) 24 | * [examples/typescript](examples/typescript) 25 | 26 | 27 | ### Installation: 28 | ``` 29 | npm install --save container-ioc 30 | ``` 31 | ### Basics: 32 | > Code examples below are written in Typescript. Check [examples/javascript](examples/javascript) for examples written in Javascript. 33 | 34 | #### Step 1. Define your interfaces and types. 35 | > Possible values for types: **Symbol**, **string**, **Object**. 36 | 37 | ```typescript 38 | interface IApplication { 39 | run(): void; 40 | } 41 | 42 | interface IService { 43 | serve(): void; 44 | } 45 | 46 | const TApplication = Symbol('IApplication'); 47 | 48 | const TService = Symbol('IService'); 49 | ``` 50 | 51 | #### Step 2. Declare dependencies with decorators **Injectable** and **Inject**. 52 | 53 | ```typescript 54 | import { Injectable, Inject } from 'container-ioc'; 55 | 56 | @Injectable() 57 | export class Application implements IApplication { 58 | constructor(@Inject(TService) private service: IService) {} 59 | 60 | run(): void { 61 | this.service.serve(); 62 | } 63 | } 64 | 65 | @Injectable() 66 | export class Service implements IService { 67 | serve(): void { 68 | // serves 69 | } 70 | } 71 | ``` 72 | 73 | #### Step 3. Create a container and register types in there. 74 | 75 | ```typescript 76 | import { Container } from 'container-ioc'; 77 | 78 | let container = new Container(); 79 | 80 | container.register([ 81 | { token: TApplication, useClass: Application }, 82 | { token: TService, useClass: Service } 83 | ]); 84 | ``` 85 | 86 | #### Step 4. Resolve value from the container. 87 | 88 | ```typescript 89 | let app = container.resolve(TApplication); 90 | 91 | app.run(); 92 | ``` 93 | 94 | #### Step 2 for Javascript. 95 | > Since Javascript does not support parameter decorators, use alternative API for declaring dependencies. In this case we don't use **Inject** decorator. See [examples/javascript](examples/javascript) for more. 96 | ```javascript 97 | 98 | @Injectable([TService]) 99 | class Service { 100 | constructor(service) { 101 | this.service = service; 102 | } 103 | } 104 | ``` 105 | 106 | ### Life Time control 107 | > By default, containers resolve singletons when using **useClass** and **useFactory**. 108 | Default life time for all items in a container can be set by passing an option object to it's contructor with **defailtLifeTime** attribute. Possible values: **LifeTime.PerRequest** (resolves instances) and **LifeTime.Persistent** (resolves singletons); 109 | 110 | ```typescript 111 | import { LifeTime } from 'container-ioc'; 112 | 113 | const container = new Container({ 114 | defaultLifeTime: LifeTime.PerRequest 115 | }); 116 | ``` 117 | > You can also specify life time individually for each item in a container by specifying **lifeTime** attribute. 118 | 119 | ```typescript 120 | container.register([ 121 | { 122 | token: TService, 123 | useClass: Service, 124 | lifeTime: LifeTime.PerRequest 125 | } 126 | ]); 127 | ``` 128 | ```typescript 129 | container.register([ 130 | { 131 | token: TService, 132 | useFactory: () => { 133 | return { 134 | serve(): void {} 135 | } 136 | }, 137 | lifeTime: LifeTime.Persistent 138 | } 139 | ]); 140 | ``` 141 | 142 | ### Hierarchical containers 143 | > If a container can't find a value within itself, it will look it up in ascendant containers. There a 3 ways to set a parent for a container. 144 | 145 | ###### 1. Container.createChild() method. 146 | ```typescript 147 | const parentContainer = new Container(); 148 | const childContainer = parentContainer.createChild(); 149 | ``` 150 | 151 | ###### 2. Container.setParent() method. 152 | ```typescript 153 | const parent = new Container(); 154 | const child = new Container(); 155 | 156 | child.setParent(parent); 157 | ``` 158 | 159 | ###### 3. Via Container's constructor with options. 160 | ```typescript 161 | const parent = new Container(); 162 | const child = new Container({ 163 | parent: parent 164 | }); 165 | ``` 166 | 167 | ### Using Factories 168 | ```typescript 169 | /* Without injections */ 170 | container.register([ 171 | { 172 | token: 'TokenForFactory', 173 | useFactory: () => { 174 | return 'any-value'; 175 | } 176 | } 177 | ]); 178 | 179 | /* With injections */ 180 | container.register([ 181 | { token: 'EnvProvider', useClass: EnvProvider }, 182 | { 183 | token: 'TokenForFactory', 184 | useFactory: (envProvider) => { 185 | // do something 186 | return 'something'; 187 | }, 188 | inject: ['EnvProvider'] 189 | } 190 | ]); 191 | ``` 192 | 193 | ### Using Values 194 | ```typescript 195 | container.register([ 196 | { token: 'IConfig', useValue: {}} 197 | ]); 198 | ``` 199 | 200 | ### Shortcut for Classes 201 | ```typescript 202 | container.register([ 203 | App 204 | ]); 205 | ``` 206 | Is the same as: 207 | ```typescript 208 | container.register([ 209 | { token: App, useClass: App } 210 | ]); 211 | ``` 212 | 213 | ## Contribution 214 | Become a contributor to this project. Feel free to submit an [issue](https://github.com/thohoh/container-ioc/issues) or a pull request. 215 | 216 | see [CONTRIBUTION.md](CONTRIBUTION.md) for more information. 217 | 218 | Please see also our [Code of Conduct](CODE_OF_CONDUCT.md). -------------------------------------------------------------------------------- /examples/javascript/basic-example.js: -------------------------------------------------------------------------------- 1 | import { Injectable, Container } from 'container-ioc'; 2 | 3 | const TApplication = Symbol('IApplication'); 4 | 5 | const TService = Symbol('IService'); 6 | 7 | @Injectable([TService]) 8 | export class Application { 9 | constructor(service) { 10 | this.service = service; 11 | } 12 | 13 | run() { 14 | this.service.serve(); 15 | } 16 | } 17 | 18 | @Injectable() 19 | export class Service { 20 | serve() { 21 | // serves 22 | } 23 | } 24 | 25 | const container = new Container(); 26 | 27 | container.register([ 28 | { token: TApplication, useClass: Application }, 29 | { token: TService, useClass: Service } 30 | ]); 31 | 32 | const app = container.resolve(TApplication); 33 | 34 | app.run(); -------------------------------------------------------------------------------- /examples/javascript/hierarchical-containers.js: -------------------------------------------------------------------------------- 1 | import { Container } from 'container-ioc'; 2 | 3 | /* 4 | If a container can't find a value within itself, it will look it up in ascendant containers. 5 | There a 3 ways to set a parent for a container: 6 | */ 7 | 8 | const grandParentContainer = new Container(); 9 | 10 | // 1. 11 | const parentContainer = grandParentContainer.createChild(); 12 | 13 | // 2. 14 | const childContainer = new Container(); 15 | childContainer.setParent(parentContainer); 16 | 17 | // 3. 18 | const deepNestedContainer = new Container({ 19 | parent: childContainer 20 | }); 21 | 22 | grandParentContainer.register([ 23 | { token: 'IConfig', useValue: {}} 24 | ]); 25 | 26 | deepNestedContainer.resolve('IConfig'); -------------------------------------------------------------------------------- /examples/javascript/life-time-control.js: -------------------------------------------------------------------------------- 1 | import { Container, LifeTime, Injectable } from 'container-ioc'; 2 | 3 | /* 4 | By default, containers resolve singletons when using useClass and useFactory. 5 | Default life time for all items in a container can be set by passing an option object to it's contructor with **defailtLifeTime** attribute. 6 | Possible values: LifeTime.PerRequest (resolves instances) and LifeTime.Persistent (resolves singletons); 7 | */ 8 | const container = new Container({ 9 | defaultLifeTime: LifeTime.PerRequest 10 | }); 11 | 12 | /* 13 | You can also specify life time individually for each item in a container by specifying lifeTime attribute. 14 | */ 15 | @Injectable() 16 | class ServiceA {} 17 | 18 | /* With classes */ 19 | container.register([ 20 | { 21 | token: 'ISerivceA', 22 | useClass: ServiceA, 23 | lifeTime: LifeTime.PerRequest 24 | } 25 | ]); 26 | 27 | /* With factories */ 28 | container.register([ 29 | { 30 | token: 'IServiceB', 31 | useFactory: () => { 32 | return { 33 | serve() {} 34 | }; 35 | }, 36 | lifeTime: LifeTime.Persistent 37 | } 38 | ]); 39 | 40 | const service1 = container.resolve('IServiceA'); 41 | const service2 = container.resolve('IServiceB'); -------------------------------------------------------------------------------- /examples/javascript/resolving-container.js: -------------------------------------------------------------------------------- 1 | import { Container, Injectable } from 'container-ioc'; 2 | 3 | const container = new Container(); 4 | 5 | @Injectable([Container]) 6 | class Builder { 7 | constructor(container) {} 8 | } 9 | 10 | container.register({ token: Builder, useClass: Builder }); 11 | 12 | const builder = container.resolve(Builder); -------------------------------------------------------------------------------- /examples/javascript/use-factory-with-injections.js: -------------------------------------------------------------------------------- 1 | import { Container, Injectable } from 'container-ioc'; 2 | 3 | const TService = Symbol('IService'); 4 | 5 | const TManager = Symbol('IManager'); 6 | 7 | @Injectable([TService]) 8 | class App { 9 | constructor(service) { 10 | this.service = service; 11 | } 12 | } 13 | 14 | @Injectable() 15 | class Service {} 16 | 17 | @Injectable() 18 | class Manager {} 19 | 20 | const container = new Container(); 21 | 22 | container.register([ 23 | { token: App, useClass: App }, 24 | { token: TManager, useClass: Manager }, 25 | { 26 | token: TService, 27 | useFactory: (manager) => { 28 | return manager; 29 | }, 30 | inject: ['IConfig'] 31 | } 32 | ]); 33 | 34 | const app = container.resolve(App); -------------------------------------------------------------------------------- /examples/javascript/use-factory.js: -------------------------------------------------------------------------------- 1 | import { Container, Injectable } from 'container-ioc'; 2 | 3 | const TService = Symbol('IService'); 4 | 5 | @Injectable([TService]) 6 | class App { 7 | constructor(service) {} 8 | } 9 | 10 | const container = new Container(); 11 | 12 | container.register([ 13 | { token: App, useClass: App }, 14 | { 15 | token: TService, 16 | useFactory: () => { 17 | return { 18 | serve () { 19 | // do stuff 20 | } 21 | }; 22 | } 23 | } 24 | ]); 25 | 26 | const app = container.resolve(App); -------------------------------------------------------------------------------- /examples/javascript/use-value.js: -------------------------------------------------------------------------------- 1 | import { Container, Injectable } from 'container-ioc'; 2 | 3 | const container = new Container(); 4 | 5 | container.register([ 6 | { token: 'IConfig', useValue: {}} 7 | ]); 8 | 9 | const app = container.resolve('IConfig'); -------------------------------------------------------------------------------- /examples/typescript/alternative-syntax.ts: -------------------------------------------------------------------------------- 1 | import { Container, Injectable } from 'container-ioc'; 2 | 3 | interface IService { 4 | [key: string]: any; 5 | } 6 | 7 | const TSerivice = Symbol('IService'); 8 | 9 | @Injectable([TSerivice]) 10 | class App { 11 | constructor(private service: IService) {} 12 | } 13 | 14 | const container = new Container(); 15 | 16 | container.register([ 17 | { token: App, useClass: App }, 18 | { token: TSerivice, useValue: {}} 19 | ]); 20 | 21 | const app = container.resolve(App); -------------------------------------------------------------------------------- /examples/typescript/basic-example.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, Container } from 'container-ioc'; 2 | 3 | interface IApplication { 4 | run(): void; 5 | } 6 | 7 | interface IService { 8 | serve(): void; 9 | } 10 | 11 | const TApplication = Symbol('IApplication'); 12 | 13 | const TService = Symbol('IService'); 14 | 15 | @Injectable() 16 | export class Application implements IApplication { 17 | constructor(@Inject(TService) private service: IService) {} 18 | 19 | run(): void { 20 | this.service.serve(); 21 | } 22 | } 23 | 24 | @Injectable() 25 | export class Service implements IService { 26 | serve(): void { 27 | // serves 28 | } 29 | } 30 | 31 | const container = new Container(); 32 | 33 | container.register([ 34 | { token: TApplication, useClass: Application }, 35 | { token: TService, useClass: Service } 36 | ]); 37 | 38 | const app: IApplication = container.resolve(TApplication); 39 | 40 | app.run(); -------------------------------------------------------------------------------- /examples/typescript/hierarchical-containers.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'container-ioc'; 2 | 3 | /* 4 | If a container can't find a value within itself, it will look it up in ascendant containers. 5 | There a 3 ways to set a parent for a container: 6 | */ 7 | 8 | const grandParentContainer = new Container(); 9 | 10 | // 1. 11 | const parentContainer = grandParentContainer.createChild(); 12 | 13 | // 2. 14 | const childContainer = new Container(); 15 | childContainer.setParent(parentContainer); 16 | 17 | // 3. 18 | const deepNestedContainer = new Container({ 19 | parent: childContainer 20 | }); 21 | 22 | grandParentContainer.register([ 23 | { token: 'IConfig', useValue: {}} 24 | ]); 25 | 26 | deepNestedContainer.resolve('IConfig'); -------------------------------------------------------------------------------- /examples/typescript/life-time-control.ts: -------------------------------------------------------------------------------- 1 | import { Container, LifeTime, Injectable } from 'container-ioc'; 2 | 3 | /* 4 | By default, containers resolve singletons when using useClass and useFactory. 5 | Default life time for all items in a container can be set by passing an option object to it's contructor with **defailtLifeTime** attribute. 6 | Possible values: LifeTime.PerRequest (resolves instances) and LifeTime.Persistent (resolves singletons); 7 | */ 8 | const container = new Container({ 9 | defaultLifeTime: LifeTime.PerRequest 10 | }); 11 | 12 | /* 13 | You can also specify life time individually for each item in a container by specifying lifeTime attribute. 14 | */ 15 | @Injectable() 16 | class ServiceA {} 17 | 18 | /* With classes */ 19 | container.register([ 20 | { 21 | token: 'ISerivceA', 22 | useClass: ServiceA, 23 | lifeTime: LifeTime.PerRequest 24 | } 25 | ]); 26 | 27 | /* With factories */ 28 | container.register([ 29 | { 30 | token: 'IServiceB', 31 | useFactory: () => { 32 | return { 33 | serve(): void { 34 | return; 35 | } 36 | }; 37 | }, 38 | lifeTime: LifeTime.Persistent 39 | } 40 | ]); 41 | 42 | const service1 = container.resolve('IServiceA'); 43 | const service2 = container.resolve('IServiceB'); -------------------------------------------------------------------------------- /examples/typescript/resolving-container.ts: -------------------------------------------------------------------------------- 1 | import { IContainer, Container, Injectable, Inject } from 'container-ioc'; 2 | 3 | const container = new Container(); 4 | 5 | @Injectable() 6 | class Builder { 7 | constructor(@Inject(Container) private container: IContainer) {} 8 | } 9 | 10 | container.register({ token: Builder, useClass: Builder }); 11 | 12 | const builder = container.resolve(Builder); -------------------------------------------------------------------------------- /examples/typescript/use-factory-with-injections.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable: no-unused-expression max-classes-per-file*/ 2 | 3 | import { Container, Injectable, Inject } from 'container-ioc'; 4 | 5 | interface IService { 6 | [key: string]: any; 7 | } 8 | 9 | interface IManager { 10 | [key: string]: any; 11 | } 12 | 13 | const TService = Symbol('IService'); 14 | 15 | const TManager = Symbol('IManager'); 16 | 17 | @Injectable() 18 | class App { 19 | constructor(@Inject(TService) private service: IService) {} 20 | } 21 | 22 | @Injectable() 23 | class Service implements IService {} 24 | 25 | @Injectable() 26 | class Manager implements IManager {} 27 | 28 | const container = new Container(); 29 | 30 | container.register([ 31 | { token: App, useClass: App }, 32 | { token: TManager, useClass: Manager }, 33 | { 34 | token: TService, 35 | useFactory: (manager: IManager) => { 36 | return manager; 37 | }, 38 | inject: [TManager] 39 | } 40 | ]); 41 | 42 | const app = container.resolve(App); -------------------------------------------------------------------------------- /examples/typescript/use-factory.ts: -------------------------------------------------------------------------------- 1 | import { Container, Injectable, Inject } from 'container-ioc'; 2 | 3 | /* tslint:disable: no-unused-expression max-classes-per-file*/ 4 | 5 | interface IService { 6 | [key: string]: any; 7 | } 8 | 9 | const TService = Symbol('IService'); 10 | 11 | @Injectable() 12 | class App { 13 | constructor(@Inject(TService) private service: IService) {} 14 | } 15 | 16 | class Service implements IService {} 17 | 18 | const container = new Container(); 19 | 20 | container.register([ 21 | { token: App, useClass: App }, 22 | { 23 | token: TService, 24 | useFactory: () => { 25 | return new Service(); 26 | } 27 | } 28 | ]); 29 | 30 | const app = container.resolve(App); -------------------------------------------------------------------------------- /examples/typescript/use-value.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'container-ioc'; 2 | 3 | const container = new Container(); 4 | 5 | container.register([ 6 | { token: 'IConfig', useValue: {}} 7 | ]); 8 | 9 | const app = container.resolve('IConfig'); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const ts = require('gulp-typescript'); 3 | const mocha = require('gulp-mocha'); 4 | const tslint = require('gulp-tslint'); 5 | 6 | const tsProjectDevelopment = ts.createProject('tsconfig.json'); 7 | const tsProjectDist = ts.createProject('tsconfig.json', { noImplicitAny: true }); 8 | 9 | gulp.task('dev', ['compile', 'test'], function() { 10 | gulp.watch(['src/**/*.ts', '!src/**/*.d.ts'], function() { 11 | gulp.run('compile', 'test'); 12 | }); 13 | }); 14 | 15 | gulp.task('compile', ['tslint'], function() { 16 | return gulp.src(['src/**/*.ts', '!src/**/*.d.ts']) 17 | .pipe(tsProjectDevelopment()) 18 | .pipe(gulp.dest('src/')); 19 | }); 20 | 21 | gulp.task('test', ['compile'], function() { 22 | return gulp.src('src/tests/**/*.spec.js', {read: false}) 23 | .pipe(mocha({ 24 | reporter: 'dot' 25 | })) 26 | }); 27 | 28 | gulp.task('compile-dist', ['tslint'], function() { 29 | return gulp.src(['src/**/*.ts', '!src/**/*.d.ts']) 30 | .pipe(tsProjectDevelopment()) 31 | .pipe(gulp.dest('dist/')); 32 | }); 33 | 34 | gulp.task('test-dist', ['compile-dist'], function () { 35 | return gulp.src('dist/tests/**/*.spec.js', {read: false}) 36 | .pipe(mocha({ 37 | reporter: 'dot' 38 | })) 39 | }); 40 | 41 | gulp.task("tslint", () => { 42 | return gulp.src(["src/**/*.ts", '!src/**/*.d.ts']) 43 | .pipe(tslint({ 44 | configuration: "./tslint.json" 45 | 46 | })) 47 | .pipe(tslint.report()); 48 | } 49 | ); 50 | 51 | gulp.task('dist', ['tslint', 'compile-dist', 'test-dist']); 52 | 53 | gulp.task("default", ["tslint", "compile", "test"]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "container-ioc", 3 | "version": "1.7.18", 4 | "description": "Dependency Injection and Inversion of Control (IoC) container", 5 | "author": "Alexander Kozlov", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/typesoft/container-ioc.git" 9 | }, 10 | "keywords": [ 11 | "ioc", 12 | "container", 13 | "dependency", 14 | "di", 15 | "dependency injection", 16 | "service locator", 17 | "inject", 18 | "injection", 19 | "inverstion of control", 20 | "injector", 21 | "injectable", 22 | "interface", 23 | "abstraction", 24 | "javascript", 25 | "js", 26 | "es6", 27 | "es7", 28 | "es8", 29 | "node", 30 | "nodejs", 31 | "node.js", 32 | "decorator", 33 | "metadata", 34 | "reflection", 35 | "browser" 36 | ], 37 | "license": "ISC", 38 | "main": "dist/lib/index.js", 39 | "types": "dist/lib/index.d.ts", 40 | "scripts": { 41 | "start": "node ./node_modules/gulp/bin/gulp.js", 42 | "dev": "node ./node_modules/gulp/bin/gulp.js dev", 43 | "compile": "node ./node_modules/gulp/bin/gulp.js compile", 44 | "test": "node ./node_modules/gulp/bin/gulp.js test", 45 | "dist": "node ./node_modules/gulp/bin/gulp.js dist" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/thohoh/container-ioc/issues" 49 | }, 50 | "homepage": "https://github.com/thohoh/container-ioc#readme", 51 | "devDependencies": { 52 | "@types/chai": "4.0.4", 53 | "@types/core-js": "0.9.43", 54 | "@types/mocha": "2.2.43", 55 | "chai": "4.1.2", 56 | "gulp": "3.9.1", 57 | "gulp-mocha": "4.3.1", 58 | "gulp-tslint": "8.1.2", 59 | "gulp-typescript": "3.2.2", 60 | "mocha": "3.5.3", 61 | "tslint": "5.7.0", 62 | "tslint-microsoft-contrib": "5.0.1", 63 | "typescript": "2.5.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/container.interface.ts: -------------------------------------------------------------------------------- 1 | import { IInjectionInstance, ProviderToken, RegistrationProvider, LifeTime } from './interfaces'; 2 | 3 | export interface IContainerOptions { 4 | parent?: IContainer; 5 | defaultLifeTime?: LifeTime; 6 | } 7 | 8 | export interface IContainer { 9 | register(provider: RegistrationProvider|RegistrationProvider[]): void; 10 | 11 | resolve(token: ProviderToken): IInjectionInstance; 12 | 13 | resolveInternal(token: ProviderToken, container: IContainer, traceMessage?: string): IInjectionInstance; 14 | 15 | /* @deprecated */ 16 | createScope(): IContainer; 17 | 18 | createChild(): IContainer; 19 | 20 | setParent(parent: IContainer): void; 21 | } -------------------------------------------------------------------------------- /src/lib/container.ts: -------------------------------------------------------------------------------- 1 | import { IConstructor, IInjectionInstance, IInjectionMd, IProvider, LifeTime, ProviderToken, RegistrationProvider } from './interfaces'; 2 | import { FactoryFunction, IFactory, IRegistryData, RegistryData } from './registry-data'; 3 | import { IContainer, IContainerOptions } from './container.interface'; 4 | import { ClassNotInjectableError, InvalidProviderProvidedError, NoProviderError } from './exceptions'; 5 | import { INJECTABLE_MD_KEY, INJECTIONS_MD_KEY } from './metadata/keys'; 6 | import { IMetadataAnnotator } from './metadata/metadata-annotator.interface'; 7 | import { AnnotatorProvider } from './metadata/index'; 8 | 9 | const MetadataAnnotator: IMetadataAnnotator = AnnotatorProvider.get(); 10 | 11 | export class Container implements IContainer { 12 | private static DEFAULT_LIFE_TIME = LifeTime.Persistent; 13 | 14 | private registry: Map = new Map(); 15 | private parent: IContainer; 16 | 17 | private defaultLifeTime: LifeTime = Container.DEFAULT_LIFE_TIME; 18 | 19 | constructor(options?: IContainerOptions) { 20 | if (options) { 21 | this.parent = options.parent; 22 | this.defaultLifeTime = options.defaultLifeTime || this.defaultLifeTime; 23 | } 24 | this.register({ token: Container, useValue: this }); 25 | } 26 | 27 | public register(provider: RegistrationProvider|RegistrationProvider[]): void { 28 | const normalizedProvider = this.normalizeProvider(provider); 29 | this.registerAll(normalizedProvider); 30 | } 31 | 32 | public resolve(token: ProviderToken): IInjectionInstance { 33 | return this.resolveInternal(token, this); 34 | } 35 | 36 | public createScope(): IContainer { 37 | return new Container({ parent: this }); 38 | } 39 | 40 | public createChild(): IContainer { 41 | return this.createScope(); 42 | } 43 | 44 | public setParent(parent: IContainer): void { 45 | this.parent = parent; 46 | } 47 | 48 | public resolveInternal(token: ProviderToken, container: IContainer, traceMessage?: string): IInjectionInstance { 49 | traceMessage = this.buildTraceMessage(token, traceMessage); 50 | 51 | const registryData = this.registry.get(token); 52 | 53 | if (!registryData) { 54 | if (!this.parent) { 55 | throw new NoProviderError(this.getTokenString(token), traceMessage ); 56 | } 57 | return this.parent.resolveInternal(token, container, traceMessage); 58 | } 59 | 60 | if (registryData.instance) { 61 | return registryData.instance; 62 | } 63 | 64 | const instance: IInjectionInstance = this.instantiateWithFactory(registryData.factory, container, traceMessage); 65 | 66 | if (registryData.lifeTime === LifeTime.Persistent) { 67 | registryData.instance = instance; 68 | this.registry.set(token, registryData); 69 | } 70 | 71 | return instance; 72 | } 73 | 74 | private registerAll(providers: IProvider[]): void { 75 | providers.forEach((p: IProvider) => this.registerOne(p)); 76 | } 77 | 78 | private registerOne(provider: IProvider): void { 79 | const registryData: IRegistryData = new RegistryData(); 80 | 81 | if (provider.useValue) { 82 | registryData.instance = provider.useValue; 83 | } else { 84 | const factoryValue = provider.useFactory || provider.useClass; 85 | const isClass: boolean = !!provider.useClass; 86 | 87 | registryData.factory = { 88 | value: factoryValue, 89 | isClass 90 | }; 91 | 92 | if (isClass) { 93 | registryData.factory.inject = this.retrieveInjectionsFromClass( registryData.factory.value); 94 | } else { 95 | registryData.factory.inject = this.convertTokensToInjectionMd( provider.inject); 96 | } 97 | 98 | registryData.lifeTime = provider.lifeTime || this.defaultLifeTime; 99 | } 100 | 101 | this.registry.set(provider.token, registryData); 102 | } 103 | 104 | private convertTokensToInjectionMd(tokens: ProviderToken[]): IInjectionMd[] { 105 | let injections: IInjectionMd[] = []; 106 | 107 | if (tokens) { 108 | injections = tokens.map((token: ProviderToken, index: number) => { 109 | return { 110 | token, 111 | parameterIndex: index 112 | }; 113 | }); 114 | } 115 | 116 | return injections; 117 | } 118 | 119 | private instantiateWithFactory(factory: IFactory, container: IContainer, traceMessage: string): IInjectionInstance { 120 | if (factory.isClass) { 121 | const injectable: boolean = this.isInjectable( factory.value); 122 | 123 | if (!injectable) { 124 | throw new ClassNotInjectableError(( factory.value).name); 125 | } 126 | } 127 | 128 | const injections = factory.inject; 129 | 130 | const resolvedInjections: any[] = injections.map(injection => container.resolveInternal(injection.token, container, traceMessage)); 131 | 132 | const args: any[] = []; 133 | injections.forEach((injection: IInjectionMd, index: number) => { 134 | args[injection.parameterIndex] = resolvedInjections[index]; 135 | }); 136 | 137 | if (factory.isClass) { 138 | return new ( factory.value)(...args); 139 | } else { 140 | return ( factory.value)(...args); 141 | } 142 | } 143 | 144 | private normalizeProvider(provider: RegistrationProvider|RegistrationProvider[]): IProvider[] { 145 | return Array.isArray(provider) 146 | ? provider.map(p => this.normalizeSingleProvider(p)) 147 | : [ this.normalizeSingleProvider(provider) ]; 148 | } 149 | 150 | private normalizeSingleProvider(provider: RegistrationProvider): IProvider { 151 | if (typeof provider === 'function') { 152 | provider = { token: provider, useClass: provider }; 153 | } else if (!(provider instanceof Object)) { 154 | throw new InvalidProviderProvidedError(provider); 155 | } 156 | return provider; 157 | } 158 | 159 | private buildTraceMessage(token: ProviderToken, message: string|undefined): string { 160 | const tokenStr: string = this.getTokenString(token); 161 | return message ? `${message} --> ${tokenStr}` : `Trace: ${tokenStr}`; 162 | } 163 | 164 | private getTokenString(token: ProviderToken): string { 165 | if (typeof token === 'function') { 166 | return ( token).name; 167 | } else if (typeof token === 'symbol') { 168 | return this.symbol2string(token); 169 | } else { 170 | return `${token}`; 171 | } 172 | } 173 | 174 | private symbol2string(symbol: symbol): string { 175 | const regExp = /\(([^)]+)\)/; 176 | const matches = regExp.exec(symbol.toString()); 177 | 178 | if (matches) { 179 | return matches[1]; 180 | } else { 181 | throw new Error(`Symbol name couldn't be retrieved, please check if it's not empty`); 182 | } 183 | } 184 | 185 | private isInjectable(cls: IConstructor): boolean { 186 | return !!(MetadataAnnotator.getMetadata(INJECTABLE_MD_KEY, cls)); 187 | } 188 | 189 | private retrieveInjectionsFromClass(cls: IConstructor): IInjectionMd[] { 190 | const injections: IInjectionMd[] = MetadataAnnotator.getMetadata(INJECTIONS_MD_KEY, cls) || []; 191 | return this.convertTokensToInjectionMd(injections); 192 | } 193 | } -------------------------------------------------------------------------------- /src/lib/decorators.ts: -------------------------------------------------------------------------------- 1 | import { IInjectionMd, ProviderToken } from './interfaces'; 2 | import { INJECTABLE_MD_KEY, INJECTIONS_MD_KEY } from './metadata/keys'; 3 | import { IMetadataAnnotator } from './metadata/metadata-annotator.interface'; 4 | import { AnnotatorProvider } from './metadata/index'; 5 | 6 | const MetadataAnnotator: IMetadataAnnotator = AnnotatorProvider.get(); 7 | 8 | export function Injectable(injections?: ProviderToken[]) { 9 | return (target: object) => { 10 | MetadataAnnotator.defineMetadata(INJECTABLE_MD_KEY, true, target); 11 | 12 | if (injections && Array.isArray(injections)) { 13 | const injectionsMd: IInjectionMd[] = MetadataAnnotator.getMetadata(INJECTIONS_MD_KEY, target) || []; 14 | injections.forEach(token => injectionsMd.push(token)); 15 | MetadataAnnotator.defineMetadata(INJECTIONS_MD_KEY, injectionsMd, target); 16 | } 17 | }; 18 | } 19 | 20 | export const Component = Injectable; 21 | 22 | export function Inject(token: any) { 23 | return (target: object, propertyKey: string | symbol, parameterIndex: number) => { 24 | const injections: IInjectionMd[] = MetadataAnnotator.getMetadata(INJECTIONS_MD_KEY, target) || []; 25 | injections[parameterIndex] = token; 26 | MetadataAnnotator.defineMetadata(INJECTIONS_MD_KEY, injections, target); 27 | }; 28 | } -------------------------------------------------------------------------------- /src/lib/exceptions.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable: max-classes-per-file */ 2 | 3 | export class InvalidProviderProvidedError extends Error { 4 | constructor(provider: any) { 5 | super(`${JSON.stringify(provider)} - is not a valid provider.`); 6 | } 7 | } 8 | 9 | export class ClassNotInjectableError extends Error { 10 | constructor(constructorName: string) { 11 | super(`Class ${constructorName} is not injectable. Check if it's decorated with @Injectable() decorator`); 12 | } 13 | } 14 | 15 | export class NoProviderError extends Error { 16 | constructor(token: string, traceMessage: string) { 17 | super(`No provider for ${token}. ${traceMessage}`); 18 | } 19 | } -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { Container } from './container'; 2 | export { IContainer } from './container.interface'; 3 | export { Inject, Injectable, Component } from './decorators'; 4 | export { InjectionToken } from './injection-token'; 5 | export { IMetadataAnnotator } from './metadata/metadata-annotator.interface'; 6 | export { AnnotatorProvider } from './metadata/index'; 7 | export { LifeTime } from './interfaces'; -------------------------------------------------------------------------------- /src/lib/injection-token.ts: -------------------------------------------------------------------------------- 1 | /* @deprecated */ 2 | export class InjectionToken { 3 | constructor(private description: string) {} 4 | 5 | public toString(): string { 6 | return this.description; 7 | } 8 | } -------------------------------------------------------------------------------- /src/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | export type ProviderToken = any; 2 | 3 | export type RegistrationProvider = IProvider | IConstructor; 4 | 5 | export enum LifeTime { 6 | Persistent, 7 | PerRequest 8 | } 9 | 10 | export interface IProvider { 11 | token: ProviderToken; 12 | lifeTime?: LifeTime; 13 | } 14 | 15 | export interface IProvider { 16 | useValue?: any; 17 | } 18 | 19 | export interface IProvider { 20 | useClass?: IConstructor; 21 | } 22 | 23 | export interface IProvider { 24 | useFactory?: any; 25 | inject?: ProviderToken[]; 26 | } 27 | 28 | export interface IInjectionMd { 29 | token: ProviderToken; 30 | parameterIndex: number; 31 | } 32 | 33 | export type IInjectionInstance = any; 34 | 35 | export interface IConstructor { 36 | new (...args: any[]): any; 37 | } -------------------------------------------------------------------------------- /src/lib/metadata/annotator-provider.ts: -------------------------------------------------------------------------------- 1 | import { IMetadataAnnotator } from './metadata-annotator.interface'; 2 | 3 | export class AnnotatorProvider { 4 | static annotator: IMetadataAnnotator; 5 | 6 | static set(annotator: IMetadataAnnotator): void { 7 | this.annotator = annotator; 8 | } 9 | 10 | static get(): IMetadataAnnotator { 11 | return this.annotator; 12 | } 13 | } -------------------------------------------------------------------------------- /src/lib/metadata/index.ts: -------------------------------------------------------------------------------- 1 | import { StaticMetadataAnnotator } from './static-metadata-annotator'; 2 | import { AnnotatorProvider } from './annotator-provider'; 3 | 4 | AnnotatorProvider.set(new StaticMetadataAnnotator()); 5 | 6 | export { AnnotatorProvider } from './annotator-provider'; -------------------------------------------------------------------------------- /src/lib/metadata/keys.ts: -------------------------------------------------------------------------------- 1 | export const INJECTIONS_MD_KEY: string = 'DI:injections'; 2 | export const INJECTABLE_MD_KEY: string = 'DI:injectable'; -------------------------------------------------------------------------------- /src/lib/metadata/metadata-annotator.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMetadataAnnotator { 2 | defineMetadata(metadataKey: string|symbol, value: T, target: any): void; 3 | getMetadata(metadataKey: string|symbol, target: any): T; 4 | } -------------------------------------------------------------------------------- /src/lib/metadata/static-metadata-annotator.ts: -------------------------------------------------------------------------------- 1 | import { IMetadataAnnotator } from './metadata-annotator.interface'; 2 | 3 | export class StaticMetadataAnnotator implements IMetadataAnnotator { 4 | public getMetadata(metadataKey: string|symbol, target: any): T { 5 | return target[metadataKey]; 6 | } 7 | 8 | public defineMetadata(metadataKey: string|symbol, value: T, target: any): void { 9 | target[metadataKey] = value; 10 | } 11 | } -------------------------------------------------------------------------------- /src/lib/registry-data.ts: -------------------------------------------------------------------------------- 1 | import { IConstructor, IInjectionInstance, IInjectionMd, LifeTime } from './interfaces'; 2 | 3 | export type FactoryFunction = (...args: any[]) => any; 4 | 5 | export interface IFactory { 6 | value: IConstructor | FactoryFunction; 7 | isClass: boolean; 8 | inject?: IInjectionMd[]; 9 | } 10 | 11 | export interface IRegistryData { 12 | instance: IInjectionInstance; 13 | factory: IFactory; 14 | lifeTime: LifeTime; 15 | } 16 | 17 | export class RegistryData { 18 | public instance: IInjectionInstance; 19 | public factory: IFactory; 20 | public lifeTime: LifeTime; 21 | } -------------------------------------------------------------------------------- /src/tests/container.spec.ts: -------------------------------------------------------------------------------- 1 | import { IContainer } from '../lib/container.interface'; 2 | import { Container } from '../lib/index'; 3 | 4 | import 'mocha'; 5 | import { expect } from 'chai'; 6 | import { InjectionToken } from '../lib/index'; 7 | import { Inject, Injectable } from '../lib/decorators'; 8 | import { LifeTime } from '../lib/interfaces'; 9 | 10 | /* tslint:disable: no-unused-expression max-classes-per-file*/ 11 | 12 | describe('Container', () => { 13 | 14 | let container: IContainer; 15 | 16 | beforeEach(() => { 17 | container = new Container(); 18 | }); 19 | 20 | describe('resolve()', () => { 21 | it('should resolve an instance when registered with a class Literal', () => { 22 | @Injectable() 23 | class TestClass {} 24 | container.register(TestClass); 25 | 26 | const instance = container.resolve(TestClass); 27 | expect(instance).to.be.ok; 28 | expect(instance instanceof TestClass).to.be.true; 29 | }); 30 | 31 | it('should resolve an instance when registered "useClass" attribute', () => { 32 | @Injectable() 33 | class TestClass {} 34 | const testToken = 'ITestClass'; 35 | 36 | container.register({ token: testToken, useClass: TestClass }); 37 | const instance = container.resolve(testToken); 38 | expect(instance).to.be.ok; 39 | expect(instance instanceof TestClass).to.be.true; 40 | }); 41 | 42 | it('should resolve an instance when registered with "useValue" attribute', () => { 43 | const value = {}; 44 | container.register({ token: 'Token', useValue: value }); 45 | const instance = container.resolve('Token'); 46 | expect(instance).to.be.ok; 47 | expect(instance === value).to.be.true; 48 | }); 49 | 50 | it('should resolve an instance if registered with array of providers', () => { 51 | @Injectable() 52 | class TestClass {} 53 | const testToken = 'ITestClass'; 54 | 55 | container.register([{ token: testToken, useClass: TestClass }]); 56 | const instance = container.resolve(testToken); 57 | expect(instance).to.be.ok; 58 | expect(instance instanceof TestClass).to.be.true; 59 | }); 60 | 61 | it('should resolve a value if registered and resolved with a token which is a string literal', () => { 62 | @Injectable() 63 | class TestClass {} 64 | const testToken = 'ITestClass'; 65 | 66 | container.register({ token: testToken, useClass: TestClass }); 67 | const instance = container.resolve(testToken); 68 | expect(instance).to.be.ok; 69 | expect(instance instanceof TestClass).to.be.true; 70 | }); 71 | 72 | it('should resolve a value when registered with a token which is an Object literal', () => { 73 | @Injectable() 74 | class TestClass {} 75 | container.register({ token: TestClass, useClass: TestClass }); 76 | const instance = container.resolve(TestClass); 77 | expect(instance).to.be.ok; 78 | expect(instance instanceof TestClass).to.be.true; 79 | }); 80 | 81 | it(`should resolve an instance found in ascendant containers if wasn't found in current container`, () => { 82 | @Injectable() 83 | class TestClass {} 84 | 85 | container.register(TestClass); 86 | const childContainer = container.createScope(); 87 | const grandChildContainer = childContainer.createScope(); 88 | 89 | const instance = grandChildContainer.resolve(TestClass); 90 | expect(instance).to.be.ok; 91 | expect(instance instanceof TestClass).to.be.true; 92 | }); 93 | 94 | it('should resolve value when registered with "useFactory"', () => { 95 | container.register({ 96 | token: 'V', 97 | useFactory: () => { 98 | return 'result'; 99 | } 100 | }); 101 | 102 | const inst = container.resolve('V'); 103 | expect(inst).to.be.equal('result'); 104 | }); 105 | 106 | it('should resolve a value if registered with "useFactory" + "inject" attributes', () => { 107 | @Injectable() 108 | class Foo { 109 | bar = 'works'; 110 | } 111 | container.register({ token: 'IFoo', useClass: Foo} ); 112 | 113 | container.register({ 114 | token: 'V', 115 | useFactory: (foo: any) => { 116 | return foo.bar; 117 | }, 118 | inject: ['IFoo'] 119 | }); 120 | 121 | const inst = container.resolve('V'); 122 | expect(inst).to.be.equal('works'); 123 | }); 124 | 125 | it('should resolve value if registered with InjectionToken', () => { 126 | interface IFactory { 127 | create(): any; 128 | } 129 | 130 | @Injectable() 131 | class ConcreteFactory implements IFactory { 132 | create(): any { 133 | return; 134 | } 135 | } 136 | 137 | const TFactory = new InjectionToken('IFactory'); 138 | 139 | container.register({ token: TFactory, useClass: ConcreteFactory } ); 140 | 141 | const concreteFactory = container.resolve(TFactory); 142 | 143 | expect(concreteFactory instanceof ConcreteFactory).to.be.true; 144 | }); 145 | 146 | describe('LifeTime', () => { 147 | it('should resolve a singleton by default if LifeTime was not specified with useClass', () => { 148 | @Injectable() 149 | class A {} 150 | 151 | container.register({ token: A, useClass: A }); 152 | 153 | const instance1 = container.resolve(A); 154 | const instance2 = container.resolve(A); 155 | 156 | expect(instance1).to.be.equal(instance2); 157 | }); 158 | 159 | it('should resolve a singleton by default if LifeTime was not specified with useFactory', () => { 160 | class A {} 161 | 162 | container.register([ 163 | { 164 | token: A, 165 | useFactory: () => new A() 166 | } 167 | ]); 168 | 169 | const instance1 = container.resolve(A); 170 | const instance2 = container.resolve(A); 171 | 172 | expect(instance1).to.be.equal(instance2); 173 | }); 174 | 175 | it('should resolve an instance with useFactory if LifeTime was set to LifeTime.PerRequest', () => { 176 | class A {} 177 | 178 | container.register([ 179 | { 180 | token: A, 181 | useFactory: () => new A(), 182 | lifeTime: LifeTime.PerRequest 183 | } 184 | ]); 185 | 186 | const instance1 = container.resolve(A); 187 | const instance2 = container.resolve(A); 188 | 189 | expect(instance1).not.to.be.equal(instance2); 190 | }); 191 | 192 | it('should resolve an instances if LifeTime was set to LifeTime.PerRequest', () => { 193 | @Injectable() 194 | class A {} 195 | 196 | container.register({ token: A, useClass: A, lifeTime: LifeTime.PerRequest }); 197 | 198 | const instance1 = container.resolve(A); 199 | const instance2 = container.resolve(A); 200 | 201 | expect(instance1).not.to.be.equal(instance2); 202 | }); 203 | 204 | it('should resolve different instances if LifeTime was set to LifeTime.PerRequest in case of nested dependencies', () => { 205 | @Injectable() 206 | class A { 207 | constructor(@Inject('IB') private b: any) {} 208 | } 209 | 210 | @Injectable() 211 | class B { 212 | 213 | } 214 | 215 | container.register({ token: A, useClass: A, lifeTime: LifeTime.PerRequest }); 216 | container.register({ token: 'IB', useClass: B}); 217 | 218 | const instance1: any = container.resolve(A); 219 | const instance2: any = container.resolve(A); 220 | 221 | expect(instance1).not.to.be.equal(instance2); 222 | expect(instance1.b).to.be.equal(instance2.b); 223 | }); 224 | 225 | it('should set default lifeTime via options', () => { 226 | const cont = new Container({ 227 | defaultLifeTime: LifeTime.PerRequest 228 | }); 229 | 230 | @Injectable() 231 | class A {} 232 | 233 | cont.register({ token: A, useClass: A }); 234 | let instance1 = cont.resolve(A); 235 | let instance2 = cont.resolve(A); 236 | 237 | expect(instance1).not.to.be.equal(instance2); 238 | 239 | cont.register({ token: 'IB', useFactory: () => ({}) }); 240 | instance1 = cont.resolve('IB'); 241 | instance2 = cont.resolve('IB'); 242 | 243 | expect(instance1).not.to.be.equal(instance2); 244 | }); 245 | }); 246 | 247 | describe('Errors', () => { 248 | it('should throw an error if provided token is not registered', () => { 249 | @Injectable() 250 | class TestClass {} 251 | container.register([{ token: 'Token', useClass: TestClass }]); 252 | 253 | const throwableFunc = () => container.resolve('NotRegisteredToken'); 254 | expect(throwableFunc).to.throw(); 255 | }); 256 | 257 | it('should correctly print token in error messages: Token is a class literal', () => { 258 | @Injectable() 259 | class B {} 260 | 261 | @Injectable() 262 | class A { 263 | constructor(@Inject(B) private b: any) {} 264 | } 265 | 266 | container.register({ token: 'IA', useClass: A }); 267 | 268 | const throwableFunc = () => container.resolve('IA'); 269 | expect(throwableFunc).to.throw('No provider for B. Trace: IA --> B'); 270 | }); 271 | 272 | it('should correctly print token in error messages: Token is an InjectionToken', () => { 273 | @Injectable() 274 | class A {} 275 | 276 | container.register({ token: 'IA', useClass: A }); 277 | 278 | interface IB { 279 | [key: string]: any; 280 | } 281 | 282 | const TB = new InjectionToken('IB'); 283 | 284 | const throwableFunc = () => container.resolve(TB); 285 | expect(throwableFunc).to.throw('No provider for IB. Trace: IB'); 286 | }); 287 | 288 | it('should correctly print token in error messages: Token is a string', () => { 289 | @Injectable() 290 | class A {} 291 | 292 | container.register({ token: 'IA', useClass: A }); 293 | 294 | const throwableFunc = () => container.resolve('str'); 295 | expect(throwableFunc).to.throw('No provider for str. Trace: str'); 296 | }); 297 | 298 | it('should throw an error with a specific message if the 1st token in the line is not registered', () => { 299 | @Injectable() 300 | class A { 301 | constructor(@Inject('d') private a: any) {} 302 | } 303 | container.register([{ token: 'IA', useClass: A }]); 304 | 305 | const throwableFunc = () => container.resolve('Fish'); 306 | expect(throwableFunc).to.throw('No provider for Fish. Trace: Fish'); 307 | }); 308 | 309 | it('should throw an error with a specific message if the 2nd token in the line is not registered', () => { 310 | @Injectable() 311 | class A { 312 | constructor(@Inject('IB') private b: any) {} 313 | } 314 | container.register([{ token: 'IA', useClass: A }]); 315 | 316 | const throwableFunc = () => container.resolve('IA'); 317 | expect(throwableFunc).to.throw('No provider for IB. Trace: IA --> IB'); 318 | }); 319 | 320 | it('should throw an error with a specific message if the 3nd token in the line is not registered', () => { 321 | @Injectable() 322 | class A { 323 | constructor(@Inject('IB') private b: any) {} 324 | } 325 | 326 | @Injectable() 327 | class B { 328 | constructor(@Inject('IC') private c: any) {} 329 | } 330 | container.register({ token: 'IA', useClass: A }); 331 | container.register({ token: 'IB', useClass: B }); 332 | 333 | const throwableFunc = () => container.resolve('IA'); 334 | expect(throwableFunc).to.throw('No provider for IC. Trace: IA --> IB --> IC'); 335 | }); 336 | 337 | it('should throw an error if registered class isnt marked with Injectable() decorator', () => { 338 | class A { 339 | } 340 | container.register({ token: 'IA', useClass: A }); 341 | 342 | const throwableFunc = () => container.resolve('IA'); 343 | expect(throwableFunc).to.throw(`Class A is not injectable. Check if it's decorated with @Injectable() decorator`); 344 | }); 345 | 346 | it('should print Symbol types properly in error messages', () => { 347 | const TB = Symbol('IB'); 348 | 349 | @Injectable() 350 | class A { 351 | constructor(@Inject(TB) private b: any) {} 352 | } 353 | container.register({ token: 'IA', useClass: A }); 354 | 355 | const throwableFunc = () => container.resolve('IA'); 356 | 357 | expect(throwableFunc).to.throw('No provider for IB. Trace: IA --> IB'); 358 | }); 359 | 360 | it('should resolve container instance when injected into class Literal', () => { 361 | @Injectable() 362 | class TestClass { 363 | constructor(@Inject(Container) public a: IContainer) {} 364 | } 365 | 366 | container.register({ token: TestClass, useClass: TestClass }); 367 | const actual = container.resolve(TestClass); 368 | 369 | expect(actual).to.be.ok; 370 | expect(actual.a).to.be.ok; 371 | expect(actual.a).to.equal(container); 372 | }); 373 | }); 374 | 375 | describe('Hierarchial', () => { 376 | it(`should start looking up dependencies by starting from the container it's first entity was resolved from`, () => { 377 | 378 | @Injectable() 379 | class C {} 380 | 381 | @Injectable() 382 | class AscendantC {} 383 | 384 | @Injectable() 385 | class B { 386 | constructor(@Inject('IC') private c: C) {} 387 | } 388 | 389 | @Injectable() 390 | class A { 391 | constructor(@Inject('IB') private b: B) {} 392 | } 393 | 394 | const childContainer = container.createChild(); 395 | 396 | container.register([ 397 | { token: 'IB', useClass: B }, 398 | { token: 'IC', useClass: AscendantC } 399 | ]); 400 | 401 | childContainer.register([ 402 | { token: 'IA', useClass: A }, 403 | { token: 'IC', useClass: C } 404 | ]); 405 | 406 | const a = childContainer.resolve('IA'); 407 | 408 | expect(a.b.c).to.be.instanceof(C); 409 | }); 410 | 411 | it(`should start looking up dependencies by starting from the container it's first entity was resolved from`, () => { 412 | @Injectable() 413 | class D {} 414 | 415 | @Injectable() 416 | class C { 417 | constructor(@Inject('ID') public d: D) {} 418 | } 419 | 420 | @Injectable() 421 | class AscendantC {} 422 | 423 | @Injectable() 424 | class B { 425 | constructor(@Inject('IC') public c: C) {} 426 | } 427 | 428 | @Injectable() 429 | class A { 430 | constructor(@Inject('IB') public b: B) {} 431 | } 432 | 433 | const childContainer = container.createChild(); 434 | 435 | container.register([ 436 | { token: 'IB', useClass: B }, 437 | { token: 'IC', useClass: AscendantC }, 438 | { token: 'ID', useClass: Function } 439 | ]); 440 | 441 | childContainer.register([ 442 | { token: 'IA', useClass: A }, 443 | { token: 'IC', useClass: C }, 444 | { token: 'ID', useClass: D } 445 | ]); 446 | 447 | const a = childContainer.resolve('IA'); 448 | 449 | expect(a.b.c).to.be.instanceof(C); 450 | expect(a.b.c.d).to.be.instanceof(D); 451 | }); 452 | }); 453 | }); 454 | 455 | describe('Constructor', () => { 456 | it('should set defaultLife through option object', () => { 457 | const cont = new Container({ defaultLifeTime: LifeTime.PerRequest }); 458 | 459 | cont.register({ token: 'A', useFactory: () => ({})}); 460 | 461 | const instance1 = cont.resolve('A'); 462 | const instance2 = cont.resolve('A'); 463 | 464 | expect(instance1).not.to.be.equal(instance2); 465 | }); 466 | 467 | it('should register itself for injection', () => { 468 | const actual = container.resolve(Container); 469 | 470 | expect(actual).to.be.ok; 471 | expect(actual).to.equal(container); 472 | }); 473 | }); 474 | 475 | describe('createChild()', () => { 476 | it('should create child container', () => { 477 | const childContainer: any = container.createChild(); 478 | expect(childContainer).to.be.ok; 479 | expect(childContainer.parent).to.equal(container); 480 | }); 481 | }); 482 | 483 | describe('setParent()', () => { 484 | it('should set parent for a container', () => { 485 | const parentContainer = new Container(); 486 | const childContainer = new Container(); 487 | childContainer.setParent(parentContainer); 488 | 489 | parentContainer.register({ token: 'A', useValue: 'string' }); 490 | 491 | const value = childContainer.resolve('A'); 492 | 493 | expect(value).to.be.equal('string'); 494 | }); 495 | }); 496 | }); -------------------------------------------------------------------------------- /src/tests/decorators.spec.ts: -------------------------------------------------------------------------------- 1 | import { IContainer } from '../lib/container.interface'; 2 | import { Container } from '../lib/index'; 3 | 4 | import 'mocha'; 5 | import { expect } from 'chai'; 6 | import { Inject, Injectable } from '../lib/decorators'; 7 | 8 | /* tslint:disable: no-unused-expression max-classes-per-file*/ 9 | 10 | describe('Decorators', () => { 11 | let container: IContainer; 12 | 13 | beforeEach(() => { 14 | container = new Container(); 15 | }); 16 | 17 | describe('@Inject()', () => { 18 | it('should inject dependency in to the constructor of a resolved class', () => { 19 | 20 | @Injectable() 21 | class A {} 22 | const providerA = { token: 'IA', useClass: A }; 23 | 24 | @Injectable() 25 | class B { 26 | constructor(@Inject('IA') private a: A) {} 27 | } 28 | const providerB = { token: 'IB', useClass: B }; 29 | 30 | container.register([providerA, providerB]); 31 | 32 | const instanceB = container.resolve('IB'); 33 | 34 | expect(instanceB.a).to.be.instanceOf(A); 35 | }); 36 | 37 | it('should throw an error when wrong token was provided not registered token', () => { 38 | @Injectable() 39 | class A {} 40 | const providerA = { token: 'IA', useClass: A }; 41 | 42 | @Injectable() 43 | class B { 44 | constructor(@Inject('IC') private a: A) {} 45 | } 46 | const providerB = { token: 'IB', useClass: B }; 47 | 48 | container.register([providerA, providerB]); 49 | 50 | const throwableFunc = () => container.resolve('IB'); 51 | expect(throwableFunc).to.throw(); 52 | }); 53 | 54 | it('should throw an error when wrong token was provided not valid token', () => { 55 | @Injectable() 56 | class A {} 57 | const providerA = { token: 'IA', useClass: A }; 58 | 59 | @Injectable() 60 | class B { 61 | constructor(@Inject('IC') private a: A) {} 62 | } 63 | const providerB = { token: 'IB', useClass: B }; 64 | 65 | container.register([providerA, providerB]); 66 | 67 | const throwableFunc = () => container.resolve(1); 68 | expect(throwableFunc).to.throw(); 69 | }); 70 | 71 | it('should resolve an instance with all the dependencies specified in Injectable decorator', () => { 72 | @Injectable() 73 | class A {} 74 | 75 | @Injectable(['IA']) 76 | class B { 77 | a: A; 78 | constructor(a: A) { 79 | this.a = a; 80 | } 81 | } 82 | 83 | container.register([ 84 | { token: 'IA', useClass: A }, 85 | { token: 'IB', useClass: B } 86 | ]); 87 | 88 | const instance = container.resolve('IB'); 89 | 90 | expect(instance.a).to.be.instanceOf(A); 91 | }); 92 | 93 | it('should chain with other decorators', () => { 94 | 95 | function RandomDecorator() { 96 | return (target: any) => { 97 | target.works = true; 98 | }; 99 | } 100 | 101 | @Injectable() 102 | @RandomDecorator() 103 | class A { 104 | static works: boolean; 105 | } 106 | 107 | container.register([ 108 | { token: 'IA', useClass: A } 109 | ]); 110 | 111 | expect(A.works).to.be.true; 112 | }); 113 | 114 | it('should chain with other decorators', () => { 115 | 116 | function RandomDecorator() { 117 | return (target: any) => { 118 | target.works = true; 119 | }; 120 | } 121 | 122 | @RandomDecorator() 123 | @Injectable() 124 | class A { 125 | static works: boolean; 126 | } 127 | 128 | container.register([ 129 | { token: 'IA', useClass: A } 130 | ]); 131 | 132 | expect(A.works).to.be.true; 133 | }); 134 | 135 | it('should inject parameters in the right order', () => { 136 | @Injectable() 137 | class A {} 138 | 139 | @Injectable() 140 | class B {} 141 | 142 | @Injectable() 143 | class C { 144 | constructor( 145 | @Inject(A) public a: A, 146 | @Inject(B) public b: B 147 | ) { 148 | 149 | } 150 | } 151 | 152 | const providers = [ 153 | { token: A, useClass: A }, 154 | { token: B, useClass: B }, 155 | { token: C, useClass: C } 156 | ]; 157 | 158 | container.register(providers); 159 | 160 | const c = container.resolve(C); 161 | 162 | expect(c.a).to.be.instanceof(A); 163 | expect(c.b).to.be.instanceof(B); 164 | }); 165 | }); 166 | 167 | describe('@Injectable()', () => { 168 | it('container should instantiate a class marked with @Injectable() decorator', () => { 169 | @Injectable() 170 | class A {} 171 | 172 | container.register(A); 173 | 174 | const instance = container.resolve(A); 175 | 176 | expect(instance).to.be.instanceOf(A); 177 | }); 178 | 179 | it('should throw an error when trying instantiating a class not marked with @Injectable', () => { 180 | class A { 181 | } 182 | 183 | container.register(A); 184 | expect(() => container.resolve(A)).to.throw(); 185 | }); 186 | 187 | it('should inject parameters in the right order when using alternative way of declaring dependencies', () => { 188 | @Injectable() 189 | class A {} 190 | 191 | @Injectable() 192 | class B {} 193 | 194 | @Injectable([A, B]) 195 | class C { 196 | constructor( 197 | public a: A, 198 | public b: B 199 | ) { 200 | 201 | } 202 | } 203 | 204 | const providers = [ 205 | { token: A, useClass: A }, 206 | { token: B, useClass: B }, 207 | { token: C, useClass: C } 208 | ]; 209 | 210 | container.register(providers); 211 | 212 | const c = container.resolve(C); 213 | 214 | expect(c.a).to.be.instanceof(A); 215 | expect(c.b).to.be.instanceof(B); 216 | }); 217 | }); 218 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "strict": true, 8 | 9 | "noImplicitAny": true, 10 | "removeComments": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "baseUrl": ".", 14 | "typeRoots": ["node_modules/@types"], 15 | "lib": ["es2016", "DOM"], 16 | "paths": { 17 | "container-ioc": ["src/lib/index.d.ts"] 18 | } 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "examples" 23 | ] 24 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "./node_modules/tslint-microsoft-contrib" 4 | ], 5 | "extends": "tslint:recommended", 6 | "rules": { 7 | "quotemark": [ 8 | true, 9 | "single", 10 | "avoid-escape" 11 | ], 12 | "eofline": false, 13 | "max-line-length": [ 14 | true, 15 | 180 16 | ], 17 | "ordered-imports": false, 18 | "trailing-comma": [ 19 | true, 20 | { 21 | "multiline": "never", 22 | "singleline": "never" 23 | } 24 | ], 25 | "member-access": false, 26 | "arrow-parens": false, 27 | "object-literal-sort-keys": false, 28 | "no-trailing-whitespace": false, 29 | "no-angle-bracket-type-assertion": false 30 | } 31 | } --------------------------------------------------------------------------------