├── .circleci └── config.yml ├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── eslint.config.mjs ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── example │ ├── example.test.ts │ ├── example.ts │ ├── service │ │ ├── container.ts │ │ ├── my-other-service.test.ts │ │ ├── my-other-service.ts │ │ ├── my-service.test.ts │ │ ├── my-service.ts │ │ └── types.ts │ └── test │ │ ├── my-other-service-mock.ts │ │ └── my-service-mock.ts ├── index.test.ts ├── index.ts └── ioc │ ├── bind.ts │ ├── container.plugin.test.ts │ ├── container.test.ts │ ├── container.ts │ ├── createDecorator.test.ts │ ├── createDecorator.ts │ ├── createResolve.arguments.test.ts │ ├── createResolve.test.ts │ ├── createResolve.ts │ ├── createWire.test.ts │ ├── createWire.ts │ ├── define.ts │ ├── options.ts │ ├── pluginOptions.ts │ ├── tags.ts │ ├── token.ts │ ├── types.ts │ └── utils.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@4.7 5 | 6 | jobs: 7 | code-style: 8 | docker: 9 | - image: cimg/node:18.7 10 | steps: 11 | - checkout 12 | - node/install-packages: 13 | pkg-manager: npm 14 | - run: 15 | name: Run Linter 16 | command: npm run lint 17 | - run: 18 | name: Run Prettier 19 | command: npm run prettier 20 | build: 21 | docker: 22 | - image: cimg/node:18.7 23 | steps: 24 | - checkout 25 | - node/install-packages: 26 | pkg-manager: npm 27 | - run: 28 | name: Run Build 29 | command: npm run build 30 | 31 | workflows: 32 | checks: 33 | jobs: 34 | - node/test: 35 | version: '18.7' 36 | pkg-manager: npm 37 | - code-style 38 | build: 39 | jobs: 40 | - build 41 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.json] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Environment (please complete the following information):** 20 | - Transpiler [e.g. Babel, Webpack, Typescript, Rollup] 21 | - Browser/Node [e.g. chrome, safari, firefox, node 11] 22 | - Library Version [e.g. 1.0.0-alpha.1] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem or blocks you from using it? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Is your feature request something you would just nice to see implemented?** 14 | A clear and concise description of what the benefits for you and others would be. 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context about the feature request here. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /coverage 4 | *.tgz 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Hauke Broer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @owja/ioc 2 | 3 | [![npm version](https://img.shields.io/npm/v/@owja/ioc/latest)](https://badge.fury.io/js/%40owja%2Fioc) 4 | [![npm version](https://img.shields.io/npm/v/@owja/ioc/next)](https://badge.fury.io/js/%40owja%2Fioc) 5 | [![size](https://img.badgesize.io/https://unpkg.com/@owja/ioc/dist/ioc.js.svg?compression=brotli&label=size&v=1)](https://unpkg.com/@owja/ioc/dist/ioc.js) 6 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/owja/ioc/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/owja/ioc/tree/master) 7 | ![npm](https://img.shields.io/npm/dm/@owja/ioc) 8 | 9 | This library implements dependency injection for javascript and typescript. 10 | 11 | ## Features 12 | 13 | * Similar syntax to InversifyJS 14 | * Can be used without decorators 15 | * Less Features but **straight forward** 16 | * Can bind dependencies as **classes**, **factories** and **static values** and provide dependencie arguments or parameters if needed 17 | * Supports binding in **singleton scope** 18 | * **Cached** - Resolves only once in each dependent class by default 19 | * **Cache can switched off** directly at the inject decorator 20 | * Made with **unit testing** in mind 21 | * Supports dependency **rebinding** and container **snapshots** and **restores** 22 | * **Lightweight** - Below 1kb brotli/gzip compressed 23 | * Does **NOT** need reflect-metadata which size is around 50 kb 24 | * 100% written in **Typescript** 25 | 26 | ## Install 27 | 28 | ```bash 29 | npm install --save-dev @owja/ioc 30 | ``` 31 | 32 | Latest preview/dev version (alpha or beta) 33 | 34 | ```bash 35 | npm install --save-dev @owja/ioc@next 36 | ``` 37 | 38 | ## The Container API 39 | 40 | ### Creating a container 41 | 42 | The container is the place where all dependencies get bound to. It is possible to have 43 | multiple container in our project in parallel. 44 | 45 | ```ts 46 | import {Container} from "@owja/ioc"; 47 | const container = new Container(); 48 | ``` 49 | 50 | ### Binding 51 | 52 | #### Binding a class 53 | 54 | This is the default way to bind a dependency. The class will get instantiated when the 55 | dependency gets resolved. You will be able to pass down it's dependencie arguments once you resolve it. 56 | 57 | 58 | ```ts 59 | container.bind(symbol).to(Service); 60 | ``` 61 | 62 | #### Binding a class in singleton scope 63 | 64 | This will create only one instance of `Service` 65 | 66 | ```ts 67 | container.bind(symbol).to(Service).inSingletonScope(); 68 | ``` 69 | 70 | #### Binding a factory 71 | 72 | Factories are functions which will get called when the dependency gets resolved 73 | 74 | ```ts 75 | container.bind(symbol).toFactory(() => new Service()); 76 | container.bind(symbol).toFactory(() => "just a string"); 77 | container.bind(symbol).toFactory((a: string) => `I need a string parameter: ${a}`); 78 | ``` 79 | 80 | A factory can configured for singleton scope too. This way will only executed once. 81 | 82 | ```ts 83 | container.bind(symbol).toFactory(() => new Service()).inSingletonScope(); 84 | ``` 85 | 86 | #### Binding a value 87 | 88 | This is always like singleton scope, but it should be avoid to instantiate 89 | dependencies here. If they are circular dependencies, they will fail. 90 | 91 | ```ts 92 | container.bind(symbol).toValue(new Service()); // Bad, should be avoid 93 | container.bind(symbol).toValue("just a string"); 94 | container.bind<() => string>(symbol).toValue(() => "i am a function"); 95 | ``` 96 | 97 | ### Rebinding 98 | 99 | This is the way how we can rebind a dependency while **unit tests**. We should not need to 100 | rebind in production code. 101 | 102 | ```ts 103 | container.rebind(symbol).toValue(new ServiceMock()); 104 | ``` 105 | 106 | ### Removing 107 | 108 | Normally this function is not used in production code. This will remove the 109 | dependency from the container. 110 | 111 | ```ts 112 | container.remove(symbol); 113 | ``` 114 | 115 | ### Getting a dependency 116 | 117 | Getting dependencies without `@inject` decorators trough `container.get()` is only meant for **unit tests**. 118 | This is also the internal way how the `@inject` decorator and the functions `wire()` and `resolve()` are getting the 119 | dependency. 120 | 121 | ```ts 122 | container.get(symbol); 123 | ``` 124 | 125 | To get a dependency without `@inject` decorator in production code use `wire()` or `resolve()`. Using `container.get()` 126 | directly to getting dependencies can result in infinite loops with circular dependencies when called inside of 127 | constructors. In addition `container.get()` does not respect the cache. 128 | 129 | > **Important Note:** You should avoid accessing the dependencies from any constructor. With circular dependencies 130 | > this can result in a infinite loop. 131 | 132 | ### Snapshot & Restore 133 | 134 | This creates a snapshot of the bound dependencies. After this we can rebind dependencies 135 | and can restore it back to its old state after we made some **unit tests**. 136 | 137 | ```ts 138 | container.snapshot(); 139 | ``` 140 | ```ts 141 | container.restore(); 142 | ``` 143 | 144 | ## The `inject` Decorator 145 | 146 | To use the decorator you have to set `experimentalDecorators` to `true` 147 | in your `tsconfig.json`. 148 | 149 | First we have to create a `inject` decorator for each container: 150 | 151 | ```ts 152 | import {createDecorator} from "@owja/ioc"; 153 | export const inject = createDecorator(container); 154 | ``` 155 | 156 | Then we can use the decorator to inject the dependency. 157 | 158 | ```ts 159 | class Example { 160 | @inject(symbol/*, [tags], ...dependencie arguments*/) 161 | readonly service!: Interface; 162 | 163 | method() { 164 | this.service.doSomething(); 165 | } 166 | } 167 | ``` 168 | 169 | ## The `wire()` Function 170 | 171 | If we do not want to use decorators, we can use the wire function. It does the same like the `inject` 172 | decorator and we have to create the function first like we do with `inject`. 173 | 174 | ```ts 175 | import {createWire} from "@owja/ioc"; 176 | export const wire = createWire(container); 177 | ``` 178 | 179 | Then we can wire up the dependent to the dependency. 180 | 181 | ```ts 182 | class Example { 183 | readonly service!: Interface; 184 | 185 | constructor() { 186 | wire(this, "service", symbol/*, [tags], ...dependencie arguments*/); 187 | } 188 | 189 | method() { 190 | this.service.doSomething(); 191 | } 192 | } 193 | ``` 194 | 195 | > Notice: With `wire()` the property, in this case `service`, has to be public. 196 | 197 | ## The `resolve()` Function 198 | 199 | A second way to resolve a dependency without decorators is to use `resolve()`. 200 | To use `resolve()` we have to create the function first. 201 | 202 | ```ts 203 | import {createResolve} from "@owja/ioc"; 204 | export const resolve = createResolve(container); 205 | ``` 206 | 207 | Then we can resolve the dependency in classes and even functions. 208 | 209 | ```ts 210 | class Example { 211 | private readonly service = resolve(symbol); 212 | 213 | method() { 214 | this.service(/*...dependencie arguments*/).doSomething(); 215 | } 216 | } 217 | ``` 218 | 219 | ```ts 220 | function Example() { 221 | const service = resolve(symbol); 222 | service(/*...dependencie arguments*/).doSomething(); 223 | } 224 | ``` 225 | 226 | > Notice: We access the dependency trough a function. 227 | > The dependency is not assigned directly to the property/constant. 228 | > If we want direct access we can use `container.get()` but we should avoid 229 | > using `get()` inside of classes because we then loose the lazy dependency 230 | > resolving/injection behavior and caching. 231 | 232 | ## The `symbol` 233 | 234 | Symbols are used to identify our dependencies. A good practice is to keep them in one place. 235 | 236 | ```ts 237 | export const TYPE = { 238 | "Service" = Symbol("Service"), 239 | // [...] 240 | } 241 | ``` 242 | 243 | Symbols can be defined with `Symbol.for()` too. This way they are not unique. 244 | Remember `Symbol('foo') === Symbol('foo')` is `false` but 245 | `Symbol.for('foo') === Symbol.for('foo')` is `true` 246 | 247 | ```ts 248 | export const TYPE = { 249 | "Service" = Symbol.for("Service"), 250 | // [...] 251 | } 252 | ``` 253 | 254 | > Since 1.0.0-beta.3 we use the symbol itself for indexing the dependencies. 255 | > Prior to this version we indexed the dependencies by the string of the symbol. 256 | 257 | ## :new: Type-Safe Token (new in 2.0) 258 | 259 | With version 2 we added the possibility to use a type-safe way to identify our dependencies. This is done with tokens: 260 | 261 | ```ts 262 | export TYPE = { 263 | "Service" = token("Service"), 264 | // [...] 265 | } 266 | ``` 267 | 268 | In this case the type `MyServiceInterface` is inherited when using `container.get(TYPE.Service)`, `resolve(TYPE.Service)` 269 | and `wire(this, "service", TYPE.Service)`and does not need to be explicitly added. In case of the decorator `@inject(TYPE.Service)` it needs to be added 270 | but it throws a type error if the types don't match: 271 | 272 | ```ts 273 | class Example { 274 | @inject(TYPE.Service/*, [tags], ...dependencie arguments*/) // throws a type error because WrongInterface is not compatible with MyServiceInterface 275 | readonly service!: WrongInterface; 276 | } 277 | ``` 278 | 279 | Correkt: 280 | 281 | ```ts 282 | class Example { 283 | @inject(TYPE.Service/*, [tags], ...dependencie arguments*/) 284 | readonly service!: MyServiceInterface; 285 | } 286 | ``` 287 | 288 | ## :new: Plugins (new in 2.0) 289 | 290 | Plugins are a way to hook into the dependency resolving process and execute code which can 291 | access the dependency and also the dependent object. 292 | 293 | A plugin can add directly to a dependency or to the container. 294 | 295 | ```ts 296 | container.bind(symbol).to(MyService).withPlugin(plugin); 297 | ``` 298 | 299 | ```ts 300 | container.addPlugin(plugin); 301 | ``` 302 | 303 | The plugin is a simple function which has access to the dependency, the target (the instance which requires the dependency), 304 | the arguments which are passed, the token or symbol which represents the dependency and the container. 305 | 306 | ```ts 307 | type Plugin = ( 308 | dependency: Dependency, 309 | target: unknown, 310 | args: symbol[], 311 | token: MaybeToken, 312 | container: Container, 313 | ) => void; 314 | ``` 315 | 316 | ### Plugin Example 317 | 318 | The following code is a plugin which links a preact view component to a service by calling forceUpdate every time the 319 | service executes the listener: 320 | 321 | ```ts 322 | import {Plugin} from "@owja/ioc"; 323 | import {Component} from "preact"; 324 | 325 | export const SUBSCRIBE = Symbol(); 326 | 327 | export const serviceListenerPlugin: Plugin = (service, component, args) => { 328 | if (args.indexOf(SUBSCRIBE) === -1 || !component) return; 329 | if (!isComponent(component)) return; 330 | 331 | const unsubscribe = service.listen(() => component.forceUpdate()); 332 | const unmount = component.componentWillUnmount; 333 | 334 | component.componentWillUnmount = () => { 335 | unsubscribe(); 336 | unmount?.(); 337 | }; 338 | }; 339 | 340 | function isComponent(target: unknown) : target is Component { 341 | return !!target && typeof target === "object" && "forceUpdate" in target; 342 | } 343 | 344 | interface Listenable { 345 | listen(listener: () => void): () => void; 346 | } 347 | ``` 348 | > Note: this will fail on runtime if `service` does not implement the `Listenable` interface because there is no type checking done 349 | 350 | This plugin is added to the dependency directly: 351 | 352 | ```ts 353 | const TYPE = { 354 | TranslationService: token("translation-service"), 355 | }; 356 | 357 | container 358 | .bind(TYPE.TranslationService) 359 | .toFactory(translationFactory) 360 | .inSingletonScope() 361 | .withPlugin(serviceListenerPlugin); 362 | ``` 363 | 364 | In a component it is then executed when the dependency is resolved: 365 | 366 | ```ts 367 | class Index extends Component { 368 | @inject(TYPE.TranslationService, [SUBSCRIBE]/*, ...dependencie arguments*/) 369 | readonly service!: TranslatorInterface; 370 | 371 | render() { 372 | return ( 373 |
{this.service.t("greeting")}
374 | ); 375 | } 376 | } 377 | ``` 378 | 379 | This works also with `wire` and `resolve`: 380 | 381 | ```ts 382 | class Index extends Component { 383 | readonly service!: TranslatorInterface; 384 | 385 | constructor() { 386 | super(); 387 | wire(this, "service", TYPE.TranslationService, [SUBSCRIBE]/*, ...dependencie arguments*/); 388 | } 389 | 390 | [...] 391 | } 392 | 393 | class Index extends Component { 394 | readonly service = resolve(TYPE.TranslationService, [SUBSCRIBE]/*, ...dependencie arguments*/); 395 | 396 | [...] 397 | } 398 | 399 | ``` 400 | 401 | ### Prevent Plugins from Execution 402 | 403 | In case you add a plugin it is executed every time the dependency is resolved. If you want to prevent this you can 404 | add the `NOPLUGINS` tag to the arguments: 405 | 406 | ```ts 407 | import {NOPLUGINS} from "@owja/ioc"; 408 | 409 | class Example { 410 | @inject(TYPE.MyService, [NOPLUGINS]/*, ...dependencie arguments*/) 411 | readonly service!: MyServiceInterface; 412 | } 413 | ``` 414 | 415 | ## Getting Started 416 | 417 | #### Step 1 - Installing the OWJA! IoC library 418 | 419 | ```bash 420 | npm install --save-dev @owja/ioc 421 | ``` 422 | 423 | #### Step 2 - Creating symbols for our dependencies 424 | 425 | Now we create the folder ***services*** and add the new file ***services/types.ts***: 426 | ```ts 427 | export const TYPE = { 428 | MyService: Symbol("MyService"), 429 | MyOtherService: Symbol("MyOtherService"), 430 | }; 431 | ``` 432 | 433 | #### Step 3 - Example services 434 | 435 | Next we create out example services. 436 | 437 | File ***services/my-service.ts*** 438 | ```ts 439 | export interface MyServiceInterface { 440 | hello: string; 441 | } 442 | 443 | export class MyService implements MyServiceInterface{ 444 | hello = "world"; 445 | } 446 | ``` 447 | 448 | File ***services/my-other-service.ts*** 449 | ```ts 450 | export interface MyOtherServiceInterface { 451 | random: number; 452 | } 453 | 454 | export class MyOtherService implements MyOtherServiceInterface { 455 | random = Math.random(); 456 | } 457 | ``` 458 | 459 | #### Step 4 - Creating a container 460 | 461 | Next we need a container to bind our dependencies to. Let's create the file ***services/container.ts*** 462 | 463 | ```ts 464 | import {Container, createDecorator} from "@owja/ioc"; 465 | 466 | import {TYPE} from "./types"; 467 | 468 | import {MyServiceInterface, MyService} from "./service/my-service"; 469 | import {MyOtherServiceInterface, MyOtherService} from "./service/my-other-service"; 470 | 471 | const container = new Container(); 472 | const inject = createDecorator(container); 473 | 474 | container.bind(TYPE.MyService).to(MyService); 475 | container.bind(TYPE.MyOtherService).to(MyOtherService); 476 | 477 | export {container, TYPE, inject}; 478 | ``` 479 | 480 | #### Step 5 - Injecting dependencies 481 | 482 | Lets create a ***example.ts*** file in our source root: 483 | 484 | ```ts 485 | import {TYPE, inject} from "./service/container"; 486 | import {MyServiceInterface} from "./service/my-service"; 487 | import {MyOtherServiceInterface} from "./service/my-other-service"; 488 | 489 | class Example { 490 | @inject(TYPE.MyService/*, [tags], ...dependencie arguments*/) 491 | readonly myService!: MyServiceInterface; 492 | 493 | @inject(TYPE.MyOtherService/*, [tags], ...dependencie arguments*/) 494 | readonly myOtherService!: MyOtherServiceInterface; 495 | } 496 | 497 | const example = new Example(); 498 | 499 | console.log(example.myService); 500 | console.log(example.myOtherService); 501 | console.log(example.myOtherService); 502 | ``` 503 | 504 | If we run this example we should see the content of our example services. 505 | 506 | The dependencies (services) will injected on the first call. This means if you rebind the service after 507 | accessing the properties of the Example class, it will not resolve a new service. If you want a new 508 | service each time you call `example.myService` you have to add the `NOCACHE` tag: 509 | 510 | ```ts 511 | // [...] 512 | import {NOCACHE} from "@owja/ioc"; 513 | 514 | class Example { 515 | // [...] 516 | 517 | @inject(TYPE.MyOtherSerice, NOCACHE/*, ...dependencie arguments*/) 518 | readonly myOtherService!: MyOtherServiceInterface; 519 | } 520 | 521 | // [...] 522 | ``` 523 | 524 | In this case the last two `console.log()` outputs should show different numbers. 525 | 526 | ## Unit testing with IoC 527 | 528 | For unit testing we first create our mocks 529 | 530 | ***test/my-service-mock.ts*** 531 | ```ts 532 | import {MyServiceInterface} from "../service/my-service"; 533 | 534 | export class MyServiceMock implements MyServiceInterface { 535 | hello = "test"; 536 | } 537 | ``` 538 | 539 | ***test/my-other-service-mock.ts*** 540 | ```ts 541 | import {MyOtherServiceInterface} from "../service/my-other-service"; 542 | 543 | export class MyOtherServiceMock implements MyOtherServiceInterface { 544 | random = 9; 545 | } 546 | ``` 547 | 548 | Within the tests we can snapshot and restore a container. 549 | We are able to make multiple snapshots in a row too. 550 | 551 | File ***example.test.ts*** 552 | ```ts 553 | import {container, TYPE} from "./service/container"; 554 | import {MyServiceInterface} from "./service/my-service"; 555 | import {MyOtherServiceInterface} from "./service/my-other-service"; 556 | 557 | import {MyServiceMock} from "./test/my-service-mock"; 558 | import {MyOtherServiceMock} from "./test/my-other-service-mock"; 559 | 560 | import {Example} from "./example"; 561 | 562 | describe("Example", () => { 563 | 564 | let example: Example; 565 | beforeEach(() => { 566 | container.snapshot(); 567 | container.rebind(TYPE.MyService).to(MyServiceMock); 568 | container.rebind(TYPE.MyOtherService).to(MyOtherServiceMock); 569 | 570 | example = new Example(); 571 | }); 572 | 573 | afterEach(() => { 574 | container.restore(); 575 | }); 576 | 577 | test("should return \"test\"", () => { 578 | expect(example.myService.hello).toBe("test"); 579 | }); 580 | 581 | test("should return \"9\"", () => { 582 | expect(example.myOtherService.random).toBe(9); 583 | }); 584 | 585 | }); 586 | ``` 587 | 588 | ## Development 589 | 590 | Current state of development can be seen in our 591 | [Github Projects](https://github.com/owja/ioc/projects). 592 | 593 | ## Inspiration 594 | 595 | This library is highly inspired by [InversifyJS](https://github.com/inversify/InversifyJS) 596 | but has other goals: 597 | 598 | 1. Make the library very lightweight (less than one kilobyte) 599 | 2. Implementing less features to make the API more straight forward 600 | 3. Always lazy inject the dependencies 601 | 4. No meta-reflect required 602 | 603 | ## License 604 | 605 | **MIT** 606 | 607 | Copyright © 2019-2022 The OWJA! Team 608 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, { 5 | ignores: ["dist/**", "*.js", "*.mjs"], 6 | }); 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | roots: ["/src"], 5 | collectCoverageFrom: ["/src/**/*.ts", "!/src/example/**"], 6 | coverageDirectory: "./coverage/", 7 | collectCoverage: true, 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@owja/ioc", 3 | "version": "2.0.0-alpha.8", 4 | "description": "dependency injection for javascript", 5 | "main": "dist/ioc.js", 6 | "module": "dist/ioc.mjs", 7 | "source": "src/index.ts", 8 | "types": "dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "module": "./dist/ioc.mjs", 13 | "import": "./dist/ioc.mjs", 14 | "require": "./dist/ioc.js" 15 | }, 16 | "./package.json": "./package.json" 17 | }, 18 | "keywords": [ 19 | "typescript", 20 | "dependency injection", 21 | "dependency inversion", 22 | "inversion of control", 23 | "ioc", 24 | "di" 25 | ], 26 | "scripts": { 27 | "prepack": "npm run build", 28 | "prebuild": "npm run clean", 29 | "build": "microbundle --format es,cjs", 30 | "clean": "shx rm -rf dist && shx rm -f owja-ioc-*.tgz && shx rm -rf coverage", 31 | "test": "jest", 32 | "lint": "eslint", 33 | "lint:fix": "eslint --fix", 34 | "prettier": "prettier src/**/*.ts *.js *.mjs --check", 35 | "prettier:fix": "prettier src/**/*.ts *.js *.mjs --write" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/owja/ioc.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/owja/ioc/issues" 43 | }, 44 | "homepage": "https://github.com/owja/ioc", 45 | "author": "Hauke Broer ", 46 | "license": "MIT", 47 | "devDependencies": { 48 | "@owja/browserslist-config": "^1.0.1", 49 | "@owja/prettier-config": "^1.0.2", 50 | "@owja/typescript-config": "^1.0.2", 51 | "@types/jest": "^29.5.12", 52 | "@typescript-eslint/eslint-plugin": "^8.4.0", 53 | "@typescript-eslint/parser": "^8.4.0", 54 | "eslint": "^9.9.1", 55 | "eslint-plugin-jest": "^28.8.2", 56 | "jest": "^29.7.0", 57 | "microbundle": "^0.15.0", 58 | "shx": "^0.3.4", 59 | "ts-jest": "^29.2.5", 60 | "typescript-eslint": "^8.4.0" 61 | }, 62 | "mangle": { 63 | "regex": "^_" 64 | }, 65 | "files": [ 66 | "/dist", 67 | "!/dist/example", 68 | "/src", 69 | "!/src/example", 70 | "!/**/*.test.ts", 71 | "!/**/*.test.d.ts" 72 | ], 73 | "browserslist": [ 74 | "extends @owja/browserslist-config" 75 | ], 76 | "prettier": "@owja/prettier-config" 77 | } 78 | -------------------------------------------------------------------------------- /src/example/example.test.ts: -------------------------------------------------------------------------------- 1 | import type {MyOtherServiceInterface} from "./service/my-other-service"; 2 | import type {MyServiceInterface} from "./service/my-service"; 3 | import {container, TYPE} from "./service/container"; 4 | 5 | import {MyServiceMock} from "./test/my-service-mock"; 6 | import {MyOtherServiceMock} from "./test/my-other-service-mock"; 7 | 8 | import {Example} from "./example"; 9 | 10 | describe("Example", () => { 11 | let example: Example; 12 | beforeEach(() => { 13 | container.snapshot(); 14 | container.rebind(TYPE.MyService).to(MyServiceMock); 15 | container.rebind(TYPE.MyOtherService).to(MyOtherServiceMock); 16 | 17 | example = new Example(); 18 | }); 19 | 20 | afterEach(() => { 21 | container.restore(); 22 | }); 23 | 24 | test('should return "test"', () => { 25 | expect(example.myService.hello).toBe("test"); 26 | }); 27 | 28 | test('should return "1" on cached service', () => { 29 | expect(example.myOtherService.random).toBe(1); 30 | expect(example.myOtherService.random).toBe(1); // twice because this is cached 31 | }); 32 | 33 | test("should increase number on service with disabled cache", () => { 34 | const number1 = example.myUncachedOtherService.random; 35 | const number2 = example.myUncachedOtherService.random; 36 | 37 | expect(number2).toBe(number1 + 1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/example/example.ts: -------------------------------------------------------------------------------- 1 | import {TYPE, inject} from "./service/container"; 2 | import {MyServiceInterface} from "./service/my-service"; 3 | import {MyOtherServiceInterface} from "./service/my-other-service"; 4 | import {NOCACHE} from "../"; 5 | 6 | export class Example { 7 | @inject(TYPE.MyService) 8 | readonly myService!: MyServiceInterface; 9 | 10 | @inject(TYPE.MyOtherService) 11 | readonly myOtherService!: MyOtherServiceInterface; 12 | 13 | @inject(TYPE.MyOtherService, [NOCACHE]) 14 | readonly myUncachedOtherService!: MyOtherServiceInterface; 15 | } 16 | -------------------------------------------------------------------------------- /src/example/service/container.ts: -------------------------------------------------------------------------------- 1 | import {Container, createDecorator} from "../../"; 2 | 3 | import {TYPE} from "./types"; 4 | 5 | import {MyServiceInterface, MyService} from "./my-service"; 6 | import {MyOtherServiceInterface, MyOtherService} from "./my-other-service"; 7 | 8 | const container = new Container(); 9 | const inject = createDecorator(container); 10 | 11 | container.bind(TYPE.MyService).to(MyService); 12 | container.bind(TYPE.MyOtherService).to(MyOtherService); 13 | 14 | export {container, TYPE, inject}; 15 | -------------------------------------------------------------------------------- /src/example/service/my-other-service.test.ts: -------------------------------------------------------------------------------- 1 | import {MyOtherService} from "./my-other-service"; 2 | 3 | describe("Example MyOtherService", () => { 4 | test("should return number between 0 and 1", () => { 5 | expect(new MyOtherService().random).toBeGreaterThanOrEqual(0); 6 | expect(new MyOtherService().random).toBeLessThanOrEqual(1); 7 | }); 8 | 9 | test("should not be the same all time (but could be)", () => { 10 | const numbers = [ 11 | new MyOtherService().random, 12 | new MyOtherService().random, 13 | new MyOtherService().random, 14 | new MyOtherService().random, 15 | new MyOtherService().random, 16 | new MyOtherService().random, 17 | new MyOtherService().random, 18 | new MyOtherService().random, 19 | new MyOtherService().random, 20 | ]; 21 | 22 | const checkNumber = new MyOtherService().random; 23 | let hasDifferences = false; 24 | for (const num of numbers) { 25 | if (checkNumber !== num) { 26 | hasDifferences = true; 27 | break; 28 | } 29 | } 30 | 31 | expect(hasDifferences).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/example/service/my-other-service.ts: -------------------------------------------------------------------------------- 1 | export interface MyOtherServiceInterface { 2 | random: number; 3 | } 4 | 5 | export class MyOtherService implements MyOtherServiceInterface { 6 | random = Math.random(); 7 | } 8 | -------------------------------------------------------------------------------- /src/example/service/my-service.test.ts: -------------------------------------------------------------------------------- 1 | import {MyService} from "./my-service"; 2 | 3 | describe("Example MyService", () => { 4 | test('should return "world"', () => { 5 | expect(new MyService().hello).toBe("world"); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/example/service/my-service.ts: -------------------------------------------------------------------------------- 1 | export interface MyServiceInterface { 2 | hello: string; 3 | } 4 | 5 | export class MyService implements MyServiceInterface { 6 | hello = "world"; 7 | } 8 | -------------------------------------------------------------------------------- /src/example/service/types.ts: -------------------------------------------------------------------------------- 1 | import type {MyOtherService} from "./my-other-service"; 2 | import {token} from "../../ioc/token"; 3 | 4 | export const TYPE = { 5 | MyService: Symbol("MyService"), 6 | MyOtherService: token("MyOtherService"), 7 | }; 8 | -------------------------------------------------------------------------------- /src/example/test/my-other-service-mock.ts: -------------------------------------------------------------------------------- 1 | import type {MyOtherServiceInterface} from "../service/my-other-service"; 2 | 3 | let number = 0; 4 | 5 | export class MyOtherServiceMock implements MyOtherServiceInterface { 6 | random = ++number; 7 | } 8 | -------------------------------------------------------------------------------- /src/example/test/my-service-mock.ts: -------------------------------------------------------------------------------- 1 | import type {MyServiceInterface} from "../service/my-service"; 2 | 3 | export class MyServiceMock implements MyServiceInterface { 4 | hello = "test"; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import {Container, token, createDecorator, createResolve, createWire, NOCACHE, NOPLUGINS} from "./"; 2 | import {Container as ContainerOriginal} from "./ioc/container"; 3 | import {token as tokenOriginal} from "./ioc/token"; 4 | import {createDecorator as createDecoratorOriginal} from "./ioc/createDecorator"; 5 | import {createWire as createWireOriginal} from "./ioc/createWire"; 6 | import {createResolve as createResolveOriginal} from "./ioc/createResolve"; 7 | import {NOCACHE as NOCACHEOriginal, NOPLUGINS as NOPLUGINSOriginal} from "./ioc/tags"; 8 | 9 | describe("Module", () => { 10 | test('should export "Container" class', () => { 11 | expect(Container).toBe(ContainerOriginal); 12 | }); 13 | 14 | test('should export "token" function', () => { 15 | expect(token).toBe(tokenOriginal); 16 | }); 17 | 18 | test('should export "createDecorator" function', () => { 19 | expect(createDecorator).toBe(createDecoratorOriginal); 20 | }); 21 | 22 | test('should export "createResolve" function', () => { 23 | expect(createResolve).toBe(createResolveOriginal); 24 | }); 25 | 26 | test('should export "createWire" function', () => { 27 | expect(createWire).toBe(createWireOriginal); 28 | }); 29 | 30 | test('should export "NOCACHE" symbol/tag', () => { 31 | expect(NOCACHE).toBe(NOCACHEOriginal); 32 | }); 33 | 34 | test('should export "NOPLUGINS" symbol/tag', () => { 35 | expect(NOPLUGINS).toBe(NOPLUGINSOriginal); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {Container} from "./ioc/container"; 2 | export type {Plugin, Factory, RegItem, NewAble, Token, Value, MaybeToken} from "./ioc/types"; 3 | export {createDecorator} from "./ioc/createDecorator"; 4 | export {createWire} from "./ioc/createWire"; 5 | export {createResolve} from "./ioc/createResolve"; 6 | export {NOCACHE, NOPLUGINS} from "./ioc/tags"; 7 | export {token} from "./ioc/token"; 8 | -------------------------------------------------------------------------------- /src/ioc/bind.ts: -------------------------------------------------------------------------------- 1 | import type {RegItem, NewAble, Factory, Value} from "./types"; 2 | import {Options} from "./options"; 3 | import {PluginOptions} from "./pluginOptions"; 4 | 5 | export class Bind> { 6 | constructor(private _regItem: RegItem) {} 7 | 8 | to>(object: Obj): Options { 9 | this._regItem.factory = (...args: Args): Dep => new object(...args); 10 | return new Options(this._regItem); 11 | } 12 | 13 | toFactory(factory: Factory): Options { 14 | this._regItem.factory = factory; 15 | return new Options(this._regItem); 16 | } 17 | 18 | toValue(value: Value): PluginOptions { 19 | if (typeof value === "undefined") throw "cannot bind a value of type undefined"; 20 | this._regItem.value = value; 21 | return new PluginOptions(this._regItem); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ioc/container.plugin.test.ts: -------------------------------------------------------------------------------- 1 | import {Container} from "./container"; 2 | import {NOCACHE, NOPLUGINS} from "./tags"; 3 | import {token} from "./token"; 4 | import type {Plugin} from "./types"; 5 | 6 | class TestClass { 7 | name = "test class"; 8 | } 9 | 10 | describe("Container 2.0", () => { 11 | let container: Container; 12 | 13 | let plugin: jest.Mock; 14 | 15 | const A = token("test A"); 16 | const B = token("test B"); 17 | 18 | beforeEach(() => { 19 | container = new Container(); 20 | plugin = jest.fn(); 21 | }); 22 | 23 | describe("with a plugin bound to depoendency A", () => { 24 | beforeEach(() => { 25 | container.bind(A).to(TestClass).withPlugin(plugin); 26 | container.bind(B).to(TestClass); 27 | }); 28 | 29 | test("should return property name of the test class A and B", () => { 30 | expect(container.get(A).name).toBe("test class"); 31 | expect(container.get(B).name).toBe("test class"); 32 | }); 33 | 34 | test("should execute the plugin when if dependency A is requested", () => { 35 | container.get(A); 36 | expect(plugin).toHaveBeenCalledTimes(1); 37 | }); 38 | 39 | test("should NOT execute the plugin when if dependency A is requested with NOPLUGINS symbol", () => { 40 | container.get(A, [NOPLUGINS]); 41 | expect(plugin).not.toBeCalled(); 42 | }); 43 | 44 | test("should not execute the plugin if dependency B is requested", () => { 45 | container.get(B); 46 | expect(plugin).not.toBeCalled(); 47 | }); 48 | 49 | describe("and if the plugin changes the name property of the depencency", () => { 50 | beforeEach(() => { 51 | const mock: Plugin = (cls: TestClass) => { 52 | cls.name = "this is changed"; 53 | }; 54 | plugin.mockImplementation(mock); 55 | }); 56 | 57 | test("should return changed property value of dependency A", () => { 58 | expect(container.get(A).name).toBe("this is changed"); 59 | }); 60 | 61 | test("should return default property value of dependency B", () => { 62 | expect(container.get(B).name).toBe("test class"); 63 | }); 64 | }); 65 | }); 66 | 67 | describe("with a plugin bound to container", () => { 68 | beforeEach(() => { 69 | container.bind(A).to(TestClass); 70 | container.bind(B).to(TestClass).inSingletonScope(); 71 | container.addPlugin(plugin); 72 | }); 73 | 74 | test("should execute the plugin on any depencency requested", () => { 75 | container.get(A); 76 | expect(plugin).toHaveBeenCalledTimes(1); 77 | container.get(B); 78 | expect(plugin).toHaveBeenCalledTimes(2); 79 | }); 80 | 81 | test("should execute plugin every time the dependency is requested, even in singleton scope", () => { 82 | container.get(A); 83 | container.get(A); 84 | container.get(A); 85 | expect(plugin).toHaveBeenCalledTimes(3); 86 | container.get(B); 87 | container.get(B); 88 | container.get(B); 89 | expect(plugin).toHaveBeenCalledTimes(6); 90 | }); 91 | }); 92 | 93 | describe("the plugin should be able to", () => { 94 | let resolved: TestClass; 95 | const fakeTarget = class {}; 96 | const fakeTags = [NOCACHE]; 97 | 98 | beforeEach(() => { 99 | container.bind(A).to(TestClass); 100 | container.addPlugin(plugin); 101 | resolved = container.get(A, fakeTags, fakeTarget); 102 | }); 103 | 104 | test("access the dependency (1st argument)", () => { 105 | expect(plugin.mock.calls[0][0]).toBe(resolved); 106 | }); 107 | 108 | test("access the arguments (5th argument)", () => { 109 | expect(plugin.mock.calls[0][2].indexOf(NOCACHE)).not.toBe(-1); 110 | }); 111 | 112 | test("access the target (2nd argument)", () => { 113 | expect(plugin.mock.calls[0][1]).toBe(fakeTarget); 114 | }); 115 | 116 | test("access the token (3th argument)", () => { 117 | expect(plugin.mock.calls[0][3]).toBe(A); 118 | }); 119 | 120 | test("access the container (4th argument)", () => { 121 | expect(plugin.mock.calls[0][4]).toBe(container); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/ioc/container.test.ts: -------------------------------------------------------------------------------- 1 | import {Container} from "./container"; 2 | import {token} from "./token"; 3 | import {NewAble} from "./types"; 4 | 5 | describe("Container using symbols", () => { 6 | let container: Container; 7 | 8 | const exampleSymbol = Symbol.for("example"); 9 | const stringToken = token("exampleStr"); 10 | 11 | beforeEach(() => { 12 | container = new Container(); 13 | }); 14 | 15 | test("can bind a factory", () => { 16 | let count = 1; 17 | container.bind(exampleSymbol).toFactory(() => `hello world ${count++}`); 18 | 19 | expect(container.get(exampleSymbol)).toBe("hello world 1"); 20 | expect(container.get(exampleSymbol)).toBe("hello world 2"); 21 | expect(container.get(exampleSymbol)).toBe("hello world 3"); 22 | 23 | container.bind(stringToken).toFactory(function () { 24 | return `hello world ${count++}`; 25 | }); 26 | 27 | expect(container.get(stringToken)).toBe("hello world 4"); 28 | expect(container.get(stringToken)).toBe("hello world 5"); 29 | expect(container.get(stringToken)).toBe("hello world 6"); 30 | }); 31 | 32 | test("can bind a factory in singleton scope", () => { 33 | let count = 1; 34 | container 35 | .bind(exampleSymbol) 36 | .toFactory(() => `hello world ${count++}`) 37 | .inSingletonScope(); 38 | 39 | expect(container.get(exampleSymbol)).toBe("hello world 1"); 40 | expect(container.get(exampleSymbol)).toBe("hello world 1"); 41 | expect(container.get(exampleSymbol)).toBe("hello world 1"); 42 | 43 | count = 1; 44 | container 45 | .bind(stringToken) 46 | .toFactory(() => `hello world ${count++}`) 47 | .inSingletonScope(); 48 | 49 | expect(container.get(stringToken)).toBe("hello world 1"); 50 | expect(container.get(stringToken)).toBe("hello world 1"); 51 | expect(container.get(stringToken)).toBe("hello world 1"); 52 | }); 53 | 54 | test("should use cached data in singleton scope", () => { 55 | const spy = jest.fn(); 56 | spy.mockReturnValue("test"); 57 | 58 | container.bind(exampleSymbol).toFactory(spy).inSingletonScope(); 59 | 60 | container.get(exampleSymbol); 61 | container.get(exampleSymbol); 62 | container.get(exampleSymbol); 63 | 64 | expect(spy).toHaveBeenCalledTimes(1); 65 | expect(container.get(exampleSymbol)).toBe("test"); 66 | }); 67 | 68 | test("can bind a constructable", () => { 69 | interface IExampleConstructable { 70 | hello(): string; 71 | } 72 | container.bind(exampleSymbol).to( 73 | class implements IExampleConstructable { 74 | count = 1; 75 | hello() { 76 | return `world ${this.count++}`; 77 | } 78 | }, 79 | ); 80 | 81 | expect(container.get(exampleSymbol).hello()).toBe("world 1"); 82 | expect(container.get(exampleSymbol).hello()).toBe("world 1"); 83 | expect(container.get(exampleSymbol).hello()).toBe("world 1"); 84 | 85 | const exampleToken = token("example"); 86 | 87 | container.bind(exampleToken).to( 88 | class implements IExampleConstructable { 89 | count = 1; 90 | hello() { 91 | return `world ${this.count++}`; 92 | } 93 | }, 94 | ); 95 | 96 | expect(container.get(exampleToken).hello()).toBe("world 1"); 97 | expect(container.get(exampleToken).hello()).toBe("world 1"); 98 | expect(container.get(exampleToken).hello()).toBe("world 1"); 99 | }); 100 | 101 | test("can bind a constructable in singleton scope", () => { 102 | interface IExampleConstructable { 103 | hello(): string; 104 | } 105 | container 106 | .bind(exampleSymbol) 107 | .to( 108 | class implements IExampleConstructable { 109 | count = 1; 110 | hello() { 111 | return `world ${this.count++}`; 112 | } 113 | }, 114 | ) 115 | .inSingletonScope(); 116 | 117 | expect(container.get(exampleSymbol).hello()).toBe("world 1"); 118 | expect(container.get(exampleSymbol).hello()).toBe("world 2"); 119 | expect(container.get(exampleSymbol).hello()).toBe("world 3"); 120 | }); 121 | 122 | test("can bind a constant value", () => { 123 | container.bind(exampleSymbol).toValue("constant world"); 124 | expect(container.get(exampleSymbol)).toBe("constant world"); 125 | 126 | container.bind(stringToken).toValue("constant world"); 127 | expect(container.get(stringToken)).toBe("constant world"); 128 | }); 129 | 130 | test("can bind a constant value of zero", () => { 131 | container.bind(exampleSymbol).toValue(0); 132 | expect(container.get(exampleSymbol)).toBe(0); 133 | 134 | const numToken = token("number"); 135 | container.bind(numToken).toValue(0); 136 | expect(container.get(numToken)).toBe(0); 137 | }); 138 | 139 | test("can bind a negative constant value", () => { 140 | container.bind(exampleSymbol).toValue(-10); 141 | expect(container.get(exampleSymbol)).toBe(-10); 142 | }); 143 | 144 | test("can bind a function value", () => { 145 | container.bind<(str: string) => string>(exampleSymbol).toValue((str: string) => "hello " + str); 146 | expect(container.get<(str: string) => string>(exampleSymbol)("world")).toBe("hello world"); 147 | }); 148 | 149 | test("can bind a constructable value", () => { 150 | class HelloWorld {} 151 | container.bind>(exampleSymbol).toValue(HelloWorld); 152 | expect(new (container.get>(exampleSymbol))()).toBeInstanceOf(HelloWorld); 153 | }); 154 | 155 | test("can bind a constant value of empty string", () => { 156 | container.bind(exampleSymbol).toValue(""); 157 | expect(container.get(exampleSymbol)).toBe(""); 158 | }); 159 | 160 | test("can not bind a constant value of undefined", () => { 161 | expect(() => container.bind(exampleSymbol).toValue(undefined)).toThrow( 162 | "cannot bind a value of type undefined", 163 | ); 164 | }); 165 | 166 | test("can not bind to a symbol more than once", () => { 167 | container.bind(exampleSymbol); 168 | expect(() => container.bind(exampleSymbol)).toThrow("object can only bound once: Symbol(example)"); 169 | }); 170 | 171 | test("can not bind to a token more than once", () => { 172 | container.bind(stringToken); 173 | expect(() => container.bind(stringToken)).toThrow("object can only bound once: Token(Symbol(exampleStr))"); 174 | }); 175 | 176 | test("can not get unbound dependency", () => { 177 | container.bind(exampleSymbol); 178 | expect(() => container.get(exampleSymbol)).toThrow("nothing bound to Symbol(example)"); 179 | }); 180 | 181 | test("can rebind to a symbol", () => { 182 | container.bind(exampleSymbol).toValue("hello world"); 183 | expect(container.get(exampleSymbol)).toBe("hello world"); 184 | 185 | container.rebind(exampleSymbol).toValue("good bye world"); 186 | expect(container.get(exampleSymbol)).toBe("good bye world"); 187 | }); 188 | 189 | test("can only rebind to a symbol if it was bound before", () => { 190 | expect(() => container.rebind(exampleSymbol)).toThrow("Symbol(example) was never bound"); 191 | }); 192 | 193 | test("can remove a symbol", () => { 194 | container.bind(exampleSymbol).toValue("hello world"); 195 | expect(container.get(exampleSymbol)).toBe("hello world"); 196 | 197 | container.remove(exampleSymbol); 198 | expect(() => container.get(exampleSymbol)).toThrow("nothing bound to Symbol(example)"); 199 | }); 200 | 201 | test("can snapshot and restore the registry", () => { 202 | container.bind(exampleSymbol).toValue("hello world"); 203 | expect(container.get(exampleSymbol)).toBe("hello world"); 204 | 205 | container.snapshot(); 206 | container.rebind(exampleSymbol).toValue("after first snapshot"); 207 | expect(container.get(exampleSymbol)).toBe("after first snapshot"); 208 | 209 | container.snapshot(); 210 | container.rebind(exampleSymbol).toValue("after second snapshot"); 211 | expect(container.get(exampleSymbol)).toBe("after second snapshot"); 212 | 213 | container.snapshot(); 214 | container.rebind(exampleSymbol).toValue("after fourth snapshot"); 215 | expect(container.get(exampleSymbol)).toBe("after fourth snapshot"); 216 | 217 | container.restore(); 218 | expect(container.get(exampleSymbol)).toBe("after second snapshot"); 219 | 220 | container.restore(); 221 | expect(container.get(exampleSymbol)).toBe("after first snapshot"); 222 | 223 | container.restore(); 224 | expect(container.get(exampleSymbol)).toBe("hello world"); 225 | 226 | container.restore(); 227 | expect(container.get(exampleSymbol)).toBe("hello world"); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /src/ioc/container.ts: -------------------------------------------------------------------------------- 1 | import type {RegItem, Token, MaybeToken, Plugin} from "./types"; 2 | import {Bind} from "./bind"; 3 | import {getType, stringifyToken} from "./token"; 4 | import {NOPLUGINS} from "./tags"; 5 | import {valueOrArrayToArray} from "./utils"; 6 | 7 | type Registry = Map; 8 | 9 | export class Container { 10 | private _registry: Registry = new Map(); 11 | private _snapshots: Registry[] = []; 12 | private _plugins: Plugin[] = []; 13 | 14 | bind = never>(token: MaybeToken): Bind { 15 | return new Bind(this._createItem(token)); 16 | } 17 | 18 | rebind = never>(token: MaybeToken): Bind { 19 | return this.remove(token).bind(token); 20 | } 21 | 22 | remove(token: MaybeToken): Container { 23 | if (this._registry.get(getType(token)) === undefined) throw `${stringifyToken(token)} was never bound`; 24 | 25 | this._registry.delete(getType(token)); 26 | 27 | return this; 28 | } 29 | 30 | get(token: Token | MaybeToken, tags?: symbol[] | symbol, target?: unknown): Dep; 31 | get>( 32 | token: Token | MaybeToken, 33 | tags: symbol[] | symbol, 34 | target: unknown, 35 | args: Args, 36 | ): Dep; 37 | get>( 38 | token: Token | MaybeToken, 39 | tags: symbol[] | symbol = [], 40 | target?: unknown, 41 | args?: Args, 42 | ): Dep { 43 | const item = | undefined>this._registry.get(getType(token)); 44 | 45 | if (!item || (!item.factory && item.value === undefined)) throw `nothing bound to ${stringifyToken(token)}`; 46 | 47 | const value: Dep = item.factory 48 | ? !item.singleton 49 | ? // eslint-disable-next-line 50 | item.factory(...((args || []) as any)) 51 | : // eslint-disable-next-line 52 | (item.cache = item.cache || item.factory(...((args || []) as any))) 53 | : item.value!; 54 | 55 | const tagsArr = valueOrArrayToArray(tags); 56 | 57 | if (tagsArr.indexOf(NOPLUGINS) === -1) { 58 | for (const plugin of item.plugins.concat(this._plugins)) { 59 | plugin(value, target, tagsArr, token, this); 60 | } 61 | } 62 | 63 | return value; 64 | } 65 | 66 | addPlugin(plugin: Plugin): Container { 67 | this._plugins.push(plugin); 68 | return this; 69 | } 70 | 71 | snapshot(): Container { 72 | this._snapshots.push(new Map(this._registry)); 73 | return this; 74 | } 75 | 76 | restore(): Container { 77 | this._registry = this._snapshots.pop() || this._registry; 78 | return this; 79 | } 80 | 81 | /* Item related */ 82 | private _createItem = []>( 83 | token: Token | MaybeToken, 84 | ): RegItem { 85 | if (this._registry.get(getType(token)) !== undefined) 86 | throw `object can only bound once: ${stringifyToken(token)}`; 87 | 88 | const item = {plugins: []}; 89 | this._registry.set(getType(token), item); 90 | 91 | return item; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ioc/createDecorator.test.ts: -------------------------------------------------------------------------------- 1 | import {Container} from "./container"; 2 | import {NOCACHE} from "./tags"; 3 | 4 | import {createDecorator} from "./createDecorator"; 5 | 6 | const container = new Container(); 7 | const inject = createDecorator(container); 8 | 9 | interface ITestClass { 10 | name: string; 11 | childOne?: ITestClass; 12 | childTwo?: ITestClass; 13 | } 14 | 15 | const factoryOneArg = (a: string) => a; 16 | const factoryTwoArg = (a: string, b: string) => `${a} - ${b}`; 17 | 18 | interface ICircular { 19 | name: string; 20 | circular?: ICircular; 21 | circularName: string; 22 | } 23 | 24 | const TYPE = { 25 | parent: Symbol.for("parent"), 26 | withCtorArgs: Symbol.for("withArgs"), 27 | with2CtorArgs: Symbol.for("with2Args"), 28 | factoryOneArg: Symbol.for("factoryOneArg"), 29 | factoryTwoArgs: Symbol.for("factoryTwoArgs"), 30 | child1: Symbol.for("child1"), 31 | child2: Symbol.for("child2"), 32 | child3: Symbol.for("child3"), 33 | child4: Symbol.for("child4"), 34 | circular1: Symbol.for("circular1"), 35 | circular2: Symbol.for("circular2"), 36 | circularFail1: Symbol.for("circularFail1"), 37 | circularFail2: Symbol.for("circularFail2"), 38 | cacheTest: Symbol.for("cacheTest"), 39 | }; 40 | 41 | class Parent implements ITestClass { 42 | name = "parent"; 43 | @inject(TYPE.child1) 44 | childOne!: ITestClass; 45 | @inject(TYPE.child2) 46 | childTwo!: ITestClass; 47 | } 48 | 49 | class WithCtorArg implements ITestClass { 50 | constructor(public name: string) {} 51 | } 52 | 53 | class ChildWithCtorArg implements ITestClass { 54 | name = "child with arg"; 55 | @inject>(TYPE.withCtorArgs, [], "with arg") 56 | childOne!: ITestClass; 57 | } 58 | 59 | type twoArgsITestClass = ITestClass & {name2: string}; 60 | class With2CtorArgs implements twoArgsITestClass { 61 | constructor( 62 | public name: string, 63 | public name2: string, 64 | ) {} 65 | } 66 | 67 | class ChildWith2CtorArgs implements ITestClass { 68 | name = "child with 2 args"; 69 | @inject>(TYPE.with2CtorArgs, [], "with", "two args") 70 | childOne!: twoArgsITestClass; 71 | } 72 | 73 | class ExtendedClassTest extends Parent {} 74 | 75 | class ChildOne implements ITestClass { 76 | name = "child one"; 77 | @inject(TYPE.child2) 78 | childOne!: ITestClass; 79 | @inject(TYPE.child3) 80 | childTwo!: ITestClass; 81 | } 82 | 83 | class ChildTwo implements ITestClass { 84 | name = "child two"; 85 | @inject(TYPE.child1) 86 | childOne!: ITestClass; 87 | } 88 | 89 | class ChildThree implements ITestClass { 90 | name = "child three"; 91 | @inject(TYPE.child4) 92 | childOne!: ITestClass; 93 | @inject(TYPE.parent) 94 | childTwo!: ITestClass; 95 | } 96 | 97 | class ChildFour implements ITestClass { 98 | name = "child four"; 99 | } 100 | 101 | class Circular1 implements ICircular { 102 | @inject(TYPE.circular2) 103 | circular!: ICircular; 104 | get circularName(): string { 105 | return this.circular.name; 106 | } 107 | constructor(public name: string) {} 108 | } 109 | 110 | class Circular2 implements ICircular { 111 | @inject(TYPE.circular1) 112 | circular!: ICircular; 113 | get circularName(): string { 114 | return this.circular.name; 115 | } 116 | constructor(public name: string) {} 117 | } 118 | 119 | class CircularFail1 implements ICircular { 120 | @inject(TYPE.circularFail2) 121 | circular!: ICircular; 122 | circularName = ""; 123 | constructor(public name: string) { 124 | this.circularName = this.circular.name; 125 | } 126 | } 127 | 128 | class CircularFail2 implements ICircular { 129 | @inject(TYPE.circularFail1) 130 | circular!: ICircular; 131 | circularName = ""; 132 | constructor(public name: string) { 133 | this.circularName = this.circular.name; 134 | } 135 | } 136 | 137 | class CacheTest { 138 | @inject(TYPE.cacheTest) 139 | cached!: number; 140 | @inject(TYPE.cacheTest, [NOCACHE]) 141 | notCached!: number; 142 | } 143 | 144 | class factoryWithArguments { 145 | @inject>(TYPE.factoryOneArg, [], "hello") 146 | factOne!: string; 147 | 148 | @inject>(TYPE.factoryTwoArgs, NOCACHE, "hello", "world") 149 | factTwo!: string; 150 | } 151 | 152 | container.bind(TYPE.parent).to(Parent); 153 | container.bind(TYPE.withCtorArgs).to(WithCtorArg); 154 | container.bind(TYPE.with2CtorArgs).to(With2CtorArgs); 155 | container.bind(TYPE.child1).to(ChildOne); 156 | container.bind(TYPE.child2).to(ChildTwo); 157 | container.bind(TYPE.child3).to(ChildThree); 158 | container.bind(TYPE.child4).to(ChildFour); 159 | container.bind(TYPE.circularFail1).toFactory(() => new CircularFail1("one")); 160 | container.bind(TYPE.circularFail2).toFactory(() => new CircularFail2("two")); 161 | container.bind(TYPE.circular1).toFactory(() => new Circular1("one")); 162 | container.bind(TYPE.circular2).toFactory(() => new Circular2("two")); 163 | 164 | let count: number; 165 | container.bind(TYPE.cacheTest).toFactory(() => ++count); 166 | container.bind(TYPE.factoryOneArg).toFactory(factoryOneArg); 167 | container.bind(TYPE.factoryTwoArgs).toFactory(factoryTwoArg); 168 | 169 | describe("Injector", () => { 170 | let instance: Parent; 171 | 172 | beforeEach(() => { 173 | instance = new Parent(); 174 | }); 175 | 176 | test("can inject first level", () => { 177 | expect(instance.childOne.name).toBe("child one"); 178 | expect(instance.childTwo.name).toBe("child two"); 179 | }); 180 | 181 | test("inject should resolve in parent class if extended", () => { 182 | const ext = new ExtendedClassTest(); 183 | expect(ext.childOne.name).toBe("child one"); 184 | expect(ext.childTwo.name).toBe("child two"); 185 | }); 186 | 187 | test("can inject deep", () => { 188 | expect(instance.childOne.childOne?.name).toBe("child two"); 189 | expect(instance.childOne.childTwo?.name).toBe("child three"); 190 | expect(instance.childOne.childTwo?.childOne?.name).toBe("child four"); 191 | }); 192 | 193 | test("can inject parent", () => { 194 | expect(instance.childOne.childOne?.childOne?.name).toBe("child one"); 195 | expect(instance.childOne.childTwo?.childTwo?.name).toBe("parent"); 196 | }); 197 | 198 | test("can inject with one arg", () => { 199 | const child = new ChildWithCtorArg(); 200 | expect(child.childOne.name).toBe("with arg"); 201 | }); 202 | 203 | test("can inject with two args", () => { 204 | const child = new ChildWith2CtorArgs(); 205 | expect(child.childOne.name).toBe("with"); 206 | expect(child.childOne.name2).toBe("two args"); 207 | }); 208 | 209 | test("can inject factories with arg(s)", () => { 210 | const child = new factoryWithArguments(); 211 | expect(child.factOne).toBe("hello"); 212 | expect(child.factTwo).toBe("hello - world"); 213 | }); 214 | 215 | test("can inject a circular dependency when accessing the dependency outside of constructor", () => { 216 | const instance1 = container.get(TYPE.circular1); 217 | const instance2 = container.get(TYPE.circular2); 218 | 219 | expect(instance1.circularName).toBe("two"); 220 | expect(instance2.circularName).toBe("one"); 221 | }); 222 | 223 | test("can not inject a circular dependency when accessing the dependency inside of constructor", () => { 224 | expect(() => container.get(TYPE.circularFail1)).toThrow("Maximum call stack size exceeded"); 225 | }); 226 | 227 | test("resolves only once with cache enabled by default", () => { 228 | count = 0; 229 | const cacheTest = new CacheTest(); 230 | expect(cacheTest.cached).toBe(1); 231 | expect(cacheTest.cached).toBe(1); 232 | expect(cacheTest.cached).toBe(1); 233 | }); 234 | 235 | test("resolves new data each request without cache enabled", () => { 236 | count = 0; 237 | const cacheTest = new CacheTest(); 238 | expect(cacheTest.notCached).toBe(1); 239 | expect(cacheTest.notCached).toBe(2); 240 | expect(cacheTest.notCached).toBe(3); 241 | }); 242 | 243 | test("resolves new data with new instance even with cache enabled", () => { 244 | count = 0; 245 | const cacheTest1 = new CacheTest(); 246 | expect(cacheTest1.cached).toBe(1); 247 | expect(cacheTest1.cached).toBe(1); 248 | 249 | count = 9; 250 | const cacheTest2 = new CacheTest(); 251 | expect(cacheTest2.cached).toBe(10); 252 | expect(cacheTest2.cached).toBe(10); 253 | 254 | // final proof 255 | expect(cacheTest1.cached).toBe(1); 256 | }); 257 | 258 | test("resolves new data with new instance even with cache disabled", () => { 259 | count = 0; 260 | const cacheTest1 = new CacheTest(); 261 | const cacheTest2 = new CacheTest(); 262 | expect(cacheTest1.notCached).toBe(1); 263 | expect(cacheTest1.notCached).toBe(2); 264 | expect(cacheTest2.notCached).toBe(3); 265 | expect(cacheTest2.notCached).toBe(4); 266 | 267 | // final proof 268 | expect(cacheTest1.notCached).toBe(5); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /src/ioc/createDecorator.ts: -------------------------------------------------------------------------------- 1 | import type {Token, MaybeToken} from "./types"; 2 | import type {Container} from "./container"; 3 | import {define} from "./define"; 4 | 5 | export function createDecorator(container: Container) { 6 | return >( 7 | token: Token | MaybeToken, 8 | tags: symbol[] | symbol = [], 9 | ...args: Args 10 | ) => { 11 | return function ( 12 | target: Target, 13 | property: Prop, 14 | ): void { 15 | define(target, property, container, token, tags, args); 16 | }; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/ioc/createResolve.arguments.test.ts: -------------------------------------------------------------------------------- 1 | import {Container} from "./container"; 2 | 3 | import {token} from "./token"; 4 | 5 | import {createResolve} from "./createResolve"; 6 | 7 | class WithArguments { 8 | constructor( 9 | public a: number, 10 | public b: number, 11 | ) {} 12 | } 13 | class WithoutArguments { 14 | public a: number; 15 | constructor() { 16 | this.a = 1; 17 | } 18 | } 19 | 20 | const TYPE = { 21 | classWithArguments: token>("classWithArguments"), 22 | classWithoutArguments: token("classWithoutArguments"), 23 | factoryWithArguments: token>("factoryWithArguments"), 24 | factoryWithoutArguments: token("factoryWithoutArguments"), 25 | }; 26 | 27 | const factory = (a: number, b: number) => new WithArguments(a, b); 28 | const container = new Container(); 29 | container.bind(TYPE.classWithArguments).to(WithArguments); 30 | container.bind(TYPE.classWithoutArguments).to(WithoutArguments); 31 | container.bind(TYPE.factoryWithArguments).toFactory(factory); 32 | container.bind(TYPE.factoryWithoutArguments).toFactory(() => new WithoutArguments()); 33 | const resolve = createResolve(container); 34 | 35 | class ResolveTest { 36 | classWithArguments = resolve(TYPE.classWithArguments); 37 | classWithArgumentsTypeSafe = resolve(TYPE.classWithArguments); 38 | classWithoutArguments = resolve(TYPE.classWithoutArguments); 39 | factoryWithArguments = resolve(TYPE.factoryWithArguments); 40 | factoryWithoutArguments = resolve(TYPE.factoryWithoutArguments); 41 | } 42 | 43 | describe("Resolve", () => { 44 | test("resolves class with constructor arguments", () => { 45 | const resolveTest = new ResolveTest(); 46 | const resolved = resolveTest.classWithArguments(1, 2); 47 | expect(resolved.a).toEqual(1); 48 | expect(resolved.b).toEqual(2); 49 | 50 | // type safe 51 | const resolvedTypeSafe = resolveTest.classWithArgumentsTypeSafe(1, 2); // only accepts two numbers 52 | expect(resolvedTypeSafe.a).toEqual(1); 53 | expect(resolvedTypeSafe.b).toEqual(2); 54 | }); 55 | 56 | test("resolves class without constructor arguments", () => { 57 | const resolveTest = new ResolveTest(); 58 | const resolved = resolveTest.classWithoutArguments(); 59 | expect(resolved.a).toEqual(1); 60 | }); 61 | 62 | test("resolves factory with constructor arguments", () => { 63 | const resolveTest = new ResolveTest(); 64 | const resolved = resolveTest.factoryWithArguments(1, 2); 65 | expect(resolved.a).toEqual(1); 66 | expect(resolved.b).toEqual(2); 67 | }); 68 | 69 | test("resolves factory without constructor arguments", () => { 70 | const resolveTest = new ResolveTest(); 71 | const resolved = resolveTest.factoryWithoutArguments(); 72 | expect(resolved.a).toEqual(1); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/ioc/createResolve.test.ts: -------------------------------------------------------------------------------- 1 | import {Container} from "./container"; 2 | 3 | import {NOCACHE, NOPLUGINS} from "./tags"; 4 | import {token} from "./token"; 5 | 6 | import {createResolve} from "./createResolve"; 7 | 8 | const TYPE = { 9 | cacheTest: token("cacheTest"), 10 | }; 11 | 12 | const container = new Container(); 13 | const resolve = createResolve(container); 14 | 15 | class ResolveTest { 16 | cached = resolve(TYPE.cacheTest); 17 | notCached = resolve(TYPE.cacheTest, [NOCACHE]); 18 | noPlugins = resolve(TYPE.cacheTest, NOPLUGINS); 19 | } 20 | 21 | let count: number; 22 | container.bind(TYPE.cacheTest).toFactory(() => ++count); 23 | 24 | describe("Resolve", () => { 25 | test("resolves new data only on first access", () => { 26 | count = 0; 27 | const cacheTest1 = new ResolveTest(); 28 | expect(cacheTest1.cached()).toBe(1); 29 | expect(cacheTest1.cached()).toBe(1); 30 | 31 | count = 9; 32 | const cacheTest2 = new ResolveTest(); 33 | expect(cacheTest2.cached()).toBe(10); 34 | expect(cacheTest2.cached()).toBe(10); 35 | 36 | // final proof 37 | expect(cacheTest1.cached()).toBe(1); 38 | }); 39 | 40 | test("resolves new data every time it get accessed", () => { 41 | count = 0; 42 | const cacheTest1 = new ResolveTest(); 43 | const cacheTest2 = new ResolveTest(); 44 | expect(cacheTest1.notCached()).toBe(1); 45 | expect(cacheTest1.notCached()).toBe(2); 46 | expect(cacheTest2.notCached()).toBe(3); 47 | expect(cacheTest2.notCached()).toBe(4); 48 | 49 | // final proof 50 | expect(cacheTest1.notCached()).toBe(5); 51 | }); 52 | 53 | test("a function can use resolve", () => { 54 | count = 0; 55 | 56 | function ResolveTestFunctionCached() { 57 | const cached = resolve(TYPE.cacheTest); 58 | cached(); 59 | cached(); 60 | cached(); 61 | return cached(); 62 | } 63 | 64 | expect(ResolveTestFunctionCached()).toBe(1); 65 | }); 66 | 67 | test("a function can use resolve without cache", () => { 68 | count = 0; 69 | 70 | function ResolveTestFunctionCacheNoCache() { 71 | const cached = resolve(TYPE.cacheTest, [NOCACHE]); 72 | cached(); 73 | cached(); 74 | cached(); 75 | return cached(); 76 | } 77 | 78 | expect(ResolveTestFunctionCacheNoCache()).toBe(4); 79 | }); 80 | 81 | describe("with a plugin", () => { 82 | const plugin = jest.fn(); 83 | let testCls: ResolveTest; 84 | 85 | container.addPlugin(plugin); 86 | 87 | beforeEach(() => { 88 | plugin.mockReset(); 89 | testCls = new ResolveTest(); 90 | }); 91 | 92 | test("should have executed the plugin", () => { 93 | testCls.cached(); 94 | expect(plugin).toHaveBeenCalledTimes(1); 95 | }); 96 | 97 | test("should NOT have executed the plugin with NOPLUGINS symbol", () => { 98 | testCls.noPlugins(); 99 | expect(plugin).not.toBeCalled(); 100 | }); 101 | 102 | test("should pass the arguments", () => { 103 | testCls.notCached(); 104 | expect(plugin.mock.calls[0][2].indexOf(NOCACHE)).not.toBe(-1); 105 | }); 106 | 107 | test("should pass the ResolveTest class as 2nd argument", () => { 108 | testCls.cached(); 109 | expect(plugin.mock.calls[0][1]).toBe(testCls); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/ioc/createResolve.ts: -------------------------------------------------------------------------------- 1 | import type {Token, MaybeToken} from "./types"; 2 | import type {Container} from "./container"; 3 | import {NOCACHE} from "./tags"; 4 | import {valueOrArrayToArray} from "./utils"; 5 | 6 | export function createResolve(container: Container) { 7 | return >( 8 | token: Token | MaybeToken, 9 | tags: symbol[] | symbol = [], 10 | ) => { 11 | let value: Dep; 12 | return function (this: R, ...args: Args): Dep { 13 | if (valueOrArrayToArray(tags).indexOf(NOCACHE) !== -1 || value === undefined) { 14 | value = container.get(token, tags, this, args); 15 | } 16 | return value; 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/ioc/createWire.test.ts: -------------------------------------------------------------------------------- 1 | import {Container} from "./container"; 2 | import {NOCACHE} from "./tags"; 3 | import {token} from "./token"; 4 | 5 | import {createWire} from "./createWire"; 6 | 7 | const TYPE = { 8 | cacheTest: token("cacheTest"), 9 | oneArg: token("oneArg"), 10 | twoArgs: token("twoArg"), 11 | factoryOneArg: token("factoryOneArg"), 12 | factoryTwoArg: token("factoryTwoArgs"), 13 | }; 14 | 15 | class OneArg { 16 | constructor(public name: string) {} 17 | } 18 | 19 | class TwoArgs { 20 | constructor( 21 | public name: string, 22 | public name2: string, 23 | ) {} 24 | } 25 | 26 | const container = new Container(); 27 | const wire = createWire(container); 28 | 29 | class WireTest { 30 | cached!: number; 31 | notCached!: number; 32 | 33 | constructor() { 34 | wire(this, "cached", TYPE.cacheTest); 35 | wire(this, "notCached", TYPE.cacheTest, [NOCACHE]); 36 | } 37 | } 38 | 39 | class ctorArgumentsWireTest { 40 | oneArg!: OneArg; 41 | twoArgs!: TwoArgs; 42 | 43 | constructor() { 44 | wire>( 45 | this, 46 | "oneArg", 47 | TYPE.oneArg, 48 | [], 49 | "with one arg", 50 | ); 51 | wire(this, "twoArgs", TYPE.twoArgs, [], "with", "two args"); 52 | } 53 | } 54 | 55 | class factoriesArgumentsWireTest { 56 | oneArg!: string; 57 | twoArgs!: string; 58 | 59 | constructor() { 60 | wire(this, "oneArg", TYPE.factoryOneArg, [], "with one arg"); 61 | wire(this, "twoArgs", TYPE.factoryTwoArg, [], "with", "two args"); 62 | } 63 | } 64 | 65 | let count: number; 66 | container.bind(TYPE.cacheTest).toFactory(() => ++count); 67 | container.bind(TYPE.oneArg).to(OneArg); 68 | container.bind(TYPE.twoArgs).to(TwoArgs); 69 | container.bind(TYPE.factoryOneArg).toFactory((a: string) => a); 70 | container.bind(TYPE.factoryTwoArg).toFactory((a: string, b: string) => `${a} - ${b}`); 71 | 72 | describe("Wire", () => { 73 | test("resolves new data only on first access", () => { 74 | count = 0; 75 | const cacheTest1 = new WireTest(); 76 | expect(cacheTest1.cached).toBe(1); 77 | expect(cacheTest1.cached).toBe(1); 78 | 79 | count = 9; 80 | const cacheTest2 = new WireTest(); 81 | expect(cacheTest2.cached).toBe(10); 82 | expect(cacheTest2.cached).toBe(10); 83 | 84 | // final proof 85 | expect(cacheTest1.cached).toBe(1); 86 | }); 87 | 88 | test("resolves new data every time it get accessed", () => { 89 | count = 0; 90 | const cacheTest1 = new WireTest(); 91 | const cacheTest2 = new WireTest(); 92 | expect(cacheTest1.notCached).toBe(1); 93 | expect(cacheTest1.notCached).toBe(2); 94 | expect(cacheTest2.notCached).toBe(3); 95 | expect(cacheTest2.notCached).toBe(4); 96 | 97 | // final proof 98 | expect(cacheTest1.notCached).toBe(5); 99 | }); 100 | 101 | test("resolve a dependency with constructor argument(s)", () => { 102 | const wireTest = new ctorArgumentsWireTest(); 103 | expect(wireTest.oneArg.name).toBe("with one arg"); 104 | expect(wireTest.twoArgs.name).toBe("with"); 105 | expect(wireTest.twoArgs.name2).toBe("two args"); 106 | }); 107 | 108 | test("resolve a dependency with factories argument(s)", () => { 109 | const wireTest = new factoriesArgumentsWireTest(); 110 | expect(wireTest.oneArg).toBe("with one arg"); 111 | expect(wireTest.twoArgs).toBe("with - two args"); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/ioc/createWire.ts: -------------------------------------------------------------------------------- 1 | import type {Token, MaybeToken} from "./types"; 2 | import type {Container} from "./container"; 3 | import {define} from "./define"; 4 | 5 | export function createWire(container: Container) { 6 | return >( 7 | target: Target, 8 | property: Prop, 9 | token: Token | MaybeToken, 10 | tags: symbol[] | symbol = [], 11 | ...args: Args 12 | ) => { 13 | define(target, property, container, token, tags, args); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/ioc/define.ts: -------------------------------------------------------------------------------- 1 | import type {Token, MaybeToken} from "./types"; 2 | import {Container} from "./container"; 3 | import {NOCACHE} from "./tags"; 4 | import {valueOrArrayToArray} from "./utils"; 5 | 6 | export function define< 7 | Dep, 8 | Target extends {[key in Prop]: Dep}, 9 | Prop extends keyof Target, 10 | Args extends Array, 11 | >( 12 | target: Target, 13 | property: Prop, 14 | container: Container, 15 | token: Token | MaybeToken, 16 | tags: symbol[] | symbol, 17 | args: Args, 18 | ) { 19 | Object.defineProperty(target, property, { 20 | get: function (this: R): Dep { 21 | const value = container.get(token, tags, this, args); 22 | if (valueOrArrayToArray(tags).indexOf(NOCACHE) === -1) 23 | Object.defineProperty(this, property, { 24 | value, 25 | enumerable: true, 26 | }); 27 | return value; 28 | }, 29 | configurable: true, 30 | enumerable: true, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/ioc/options.ts: -------------------------------------------------------------------------------- 1 | import {PluginOptions} from "./pluginOptions"; 2 | 3 | export class Options> extends PluginOptions { 4 | inSingletonScope(): PluginOptions { 5 | this._regItem.singleton = true; 6 | return this; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ioc/pluginOptions.ts: -------------------------------------------------------------------------------- 1 | import type {RegItem, Plugin} from "./types"; 2 | 3 | export class PluginOptions> { 4 | constructor(protected _regItem: RegItem) {} 5 | 6 | withPlugin(plugin: Plugin): PluginOptions { 7 | this._regItem.plugins.push(plugin); 8 | return this; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ioc/tags.ts: -------------------------------------------------------------------------------- 1 | export const NOCACHE = Symbol("NOCACHE"); 2 | export const NOPLUGINS = Symbol("NOPLUGINS"); 3 | -------------------------------------------------------------------------------- /src/ioc/token.ts: -------------------------------------------------------------------------------- 1 | import type {MaybeToken, Token} from "./types"; 2 | import {isSymbol} from "./utils"; 3 | 4 | export const token = = unknown[]>(name: string) => 5 | ({type: Symbol(name)}) as Token; 6 | 7 | export const stringifyToken = (token: MaybeToken): string => 8 | !isSymbol(token) ? `Token(${token.type.toString()})` : token.toString(); 9 | 10 | export const getType = (token: MaybeToken): symbol => (!isSymbol(token) ? token.type : token); 11 | -------------------------------------------------------------------------------- /src/ioc/types.ts: -------------------------------------------------------------------------------- 1 | import type {Container} from "./container"; 2 | 3 | export interface RegItem = []> { 4 | value?: Value; 5 | factory?: Factory; 6 | cache?: Dep; 7 | singleton?: boolean; 8 | plugins: Plugin[]; 9 | } 10 | 11 | export type Plugin = ( 12 | dependency: Dep, 13 | target: unknown, 14 | tags: symbol[], 15 | token: MaybeToken, 16 | container: Container, 17 | ) => void; 18 | 19 | export interface NewAble = []> { 20 | new (...args: Args): Dep; 21 | } 22 | 23 | export type Factory> = (...args: Args) => Dep; 24 | export type Value = Dependency; 25 | 26 | // tokens 27 | export type MaybeToken = unknown[]> = Token | symbol; 28 | 29 | declare const dependencyMarker: unique symbol; 30 | declare const argumentsMarker: unique symbol; 31 | export interface Token = never> { 32 | type: symbol; 33 | [dependencyMarker]: Dep; 34 | [argumentsMarker]: Args; 35 | } 36 | -------------------------------------------------------------------------------- /src/ioc/utils.ts: -------------------------------------------------------------------------------- 1 | export const isSymbol = (t: unknown): t is symbol => typeof t == "symbol"; 2 | 3 | export const valueOrArrayToArray = (smt: symbol[] | symbol): symbol[] => (isSymbol(smt) ? [smt] : smt); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@owja/typescript-config", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["src/example", "src/**/*.test.ts"] 5 | } 6 | --------------------------------------------------------------------------------