@brianmcd/di 0.0.1

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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +320 -0
  3. package/dist/cjs/index.d.ts +12 -0
  4. package/dist/cjs/index.d.ts.map +1 -0
  5. package/dist/cjs/index.js +16 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/cjs/lib/container-builder.d.ts +76 -0
  8. package/dist/cjs/lib/container-builder.d.ts.map +1 -0
  9. package/dist/cjs/lib/container-builder.js +283 -0
  10. package/dist/cjs/lib/container-builder.js.map +1 -0
  11. package/dist/cjs/lib/container.d.ts +52 -0
  12. package/dist/cjs/lib/container.d.ts.map +1 -0
  13. package/dist/cjs/lib/container.js +113 -0
  14. package/dist/cjs/lib/container.js.map +1 -0
  15. package/dist/cjs/lib/scoped-container.d.ts +36 -0
  16. package/dist/cjs/lib/scoped-container.d.ts.map +1 -0
  17. package/dist/cjs/lib/scoped-container.js +119 -0
  18. package/dist/cjs/lib/scoped-container.js.map +1 -0
  19. package/dist/cjs/lib/types/factory-provider.d.ts +18 -0
  20. package/dist/cjs/lib/types/factory-provider.d.ts.map +1 -0
  21. package/dist/cjs/lib/types/factory-provider.js +3 -0
  22. package/dist/cjs/lib/types/factory-provider.js.map +1 -0
  23. package/dist/cjs/lib/types/injectable-class.d.ts +11 -0
  24. package/dist/cjs/lib/types/injectable-class.d.ts.map +1 -0
  25. package/dist/cjs/lib/types/injectable-class.js +3 -0
  26. package/dist/cjs/lib/types/injectable-class.js.map +1 -0
  27. package/dist/cjs/lib/types/lifecycle.d.ts +13 -0
  28. package/dist/cjs/lib/types/lifecycle.d.ts.map +1 -0
  29. package/dist/cjs/lib/types/lifecycle.js +3 -0
  30. package/dist/cjs/lib/types/lifecycle.js.map +1 -0
  31. package/dist/cjs/lib/types/options.d.ts +5 -0
  32. package/dist/cjs/lib/types/options.d.ts.map +1 -0
  33. package/dist/cjs/lib/types/options.js +3 -0
  34. package/dist/cjs/lib/types/options.js.map +1 -0
  35. package/dist/cjs/lib/types/resolvers.d.ts +15 -0
  36. package/dist/cjs/lib/types/resolvers.d.ts.map +1 -0
  37. package/dist/cjs/lib/types/resolvers.js +3 -0
  38. package/dist/cjs/lib/types/resolvers.js.map +1 -0
  39. package/dist/cjs/lib/types/scope.d.ts +6 -0
  40. package/dist/cjs/lib/types/scope.d.ts.map +1 -0
  41. package/dist/cjs/lib/types/scope.js +8 -0
  42. package/dist/cjs/lib/types/scope.js.map +1 -0
  43. package/dist/cjs/lib/types/tokens.d.ts +13 -0
  44. package/dist/cjs/lib/types/tokens.d.ts.map +1 -0
  45. package/dist/cjs/lib/types/tokens.js +3 -0
  46. package/dist/cjs/lib/types/tokens.js.map +1 -0
  47. package/dist/cjs/lib/utils/create-token.d.ts +10 -0
  48. package/dist/cjs/lib/utils/create-token.d.ts.map +1 -0
  49. package/dist/cjs/lib/utils/create-token.js +14 -0
  50. package/dist/cjs/lib/utils/create-token.js.map +1 -0
  51. package/dist/cjs/lib/utils/define-factory.d.ts +28 -0
  52. package/dist/cjs/lib/utils/define-factory.d.ts.map +1 -0
  53. package/dist/cjs/lib/utils/define-factory.js +31 -0
  54. package/dist/cjs/lib/utils/define-factory.js.map +1 -0
  55. package/dist/cjs/lib/utils/has-on-destroy.d.ts +6 -0
  56. package/dist/cjs/lib/utils/has-on-destroy.d.ts.map +1 -0
  57. package/dist/cjs/lib/utils/has-on-destroy.js +10 -0
  58. package/dist/cjs/lib/utils/has-on-destroy.js.map +1 -0
  59. package/dist/cjs/lib/utils/has-on-init.d.ts +6 -0
  60. package/dist/cjs/lib/utils/has-on-init.d.ts.map +1 -0
  61. package/dist/cjs/lib/utils/has-on-init.js +10 -0
  62. package/dist/cjs/lib/utils/has-on-init.js.map +1 -0
  63. package/dist/cjs/lib/utils/token-to-string.d.ts +6 -0
  64. package/dist/cjs/lib/utils/token-to-string.d.ts.map +1 -0
  65. package/dist/cjs/lib/utils/token-to-string.js +13 -0
  66. package/dist/cjs/lib/utils/token-to-string.js.map +1 -0
  67. package/dist/cjs/package.json +1 -0
  68. package/dist/esm/index.d.ts +12 -0
  69. package/dist/esm/index.d.ts.map +1 -0
  70. package/dist/esm/index.js +7 -0
  71. package/dist/esm/index.js.map +1 -0
  72. package/dist/esm/lib/container-builder.d.ts +76 -0
  73. package/dist/esm/lib/container-builder.d.ts.map +1 -0
  74. package/dist/esm/lib/container-builder.js +279 -0
  75. package/dist/esm/lib/container-builder.js.map +1 -0
  76. package/dist/esm/lib/container.d.ts +52 -0
  77. package/dist/esm/lib/container.d.ts.map +1 -0
  78. package/dist/esm/lib/container.js +109 -0
  79. package/dist/esm/lib/container.js.map +1 -0
  80. package/dist/esm/lib/scoped-container.d.ts +36 -0
  81. package/dist/esm/lib/scoped-container.d.ts.map +1 -0
  82. package/dist/esm/lib/scoped-container.js +115 -0
  83. package/dist/esm/lib/scoped-container.js.map +1 -0
  84. package/dist/esm/lib/types/factory-provider.d.ts +18 -0
  85. package/dist/esm/lib/types/factory-provider.d.ts.map +1 -0
  86. package/dist/esm/lib/types/factory-provider.js +2 -0
  87. package/dist/esm/lib/types/factory-provider.js.map +1 -0
  88. package/dist/esm/lib/types/injectable-class.d.ts +11 -0
  89. package/dist/esm/lib/types/injectable-class.d.ts.map +1 -0
  90. package/dist/esm/lib/types/injectable-class.js +2 -0
  91. package/dist/esm/lib/types/injectable-class.js.map +1 -0
  92. package/dist/esm/lib/types/lifecycle.d.ts +13 -0
  93. package/dist/esm/lib/types/lifecycle.d.ts.map +1 -0
  94. package/dist/esm/lib/types/lifecycle.js +2 -0
  95. package/dist/esm/lib/types/lifecycle.js.map +1 -0
  96. package/dist/esm/lib/types/options.d.ts +5 -0
  97. package/dist/esm/lib/types/options.d.ts.map +1 -0
  98. package/dist/esm/lib/types/options.js +2 -0
  99. package/dist/esm/lib/types/options.js.map +1 -0
  100. package/dist/esm/lib/types/resolvers.d.ts +15 -0
  101. package/dist/esm/lib/types/resolvers.d.ts.map +1 -0
  102. package/dist/esm/lib/types/resolvers.js +2 -0
  103. package/dist/esm/lib/types/resolvers.js.map +1 -0
  104. package/dist/esm/lib/types/scope.d.ts +6 -0
  105. package/dist/esm/lib/types/scope.d.ts.map +1 -0
  106. package/dist/esm/lib/types/scope.js +5 -0
  107. package/dist/esm/lib/types/scope.js.map +1 -0
  108. package/dist/esm/lib/types/tokens.d.ts +13 -0
  109. package/dist/esm/lib/types/tokens.d.ts.map +1 -0
  110. package/dist/esm/lib/types/tokens.js +2 -0
  111. package/dist/esm/lib/types/tokens.js.map +1 -0
  112. package/dist/esm/lib/utils/create-token.d.ts +10 -0
  113. package/dist/esm/lib/utils/create-token.d.ts.map +1 -0
  114. package/dist/esm/lib/utils/create-token.js +11 -0
  115. package/dist/esm/lib/utils/create-token.js.map +1 -0
  116. package/dist/esm/lib/utils/define-factory.d.ts +28 -0
  117. package/dist/esm/lib/utils/define-factory.d.ts.map +1 -0
  118. package/dist/esm/lib/utils/define-factory.js +28 -0
  119. package/dist/esm/lib/utils/define-factory.js.map +1 -0
  120. package/dist/esm/lib/utils/has-on-destroy.d.ts +6 -0
  121. package/dist/esm/lib/utils/has-on-destroy.d.ts.map +1 -0
  122. package/dist/esm/lib/utils/has-on-destroy.js +7 -0
  123. package/dist/esm/lib/utils/has-on-destroy.js.map +1 -0
  124. package/dist/esm/lib/utils/has-on-init.d.ts +6 -0
  125. package/dist/esm/lib/utils/has-on-init.d.ts.map +1 -0
  126. package/dist/esm/lib/utils/has-on-init.js +7 -0
  127. package/dist/esm/lib/utils/has-on-init.js.map +1 -0
  128. package/dist/esm/lib/utils/token-to-string.d.ts +6 -0
  129. package/dist/esm/lib/utils/token-to-string.d.ts.map +1 -0
  130. package/dist/esm/lib/utils/token-to-string.js +10 -0
  131. package/dist/esm/lib/utils/token-to-string.js.map +1 -0
  132. package/package.json +75 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brian McDaniel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # @brianmcd/di
2
+
3
+ A lightweight, type-safe dependency injection container for Node.
4
+
5
+ [![npm version](https://badge.fury.io/js/%40brianmcd%2Fdi.svg)](https://www.npmjs.com/package/@brianmcd/di)
6
+ [![CI](https://github.com/brianmcd/di/actions/workflows/ci.yml/badge.svg)](https://github.com/brianmcd/di/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Motivation
10
+
11
+ Why yet another DI container for Node?
12
+
13
+ I couldn't find one with the following feature set:
14
+
15
+ - **No decorators**: I don't want to rely on experimental decorators, emitDecoratorMetadata, or reflect-metadata.
16
+ - **Async factories**: When using a factory provider, the factory needs to be able to be async.
17
+ - **Lifecycle hooks**: When providing a class, I want to be able to have async `onInit` and `onDestroy` methods so that I can do initialization and cleanup.
18
+ - **Scoped containers**: By default, providers create singletons, but sometimes you need things to be scoped in some way (e.g. request-scoped).
19
+ - **Type safety**: If I have a mismatch between what I've declared and what I'm using, I want a compiler error.
20
+
21
+ These are the core design goals of `@brianmcd/di`. Beyond these, the goal is to keep the library small and simple.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @brianmcd/di
27
+ ```
28
+
29
+ ## Example
30
+
31
+ ```typescript
32
+ import { ContainerBuilder, OnInit, OnDestroy } from '@brianmcd/di';
33
+
34
+ class UserRepository implements OnInit, OnDestroy {
35
+ public async onInit() {
36
+ // Do some async or sync initialization, if you want.
37
+ }
38
+
39
+ public async onDestroy() {
40
+ // Do some async or sync cleanup, if you want.
41
+ }
42
+
43
+ public async findById(id: string) {
44
+ return { id, name: 'Alice' };
45
+ }
46
+ }
47
+
48
+ class UserService {
49
+ static readonly deps = [UserRepository] as const;
50
+
51
+ constructor(private readonly userRepo: UserRepository) {}
52
+
53
+ public async getUser(id: string) {
54
+ return this.userRepo.findById(id);
55
+ }
56
+ }
57
+
58
+ // Build the container - dependencies are resolved automatically and registration order doesn't matter.
59
+ const container = await new ContainerBuilder()
60
+ .registerClass(UserRepository)
61
+ .registerClass(UserService)
62
+ .build();
63
+
64
+ // Use your services
65
+ const userService = container.get(UserService);
66
+ const user = await userService.getUser('123');
67
+ console.log(user); // { id: '123', name: 'Alice' }
68
+
69
+ // Cleanup - calls onDestroy in reverse order.
70
+ await container.destroy();
71
+ ```
72
+
73
+ ## Overview
74
+
75
+ To create a new `Container`, use the `ContainerBuilder`. The gist of it is that you register your providers with a `ContainerBuilder`, then call `.build()`. The `ContainerBuilder` takes care of instantiating your dependencies in the correct order, ensuring that dependencies are created before their dependents. If all goes well, you'll get a `Container` instance returned from `.build()`.
76
+
77
+ To add a provider, call the appropriate method on the `ContainerBuilder`.
78
+
79
+ There are 3 provider types you can use:
80
+
81
+ 1. Class providers: call `.registerClass(SomeClass)`. Class providers use constructor injection and need to have a static `deps` array that declares their dependencies. There's type safety to ensure that the `deps` array matches your constructor signature.
82
+
83
+ 2. Factory providers: call `.registerFactory(someFactory)`. Factories are useful for making third party libraries injectable, and they can be sync or async. Use the `defineFactory` helper function to define your factory so you get type safety around injected deps.
84
+
85
+ 3. Value providers: call `.registerValue(token, value)`. This is useful for making static data injectable. Use `createToken` to create the token.
86
+
87
+ Note: Each token can only be registered once. Attempting to register the same token twice will throw an error. Use `override()` if you need to replace a registration (e.g., for testing).
88
+
89
+ ### Class Providers
90
+
91
+ #### Deps Array
92
+
93
+ Class providers work by declaring their dependencies in a static `deps` array.
94
+
95
+ In the initial example, our `UserService` injects the `UserRepository`, so it must declare the `UserRepository` in its deps array. The deps array is how we tell the container what to inject without resorting to decorators.
96
+
97
+ ```typescript
98
+ class UserService {
99
+ static readonly deps = [UserRepository] as const;
100
+
101
+ constructor(private readonly userRepo: UserRepository) {}
102
+
103
+ public async getUser(id: string) {
104
+ return this.userRepo.findById(id);
105
+ }
106
+ }
107
+ ```
108
+
109
+ #### Lifecycle Hooks
110
+
111
+ When creating singleton services, it's common to need to do some async setup and teardown, such as connecting/disconnecting from a database or priming a cache.
112
+ You can do this by declaring `onInit` and `onDestroy` methods in your service, both of which can be sync or async. You can optionally implement the `OnInit` and/or `OnDestroy` interfaces, but they aren't required.
113
+
114
+ ```typescript
115
+ import { OnInit, OnDestroy } from '@brianmcd/di';
116
+
117
+ class UserService implements OnInit {
118
+ static readonly deps = [UserRepository] as const;
119
+
120
+ constructor(private readonly userRepo: UserRepository) {}
121
+
122
+ public async onInit(): Promise<void> {
123
+ // Do some async stuff.
124
+ }
125
+
126
+ public async onDestroy(): Promise<void> {
127
+ // Do some async stuff.
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### Factory Providers
133
+
134
+ Factory providers let you register a function that returns the provided instance. Factory functions are injectable, so you can use other dependencies in them. You can also register an `onDestroy` hook, which can be async or sync, to do cleanup when the Container is destroyed.
135
+
136
+ Note: Unlike class providers, factories do **not** support `onInit`. If you need initialization logic, perform it in the factory function itself (which can be async).
137
+
138
+ **defineFactory**
139
+
140
+ Use the `defineFactory` helper function to define a factory. `defineFactory` gives you type safety between the factory's `deps` array and the factory function.
141
+
142
+ ```typescript
143
+ import { Database } from 'some-third-party-lib';
144
+ import { createToken, defineFactory } from '@brianmcd/di';
145
+
146
+ export const DATABASE = createToken<Database>('DATABASE');
147
+
148
+ export const dbFactory = defineFactory({
149
+ provide: DATABASE,
150
+ deps: [DB_OPTIONS] as const, // Assuming this was provided in your `ContainerBuilder`.
151
+ factory: (options) => new Database(options.connectionString),
152
+ // onDestroy.deps can reference the factory's own token (DATABASE) to receive
153
+ // the created instance for cleanup.
154
+ onDestroy: {
155
+ deps: [DATABASE] as const,
156
+ handler: async (db) => await db.destroy(),
157
+ },
158
+ });
159
+ ```
160
+
161
+ ### Value Providers
162
+
163
+ Value providers register static data. Note that values are considered "externally managed" - lifecycle hooks (`onInit`/`onDestroy`) are never called on values, even if the value object happens to have those methods.
164
+
165
+ ```typescript
166
+ const CONFIG = createToken<{ value: string }>('CONFIG');
167
+
168
+ const container = await new ContainerBuilder().registerValue(CONFIG, { value: 'test' }).build();
169
+
170
+ expect(container.get(CONFIG)).toEqual({ value: 'test' });
171
+ ```
172
+
173
+ ## Scoped Containers
174
+
175
+ The default behavior of the Container is to create singleton instances whose lifetime is the lifetime of the application. Sometimes, however, you have a set of providers that need to create new instances within some other scope, such as per-request. You can achieve that with Scoped Containers and defining the scope when you register your provider.
176
+
177
+ ### Provider Scope
178
+
179
+ There are 2 scope options when registering a provider:
180
+
181
+ 1. Singleton (default): The provider is instantiated once when `.build()` is called on the `ContainerBuilder`.
182
+ 2. Scoped: The provider can only be created in a ScopedContainer. This is how you implement request-scoping.
183
+
184
+ **Important:** Singleton providers cannot depend on Scoped providers. This constraint is validated at build time - you'll get an error if a singleton tries to inject a scoped dependency.
185
+
186
+ ### Using a Scoped Container
187
+
188
+ It goes like this:
189
+
190
+ 1. Register your providers in a `ContainerBuilder`. Use `{ scope: Scope.Scoped }` for any providers that should be scoped to a ScopedContainer.
191
+ 2. Call `.build` to get your `Container`.
192
+ 3. In your application, call `container.createScope()` to get a new `ScopedContainer`. This is like a clean slate for any scoped dependencies. In express, you'd probably do this in a middleware and attach the `ScopedContainer` to the request. In GraphQL, you'd probably do this in the context creation function and attach the `ScopedContainer` to the context.
193
+
194
+ `ScopedContainer`s provide a superset of the providers available in a `Container`: they can access all of the Singletons from their parent Container, plus they can instantiate Scoped providers.
195
+
196
+ Within a `ScopedContainer`, Scoped dependencies are created **once** and then cached. Each `ScopedContainer` you create gets its own cache.
197
+
198
+ When you're done with your `ScopedContainer`, be sure to call `.destroy()` on it to run any `onDestroy` hooks.
199
+
200
+ ## Creating Reusable Packages
201
+
202
+ A common pattern is to break applications up into separate packages or libraries. `@brianmcd/di` supports this use case well via `ContainerBuilder`'s `merge` method.
203
+
204
+ ### Recipe
205
+
206
+ In your library, register your providers with a `ContainerBuilder`, but don't call `.build()` on it. Export the `ContainerBuilder` instance.
207
+
208
+ In your consuming application, simply call `.merge(yourLibraryContainerBuilder)`. This will merge all of the library's providers into the application's `ContainerBuilder`.
209
+
210
+ Note: a `ContainerBuilder` forms a single namespace, so you can't provide the same token in your library and in your application.
211
+
212
+ ### Example
213
+
214
+ ```typescript
215
+ // In your library package (e.g., @myorg/auth)
216
+ import { ContainerBuilder } from '@brianmcd/di';
217
+
218
+ class AuthService {
219
+ // ...
220
+ }
221
+
222
+ // Export the ContainerBuilder, not a built Container
223
+ export const authModule = new ContainerBuilder().registerClass(AuthService);
224
+
225
+ // In your application
226
+ import { ContainerBuilder } from '@brianmcd/di';
227
+ import { authModule } from '@myorg/auth';
228
+
229
+ const container = await new ContainerBuilder()
230
+ .merge(authModule)
231
+ .registerClass(MyAppService)
232
+ .build();
233
+ ```
234
+
235
+ ## API Reference
236
+
237
+ ### ContainerBuilder
238
+
239
+ Fluent builder for constructing Containers. Call `.build()` at the end to get your initialized `Container`.
240
+
241
+ #### Methods
242
+
243
+ - `registerValue<T>(token, value): this` - Register a plain value
244
+ - `registerClass<T>(Class, options?): this` - Register a class with static `deps` property
245
+ - `registerFactory<T>(provider, options?): this` - Register a factory provider
246
+ - `merge(otherBuilder): this` - Merge registrations from another builder
247
+ - `override<T>(token, value): this` - Override an existing registration (useful for testing)
248
+ - `build(options?: { init?: boolean }): Promise<Container>` - Build the container. By default, also calls `init()` on the container. Set `{ init: false }` to skip automatic initialization if you need manual control over when `onInit` hooks run (useful for testing or staged startup).
249
+
250
+ ### Container
251
+
252
+ The core DI container that holds service instances.
253
+
254
+ #### Methods
255
+
256
+ - `get<T>(token: Token<T>): T` - Retrieve a service by its token.
257
+ - `init(): Promise<void>` - Initialize all services (calls `onInit` on all services, ensuring dependencies are initialized before dependents).
258
+ - `destroy(): Promise<void>` - Destroy all services (calls `onDestroy` in reverse order, ensuring dependencies are destroyed after dependents).
259
+ - `createScope(): ScopedContainer` - Create a new scoped container for scoped dependencies.
260
+
261
+ ### ScopedContainer
262
+
263
+ Container for scoped instances, created via `container.createScope()`.
264
+
265
+ #### Methods
266
+
267
+ - `get<T>(token: Token<T>): Promise<T>` - Retrieve or create a scoped instance. Returns a Promise because scoped providers may have async factories that need to be resolved on-demand. The instance is cached for the lifetime of the `ScopedContainer`. For singleton tokens, falls back to the parent `Container`.
268
+ - `destroy(): Promise<void>` - Run `onDestroy` on all `Scope.Scoped` instances that were created.
269
+
270
+ ### Helper Functions
271
+
272
+ - `createToken<T>(name): TypedToken<T>` - Create a typed token for non-class dependencies.
273
+ - `defineFactory(config): FactoryProvider` - Define a factory with type safety.
274
+
275
+ ### Interfaces
276
+
277
+ - `OnInit` - Implement `onInit(): Promise<void> | void` for initialization logic in class providers.
278
+ - `OnDestroy` - Implement `onDestroy(): Promise<void> | void` for cleanup logic in class providers.
279
+
280
+ ## Testing
281
+
282
+ Use `merge()` and `override()` to easily mock dependencies:
283
+
284
+ ```typescript
285
+ // Create a module with your production services
286
+ const createAppModule = () =>
287
+ new ContainerBuilder()
288
+ .registerValue(CONFIG, productionConfig)
289
+ .registerFactory(databaseFactory)
290
+ .registerClass(UserService);
291
+
292
+ // In tests, merge and override specific dependencies
293
+ const testContainer = await new ContainerBuilder()
294
+ .merge(createAppModule())
295
+ .override(CONFIG, testConfig)
296
+ .override(DATABASE, mockDatabase)
297
+ .build();
298
+
299
+ // UserService now uses mockDatabase
300
+ const userService = testContainer.get(UserService);
301
+ ```
302
+
303
+ ## Type Safety
304
+
305
+ The goal of this library is to provide type safety without limiting or complicating the library.
306
+
307
+ To accomplish this, there are some tradeoffs to be aware of:
308
+
309
+ 1. The deps array is typechecked with the constructor parameters, but the compiler error will be thrown by the `ContainerBuilder` when you register the provider, not in the class. I explored ways to move the error to the class, but all of them required clumsy syntax.
310
+ 2. There is **no** _compile-time_ enforcement that the dependencies you declare in your deps array are actually provided in the `ContainerBuilder`, but you **will** get a _runtime_ error in this case as soon as you call `.build()`.
311
+
312
+ So in general, there **is** compile-time type safety around dependency usage, but there **is not** compile-time type safety around dependency existence. Since you will get runtime errors as soon as you call `.build()`, this isn't a big limitation in practice, and it allows us to keep the library much simpler and more flexible.
313
+
314
+ ## Acknowledgements
315
+
316
+ The API for this library is inspired by the dependency injection in [Nest.js](https://nestjs.com/), Angular/AngularJS, and [typed-inject](https://github.com/nicojs/typed-inject).
317
+
318
+ ## License
319
+
320
+ MIT
@@ -0,0 +1,12 @@
1
+ export { Container } from './lib/container.js';
2
+ export { ContainerBuilder } from './lib/container-builder.js';
3
+ export { ScopedContainer } from './lib/scoped-container.js';
4
+ export type { OnDestroy, OnInit } from './lib/types/lifecycle.js';
5
+ export type { FactoryProvider } from './lib/types/factory-provider.js';
6
+ export type { RegistrationOptions } from './lib/types/options.js';
7
+ export { Scope } from './lib/types/scope.js';
8
+ export type { Scope as ScopeType } from './lib/types/scope.js';
9
+ export type { Token, TypedToken } from './lib/types/tokens.js';
10
+ export { createToken } from './lib/utils/create-token.js';
11
+ export { defineFactory } from './lib/utils/define-factory.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC7C,YAAY,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC/D,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineFactory = exports.createToken = exports.Scope = exports.ScopedContainer = exports.ContainerBuilder = exports.Container = void 0;
4
+ var container_js_1 = require("./lib/container.js");
5
+ Object.defineProperty(exports, "Container", { enumerable: true, get: function () { return container_js_1.Container; } });
6
+ var container_builder_js_1 = require("./lib/container-builder.js");
7
+ Object.defineProperty(exports, "ContainerBuilder", { enumerable: true, get: function () { return container_builder_js_1.ContainerBuilder; } });
8
+ var scoped_container_js_1 = require("./lib/scoped-container.js");
9
+ Object.defineProperty(exports, "ScopedContainer", { enumerable: true, get: function () { return scoped_container_js_1.ScopedContainer; } });
10
+ var scope_js_1 = require("./lib/types/scope.js");
11
+ Object.defineProperty(exports, "Scope", { enumerable: true, get: function () { return scope_js_1.Scope; } });
12
+ var create_token_js_1 = require("./lib/utils/create-token.js");
13
+ Object.defineProperty(exports, "createToken", { enumerable: true, get: function () { return create_token_js_1.createToken; } });
14
+ var define_factory_js_1 = require("./lib/utils/define-factory.js");
15
+ Object.defineProperty(exports, "defineFactory", { enumerable: true, get: function () { return define_factory_js_1.defineFactory; } });
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,mDAA+C;AAAtC,yGAAA,SAAS,OAAA;AAClB,mEAA8D;AAArD,wHAAA,gBAAgB,OAAA;AACzB,iEAA4D;AAAnD,sHAAA,eAAe,OAAA;AAKxB,iDAA6C;AAApC,iGAAA,KAAK,OAAA;AAId,+DAA0D;AAAjD,8GAAA,WAAW,OAAA;AACpB,mEAA8D;AAArD,kHAAA,aAAa,OAAA"}
@@ -0,0 +1,76 @@
1
+ import { Container } from './container.js';
2
+ import type { FactoryProvider } from './types/factory-provider.js';
3
+ import type { InjectableClass } from './types/injectable-class.js';
4
+ import type { RegistrationOptions } from './types/options.js';
5
+ import type { Token } from './types/tokens.js';
6
+ /**
7
+ * ContainerBuilder is the class you use to configure a Container.
8
+ *
9
+ * The ContainerBuilder has 2 main responsibilities:
10
+ * 1. Collect all provider registrations, which can be defined in any order.
11
+ * - Handled by register* methods.
12
+ * 2. Instantiate instances and register them in a new Container.
13
+ * - Handled by `.build()`.
14
+ *
15
+ * The ContainerBuilder does a topological sort to ensure that instances are created and destroyed
16
+ * in the correct order (e.g. all of a providers dependencies must be created before that
17
+ * provider's instance can be created, and destruction needs to happen in the opposite order).
18
+ */
19
+ export declare class ContainerBuilder {
20
+ private readonly registrations;
21
+ /**
22
+ * Register a static value.
23
+ */
24
+ registerValue<T>(token: Token<T>, value: T): this;
25
+ /**
26
+ * Register a class with static deps property.
27
+ * Dependencies are resolved automatically during build().
28
+ * Type safety between the deps array and constructor params is enforced at this method.
29
+ */
30
+ registerClass<T, Deps extends readonly Token<any>[]>(Class: InjectableClass<T, Deps>, options?: RegistrationOptions): this;
31
+ /**
32
+ * Register a factory provider, which can be async or sync and can inject other dependencies.
33
+ */
34
+ registerFactory<T, Deps extends readonly Token<any>[], DestroyDeps extends readonly Token<any>[]>(provider: FactoryProvider<T, Deps, DestroyDeps>, options?: RegistrationOptions): this;
35
+ /**
36
+ * Merge providers from another container.
37
+ *
38
+ * This is useful for creating standalone packages/libraries that export a ContainerBuilder.
39
+ * You can use `merge` to bring these standalone packages together into a single Container.
40
+ */
41
+ merge(other: ContainerBuilder): this;
42
+ /**
43
+ * Override an existing provider with a value; intended for mocking during tests.
44
+ */
45
+ override<T>(token: Token<T>, value: T): this;
46
+ /**
47
+ * Build the container - topologically sorts providers and instantiates singletons.
48
+ * Scoped providers are validated but not instantiated - they're stored for later use in Scopes.
49
+ * By default, also initializes the container (calls onInit on all services).
50
+ */
51
+ build(options?: {
52
+ init?: boolean;
53
+ }): Promise<Container>;
54
+ /**
55
+ * Topologically sort providers by their dependencies using Kahn's algorithm.
56
+ *
57
+ * This ensures that dependent providers are created and initialized BEFORE the providers that
58
+ * depend on them, and that dependent providers are destroyed AFTER providers that depend on them.
59
+ *
60
+ * Also validates that singleton providers do not depend on scoped providers.
61
+ */
62
+ private topologicalSort;
63
+ /**
64
+ * Find a cycle path using DFS with coloring.
65
+ * Returns the cycle as an array of tokens ending with the repeated token.
66
+ */
67
+ private findCyclePath;
68
+ private addRegistration;
69
+ private isScoped;
70
+ private isSingleton;
71
+ /**
72
+ * Resolve dependencies from the instances map.
73
+ */
74
+ private resolveDeps;
75
+ }
76
+ //# sourceMappingURL=container-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-builder.d.ts","sourceRoot":"","sources":["../../../src/lib/container-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgD;IAE9E;;OAEG;IACI,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAKxD;;;;OAIG;IACI,aAAa,CAAC,CAAC,EAAE,IAAI,SAAS,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,EACxD,KAAK,EAAE,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC,EAC/B,OAAO,CAAC,EAAE,mBAAmB,GAC5B,IAAI;IAYP;;OAEG;IACI,eAAe,CACpB,CAAC,EACD,IAAI,SAAS,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,EAClC,WAAW,SAAS,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,EACzC,QAAQ,EAAE,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI;IAYvF;;;;;OAKG;IACI,KAAK,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAO3C;;OAEG;IACI,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAQnD;;;;OAIG;IACU,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IA+DpE;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IA+EvB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAmDrB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,OAAO,CAAC,WAAW;CAYpB"}