@lppedd/di-wise-neo 0.3.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.
Files changed (42) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +488 -0
  3. package/dist/cjs/index.d.ts +772 -0
  4. package/dist/cjs/index.js +1055 -0
  5. package/dist/cjs/index.js.map +1 -0
  6. package/dist/es/index.d.mts +772 -0
  7. package/dist/es/index.mjs +1037 -0
  8. package/dist/es/index.mjs.map +1 -0
  9. package/package.json +77 -0
  10. package/src/container.ts +292 -0
  11. package/src/decorators/autoRegister.ts +24 -0
  12. package/src/decorators/decorators.ts +47 -0
  13. package/src/decorators/index.ts +7 -0
  14. package/src/decorators/inject.ts +42 -0
  15. package/src/decorators/injectAll.ts +45 -0
  16. package/src/decorators/injectable.ts +61 -0
  17. package/src/decorators/optional.ts +39 -0
  18. package/src/decorators/optionalAll.ts +42 -0
  19. package/src/decorators/scoped.ts +32 -0
  20. package/src/defaultContainer.ts +519 -0
  21. package/src/errors.ts +47 -0
  22. package/src/index.ts +16 -0
  23. package/src/inject.ts +88 -0
  24. package/src/injectAll.ts +21 -0
  25. package/src/injectionContext.ts +46 -0
  26. package/src/injector.ts +117 -0
  27. package/src/metadata.ts +41 -0
  28. package/src/middleware.ts +85 -0
  29. package/src/optional.ts +65 -0
  30. package/src/optionalAll.ts +19 -0
  31. package/src/provider.ts +61 -0
  32. package/src/scope.ts +8 -0
  33. package/src/token.ts +84 -0
  34. package/src/tokenRegistry.ts +165 -0
  35. package/src/tokensRef.ts +46 -0
  36. package/src/utils/context.ts +19 -0
  37. package/src/utils/disposable.ts +10 -0
  38. package/src/utils/invariant.ts +6 -0
  39. package/src/utils/keyedStack.ts +26 -0
  40. package/src/utils/typeName.ts +28 -0
  41. package/src/utils/weakRefMap.ts +28 -0
  42. package/src/valueRef.ts +3 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Edoardo Luppi
4
+ Copyright (c) 2024-2025 Xuanbo Cheng
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,488 @@
1
+ <!--suppress HtmlDeprecatedAttribute -->
2
+ <div align="center">
3
+ <h1>di-wise-neo</h1>
4
+ <p>Lightweight, type-safe, flexible dependency injection library for TypeScript and JavaScript</p>
5
+ <img src="./.github/images/neo-wall.jpg" title="di-wise-neo" alt="di-wise-neo" style="border: 3px solid black; border-radius: 15px;" />
6
+ <div><sub>yes, I like The Matrix</sub></div>
7
+ </div>
8
+
9
+ > [!NOTE]
10
+ >
11
+ > **di-wise-neo** is a fork of [di-wise][di-wise], aiming to provide a simpler yet more powerful API,
12
+ > in part thanks to TypeScript's experimental decorators. Shout out to [@exuanbo](https://github.com/exuanbo)
13
+ > for the strong foundations!
14
+
15
+ ## Table of Contents
16
+
17
+ - [Why yet another library](#why-yet-another-library)
18
+ - [Installation](#installation)
19
+ - [Ergonomics](#ergonomics)
20
+ - [Quickstart](#quickstart)
21
+ - [Container scopes](#container-scopes)
22
+ - [Token registration](#token-registration)
23
+ - [Function-based injection](#function-based-injection)
24
+ - [Decorator-based injection](#decorator-based-injection)
25
+ - [Testing support](#testing-support)
26
+
27
+ ## Why yet another library
28
+
29
+ I've been developing VS Code extensions for a while as part of my daily work.
30
+ It's enjoyable work! However, extensions always reach that tipping point where
31
+ feature bloat, and the many different UI interactions which arise from that,
32
+ make writing, reading, and understanding the codebase a challenge.
33
+
34
+ Part of the problem is the crazy amount of parameter passing, and the many exported
35
+ global values floating around waiting to be imported and to generate yet another
36
+ coupling point.
37
+
38
+ My background with Java is full of such cases, that have been (partially) mitigated
39
+ by introducing dependency-injection libraries based on Java's powerful Contexts and
40
+ Dependency Injection (see [Weld][cdi-weld], the reference implementation).
41
+
42
+ So why not apply the same concept to our TypeScript projects?
43
+ I've posted on Reddit just to get a feel of what the ecosystem offers, and was
44
+ pointed to libraries such as [tsyringe][tsyringe], [InversifyJS][InversifyJS], or [Awilix][Awilix].
45
+ I've also explored on my own and discovered [redi][redi] and [di-wise][di-wise].
46
+
47
+ What I was looking for is a lightweight solution that offers:
48
+
49
+ - full type safety
50
+ - scoped resolution of dependencies
51
+ - optional decorator support for constructor and method injection.
52
+ Yes I know, forget type-safety with decorators, but they are extremely
53
+ intuitive to pick up for Java devs.
54
+ - no dependency on [reflect-metadata][reflect-metadata], as I'm an ESBuild user
55
+ and ESBuild [does not][esbuild-issue] support `emitDecoratorMetadata`
56
+
57
+ Unfortunately both [tsyringe][tsyringe] and [InversifyJS][InversifyJS] require
58
+ [reflect-metadata][reflect-metadata] to run correctly. [Awilix][Awilix] looks good,
59
+ but it's probably too much for what I need to do, and it does not support decorators.
60
+ Plus, the API just didn't click for me.
61
+
62
+ [redi][redi] focuses _only_ on constructor injection via decorators, which is nice.
63
+ However, it falls short when it comes to type safety and resolution scopes:
64
+ it only supports singletons, with a decorator-based trick to create fresh instances.
65
+
66
+ And lastly, [di-wise][di-wise]. This small library was quite the surprise! Easy to pick up,
67
+ no scope creep, injection context support, and full type safety via Angular-like
68
+ `inject<T>()` functions (that's more like a service locator, but whatever).
69
+ The only problems are the slightly overcomplicated API - especially regarding typings - and
70
+ the use of ECMAScript Stage 3 decorators, which do not support decorating method parameters :sob:
71
+
72
+ So what's the right move? Forking the best pick and refactoring it to suite my
73
+ production needs.
74
+
75
+ ## Installation
76
+
77
+ ```sh
78
+ npm install @lppedd/di-wise-neo
79
+ ```
80
+
81
+ ```sh
82
+ pnpm add @lppedd/di-wise-neo
83
+ ```
84
+
85
+ ```sh
86
+ yarn add @lppedd/di-wise-neo
87
+ ```
88
+
89
+ ## Ergonomics
90
+
91
+ - Does **not** depend on other libraries
92
+ - Does **not** use [reflect-metadata](https://www.npmjs.com/package/reflect-metadata) to drive decorators
93
+ - **Can** be used from JavaScript with function-based injection
94
+
95
+ ## Quickstart
96
+
97
+ ```ts
98
+ //
99
+ // A couple of classes to cover the example
100
+ //
101
+
102
+ export class ExtensionContext { /* ... */ }
103
+
104
+ // Both the secret store and the contribution registrar
105
+ // require the extension context to read and set values
106
+
107
+ export class SecretStore {
108
+ // We can use function-based injection, which gives us type safety
109
+ readonly context = inject(ExtensionContext);
110
+
111
+ // Or even
112
+ // constructor(readonly context = inject(ExtensionContext)) {}
113
+
114
+ /* ... */
115
+ }
116
+
117
+ export class ContributionRegistrar {
118
+ // We can also opt to use decorator-based constructor injection
119
+ constructor(@Inject(ExtensionContext) readonly context: ExtensionContext) {}
120
+
121
+ /* ... */
122
+
123
+ // Or method injection. The @Optional decorator injects "T | undefined".
124
+ protected withSecretStore(@Optional(SecretStore) store: SecretStore | undefined): void {
125
+ if (store?.isSet("key")) {
126
+ /* ... */
127
+ }
128
+ }
129
+ }
130
+
131
+ //
132
+ // Using di-wise-neo
133
+ //
134
+
135
+ // Create a new DI container
136
+ const container = createContainer({
137
+ // Optionally override the default "transient" registration scope.
138
+ // I prefer to use "container" (a.k.a. singleton) scope, but "transient" is the better default.
139
+ defaultScope: Scope.Container,
140
+ });
141
+
142
+ // Register our managed dependencies into the container
143
+ container.register(ExtensionContext)
144
+ .register(SecretStore)
145
+ .register(ContributionRegistrar);
146
+
147
+ // Get the contribution registrar.
148
+ // The container will create a new managed instance for us, with all dependencies injected.
149
+ const registrar = container.resolve(ContributionRegistrar);
150
+ registrar.registerCommand("my.command", () => { console.log("hey!"); });
151
+ ```
152
+
153
+ ## Container scopes
154
+
155
+ The [Container][source-container] supports four **scope** types that determine how and when
156
+ values are cached and reused.
157
+
158
+ ### Inherited
159
+
160
+ Inherits the scope from the requesting (dependent) token.
161
+ If there is no dependent (i.e., during top-level resolution), it behaves like **Transient**.
162
+
163
+ ### Transient
164
+
165
+ Creates a new value every time the dependency is resolved, which means values are never cached.
166
+
167
+ - a class registered via `ClassProvider` is instantiated on each resolution
168
+ - a factory function registered via `FactoryProvider` is invoked on each resolution
169
+ - a value registered via `ValueProvider` is always returned as-is
170
+
171
+ ### Resolution
172
+
173
+ Creates and caches a single value per resolution graph.
174
+ The same value is reused during a single resolution request, but a new one is created
175
+ for each separate request.
176
+
177
+ ### Container
178
+
179
+ Creates and caches a single value per container.
180
+ If the value is not found in the current container, it is looked up in the parent container,
181
+ and so on.
182
+
183
+ It effectively behaves like a **singleton** scope, but allows container-specific overrides.
184
+
185
+ ## Token registration
186
+
187
+ The container allows registering tokens via _providers_. The generic usage is:
188
+
189
+ ```ts
190
+ container.register(/* ... */);
191
+ ```
192
+
193
+ An explicit **scope** can be specified using the third argument, when applicable.
194
+ If omitted, the default scope is **Transient**.
195
+
196
+ ```ts
197
+ container.register(token, provider, { scope: Scope.Resolution });
198
+ ```
199
+
200
+ ### ClassProvider
201
+
202
+ You can register a class by passing it directly to the `register` method:
203
+
204
+ ```ts
205
+ container.register(SecretStore);
206
+ ```
207
+
208
+ Alternatively, use an explicit `ClassProvider` object - useful when registering
209
+ an interface or abstract type:
210
+
211
+ ```ts
212
+ const Store = Type<Store>("Store");
213
+ container.register(Store, {
214
+ useClass: SecretStore, // class SecretStore implements Store
215
+ });
216
+ ```
217
+
218
+ Upon resolving `Store`, the container creates an instance of `SecretStore`,
219
+ caching it according to the configured scope.
220
+
221
+ ### FactoryProvider
222
+
223
+ A lazily computed value can be registered using a factory function:
224
+
225
+ ```ts
226
+ const Env = Type<string>("Env")
227
+ container.register(Env, {
228
+ useFactory: () => isNode() ? "Node.js" : "browser",
229
+ });
230
+ ```
231
+
232
+ The factory function is invoked upon token resolution, and its result is cached
233
+ according to the configured scope.
234
+
235
+ ### ValueProvider
236
+
237
+ A static value - always taken as-is and unaffected by scopes - can be registered using:
238
+
239
+ ```ts
240
+ const PID = Type<number>("PID");
241
+ const processId = spawnProcess();
242
+ container.register(PID, {
243
+ useValue: processId,
244
+ });
245
+ ```
246
+
247
+ This is especially useful when injecting third-party values that are not created
248
+ through the DI container.
249
+
250
+ ### ExistingProvider
251
+
252
+ Registers an alias to another token, allowing multiple identifiers to resolve to the same value.
253
+ Using the previous `PID` example, we can register a `TaskID` alias:
254
+
255
+ ```ts
256
+ const TaskID = Type<number>("TaskID");
257
+ container.register(TaskID, {
258
+ useExisting: PID,
259
+ });
260
+ ```
261
+
262
+ The container will translate `TaskID` to `PID` before resolving the value.
263
+
264
+ ## Function-based injection
265
+
266
+ The primary way to perform dependency injection in **di-wise-neo** is through
267
+ functions like `inject(T)`, `injectAll(T)`, `optional(T)`, and `optionalAll(T)`.
268
+ This approach is recommended because it preserves full type safety.
269
+
270
+ ### Injection context
271
+
272
+ All injection functions must be invoked inside an _injection context_, which stores
273
+ the currently active container.
274
+ The _injection context_ is available in these situations:
275
+
276
+ - inside the `constructor` of a class instantiated by the DI container
277
+ - in property initializers of such classes
278
+ - within factory functions used by `FactoryProvider`
279
+
280
+ ### `inject<T>(Token): T`
281
+
282
+ Injects the value associated with a token, throwing an error if the token is not
283
+ registered in the container.
284
+
285
+ ```ts
286
+ export class ProcessManager {
287
+ constructor(readonly rootPID /*: number */ = inject(PID)) {}
288
+
289
+ /* ... */
290
+ }
291
+ ```
292
+
293
+ If `PID` cannot be resolved, a resolution error with detailed information is thrown.
294
+
295
+ ### `injectAll<T>(Token): T[]`
296
+
297
+ Injects all values associated with a token, throwing an error if the token has
298
+ never been registered in the container.
299
+
300
+ ```ts
301
+ export class ExtensionContext {
302
+ readonly stores /*: Store[] */ = injectAll(Store);
303
+
304
+ /* ... */
305
+
306
+ clearStorage(): void {
307
+ this.stores.forEach((store) => store.clear());
308
+ }
309
+ }
310
+ ```
311
+
312
+ ### `optional<T>(Token): T`
313
+
314
+ Injects the value associated with a token, or `undefined` if the token is not
315
+ registered in the container.
316
+
317
+ ```ts
318
+ export class ProcessManager {
319
+ constructor(readonly rootPID /*: number | undefined */ = optional(PID)) {}
320
+
321
+ /* ... */
322
+ }
323
+ ```
324
+
325
+ ### `optionalAll<T>(Token): T[]`
326
+
327
+ Injects all values associated with a token, or an **empty array** if the token
328
+ has never been registered in the container.
329
+
330
+ ```ts
331
+ export class ExtensionContext {
332
+ // The type does not change compared to injectAll(T), but the call does not fail
333
+ readonly stores /*: Store[] */ = optionalAll(Store);
334
+
335
+ /* ... */
336
+ }
337
+ ```
338
+
339
+ ## Decorator-based injection
340
+
341
+ You can also perform dependency injection using TypeScript's experimental decorators.
342
+
343
+ **di-wise-neo** supports decorating constructor's and instance method's parameters.
344
+ It does not support property injection by design.
345
+
346
+ ### `@Inject(Token)`
347
+
348
+ Injects the value associated with a token, throwing an error if the token is not
349
+ registered in the container.
350
+
351
+ ```ts
352
+ export class ProcessManager {
353
+ constructor(@Inject(PID) readonly rootPID: number) {}
354
+
355
+ /* ... */
356
+
357
+ // The method is called immediately after instance construction
358
+ notifyListener(@Inject(ProcessListener) listeners: ProcessListener): void {
359
+ listener.processStarted(this.rootPID);
360
+ }
361
+ }
362
+ ```
363
+
364
+ If `PID` cannot be resolved, a resolution error with detailed information is thrown.
365
+
366
+ ### `@InjectAll(Token)`
367
+
368
+ Injects all values associated with a token, throwing an error if the token has
369
+ never been registered in the container.
370
+
371
+ ```ts
372
+ export class ExtensionContext {
373
+ constructor(@InjectAll(Store) readonly stores: Store[]) {}
374
+
375
+ /* ... */
376
+
377
+ clearStorage(): void {
378
+ this.stores.forEach((store) => store.clear());
379
+ }
380
+ }
381
+ ```
382
+
383
+ ### `@Optional(Token)`
384
+
385
+ Injects the value associated with a token, or `undefined` if the token is not
386
+ registered in the container.
387
+
388
+ ```ts
389
+ export class ProcessManager {
390
+ constructor(@Optional(PID) readonly rootPID: number | undefined) {}
391
+
392
+ /* ... */
393
+ }
394
+ ```
395
+
396
+ ### `@OptionalAll(Token)`
397
+
398
+ Injects all values associated with a token, or an **empty array** if the token
399
+ has never been registered in the container.
400
+
401
+ ```ts
402
+ export class ExtensionContext {
403
+ // The type does not change compared to @InjectAll, but construction does not fail
404
+ constructor(@OptionalAll(Store) readonly stores: Store[]) {}
405
+
406
+ /* ... */
407
+ }
408
+ ```
409
+
410
+ ### Forward references
411
+
412
+ Sometimes you may need to reference a token or class that is declared later in the file.
413
+ Normally, attempting to do that would result in a `ReferenceError`:
414
+
415
+ > ReferenceError: Cannot access 'Store' before initialization
416
+
417
+ We can work around this problem by using the `forwardRef` helper function:
418
+
419
+ ```ts
420
+ export class ExtensionContext {
421
+ constructor(@OptionalAll(forwardRef(() => Store)) readonly stores: Store[]) {}
422
+
423
+ /* ... */
424
+ }
425
+ ```
426
+
427
+ ## Testing support
428
+
429
+ Testing is an important part of software development, and dependency injection is meant to make it easier.
430
+ The **di-wise-neo** container API exposes methods to more easily integrate with testing scenarios.
431
+
432
+ ### `resetRegistry`
433
+
434
+ Removes all registrations from the container's internal registry, effectively resetting it to its initial state.
435
+ This is useful for ensuring isolation between tests.
436
+
437
+ ```ts
438
+ describe("My test suite", () => {
439
+ const container = createContainer();
440
+
441
+ afterEach(() => {
442
+ container.resetRegistry();
443
+ });
444
+
445
+ /* ... */
446
+ });
447
+ ```
448
+
449
+ ### `dispose`
450
+
451
+ Another way to ensure isolation between tests is to completely replace the DI container after each test run.
452
+ The **di-wise-neo** container supports being _disposed_, preventing further registrations or resolutions.
453
+
454
+ ```ts
455
+ describe("My test suite", () => {
456
+ let container = createContainer();
457
+
458
+ afterEach(() => {
459
+ container.dispose();
460
+ container = createContainer();
461
+ });
462
+
463
+ /* ... */
464
+ });
465
+ ```
466
+
467
+ ## Credits
468
+
469
+ **di-wise-neo** is a fork of [di-wise][di-wise].
470
+ All credits to the original author for focusing on a clean architecture and on code quality.
471
+
472
+ ## License
473
+
474
+ [MIT license](https://github.com/lppedd/di-wise-neo/blob/main/LICENSE)
475
+
476
+ 2025-present [Edoardo Luppi](https://github.com/lppedd)
477
+ 2024-2025 [Xuanbo Cheng](https://github.com/exuanbo)
478
+
479
+ <!-- @formatter:off -->
480
+ [cdi-weld]: https://weld.cdi-spec.org
481
+ [tsyringe]: https://github.com/microsoft/tsyringe
482
+ [Awilix]: https://github.com/jeffijoe/awilix
483
+ [InversifyJS]: https://github.com/inversify/InversifyJS
484
+ [redi]: https://github.com/wzhudev/redi
485
+ [di-wise]: https://github.com/exuanbo/di-wise
486
+ [reflect-metadata]: https://github.com/microsoft/reflect-metadata
487
+ [esbuild-issue]: https://github.com/evanw/esbuild/issues/257
488
+ [source-container]: https://github.com/lppedd/di-wise-neo/blob/main/src/container.ts#L29