@lppedd/di-wise-neo 0.5.2 → 0.6.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 CHANGED
@@ -3,7 +3,8 @@
3
3
  <p align="center">Lightweight, type-safe, flexible dependency injection library for TypeScript and JavaScript</p>
4
4
  <div align="center">
5
5
 
6
- [![test](https://img.shields.io/github/actions/workflow/status/lppedd/di-wise-neo/test.yml.svg?branch=main)](https://github.com/lppedd/di-wise-neo/actions/workflows/test.yml)
6
+ [![build](https://img.shields.io/github/actions/workflow/status/lppedd/di-wise-neo/test.yml.svg?branch=main)](https://github.com/lppedd/di-wise-neo/actions/workflows/test.yml)
7
+ [![coverage](https://img.shields.io/codecov/c/github/lppedd/di-wise-neo/main?token=R9XZFTQ0BA)](https://app.codecov.io/gh/lppedd/di-wise-neo/tree/main/src)
7
8
  [![npm](https://img.shields.io/npm/v/@lppedd/di-wise-neo?color=%23de1f1f&logo=npm)](https://www.npmjs.com/package/@lppedd/di-wise-neo)
8
9
  [![npm gzipped size](https://img.shields.io/bundlejs/size/@lppedd/di-wise-neo)](https://bundlejs.com/?q=@lppedd/di-wise-neo)
9
10
  [![license](https://img.shields.io/github/license/lppedd/di-wise-neo?color=blue)](https://github.com/lppedd/di-wise-neo/blob/main/LICENSE)
@@ -19,7 +20,6 @@
19
20
 
20
21
  ## Table of Contents
21
22
 
22
- - [Why yet another library](#why-yet-another-library)
23
23
  - [Installation](#installation)
24
24
  - [API reference](#api-reference)
25
25
  - [Ergonomics & Requirements](#ergonomics)
@@ -79,7 +79,7 @@ the use of ECMAScript Stage 3 decorators, which do not support decorating method
79
79
  So what's the right move? Forking the best pick and refactoring it to suite my
80
80
  production needs.
81
81
 
82
- ## Installation
82
+ ### Installation
83
83
 
84
84
  ```sh
85
85
  npm i @lppedd/di-wise-neo
@@ -93,11 +93,11 @@ pnpm add @lppedd/di-wise-neo
93
93
  yarn add @lppedd/di-wise-neo
94
94
  ```
95
95
 
96
- ## API reference
96
+ ### API reference
97
97
 
98
98
  You can find the complete API reference at [lppedd.github.io/di-wise-neo](https://lppedd.github.io/di-wise-neo)
99
99
 
100
- ## Ergonomics
100
+ ### Ergonomics
101
101
 
102
102
  - Does **not** depend on other libraries
103
103
  - Does **not** use [reflect-metadata](https://www.npmjs.com/package/reflect-metadata) to drive decorators
@@ -281,7 +281,9 @@ The container will translate `TaskID` to `PID` before resolving the value.
281
281
 
282
282
  The primary way to perform dependency injection in **di-wise-neo** is through
283
283
  functions like `inject(T)`, `injectAll(T)`, `optional(T)`, and `optionalAll(T)`.
284
- This approach is recommended because it preserves full type safety.
284
+
285
+ > [!TIP]
286
+ > Using injection functions is recommended because it preserves type safety.
285
287
 
286
288
  ### Injection context
287
289
 
@@ -473,10 +475,11 @@ In this example, `ExtensionContext` will be registered with **Resolution** scope
473
475
 
474
476
  ### `@AutoRegister`
475
477
 
476
- Enables automatic registration of the decorated class if it has not been registered explicitly.
478
+ Enables automatic registration of the decorated class when it is resolved,
479
+ if it has not been registered beforehand.
477
480
 
478
481
  ```ts
479
- @AutoRegister
482
+ @AutoRegister()
480
483
  export class ExtensionContext {
481
484
  /* ... */
482
485
  }
@@ -487,19 +490,20 @@ container.resolve(ExtensionContext);
487
490
 
488
491
  ### `@EagerInstantiate`
489
492
 
490
- Marks a class for eager instantiation when registered with **Container** scope.
493
+ Sets the default class scope to **Container** and marks the class for eager instantiation
494
+ upon registration.
491
495
 
492
496
  This causes the container to immediately create and cache the instance of the class
493
497
  at registration time, instead of deferring instantiation until the first resolution.
494
498
 
495
499
  ```ts
496
- @EagerInstantiate
497
- @Scoped(Scope.Container)
500
+ @EagerInstantiate()
498
501
  export class ExtensionContext {
499
502
  /* ... */
500
503
  }
501
504
 
502
- // The container immediately creates and caches the instance
505
+ // ExtensionContext is registered with Container scope,
506
+ // and an instance is immediately created and cached by the container
503
507
  container.register(ExtensionContext);
504
508
  ```
505
509
 
@@ -422,7 +422,7 @@ declare function createContainer(options?: Partial<ContainerOptions>): Container
422
422
  *
423
423
  * @example
424
424
  * ```ts
425
- * @AutoRegister
425
+ * @AutoRegister()
426
426
  * class Wizard {}
427
427
  *
428
428
  * const wizard = container.resolve(Wizard);
@@ -431,28 +431,28 @@ declare function createContainer(options?: Partial<ContainerOptions>): Container
431
431
  *
432
432
  * @__NO_SIDE_EFFECTS__
433
433
  */
434
- declare function AutoRegister<Ctor extends Constructor<any>>(Class: Ctor): void;
434
+ declare function AutoRegister(): ClassDecorator;
435
435
 
436
436
  /**
437
- * Class decorator that enables eager instantiation of a class when it is registered
438
- * in the container with `Scope.Container`.
437
+ * Class decorator that sets the class scope to **Container** and enables
438
+ * eager instantiation when the class is registered in the container.
439
439
  *
440
440
  * This causes the container to immediately create and cache the instance of the class,
441
441
  * instead of deferring instantiation until the first resolution.
442
442
  *
443
443
  * @example
444
444
  * ```ts
445
- * @EagerInstantiate
446
- * @Scoped(Scope.Container)
445
+ * @EagerInstantiate()
447
446
  * class Wizard {}
448
447
  *
449
- * // A Wizard instance is immediately created and cached by the container
448
+ * // Wizard is registered with Container scope, and an instance
449
+ * // is immediately created and cached by the container
450
450
  * const wizard = container.register(Wizard);
451
451
  * ```
452
452
  *
453
453
  * @__NO_SIDE_EFFECTS__
454
454
  */
455
- declare function EagerInstantiate<Ctor extends Constructor<any>>(Class: Ctor): void;
455
+ declare function EagerInstantiate(): ClassDecorator;
456
456
 
457
457
  interface TokensRef<Value = any> {
458
458
  readonly getRefTokens: () => Set<Token<Value>>;
package/dist/cjs/index.js CHANGED
@@ -192,6 +192,22 @@ function getMetadata(Class) {
192
192
  }
193
193
  });
194
194
  }
195
+ if (metadata.provider.useClass !== Class) {
196
+ // This is part of the class identity mapping API (see setClassIdentityMapping).
197
+ //
198
+ // Scenario:
199
+ // 1. Metadata is created for class A (the original class) because of a parameter decorator.
200
+ // 2. Later, because of a class decorator that extends the decorated class, a third-party
201
+ // registers a class identity mapping from class B to class A, where B is the class
202
+ // generated from the class decorator by extending A.
203
+ //
204
+ // We must update useClass to be the extender class B so that instances created by the
205
+ // DI container match the consumer's registered class. Without this update, the DI
206
+ // system would instantiate the original class A, causing behavior inconsistencies.
207
+ //
208
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
209
+ metadata.provider.useClass = Class;
210
+ }
195
211
  return metadata;
196
212
  }
197
213
  const classIdentityMap = new WeakMap();
@@ -520,7 +536,7 @@ function isDisposable(value) {
520
536
  // The provider is of type ClassProvider, initialized by getMetadata
521
537
  provider: metadata.provider,
522
538
  options: {
523
- scope: metadata.scope ?? this.myOptions.defaultScope
539
+ scope: metadata.scope?.value ?? this.myOptions.defaultScope
524
540
  },
525
541
  dependencies: metadata.dependencies
526
542
  };
@@ -537,7 +553,7 @@ function isDisposable(value) {
537
553
  }
538
554
  // Eager-instantiate only if the class is container-scoped
539
555
  if (metadata.eagerInstantiate && registration.options?.scope === Scope.Container) {
540
- this.resolve(Class);
556
+ this.resolveProviderValue(registration, registration.provider);
541
557
  }
542
558
  } else {
543
559
  const [token, provider, options] = args;
@@ -548,7 +564,7 @@ function isDisposable(value) {
548
564
  options: {
549
565
  // The explicit registration options override what is specified
550
566
  // via class decorators (e.g., @Scoped)
551
- scope: metadata.scope ?? this.myOptions.defaultScope,
567
+ scope: metadata.scope?.value ?? this.myOptions.defaultScope,
552
568
  ...options
553
569
  },
554
570
  dependencies: metadata.dependencies
@@ -556,7 +572,7 @@ function isDisposable(value) {
556
572
  this.myTokenRegistry.set(token, registration);
557
573
  // Eager-instantiate only if the provided class is container-scoped
558
574
  if (metadata.eagerInstantiate && registration.options?.scope === Scope.Container) {
559
- this.resolve(token);
575
+ this.resolveProviderValue(registration, registration.provider);
560
576
  }
561
577
  } else {
562
578
  if (isExistingProvider(provider)) {
@@ -672,7 +688,7 @@ function isDisposable(value) {
672
688
  metadata.eagerInstantiate = eagerInstantiate;
673
689
  }
674
690
  }
675
- const scope = this.resolveScope(metadata.scope);
691
+ const scope = this.resolveScope(metadata.scope?.value);
676
692
  if (optional && scope === Scope.Container) {
677
693
  // It would not be possible to resolve the class in container scope,
678
694
  // as that would require prior registration.
@@ -862,7 +878,7 @@ function isDisposable(value) {
862
878
  *
863
879
  * @example
864
880
  * ```ts
865
- * @AutoRegister
881
+ * @AutoRegister()
866
882
  * class Wizard {}
867
883
  *
868
884
  * const wizard = container.resolve(Wizard);
@@ -870,32 +886,45 @@ function isDisposable(value) {
870
886
  * ```
871
887
  *
872
888
  * @__NO_SIDE_EFFECTS__
873
- */ function AutoRegister(Class) {
874
- const metadata = getMetadata(Class);
875
- metadata.autoRegister = true;
889
+ */ function AutoRegister() {
890
+ return function(Class) {
891
+ const metadata = getMetadata(Class);
892
+ metadata.autoRegister = true;
893
+ };
876
894
  }
877
895
 
878
896
  /**
879
- * Class decorator that enables eager instantiation of a class when it is registered
880
- * in the container with `Scope.Container`.
897
+ * Class decorator that sets the class scope to **Container** and enables
898
+ * eager instantiation when the class is registered in the container.
881
899
  *
882
900
  * This causes the container to immediately create and cache the instance of the class,
883
901
  * instead of deferring instantiation until the first resolution.
884
902
  *
885
903
  * @example
886
904
  * ```ts
887
- * @EagerInstantiate
888
- * @Scoped(Scope.Container)
905
+ * @EagerInstantiate()
889
906
  * class Wizard {}
890
907
  *
891
- * // A Wizard instance is immediately created and cached by the container
908
+ * // Wizard is registered with Container scope, and an instance
909
+ * // is immediately created and cached by the container
892
910
  * const wizard = container.register(Wizard);
893
911
  * ```
894
912
  *
895
913
  * @__NO_SIDE_EFFECTS__
896
- */ function EagerInstantiate(Class) {
897
- const metadata = getMetadata(Class);
898
- metadata.eagerInstantiate = true;
914
+ */ function EagerInstantiate() {
915
+ return function(Class) {
916
+ const metadata = getMetadata(Class);
917
+ const currentScope = metadata.scope;
918
+ assert(!currentScope || currentScope.value === Scope.Container, ()=>{
919
+ const { value, appliedBy } = currentScope;
920
+ return `class ${Class.name}: Scope.${value} was already set by @${appliedBy},\n ` + `but @EagerInstantiate is trying to set a conflicting Scope.Container.\n ` + `Only one decorator should set the class scope, or all must agree on the same value.`;
921
+ });
922
+ metadata.eagerInstantiate = true;
923
+ metadata.scope = {
924
+ value: Scope.Container,
925
+ appliedBy: "EagerInstantiate"
926
+ };
927
+ };
899
928
  }
900
929
 
901
930
  function forwardRef(token) {
@@ -1027,7 +1056,16 @@ function OptionalAll(token) {
1027
1056
  */ function Scoped(scope) {
1028
1057
  return function(Class) {
1029
1058
  const metadata = getMetadata(Class);
1030
- metadata.scope = scope;
1059
+ const currentScope = metadata.scope;
1060
+ assert(!currentScope || currentScope.value === scope, ()=>{
1061
+ const { value, appliedBy } = currentScope;
1062
+ const by = appliedBy === "Scoped" ? `another @${appliedBy} decorator` : `@${appliedBy}`;
1063
+ return `class ${Class.name}: Scope.${value} was already set by ${by},\n ` + `but @Scoped is trying to set a conflicting Scope.${scope}.\n ` + `Only one decorator should set the class scope, or all must agree on the same value.`;
1064
+ });
1065
+ metadata.scope = {
1066
+ value: scope,
1067
+ appliedBy: "Scoped"
1068
+ };
1031
1069
  };
1032
1070
  }
1033
1071