ts-ioc-container 47.5.0 → 48.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -100
- package/cjm/index.js +2 -3
- package/cjm/injector/inject.js +3 -7
- package/cjm/provider/IProvider.js +1 -13
- package/cjm/utils/array.js +0 -11
- package/esm/index.js +1 -1
- package/esm/injector/inject.js +3 -7
- package/esm/provider/IProvider.js +0 -11
- package/esm/utils/array.js +0 -10
- package/package.json +20 -3
- package/typings/index.d.ts +1 -1
- package/typings/provider/IProvider.d.ts +0 -1
- package/typings/utils/array.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# TypeScript Dependency Injection Container
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
@@ -7,51 +7,68 @@
|
|
|
7
7
|

|
|
8
8
|
[](https://github.com/semantic-release/semantic-release)
|
|
9
9
|
|
|
10
|
+
`ts-ioc-container` is a fast, lightweight TypeScript dependency injection
|
|
11
|
+
container for applications that need more than basic constructor injection:
|
|
12
|
+
scoped lifecycles, decorators, typed tokens, lazy dependencies, lifecycle hooks,
|
|
13
|
+
provider pipelines, aliases, and custom injector strategies.
|
|
14
|
+
|
|
15
|
+
Use it as a `tsyringe` alternative when you want a clear API with more flexible
|
|
16
|
+
customization for real application architecture. Use it as an Inversify or
|
|
17
|
+
Awilix alternative when you want a thinner, cleaner API with explicit scopes and
|
|
18
|
+
no global container objects.
|
|
19
|
+
|
|
10
20
|
## Advantages
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- clean API
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
21
|
+
|
|
22
|
+
- fast TypeScript dependency resolution
|
|
23
|
+
- lightweight and dependency-minimal
|
|
24
|
+
- clean API for classes, keys, tokens, aliases, and scopes
|
|
25
|
+
- no global container object; pass containers and scopes explicitly
|
|
26
|
+
- supports tagged application, request, transaction, page, and widget scopes
|
|
27
|
+
- decorator support with `@register`, `@inject`, `@onConstruct`, and `@onDispose`
|
|
18
28
|
- can [inject properties](#inject-property)
|
|
19
29
|
- can inject [lazy dependencies](#lazy)
|
|
20
|
-
- composable and
|
|
30
|
+
- composable provider and registration pipelines
|
|
31
|
+
- custom injectors, hooks, and provider behavior
|
|
32
|
+
- product behavior covered by executable specs
|
|
21
33
|
|
|
22
34
|
## Content
|
|
23
35
|
|
|
24
36
|
- [Setup](#setup)
|
|
25
37
|
- [Quickstart](#quickstart)
|
|
26
38
|
- [Cheatsheet](#cheatsheet)
|
|
39
|
+
- [Spec-driven workflow](#spec-driven-workflow)
|
|
40
|
+
- [Product capability map](#product-capability-map)
|
|
41
|
+
- [Acceptance specs](#acceptance-specs)
|
|
42
|
+
- [tsyringe alternative](https://igorbabkin.github.io/ts-ioc-container/tsyringe-alternative)
|
|
43
|
+
- [Inversify and Awilix alternative](https://igorbabkin.github.io/ts-ioc-container/inversify-awilix-alternative)
|
|
27
44
|
- [Recipes](#recipes)
|
|
28
45
|
- [Container](#container)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
- [Basic usage](#basic-usage)
|
|
47
|
+
- [Scope](#scope) `tags`
|
|
48
|
+
- [Dynamic Tag Management](#dynamic-tag-management) `addTags`
|
|
49
|
+
- [Instances](#instances)
|
|
50
|
+
- [Dispose](#dispose)
|
|
51
|
+
- [Lazy](#lazy) `lazy`
|
|
52
|
+
- [Lazy with registerPipe](#lazy-with-registerpipe) `lazy()`
|
|
36
53
|
- [Injector](#injector)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
- [Metadata](#metadata) `@inject`
|
|
55
|
+
- [Simple](#simple)
|
|
56
|
+
- [Proxy](#proxy)
|
|
40
57
|
- [Provider](#provider) `provider`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
- [Singleton](#singleton) `singleton`
|
|
59
|
+
- [Arguments](#arguments) `setArgs` `setArgsFn`
|
|
60
|
+
- [Visibility](#visibility) `visible`
|
|
61
|
+
- [Alias](#alias) `asAlias`
|
|
62
|
+
- [Decorator](#decorator) `decorate`
|
|
46
63
|
- [Registration](#registration) `@register`
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
- [Key](#key) `asKey`
|
|
65
|
+
- [Scope](#scope) `scope`
|
|
49
66
|
- [Module](#module)
|
|
50
67
|
- [Hook](#hook) `@hook`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
- [OnConstruct](#onconstruct) `@onConstruct`
|
|
69
|
+
- [OnDispose](#ondispose) `@onDispose`
|
|
70
|
+
- [Inject Property](#inject-property)
|
|
71
|
+
- [Inject Method](#inject-method)
|
|
55
72
|
- [Mock](#mock)
|
|
56
73
|
- [Error](#error)
|
|
57
74
|
|
|
@@ -60,16 +77,19 @@
|
|
|
60
77
|
```shell script
|
|
61
78
|
npm install ts-ioc-container reflect-metadata
|
|
62
79
|
```
|
|
80
|
+
|
|
63
81
|
```shell script
|
|
64
82
|
yarn add ts-ioc-container reflect-metadata
|
|
65
83
|
```
|
|
66
84
|
|
|
67
85
|
Just put it in the entrypoint file of your project. It should be the first line of the code.
|
|
86
|
+
|
|
68
87
|
```typescript
|
|
69
88
|
import 'reflect-metadata';
|
|
70
89
|
```
|
|
71
90
|
|
|
72
91
|
And `tsconfig.json` should have next options:
|
|
92
|
+
|
|
73
93
|
```json
|
|
74
94
|
{
|
|
75
95
|
"compilerOptions": {
|
|
@@ -105,8 +125,9 @@ container.resolve(App).start();
|
|
|
105
125
|
|
|
106
126
|
## Cheatsheet
|
|
107
127
|
|
|
108
|
-
- Register class with key: `@register(bindTo('Key')) class Service {}`
|
|
128
|
+
- Register class with key (preferred): `@register(bindTo('Key')) class Service {}` then `container.addRegistration(R.fromClass(Service))`
|
|
109
129
|
- Register value: `R.fromValue(config).bindToKey('Config')`
|
|
130
|
+
- Register factory: `R.fromFn((c) => createX(c)).bindToKey('X')`
|
|
110
131
|
- Singleton: `@register(singleton())`
|
|
111
132
|
- Scoped registration: `@register(scope((s) => s.hasTag('request')))`
|
|
112
133
|
- Resolve by alias: `container.resolveByAlias('Alias')`
|
|
@@ -115,12 +136,56 @@ container.resolve(App).start();
|
|
|
115
136
|
- Inject decorator: `@inject('Key')`
|
|
116
137
|
- Property inject: `injectProp(target, 'propName', select.token('Key'))`
|
|
117
138
|
|
|
139
|
+
> [!TIP]
|
|
140
|
+
> For classes, prefer the `@register(bindTo('Key'))` decorator over the fluent
|
|
141
|
+
> `R.fromClass(Class).bindToKey('Key')` chain. The decorator co-locates the binding
|
|
142
|
+
> with the class and reads consistently with other registration pipes
|
|
143
|
+
> (`scope`, `singleton`, `setArgsFn`, ...). Use the fluent `bindToKey` chain only
|
|
144
|
+
> for `R.fromValue(...)` and `R.fromFn(...)` (which have no class to decorate)
|
|
145
|
+
> or for third-party classes you don't own.
|
|
146
|
+
|
|
147
|
+
## Spec-driven workflow
|
|
148
|
+
|
|
149
|
+
Public behavior is described as product capabilities before it is implemented.
|
|
150
|
+
The repository keeps the same chain visible in specs, tests, docs, and this
|
|
151
|
+
README:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
Business capability -> user story -> acceptance criteria -> executable spec test -> docs and ADRs
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- ADRs in `docs/adr/` explain durable architecture and process decisions.
|
|
158
|
+
- Specs in `specs/` describe epics, stories, use cases, and acceptance criteria.
|
|
159
|
+
- Acceptance tests in `__tests__/specs/` execute the public contract.
|
|
160
|
+
- README examples in `__tests__/readme/` stay executable and are rendered from `.readme.hbs.md`.
|
|
161
|
+
|
|
162
|
+
### Product capability map
|
|
163
|
+
|
|
164
|
+
| Capability | User outcome | Spec | Acceptance test |
|
|
165
|
+
| ----------------------- | -------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------- |
|
|
166
|
+
| Dependency resolution | Resolve dependencies by key, class, token, or constructor injection. | `specs/epics/dependency-resolution.md` | `__tests__/specs/dependency-resolution.spec.ts` |
|
|
167
|
+
| Scoped lifecycle | Isolate application, request, transaction, page, or widget lifecycles. | `specs/epics/scoped-lifecycle.md` | `__tests__/specs/scoped-lifecycle.spec.ts` |
|
|
168
|
+
| Dependency registration | Describe classes, values, factories, keys, aliases, and scoped services. | `specs/epics/dependency-registration.md` | `__tests__/specs/dependency-registration.spec.ts` |
|
|
169
|
+
| Provider behavior | Cache, decorate, delay, restrict, or parameterize dependency creation. | `specs/epics/provider-behavior.md` | `__tests__/specs/provider-behavior.spec.ts` |
|
|
170
|
+
| Token-based injection | Make dependency requests explicit, typed, reusable, and composable. | `specs/epics/token-based-injection.md` | `__tests__/specs/token-based-injection.spec.ts` |
|
|
171
|
+
| Injector strategies | Support metadata, simple container, proxy, and custom injection styles. | `specs/epics/injector-strategies.md` | `__tests__/specs/injector-strategies.spec.ts` |
|
|
172
|
+
| Lifecycle hooks | Run initialization, cleanup, property injection, and custom hook behavior. | `specs/epics/lifecycle-hooks.md` | `__tests__/specs/lifecycle-hooks.spec.ts` |
|
|
173
|
+
| Container modules | Package container configuration by feature, environment, or lifecycle. | `specs/epics/container-modules.md` | `__tests__/specs/container-modules.spec.ts` |
|
|
174
|
+
| Metadata utilities | Attach labels, tags, and reusable method behavior to application code. | `specs/epics/metadata-utilities.md` | `__tests__/specs/metadata-utilities.spec.ts` |
|
|
175
|
+
| Errors and boundaries | Make misconfiguration and unsupported usage diagnosable. | `specs/epics/errors-and-boundaries.md` | `__tests__/specs/errors-and-boundaries.spec.ts` |
|
|
176
|
+
|
|
177
|
+
### Acceptance specs
|
|
178
|
+
|
|
179
|
+
Use `pnpm run test:spec` to run only the executable acceptance specs. These
|
|
180
|
+
tests are intentionally product-facing; lower-level regression and implementation
|
|
181
|
+
tests stay in the feature folders under `__tests__/`.
|
|
182
|
+
|
|
118
183
|
## Recipes
|
|
119
184
|
|
|
120
185
|
### Express/Next handler (per-request scope)
|
|
186
|
+
|
|
121
187
|
```typescript
|
|
122
|
-
const app = new Container({ tags: ['application'] })
|
|
123
|
-
.addRegistration(R.fromClass(Logger).pipe(singleton()));
|
|
188
|
+
const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger).pipe(singleton()));
|
|
124
189
|
|
|
125
190
|
function handleRequest() {
|
|
126
191
|
const requestScope = app.createScope({ tags: ['request'] });
|
|
@@ -130,6 +195,7 @@ function handleRequest() {
|
|
|
130
195
|
```
|
|
131
196
|
|
|
132
197
|
### Background worker (singleton client, transient jobs)
|
|
198
|
+
|
|
133
199
|
```typescript
|
|
134
200
|
@register(singleton())
|
|
135
201
|
class QueueClient {}
|
|
@@ -144,10 +210,13 @@ const worker = new Container({ tags: ['worker'] })
|
|
|
144
210
|
```
|
|
145
211
|
|
|
146
212
|
### Frontend widget/page scope with lazy dependency
|
|
213
|
+
|
|
147
214
|
```typescript
|
|
148
215
|
@register(bindTo('FeatureFlags'), singleton())
|
|
149
216
|
class FeatureFlags {
|
|
150
|
-
load() {
|
|
217
|
+
load() {
|
|
218
|
+
/* fetch flags */
|
|
219
|
+
}
|
|
151
220
|
}
|
|
152
221
|
|
|
153
222
|
class Widget {
|
|
@@ -160,6 +229,7 @@ const page = new Container({ tags: ['page'] })
|
|
|
160
229
|
```
|
|
161
230
|
|
|
162
231
|
## Container
|
|
232
|
+
|
|
163
233
|
`IContainer` consists of:
|
|
164
234
|
|
|
165
235
|
- Provider is dependency factory which creates dependency
|
|
@@ -170,7 +240,7 @@ const page = new Container({ tags: ['page'] })
|
|
|
170
240
|
|
|
171
241
|
```typescript
|
|
172
242
|
import 'reflect-metadata';
|
|
173
|
-
import { Container, type IContainer, inject, Registration as R, select } from 'ts-ioc-container';
|
|
243
|
+
import { bindTo, Container, type IContainer, inject, register, Registration as R, select } from 'ts-ioc-container';
|
|
174
244
|
|
|
175
245
|
/**
|
|
176
246
|
* User Management Domain - Basic Dependency Injection
|
|
@@ -193,6 +263,7 @@ describe('Basic usage', function () {
|
|
|
193
263
|
}
|
|
194
264
|
|
|
195
265
|
// Concrete implementation
|
|
266
|
+
@register(bindTo('IUserRepository'))
|
|
196
267
|
class UserRepository implements IUserRepository {
|
|
197
268
|
private users: User[] = [{ id: '1', email: 'admin@example.com', passwordHash: 'hashed_password' }];
|
|
198
269
|
|
|
@@ -213,7 +284,7 @@ describe('Basic usage', function () {
|
|
|
213
284
|
}
|
|
214
285
|
|
|
215
286
|
// Wire up the container
|
|
216
|
-
const container = new Container().addRegistration(R.fromClass(UserRepository)
|
|
287
|
+
const container = new Container().addRegistration(R.fromClass(UserRepository));
|
|
217
288
|
|
|
218
289
|
// Resolve AuthService - UserRepository is automatically injected
|
|
219
290
|
const authService = container.resolve(AuthService);
|
|
@@ -246,6 +317,7 @@ describe('Basic usage', function () {
|
|
|
246
317
|
```
|
|
247
318
|
|
|
248
319
|
### Scope
|
|
320
|
+
|
|
249
321
|
Sometimes you need to create a scope of container. For example, when you want to create a scope per request in web application. You can assign tags to scope and provider and resolve dependencies only from certain scope.
|
|
250
322
|
|
|
251
323
|
> [!IMPORTANT]
|
|
@@ -349,6 +421,7 @@ describe('Scopes', function () {
|
|
|
349
421
|
```
|
|
350
422
|
|
|
351
423
|
### Dynamic Tag Management
|
|
424
|
+
|
|
352
425
|
You can dynamically add tags to a container after it's been created using the `addTags()` method. This is useful for environment-based configuration, feature flags, and progressive container setup.
|
|
353
426
|
|
|
354
427
|
- Tags can be added one at a time or multiple at once
|
|
@@ -458,6 +531,7 @@ describe('addTags', () => {
|
|
|
458
531
|
```
|
|
459
532
|
|
|
460
533
|
### Instances
|
|
534
|
+
|
|
461
535
|
Sometimes you want to get all instances from container and its scopes. For example, when you want to dispose all instances of container.
|
|
462
536
|
|
|
463
537
|
- you can get instances from container and scope which were created by injector
|
|
@@ -542,6 +616,7 @@ describe('Instances', function () {
|
|
|
542
616
|
```
|
|
543
617
|
|
|
544
618
|
### Check Registration
|
|
619
|
+
|
|
545
620
|
Sometimes you want to check if a registration with a specific key exists in the container. This is useful for conditional registration logic, validation, and debugging.
|
|
546
621
|
|
|
547
622
|
- `hasRegistration(key)` checks if a registration exists in the current container or parent containers
|
|
@@ -643,6 +718,7 @@ describe('hasRegistration', function () {
|
|
|
643
718
|
```
|
|
644
719
|
|
|
645
720
|
### Dispose
|
|
721
|
+
|
|
646
722
|
Sometimes you want to dispose a container or scope. For example, when a request, page, widget, or other local lifecycle ends.
|
|
647
723
|
|
|
648
724
|
- container can be disposed
|
|
@@ -653,7 +729,7 @@ Sometimes you want to dispose a container or scope. For example, when a request,
|
|
|
653
729
|
|
|
654
730
|
```typescript
|
|
655
731
|
import 'reflect-metadata';
|
|
656
|
-
import { Container, ContainerDisposedError, Registration as R, select } from 'ts-ioc-container';
|
|
732
|
+
import { bindTo, Container, ContainerDisposedError, register, Registration as R, select } from 'ts-ioc-container';
|
|
657
733
|
|
|
658
734
|
/**
|
|
659
735
|
* User Management Domain - Resource Cleanup
|
|
@@ -672,6 +748,7 @@ import { Container, ContainerDisposedError, Registration as R, select } from 'ts
|
|
|
672
748
|
*/
|
|
673
749
|
|
|
674
750
|
// Simulates a database connection that must be closed
|
|
751
|
+
@register(bindTo('IDatabase'))
|
|
675
752
|
class DatabaseConnection {
|
|
676
753
|
isClosed = false;
|
|
677
754
|
|
|
@@ -689,9 +766,7 @@ class DatabaseConnection {
|
|
|
689
766
|
|
|
690
767
|
describe('Disposing', function () {
|
|
691
768
|
it('should dispose container and prevent further usage', function () {
|
|
692
|
-
const appContainer = new Container({ tags: ['application'] }).addRegistration(
|
|
693
|
-
R.fromClass(DatabaseConnection).bindTo('IDatabase'),
|
|
694
|
-
);
|
|
769
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(DatabaseConnection));
|
|
695
770
|
|
|
696
771
|
// Create a request scope with a database connection
|
|
697
772
|
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
@@ -714,9 +789,7 @@ describe('Disposing', function () {
|
|
|
714
789
|
});
|
|
715
790
|
|
|
716
791
|
it('should clean up request-scoped resources on request end', function () {
|
|
717
|
-
const appContainer = new Container({ tags: ['application'] }).addRegistration(
|
|
718
|
-
R.fromClass(DatabaseConnection).bindTo('IDatabase'),
|
|
719
|
-
);
|
|
792
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(DatabaseConnection));
|
|
720
793
|
|
|
721
794
|
// Simulate Express.js request lifecycle
|
|
722
795
|
function handleRequest(): { connection: DatabaseConnection; scope: Container } {
|
|
@@ -751,6 +824,7 @@ describe('Disposing', function () {
|
|
|
751
824
|
```
|
|
752
825
|
|
|
753
826
|
### Lazy
|
|
827
|
+
|
|
754
828
|
Sometimes you want to create dependency only when somebody want to invoke it's method or property. This is what `lazy` is for.
|
|
755
829
|
|
|
756
830
|
- Lazy class instances are wrapped in a JavaScript `Proxy`; the real class instance is created on first property or method access.
|
|
@@ -886,9 +960,11 @@ describe('lazy provider', () => {
|
|
|
886
960
|
```
|
|
887
961
|
|
|
888
962
|
### Lazy with registerPipe
|
|
963
|
+
|
|
889
964
|
The `lazy()` registerPipe can be used in two ways: with the `@register` decorator or directly on the `Provider` pipe. This allows you to defer expensive class instance initialization until first access.
|
|
890
965
|
|
|
891
966
|
**Use cases:**
|
|
967
|
+
|
|
892
968
|
- Defer expensive initialization (database connections, SMTP, external APIs)
|
|
893
969
|
- Conditional features that may not be used
|
|
894
970
|
- Breaking circular dependencies
|
|
@@ -901,7 +977,7 @@ The `lazy()` registerPipe can be used in two ways: with the `@register` decorato
|
|
|
901
977
|
|
|
902
978
|
```typescript
|
|
903
979
|
import 'reflect-metadata';
|
|
904
|
-
import { bindTo, Container, inject, lazy, Provider, register, Registration as R, singleton } from 'ts-ioc-container';
|
|
980
|
+
import { args, bindTo, Container, inject, lazy, Provider, register, Registration as R, singleton } from 'ts-ioc-container';
|
|
905
981
|
|
|
906
982
|
/**
|
|
907
983
|
* Lazy Loading with registerPipe
|
|
@@ -1073,13 +1149,9 @@ describe('lazy registerPipe', () => {
|
|
|
1073
1149
|
it('should allow selective lazy loading - email lazy, SMS eager', () => {
|
|
1074
1150
|
const container = new Container()
|
|
1075
1151
|
// EmailService is lazy - won't connect to SMTP until used
|
|
1076
|
-
.addRegistration(
|
|
1077
|
-
R.fromClass(EmailService)
|
|
1078
|
-
.bindToKey('EmailService')
|
|
1079
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1080
|
-
)
|
|
1152
|
+
.addRegistration(R.fromClass(EmailService).pipe(singleton(), (p) => p.lazy()))
|
|
1081
1153
|
// SmsService is eager - connects to gateway immediately
|
|
1082
|
-
.addRegistration(R.fromClass(SmsService).
|
|
1154
|
+
.addRegistration(R.fromClass(SmsService).pipe(singleton()))
|
|
1083
1155
|
.addRegistration(R.fromClass(NotificationService));
|
|
1084
1156
|
|
|
1085
1157
|
// Resolve NotificationService
|
|
@@ -1096,12 +1168,8 @@ describe('lazy registerPipe', () => {
|
|
|
1096
1168
|
|
|
1097
1169
|
it('should initialize lazy email service when first accessed', () => {
|
|
1098
1170
|
const container = new Container()
|
|
1099
|
-
.addRegistration(
|
|
1100
|
-
|
|
1101
|
-
.bindToKey('EmailService')
|
|
1102
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1103
|
-
)
|
|
1104
|
-
.addRegistration(R.fromClass(SmsService).bindToKey('SmsService').pipe(singleton()))
|
|
1171
|
+
.addRegistration(R.fromClass(EmailService).pipe(singleton(), (p) => p.lazy()))
|
|
1172
|
+
.addRegistration(R.fromClass(SmsService).pipe(singleton()))
|
|
1105
1173
|
.addRegistration(R.fromClass(NotificationService));
|
|
1106
1174
|
|
|
1107
1175
|
const notifications = container.resolve<NotificationService>(NotificationService);
|
|
@@ -1116,16 +1184,8 @@ describe('lazy registerPipe', () => {
|
|
|
1116
1184
|
it('should work with multiple lazy providers', () => {
|
|
1117
1185
|
const container = new Container()
|
|
1118
1186
|
// Both services are lazy
|
|
1119
|
-
.addRegistration(
|
|
1120
|
-
|
|
1121
|
-
.bindToKey('EmailService')
|
|
1122
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1123
|
-
)
|
|
1124
|
-
.addRegistration(
|
|
1125
|
-
R.fromClass(SmsService)
|
|
1126
|
-
.bindToKey('SmsService')
|
|
1127
|
-
.pipe(singleton(), (p) => p.lazy()),
|
|
1128
|
-
)
|
|
1187
|
+
.addRegistration(R.fromClass(EmailService).pipe(singleton(), (p) => p.lazy()))
|
|
1188
|
+
.addRegistration(R.fromClass(SmsService).pipe(singleton(), (p) => p.lazy()))
|
|
1129
1189
|
.addRegistration(R.fromClass(NotificationService));
|
|
1130
1190
|
|
|
1131
1191
|
const notifications = container.resolve<NotificationService>(NotificationService);
|
|
@@ -1214,10 +1274,11 @@ describe('lazy registerPipe', () => {
|
|
|
1214
1274
|
* lazy() works seamlessly with other provider transformations.
|
|
1215
1275
|
*/
|
|
1216
1276
|
describe('combining with other pipes', () => {
|
|
1277
|
+
@register(bindTo('Config'))
|
|
1217
1278
|
class ConfigService {
|
|
1218
1279
|
constructor(
|
|
1219
|
-
public apiUrl: string,
|
|
1220
|
-
public timeout: number,
|
|
1280
|
+
@inject(args(0)) public apiUrl: string,
|
|
1281
|
+
@inject(args(1)) public timeout: number,
|
|
1221
1282
|
) {
|
|
1222
1283
|
initLog.push(`ConfigService initialized with ${apiUrl}`);
|
|
1223
1284
|
}
|
|
@@ -1226,7 +1287,6 @@ describe('lazy registerPipe', () => {
|
|
|
1226
1287
|
it('should combine lazy with args and singleton', () => {
|
|
1227
1288
|
const container = new Container().addRegistration(
|
|
1228
1289
|
R.fromClass(ConfigService)
|
|
1229
|
-
.bindToKey('Config')
|
|
1230
1290
|
.pipe(
|
|
1231
1291
|
(p) => p.setArgs(() => ['https://api.example.com', 5000]),
|
|
1232
1292
|
(p) => p.lazy(),
|
|
@@ -1263,6 +1323,7 @@ describe('lazy registerPipe', () => {
|
|
|
1263
1323
|
* - Report generators
|
|
1264
1324
|
*/
|
|
1265
1325
|
describe('real-world example - feature flags', () => {
|
|
1326
|
+
@register(singleton())
|
|
1266
1327
|
class FeatureFlagService {
|
|
1267
1328
|
constructor() {
|
|
1268
1329
|
initLog.push('FeatureFlagService initialized');
|
|
@@ -1302,7 +1363,7 @@ describe('lazy registerPipe', () => {
|
|
|
1302
1363
|
|
|
1303
1364
|
it('should not initialize premium features for standard users', () => {
|
|
1304
1365
|
const container = new Container()
|
|
1305
|
-
.addRegistration(R.fromClass(FeatureFlagService)
|
|
1366
|
+
.addRegistration(R.fromClass(FeatureFlagService))
|
|
1306
1367
|
.addRegistration(R.fromClass(PremiumFeature))
|
|
1307
1368
|
.addRegistration(R.fromClass(Application));
|
|
1308
1369
|
|
|
@@ -1316,7 +1377,7 @@ describe('lazy registerPipe', () => {
|
|
|
1316
1377
|
|
|
1317
1378
|
it('should initialize premium features only for premium users', () => {
|
|
1318
1379
|
const container = new Container()
|
|
1319
|
-
.addRegistration(R.fromClass(FeatureFlagService)
|
|
1380
|
+
.addRegistration(R.fromClass(FeatureFlagService))
|
|
1320
1381
|
.addRegistration(R.fromClass(PremiumFeature))
|
|
1321
1382
|
.addRegistration(R.fromClass(Application));
|
|
1322
1383
|
|
|
@@ -1333,6 +1394,7 @@ describe('lazy registerPipe', () => {
|
|
|
1333
1394
|
```
|
|
1334
1395
|
|
|
1335
1396
|
## Injector
|
|
1397
|
+
|
|
1336
1398
|
`IInjector` is used to describe how dependencies should be injected to constructor.
|
|
1337
1399
|
|
|
1338
1400
|
- `MetadataInjector` - injects dependencies using `@inject` decorator
|
|
@@ -1340,11 +1402,12 @@ describe('lazy registerPipe', () => {
|
|
|
1340
1402
|
- `SimpleInjector` - just passes container to constructor with others arguments
|
|
1341
1403
|
|
|
1342
1404
|
### Metadata
|
|
1405
|
+
|
|
1343
1406
|
This type of injector uses `@inject` decorator to mark where dependencies should be injected. It's bases on `reflect-metadata` package. That's why I call it `MetadataInjector`.
|
|
1344
1407
|
Also you can [inject property.](#inject-property)
|
|
1345
1408
|
|
|
1346
1409
|
```typescript
|
|
1347
|
-
import { Container, inject, Registration as R } from 'ts-ioc-container';
|
|
1410
|
+
import { bindTo, Container, inject, register, Registration as R } from 'ts-ioc-container';
|
|
1348
1411
|
|
|
1349
1412
|
/**
|
|
1350
1413
|
* User Management Domain - Metadata Injection
|
|
@@ -1361,6 +1424,7 @@ import { Container, inject, Registration as R } from 'ts-ioc-container';
|
|
|
1361
1424
|
* Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
|
|
1362
1425
|
*/
|
|
1363
1426
|
|
|
1427
|
+
@register(bindTo('ILogger'))
|
|
1364
1428
|
class Logger {
|
|
1365
1429
|
name = 'Logger';
|
|
1366
1430
|
}
|
|
@@ -1379,9 +1443,7 @@ class App {
|
|
|
1379
1443
|
|
|
1380
1444
|
describe('Metadata Injector', function () {
|
|
1381
1445
|
it('should inject dependencies using @inject decorator', function () {
|
|
1382
|
-
const container = new Container({ tags: ['application'] }).addRegistration(
|
|
1383
|
-
R.fromClass(Logger).bindToKey('ILogger'),
|
|
1384
|
-
);
|
|
1446
|
+
const container = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
1385
1447
|
|
|
1386
1448
|
// Container reads @inject metadata and resolves 'ILogger' for the logger parameter
|
|
1387
1449
|
const app = container.resolve(App);
|
|
@@ -1393,10 +1455,11 @@ describe('Metadata Injector', function () {
|
|
|
1393
1455
|
```
|
|
1394
1456
|
|
|
1395
1457
|
### Simple
|
|
1458
|
+
|
|
1396
1459
|
This type of injector just passes container to constructor with others arguments.
|
|
1397
1460
|
|
|
1398
1461
|
```typescript
|
|
1399
|
-
import { Container, type IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';
|
|
1462
|
+
import { bindTo, Container, type IContainer, register, Registration as R, SimpleInjector } from 'ts-ioc-container';
|
|
1400
1463
|
|
|
1401
1464
|
/**
|
|
1402
1465
|
* Command Pattern - Simple Injector
|
|
@@ -1424,6 +1487,7 @@ class CreateUserCommand implements ICommand {
|
|
|
1424
1487
|
constructor(readonly username: string) {}
|
|
1425
1488
|
}
|
|
1426
1489
|
|
|
1490
|
+
@register(bindTo('HandlerCreateUser'))
|
|
1427
1491
|
class CreateUserHandler implements ICommandHandler {
|
|
1428
1492
|
handle(command: CreateUserCommand): string {
|
|
1429
1493
|
return `User ${command.username} created`;
|
|
@@ -1433,6 +1497,7 @@ class CreateUserHandler implements ICommandHandler {
|
|
|
1433
1497
|
describe('SimpleInjector', function () {
|
|
1434
1498
|
it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
|
|
1435
1499
|
// Dispatcher needs the container to find handlers dynamically based on command type
|
|
1500
|
+
@register(bindTo('Dispatcher'))
|
|
1436
1501
|
class CommandDispatcher {
|
|
1437
1502
|
constructor(private container: IContainer) {}
|
|
1438
1503
|
|
|
@@ -1445,8 +1510,8 @@ describe('SimpleInjector', function () {
|
|
|
1445
1510
|
}
|
|
1446
1511
|
|
|
1447
1512
|
const container = new Container({ injector: new SimpleInjector() })
|
|
1448
|
-
.addRegistration(R.fromClass(CommandDispatcher)
|
|
1449
|
-
.addRegistration(R.fromClass(CreateUserHandler)
|
|
1513
|
+
.addRegistration(R.fromClass(CommandDispatcher))
|
|
1514
|
+
.addRegistration(R.fromClass(CreateUserHandler));
|
|
1450
1515
|
|
|
1451
1516
|
const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
|
|
1452
1517
|
const result = dispatcher.dispatch(new CreateUserCommand('alice'));
|
|
@@ -1467,9 +1532,7 @@ describe('SimpleInjector', function () {
|
|
|
1467
1532
|
}
|
|
1468
1533
|
}
|
|
1469
1534
|
|
|
1470
|
-
const container = new Container({ injector: new SimpleInjector() }).addRegistration(
|
|
1471
|
-
R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
|
|
1472
|
-
);
|
|
1535
|
+
const container = new Container({ injector: new SimpleInjector() }).addRegistration(R.fromClass(WidgetFactory));
|
|
1473
1536
|
|
|
1474
1537
|
// Pass "dark" as the theme argument
|
|
1475
1538
|
const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
|
|
@@ -1481,16 +1544,18 @@ describe('SimpleInjector', function () {
|
|
|
1481
1544
|
```
|
|
1482
1545
|
|
|
1483
1546
|
### Proxy
|
|
1547
|
+
|
|
1484
1548
|
This type of injector injects dependencies as dictionary `Record<string, unknown>`.
|
|
1485
1549
|
|
|
1486
1550
|
- **`args` reserved keyword**: accessing `deps.args` returns the raw `args[]` array passed at resolve time
|
|
1487
1551
|
- **Alias convention**: any property name containing `"alias"` (case-insensitive) is resolved via `resolveByAlias` instead of `resolve`
|
|
1488
1552
|
|
|
1489
1553
|
```typescript
|
|
1490
|
-
import { Container, ProxyInjector, Registration as R } from 'ts-ioc-container';
|
|
1554
|
+
import { bindTo, Container, ProxyInjector, register, Registration as R } from 'ts-ioc-container';
|
|
1491
1555
|
|
|
1492
1556
|
describe('ProxyInjector', function () {
|
|
1493
1557
|
it('should inject dependencies as a props object', function () {
|
|
1558
|
+
@register(bindTo('logger'))
|
|
1494
1559
|
class Logger {
|
|
1495
1560
|
log(msg: string) {
|
|
1496
1561
|
return `Logged: ${msg}`;
|
|
@@ -1517,9 +1582,9 @@ describe('ProxyInjector', function () {
|
|
|
1517
1582
|
}
|
|
1518
1583
|
|
|
1519
1584
|
const container = new Container({ injector: new ProxyInjector() })
|
|
1520
|
-
.addRegistration(R.fromClass(Logger)
|
|
1585
|
+
.addRegistration(R.fromClass(Logger))
|
|
1521
1586
|
.addRegistration(R.fromValue('USER:').bindToKey('prefix'))
|
|
1522
|
-
.addRegistration(R.fromClass(UserController)
|
|
1587
|
+
.addRegistration(R.fromClass(UserController));
|
|
1523
1588
|
|
|
1524
1589
|
const controller = container.resolve<UserController>('UserController');
|
|
1525
1590
|
|
|
@@ -1527,6 +1592,7 @@ describe('ProxyInjector', function () {
|
|
|
1527
1592
|
});
|
|
1528
1593
|
|
|
1529
1594
|
it('should expose runtime args through the reserved "args" property', function () {
|
|
1595
|
+
@register(bindTo('database'))
|
|
1530
1596
|
class Database {}
|
|
1531
1597
|
|
|
1532
1598
|
class ReportGenerator {
|
|
@@ -1544,8 +1610,8 @@ describe('ProxyInjector', function () {
|
|
|
1544
1610
|
}
|
|
1545
1611
|
|
|
1546
1612
|
const container = new Container({ injector: new ProxyInjector() })
|
|
1547
|
-
.addRegistration(R.fromClass(Database)
|
|
1548
|
-
.addRegistration(R.fromClass(ReportGenerator)
|
|
1613
|
+
.addRegistration(R.fromClass(Database))
|
|
1614
|
+
.addRegistration(R.fromClass(ReportGenerator));
|
|
1549
1615
|
|
|
1550
1616
|
const generator = container.resolve<ReportGenerator>('ReportGenerator', {
|
|
1551
1617
|
args: ['PDF'],
|
|
@@ -1582,6 +1648,7 @@ describe('ProxyInjector', function () {
|
|
|
1582
1648
|
```
|
|
1583
1649
|
|
|
1584
1650
|
## Provider
|
|
1651
|
+
|
|
1585
1652
|
Provider is dependency factory which creates dependency.
|
|
1586
1653
|
|
|
1587
1654
|
- `Provider.fromClass(Logger)`
|
|
@@ -1590,10 +1657,12 @@ Provider is dependency factory which creates dependency.
|
|
|
1590
1657
|
|
|
1591
1658
|
```typescript
|
|
1592
1659
|
import {
|
|
1660
|
+
args,
|
|
1593
1661
|
setArgs,
|
|
1594
1662
|
setArgsFn,
|
|
1595
1663
|
bindTo,
|
|
1596
1664
|
Container,
|
|
1665
|
+
inject,
|
|
1597
1666
|
lazy,
|
|
1598
1667
|
Provider,
|
|
1599
1668
|
register,
|
|
@@ -1667,7 +1736,7 @@ describe('Provider', () => {
|
|
|
1667
1736
|
|
|
1668
1737
|
it('supports args decorator for providing extra arguments', () => {
|
|
1669
1738
|
class FileService {
|
|
1670
|
-
constructor(readonly basePath: string) {}
|
|
1739
|
+
constructor(@inject(args(0)) readonly basePath: string) {}
|
|
1671
1740
|
}
|
|
1672
1741
|
|
|
1673
1742
|
const container = new Container().register(
|
|
@@ -1681,7 +1750,7 @@ describe('Provider', () => {
|
|
|
1681
1750
|
|
|
1682
1751
|
it('supports argsFn decorator for dynamic arguments', () => {
|
|
1683
1752
|
class Database {
|
|
1684
|
-
constructor(readonly connectionString: string) {}
|
|
1753
|
+
constructor(@inject(args(0)) readonly connectionString: string) {}
|
|
1685
1754
|
}
|
|
1686
1755
|
|
|
1687
1756
|
const container = new Container().register('DbPath', Provider.fromValue('localhost:5432')).register(
|
|
@@ -1735,6 +1804,7 @@ describe('Provider', () => {
|
|
|
1735
1804
|
```
|
|
1736
1805
|
|
|
1737
1806
|
### Singleton
|
|
1807
|
+
|
|
1738
1808
|
Sometimes you need to create only one instance of dependency per scope. For example, you want to create only one logger per scope.
|
|
1739
1809
|
|
|
1740
1810
|
- Singleton provider creates only one instance in every scope where it's resolved.
|
|
@@ -1827,23 +1897,30 @@ describe('Singleton', function () {
|
|
|
1827
1897
|
```
|
|
1828
1898
|
|
|
1829
1899
|
### Arguments
|
|
1900
|
+
|
|
1830
1901
|
Sometimes you want to bind some arguments to provider. This is what `ArgsProvider` is for.
|
|
1902
|
+
|
|
1831
1903
|
- `provider(setArgs('someArgument'))`
|
|
1832
1904
|
- `provider(setArgsFn((container) => [container.resolve(Logger), 'someValue']))`
|
|
1833
1905
|
- `Provider.fromClass(Logger).pipe(setArgs('someArgument'))`
|
|
1834
|
-
- Use `setArgsFn(resolveByArgs)` to resolve `InjectionToken` args at resolution time (tokens are resolved, primitives pass through)
|
|
1835
1906
|
|
|
1836
1907
|
### Token as argument
|
|
1837
|
-
|
|
1908
|
+
|
|
1909
|
+
When you pass an `InjectionToken` via `token.args(...)`, the container resolves it before the value reaches the constructor. Bare constructors are **not** auto-resolved — wrap a class in `ClassToken` to opt into resolution.
|
|
1910
|
+
|
|
1838
1911
|
- `ServiceToken.args(ValueToken)` — `ValueToken` is resolved from the container, its value is passed as arg
|
|
1912
|
+
- `ServiceToken.args(new ClassToken(SomeService))` — `SomeService` is constructed by the container
|
|
1839
1913
|
- `ServiceToken.args('literal')` — literal value passed directly
|
|
1840
1914
|
|
|
1841
1915
|
### Positional arg injection with `args(index)`
|
|
1842
|
-
|
|
1916
|
+
|
|
1917
|
+
Constructor parameters that should pick up positional args from `ProviderOptions` must be annotated with `@inject(args(index))`. Parameters without `@inject` resolve to `undefined`.
|
|
1918
|
+
|
|
1843
1919
|
- `@inject(args(0))` — resolves the first element of the `args` array passed at resolution time
|
|
1844
1920
|
- Works together with `token.args(...)` to pass typed dependencies through the args context
|
|
1845
1921
|
|
|
1846
1922
|
### Immutable token chaining
|
|
1923
|
+
|
|
1847
1924
|
`token.args(...)`, `token.argsFn(...)`, and `token.lazy()` all return **new token instances** — the parent token is never mutated. This allows the same token to be specialized in multiple independent ways (one-way linked list: parent → many children).
|
|
1848
1925
|
|
|
1849
1926
|
```typescript
|
|
@@ -1860,10 +1937,10 @@ import {
|
|
|
1860
1937
|
bindTo,
|
|
1861
1938
|
Container,
|
|
1862
1939
|
inject,
|
|
1940
|
+
args,
|
|
1863
1941
|
MultiCache,
|
|
1864
1942
|
register,
|
|
1865
1943
|
Registration as R,
|
|
1866
|
-
resolveByArgs,
|
|
1867
1944
|
singleton,
|
|
1868
1945
|
SingleToken,
|
|
1869
1946
|
} from 'ts-ioc-container';
|
|
@@ -1886,7 +1963,7 @@ describe('ArgsProvider', function () {
|
|
|
1886
1963
|
describe('Static Arguments', () => {
|
|
1887
1964
|
it('can pass static arguments to constructor', function () {
|
|
1888
1965
|
class FileLogger {
|
|
1889
|
-
constructor(public filename: string) {}
|
|
1966
|
+
constructor(@inject(args(0)) public filename: string) {}
|
|
1890
1967
|
}
|
|
1891
1968
|
|
|
1892
1969
|
// Pre-configure the logger with a filename
|
|
@@ -1899,7 +1976,7 @@ describe('ArgsProvider', function () {
|
|
|
1899
1976
|
|
|
1900
1977
|
it('prioritizes provided args over resolve args', function () {
|
|
1901
1978
|
class Logger {
|
|
1902
|
-
constructor(public context: string) {}
|
|
1979
|
+
constructor(@inject(args(0)) public context: string) {}
|
|
1903
1980
|
}
|
|
1904
1981
|
|
|
1905
1982
|
// 'FixedContext' wins over any runtime args
|
|
@@ -1920,7 +1997,7 @@ describe('ArgsProvider', function () {
|
|
|
1920
1997
|
}
|
|
1921
1998
|
|
|
1922
1999
|
class Service {
|
|
1923
|
-
constructor(public env: string) {}
|
|
2000
|
+
constructor(@inject(args(0)) public env: string) {}
|
|
1924
2001
|
}
|
|
1925
2002
|
|
|
1926
2003
|
const root = createContainer()
|
|
@@ -1960,17 +2037,18 @@ describe('ArgsProvider', function () {
|
|
|
1960
2037
|
name = 'TodoRepository';
|
|
1961
2038
|
}
|
|
1962
2039
|
|
|
1963
|
-
// EntityManager is generic - it works with ANY repository
|
|
1964
|
-
//
|
|
2040
|
+
// EntityManager is generic - it works with ANY repository.
|
|
2041
|
+
// The repository is the first arg passed via `EntityManagerToken.args(...)`.
|
|
2042
|
+
// `@inject(args(0))` reads it; the container auto-resolves InjectionToken args
|
|
2043
|
+
// before they reach the constructor.
|
|
1965
2044
|
const EntityManagerToken = new SingleToken<EntityManager>('EntityManager');
|
|
1966
2045
|
|
|
1967
2046
|
@register(
|
|
1968
2047
|
bindTo(EntityManagerToken),
|
|
1969
|
-
setArgsFn(resolveByArgs), // <--- Key magic: resolves dependencies based on arguments passed to token
|
|
1970
2048
|
singleton(MultiCache.fromFirstArg), // Cache unique instance per repository type
|
|
1971
2049
|
)
|
|
1972
2050
|
class EntityManager {
|
|
1973
|
-
constructor(public repository: IRepository) {}
|
|
2051
|
+
constructor(@inject(args(0)) public repository: IRepository) {}
|
|
1974
2052
|
}
|
|
1975
2053
|
|
|
1976
2054
|
class App {
|
|
@@ -2022,6 +2100,7 @@ describe('ArgsProvider', function () {
|
|
|
2022
2100
|
```
|
|
2023
2101
|
|
|
2024
2102
|
### Visibility
|
|
2103
|
+
|
|
2025
2104
|
Sometimes you want to hide dependency if somebody wants to resolve it from certain scope. This uses `ScopeAccessRule` to control access.
|
|
2026
2105
|
|
|
2027
2106
|
> [!IMPORTANT]
|
|
@@ -2122,7 +2201,9 @@ describe('Visibility', function () {
|
|
|
2122
2201
|
```
|
|
2123
2202
|
|
|
2124
2203
|
### Alias
|
|
2204
|
+
|
|
2125
2205
|
Alias is needed to group keys
|
|
2206
|
+
|
|
2126
2207
|
- `@register(asAlias('logger'))` helper assigns `logger` alias to registration.
|
|
2127
2208
|
- `by.aliases((it) => it.has('logger') || it.has('a'))` resolves dependencies which have `logger` or `a` aliases
|
|
2128
2209
|
- `Provider.fromClass(Logger).pipe(alias('logger'))`
|
|
@@ -2267,11 +2348,14 @@ describe('alias', () => {
|
|
|
2267
2348
|
```
|
|
2268
2349
|
|
|
2269
2350
|
### Decorator
|
|
2351
|
+
|
|
2270
2352
|
Sometimes you want to decorate you class with some logic. This is what `DecoratorProvider` is for.
|
|
2353
|
+
|
|
2271
2354
|
- `provider(decorate((instance, container) => new LoggerDecorator(instance)))`
|
|
2272
2355
|
|
|
2273
2356
|
```typescript
|
|
2274
2357
|
import {
|
|
2358
|
+
args,
|
|
2275
2359
|
bindTo,
|
|
2276
2360
|
Container,
|
|
2277
2361
|
decorate,
|
|
@@ -2325,7 +2409,7 @@ describe('Decorator Pattern', () => {
|
|
|
2325
2409
|
// Decorator: Wraps any IRepository with logging behavior
|
|
2326
2410
|
class LoggingRepository implements IRepository {
|
|
2327
2411
|
constructor(
|
|
2328
|
-
private repository: IRepository,
|
|
2412
|
+
@inject(args(0)) private repository: IRepository,
|
|
2329
2413
|
@inject(s.token('Logger').lazy()) private logger: Logger,
|
|
2330
2414
|
) {}
|
|
2331
2415
|
|
|
@@ -2381,7 +2465,9 @@ describe('Decorator Pattern', () => {
|
|
|
2381
2465
|
```
|
|
2382
2466
|
|
|
2383
2467
|
## Registration
|
|
2468
|
+
|
|
2384
2469
|
Registration is provider factory which registers provider in container.
|
|
2470
|
+
|
|
2385
2471
|
- `@register(asKey('logger'))`
|
|
2386
2472
|
- `Registration.fromClass(Logger).to('logger')`
|
|
2387
2473
|
- `Registration.fromClass(Logger)`
|
|
@@ -2389,6 +2475,7 @@ Registration is provider factory which registers provider in container.
|
|
|
2389
2475
|
- `Registration.fromFn((container, ...args) => container.resolve(Logger, {args}))`
|
|
2390
2476
|
|
|
2391
2477
|
### Key
|
|
2478
|
+
|
|
2392
2479
|
Sometimes you want to register provider with certain key. This is what `key` is for.
|
|
2393
2480
|
|
|
2394
2481
|
- by default, key is class name
|
|
@@ -2481,7 +2568,9 @@ describe('Registration module', function () {
|
|
|
2481
2568
|
```
|
|
2482
2569
|
|
|
2483
2570
|
### Scope
|
|
2571
|
+
|
|
2484
2572
|
Sometimes you need to register provider only in scope which matches to certain condition and their sub scopes. Especially if you want to register dependency as singleton for some tags, for example `root`. This uses `ScopeMatchRule` to determine which scopes should have the provider.
|
|
2573
|
+
|
|
2485
2574
|
- `@register(scope((container) => container.hasTag('root'))` - register provider only in root scope
|
|
2486
2575
|
- `Registration.fromClass(Logger).when((container) => container.hasTag('root'))`
|
|
2487
2576
|
|
|
@@ -2523,6 +2612,7 @@ describe('ScopeProvider', function () {
|
|
|
2523
2612
|
```
|
|
2524
2613
|
|
|
2525
2614
|
## Module
|
|
2615
|
+
|
|
2526
2616
|
Sometimes you want to encapsulate registration logic in separate module. This is what `IContainerModule` is for.
|
|
2527
2617
|
|
|
2528
2618
|
```typescript
|
|
@@ -2646,9 +2736,11 @@ describe('Container Modules', function () {
|
|
|
2646
2736
|
```
|
|
2647
2737
|
|
|
2648
2738
|
## Hook
|
|
2739
|
+
|
|
2649
2740
|
Sometimes you need to invoke methods after construct or dispose of class. This is what hooks are for.
|
|
2650
2741
|
|
|
2651
2742
|
### OnConstruct
|
|
2743
|
+
|
|
2652
2744
|
```typescript
|
|
2653
2745
|
import {
|
|
2654
2746
|
AddOnConstructHookModule,
|
|
@@ -2709,6 +2801,7 @@ describe('onConstruct', function () {
|
|
|
2709
2801
|
```
|
|
2710
2802
|
|
|
2711
2803
|
### OnDispose
|
|
2804
|
+
|
|
2712
2805
|
```typescript
|
|
2713
2806
|
import {
|
|
2714
2807
|
AddOnDisposeHookModule,
|
|
@@ -2823,6 +2916,10 @@ describe('inject property', () => {
|
|
|
2823
2916
|
|
|
2824
2917
|
## Error
|
|
2825
2918
|
|
|
2919
|
+
The product-facing error contract is described in
|
|
2920
|
+
`specs/epics/errors-and-boundaries.md` and executed by
|
|
2921
|
+
`__tests__/specs/errors-and-boundaries.spec.ts`.
|
|
2922
|
+
|
|
2826
2923
|
- [DependencyNotFoundError.ts](..%2F..%2Flib%2Ferrors%2FDependencyNotFoundError.ts)
|
|
2827
2924
|
- [MethodNotImplementedError.ts](..%2F..%2Flib%2Ferrors%2FMethodNotImplementedError.ts)
|
|
2828
2925
|
- [DependencyMissingKeyError.ts](..%2F..%2Flib%2Ferrors%2FDependencyMissingKeyError.ts)
|
package/cjm/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SingleAliasToken = exports.toGroupAlias = exports.GroupAliasToken = exports.InjectionToken = exports.HooksRunner = exports.AddOnDisposeHookModule = exports.onDispose = exports.onDisposeHooksRunner = exports.AddOnConstructHookModule = exports.onConstruct = exports.onConstructHooksRunner = exports.injectProp = exports.createHookContext = exports.createHookContextFactory = exports.HookContext = exports.hasHooks = exports.hook = exports.getHooks = exports.UnexpectedHookResultError = exports.ContainerDisposedError = exports.MethodNotImplementedError = exports.DependencyMissingKeyError = exports.DependencyNotFoundError = exports.Registration = exports.bindTo = exports.register = exports.scope = exports.decorate = exports.MultiCache = exports.multiCache = exports.SingletonProvider = exports.singleton = exports.Provider = exports.
|
|
4
|
-
exports.resolveConstructor = exports.Is = exports.pipe = exports.select = exports.once = exports.shallowCache = exports.debounce = exports.throttle = exports.handleAsyncError = exports.handleError = exports.getMethodTags = exports.methodTag = exports.getMethodLabels = exports.methodLabel = exports.getMethodMeta = exports.methodMeta = exports.getParamTags = exports.paramTag = exports.getParamLabels = exports.paramLabel = exports.getParamMeta = exports.paramMeta = exports.getClassTags = exports.classTag = exports.getClassLabels = exports.classLabel = exports.getClassMeta = exports.classMeta = exports.GroupInstanceToken = exports.ConstantToken = exports.FunctionToken = exports.SingleToken = exports.ClassToken =
|
|
3
|
+
exports.toSingleAlias = exports.SingleAliasToken = exports.toGroupAlias = exports.GroupAliasToken = exports.InjectionToken = exports.HooksRunner = exports.AddOnDisposeHookModule = exports.onDispose = exports.onDisposeHooksRunner = exports.AddOnConstructHookModule = exports.onConstruct = exports.onConstructHooksRunner = exports.injectProp = exports.createHookContext = exports.createHookContextFactory = exports.HookContext = exports.hasHooks = exports.hook = exports.getHooks = exports.UnexpectedHookResultError = exports.ContainerDisposedError = exports.MethodNotImplementedError = exports.DependencyMissingKeyError = exports.DependencyNotFoundError = exports.Registration = exports.bindTo = exports.register = exports.scope = exports.decorate = exports.MultiCache = exports.multiCache = exports.SingletonProvider = exports.singleton = exports.Provider = exports.ProviderDecorator = exports.setArgs = exports.setArgsFn = exports.lazy = exports.scopeAccess = exports.ProxyInjector = exports.SimpleInjector = exports.MetadataInjector = exports.Injector = exports.argsFn = exports.args = exports.resolveArgs = exports.inject = exports.EmptyContainer = exports.Container = exports.isDependencyKey = void 0;
|
|
4
|
+
exports.resolveConstructor = exports.Is = exports.pipe = exports.select = exports.once = exports.shallowCache = exports.debounce = exports.throttle = exports.handleAsyncError = exports.handleError = exports.getMethodTags = exports.methodTag = exports.getMethodLabels = exports.methodLabel = exports.getMethodMeta = exports.methodMeta = exports.getParamTags = exports.paramTag = exports.getParamLabels = exports.paramLabel = exports.getParamMeta = exports.paramMeta = exports.getClassTags = exports.classTag = exports.getClassLabels = exports.classLabel = exports.getClassMeta = exports.classMeta = exports.GroupInstanceToken = exports.ConstantToken = exports.FunctionToken = exports.SingleToken = exports.ClassToken = void 0;
|
|
5
5
|
// Containers
|
|
6
6
|
var IContainer_1 = require("./container/IContainer");
|
|
7
7
|
Object.defineProperty(exports, "isDependencyKey", { enumerable: true, get: function () { return IContainer_1.isDependencyKey; } });
|
|
@@ -30,7 +30,6 @@ Object.defineProperty(exports, "lazy", { enumerable: true, get: function () { re
|
|
|
30
30
|
Object.defineProperty(exports, "setArgsFn", { enumerable: true, get: function () { return IProvider_1.setArgsFn; } });
|
|
31
31
|
Object.defineProperty(exports, "setArgs", { enumerable: true, get: function () { return IProvider_1.setArgs; } });
|
|
32
32
|
Object.defineProperty(exports, "ProviderDecorator", { enumerable: true, get: function () { return IProvider_1.ProviderDecorator; } });
|
|
33
|
-
Object.defineProperty(exports, "resolveByArgs", { enumerable: true, get: function () { return IProvider_1.resolveByArgs; } });
|
|
34
33
|
var Provider_1 = require("./provider/Provider");
|
|
35
34
|
Object.defineProperty(exports, "Provider", { enumerable: true, get: function () { return Provider_1.Provider; } });
|
|
36
35
|
var SingletonProvider_1 = require("./provider/SingletonProvider");
|
package/cjm/injector/inject.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.resolveArgs = exports.argsFn = exports.args = exports.inject = void 0;
|
|
|
4
4
|
const InjectionToken_1 = require("../token/InjectionToken");
|
|
5
5
|
const ConstantToken_1 = require("../token/ConstantToken");
|
|
6
6
|
const toToken_1 = require("../token/toToken");
|
|
7
|
-
const array_1 = require("../utils/array");
|
|
8
7
|
const basic_1 = require("../utils/basic");
|
|
9
8
|
const parameter_1 = require("../metadata/parameter");
|
|
10
9
|
const hookMetaKey = (methodName = 'constructor') => `inject:${methodName}`;
|
|
@@ -18,14 +17,11 @@ const args = (index) => (c, { args = [] }) => {
|
|
|
18
17
|
exports.args = args;
|
|
19
18
|
const argsFn = (fn) => (c, options) => fn(options.args ?? []);
|
|
20
19
|
exports.argsFn = argsFn;
|
|
20
|
+
const resolveTokens = (scope, deps) => deps.map((v) => ((0, InjectionToken_1.isInjectionToken)(v) ? v : new ConstantToken_1.ConstantToken(v))).map((t) => t.resolve(scope));
|
|
21
21
|
const resolveArgs = (Target, methodName) => {
|
|
22
|
-
const
|
|
22
|
+
const argsMetaTokens = (0, parameter_1.getParamMeta)(hookMetaKey(methodName), Target);
|
|
23
23
|
return (scope, { args = [], lazy }) => {
|
|
24
|
-
|
|
25
|
-
return !(0, InjectionToken_1.isInjectionToken)(v) ? new ConstantToken_1.ConstantToken(v) : v;
|
|
26
|
-
});
|
|
27
|
-
const allTokens = (0, array_1.fillEmptyIndexes)(argsTokens, depsTokens);
|
|
28
|
-
return allTokens.map((fn) => fn.resolve(scope, { args, lazy }));
|
|
24
|
+
return argsMetaTokens.map((fn) => fn.resolve(scope, { args: resolveTokens(scope, args), lazy }));
|
|
29
25
|
};
|
|
30
26
|
};
|
|
31
27
|
exports.resolveArgs = resolveArgs;
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ProviderDecorator = exports.lazy = exports.scopeAccess = exports.
|
|
3
|
+
exports.ProviderDecorator = exports.lazy = exports.scopeAccess = exports.setArgsFn = exports.setArgs = void 0;
|
|
4
4
|
const ProviderPipe_1 = require("./ProviderPipe");
|
|
5
|
-
const InjectionToken_1 = require("../token/InjectionToken");
|
|
6
|
-
const basic_1 = require("../utils/basic");
|
|
7
5
|
const setArgs = (...extraArgs) => (0, ProviderPipe_1.registerPipe)((p) => p.setArgs(() => extraArgs));
|
|
8
6
|
exports.setArgs = setArgs;
|
|
9
7
|
const setArgsFn = (fn) => (0, ProviderPipe_1.registerPipe)((p) => p.setArgs(fn));
|
|
10
8
|
exports.setArgsFn = setArgsFn;
|
|
11
|
-
const resolveByArgs = (s, { args = [] } = {}) => args.map((d) => {
|
|
12
|
-
if (d instanceof InjectionToken_1.InjectionToken) {
|
|
13
|
-
return d.resolve(s);
|
|
14
|
-
}
|
|
15
|
-
if (basic_1.Is.constructor(d)) {
|
|
16
|
-
return s.resolve(d);
|
|
17
|
-
}
|
|
18
|
-
return d;
|
|
19
|
-
});
|
|
20
|
-
exports.resolveByArgs = resolveByArgs;
|
|
21
9
|
const scopeAccess = (rule) => (0, ProviderPipe_1.registerPipe)((p) => p.setAccessRule(rule));
|
|
22
10
|
exports.scopeAccess = scopeAccess;
|
|
23
11
|
const lazy = () => (0, ProviderPipe_1.registerPipe)((p) => p.lazy());
|
package/cjm/utils/array.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Filter = void 0;
|
|
4
|
-
exports.fillEmptyIndexes = fillEmptyIndexes;
|
|
5
4
|
exports.Filter = {
|
|
6
5
|
exclude: (arr) => {
|
|
7
6
|
const excludeSet = arr instanceof Array ? new Set(arr) : arr;
|
|
8
7
|
return (v) => !excludeSet.has(v);
|
|
9
8
|
},
|
|
10
9
|
};
|
|
11
|
-
function fillEmptyIndexes(baseArr, insertArr) {
|
|
12
|
-
const a = [...baseArr];
|
|
13
|
-
const b = [...insertArr];
|
|
14
|
-
for (let i = 0; i < a.length; i++) {
|
|
15
|
-
if (a[i] === undefined) {
|
|
16
|
-
a[i] = b.shift();
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return a.concat(b);
|
|
20
|
-
}
|
package/esm/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { MetadataInjector } from './injector/MetadataInjector';
|
|
|
9
9
|
export { SimpleInjector } from './injector/SimpleInjector';
|
|
10
10
|
export { ProxyInjector } from './injector/ProxyInjector';
|
|
11
11
|
// Providers
|
|
12
|
-
export { scopeAccess, lazy, setArgsFn, setArgs, ProviderDecorator,
|
|
12
|
+
export { scopeAccess, lazy, setArgsFn, setArgs, ProviderDecorator, } from './provider/IProvider';
|
|
13
13
|
export { Provider } from './provider/Provider';
|
|
14
14
|
export { singleton, SingletonProvider } from './provider/SingletonProvider';
|
|
15
15
|
export { multiCache, MultiCache } from './provider/Cache';
|
package/esm/injector/inject.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isInjectionToken } from '../token/InjectionToken';
|
|
2
2
|
import { ConstantToken } from '../token/ConstantToken';
|
|
3
3
|
import { toToken } from '../token/toToken';
|
|
4
|
-
import { fillEmptyIndexes } from '../utils/array';
|
|
5
4
|
import { Is } from '../utils/basic';
|
|
6
5
|
import { getParamMeta, paramMeta } from '../metadata/parameter';
|
|
7
6
|
const hookMetaKey = (methodName = 'constructor') => `inject:${methodName}`;
|
|
@@ -12,13 +11,10 @@ export const args = (index) => (c, { args = [] }) => {
|
|
|
12
11
|
return args[index];
|
|
13
12
|
};
|
|
14
13
|
export const argsFn = (fn) => (c, options) => fn(options.args ?? []);
|
|
14
|
+
const resolveTokens = (scope, deps) => deps.map((v) => (isInjectionToken(v) ? v : new ConstantToken(v))).map((t) => t.resolve(scope));
|
|
15
15
|
export const resolveArgs = (Target, methodName) => {
|
|
16
|
-
const
|
|
16
|
+
const argsMetaTokens = getParamMeta(hookMetaKey(methodName), Target);
|
|
17
17
|
return (scope, { args = [], lazy }) => {
|
|
18
|
-
|
|
19
|
-
return !isInjectionToken(v) ? new ConstantToken(v) : v;
|
|
20
|
-
});
|
|
21
|
-
const allTokens = fillEmptyIndexes(argsTokens, depsTokens);
|
|
22
|
-
return allTokens.map((fn) => fn.resolve(scope, { args, lazy }));
|
|
18
|
+
return argsMetaTokens.map((fn) => fn.resolve(scope, { args: resolveTokens(scope, args), lazy }));
|
|
23
19
|
};
|
|
24
20
|
};
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { isProviderPipe, registerPipe } from './ProviderPipe';
|
|
2
|
-
import { InjectionToken } from '../token/InjectionToken';
|
|
3
|
-
import { Is } from '../utils/basic';
|
|
4
2
|
export const setArgs = (...extraArgs) => registerPipe((p) => p.setArgs(() => extraArgs));
|
|
5
3
|
export const setArgsFn = (fn) => registerPipe((p) => p.setArgs(fn));
|
|
6
|
-
export const resolveByArgs = (s, { args = [] } = {}) => args.map((d) => {
|
|
7
|
-
if (d instanceof InjectionToken) {
|
|
8
|
-
return d.resolve(s);
|
|
9
|
-
}
|
|
10
|
-
if (Is.constructor(d)) {
|
|
11
|
-
return s.resolve(d);
|
|
12
|
-
}
|
|
13
|
-
return d;
|
|
14
|
-
});
|
|
15
4
|
export const scopeAccess = (rule) => registerPipe((p) => p.setAccessRule(rule));
|
|
16
5
|
export const lazy = () => registerPipe((p) => p.lazy());
|
|
17
6
|
export class ProviderDecorator {
|
package/esm/utils/array.js
CHANGED
|
@@ -4,13 +4,3 @@ export const Filter = {
|
|
|
4
4
|
return (v) => !excludeSet.has(v);
|
|
5
5
|
},
|
|
6
6
|
};
|
|
7
|
-
export function fillEmptyIndexes(baseArr, insertArr) {
|
|
8
|
-
const a = [...baseArr];
|
|
9
|
-
const b = [...insertArr];
|
|
10
|
-
for (let i = 0; i < a.length; i++) {
|
|
11
|
-
if (a[i] === undefined) {
|
|
12
|
-
a[i] = b.shift();
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return a.concat(b);
|
|
16
|
-
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-ioc-container",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "48.0.0",
|
|
4
|
+
"description": "Fast, lightweight TypeScript dependency injection container with a clean API, scoped lifecycles, decorators, tokens, hooks, lazy injection, customizable providers, and no global container objects.",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"docs"
|
|
7
7
|
],
|
|
@@ -21,10 +21,24 @@
|
|
|
21
21
|
"inversion-of-control",
|
|
22
22
|
"container",
|
|
23
23
|
"typescript",
|
|
24
|
+
"typescript-di",
|
|
25
|
+
"typescript-ioc",
|
|
26
|
+
"dependency-injection-container",
|
|
24
27
|
"ioc",
|
|
25
28
|
"di",
|
|
29
|
+
"ioc-container",
|
|
30
|
+
"tsyringe-alternative",
|
|
31
|
+
"inversify-alternative",
|
|
32
|
+
"awilix-alternative",
|
|
33
|
+
"clean-api",
|
|
34
|
+
"no-global-container",
|
|
26
35
|
"scope",
|
|
36
|
+
"scoped-dependencies",
|
|
27
37
|
"hook",
|
|
38
|
+
"lifecycle-hooks",
|
|
39
|
+
"decorators",
|
|
40
|
+
"lazy-injection",
|
|
41
|
+
"service-container",
|
|
28
42
|
"inject"
|
|
29
43
|
],
|
|
30
44
|
"directories": {
|
|
@@ -47,12 +61,14 @@
|
|
|
47
61
|
"generate:docs": "scripts/generate-readme/generate-readme.ts && git add README.md",
|
|
48
62
|
"build": "npm run build:cjm && npm run build:esm && npm run build:types",
|
|
49
63
|
"test": "vitest run",
|
|
64
|
+
"test:spec": "vitest run __tests__/specs",
|
|
65
|
+
"bench:spec": "vitest bench __benchmarks__/specs/*.bench.ts",
|
|
50
66
|
"test:coverage": "vitest run --coverage --coverage.reporter=lcov",
|
|
51
67
|
"type-check": "tsc --noEmit",
|
|
52
68
|
"type-check:watch": "tsc --noEmit --watch",
|
|
53
69
|
"commit": "cz",
|
|
54
70
|
"format": "prettier --write \"**/*.ts\"",
|
|
55
|
-
"lint": "eslint lib/**/*.ts
|
|
71
|
+
"lint": "eslint lib/**/*.ts __tests__/**/*.ts __benchmarks__/**/*.ts scripts/**/*.ts",
|
|
56
72
|
"lint:fix": "npm run lint --fix",
|
|
57
73
|
"audit": "npm audit",
|
|
58
74
|
"prepare": "husky",
|
|
@@ -90,6 +106,7 @@
|
|
|
90
106
|
"rimraf": "6.1.3",
|
|
91
107
|
"semantic-release": "^25.0.2",
|
|
92
108
|
"tsx": "^4.21.0",
|
|
109
|
+
"tsyringe": "^4.10.0",
|
|
93
110
|
"typescript": "5.8.3",
|
|
94
111
|
"unplugin-swc": "^1.5.9",
|
|
95
112
|
"vite": "^6.4.1",
|
package/typings/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { type IInjector, type InjectOptions, type IInjectFnResolver, Injector }
|
|
|
6
6
|
export { MetadataInjector } from './injector/MetadataInjector';
|
|
7
7
|
export { SimpleInjector } from './injector/SimpleInjector';
|
|
8
8
|
export { ProxyInjector } from './injector/ProxyInjector';
|
|
9
|
-
export { type ResolveDependency, type IProvider, scopeAccess, lazy, setArgsFn, setArgs, type ArgsFn, ProviderDecorator,
|
|
9
|
+
export { type ResolveDependency, type IProvider, scopeAccess, lazy, setArgsFn, setArgs, type ArgsFn, ProviderDecorator, type IMapper, type ProviderOptions, } from './provider/IProvider';
|
|
10
10
|
export { Provider } from './provider/Provider';
|
|
11
11
|
export { singleton, SingletonProvider } from './provider/SingletonProvider';
|
|
12
12
|
export { type Cache, multiCache, MultiCache } from './provider/Cache';
|
|
@@ -26,7 +26,6 @@ export interface IProvider<T = any> {
|
|
|
26
26
|
}
|
|
27
27
|
export declare const setArgs: <T>(...extraArgs: unknown[]) => ProviderPipe<T>;
|
|
28
28
|
export declare const setArgsFn: <T>(fn: ArgsFn) => ProviderPipe<T>;
|
|
29
|
-
export declare const resolveByArgs: ArgsFn;
|
|
30
29
|
export declare const scopeAccess: <T>(rule: ScopeAccessRule) => ProviderPipe<T>;
|
|
31
30
|
export declare const lazy: <T>() => ProviderPipe<T>;
|
|
32
31
|
export declare abstract class ProviderDecorator<T> implements IProvider<T> {
|
package/typings/utils/array.d.ts
CHANGED