@zudojs/container 1.1.2 → 1.2.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
@@ -47,6 +47,21 @@ container.registerFactory(API, (db) => new Api(db), [DB]); // deps via inject li
47
47
  container.registerExisting("db-alias", DB); // alias to another token
48
48
  ```
49
49
 
50
+ A factory's parameters are typed from its `inject` list, in order: with
51
+ `DB = createToken<Db>("Db")`, `(db) => new Api(db)` receives `db: Db`, and a
52
+ factory whose parameters do not match the tokens is a compile error. Class
53
+ tokens type the same way; plain string and symbol tokens give `unknown`.
54
+ `factoryProvider(factory, inject)` and `provideFactory(token, factory,
55
+ inject)` infer the same way.
56
+
57
+ ```typescript
58
+ container.registerFactory(
59
+ REPORT,
60
+ (db, clock) => new Report(db, clock), // db: Db, clock: Clock
61
+ [DB, Clock],
62
+ );
63
+ ```
64
+
50
65
  ## Tokens
51
66
 
52
67
  Tokens can be strings, symbols, classes, or `InjectionToken`s from
@@ -58,6 +73,9 @@ Tokens can be strings, symbols, classes, or `InjectionToken`s from
58
73
  - `createGlobalToken(key)` uses `Symbol.for(key)`: any other call with the
59
74
  same key anywhere in the process yields the **same** token. That is by
60
75
  design for cross-package sharing; namespace your keys (e.g. `"myapp:db"`).
76
+ - Error messages and default registration names show a symbol token by its
77
+ description: `createToken<Db>("Database")` appears as `Database`, not
78
+ `Symbol(Database)` (`describeToken(token)` returns the same string).
61
79
 
62
80
  ## Lifetimes (`scope` option)
63
81
 
@@ -185,7 +203,13 @@ const container = createContainer({
185
203
  registers it on the fly as `TRANSIENT`. When registrations are frozen the
186
204
  class is instantiated _ephemerally_ without being registered. Set to
187
205
  `false` to require explicit registration (`canResolve`/`resolveOptional`
188
- respect this).
206
+ respect this). Only a class whose constructor declares no required
207
+ parameters (`Class.length === 0`) is auto-registered: auto-registration
208
+ has no inject list, so `resolve(NeedsDep)` for
209
+ `constructor(dep: Dep)` throws `RegistrationNotFoundError` naming the
210
+ class and telling you to register it with an `inject` list, instead of
211
+ building it with `dep = undefined`. Parameters with defaults do not
212
+ count.
189
213
  - `detectCircularDependencies` — circular chains throw
190
214
  `CircularDependencyError` with the full chain. When disabled,
191
215
  `maxResolutionDepth` still stops runaway recursion.
@@ -12,7 +12,7 @@
12
12
  * - TRANSIENT instances are never cached and never tracked — callers own
13
13
  * their disposal.
14
14
  */
15
- import type { ContainerProvider, ProviderToken } from "../containerProvider/containerProvider.core.js";
15
+ import type { ContainerProvider, InjectedFactory, ProviderToken } from "../containerProvider/containerProvider.core.js";
16
16
  import type { ContainerRegistration, CreateRegistrationOptions, RegistrationToken, ResolvedTokens } from "../containerRegistration/containerRegistration.core.js";
17
17
  import type { ResolutionCache, ResolutionResult } from "../containerResolution/containerResolution.type.js";
18
18
  import type { ContainerOptions, ResolvedContainerOptions } from "../containerOptions/containerOptions.type.js";
@@ -51,7 +51,14 @@ export declare class Container implements ContainerLike {
51
51
  * any other lifetime would be misleading.
52
52
  */
53
53
  registerValue<T>(token: RegistrationToken<T>, value: T, options?: CreateRegistrationOptions): ContainerRegistration<T>;
54
- registerFactory<T>(token: RegistrationToken<T>, factory: (...deps: unknown[]) => T, inject?: readonly ProviderToken[], options?: CreateRegistrationOptions): ContainerRegistration<T>;
54
+ /**
55
+ * Registers a factory. Its parameters are typed from the `inject` tokens,
56
+ * in order: `registerFactory(API, (db) => new Api(db), [DB])` types `db`
57
+ * as `Db` when `DB` is a `createToken<Db>()` or class token, and rejects a
58
+ * factory whose parameters do not match. String/symbol tokens give
59
+ * `unknown`.
60
+ */
61
+ registerFactory<T, const Deps extends readonly ProviderToken[] = readonly []>(token: RegistrationToken<T>, factory: InjectedFactory<T, Deps>, inject?: Deps, options?: CreateRegistrationOptions): ContainerRegistration<T>;
55
62
  registerExisting<T>(token: RegistrationToken<T>, existing: ProviderToken<T>, options?: CreateRegistrationOptions): ContainerRegistration<T>;
56
63
  /**
57
64
  * Resolves a dependency at the container root.
@@ -87,6 +87,13 @@ export class Container {
87
87
  scope: Scope.SINGLETON,
88
88
  });
89
89
  }
90
+ /**
91
+ * Registers a factory. Its parameters are typed from the `inject` tokens,
92
+ * in order: `registerFactory(API, (db) => new Api(db), [DB])` types `db`
93
+ * as `Db` when `DB` is a `createToken<Db>()` or class token, and rejects a
94
+ * factory whose parameters do not match. String/symbol tokens give
95
+ * `unknown`.
96
+ */
90
97
  registerFactory(token, factory, inject = [], options = {}) {
91
98
  return this.register(token, factoryProvider(factory, inject), options);
92
99
  }
@@ -4,6 +4,17 @@
4
4
  */
5
5
  import type { Constructor, InjectionToken, Token } from "../containerToken/containerToken.type.js";
6
6
  export type ProviderToken<T = unknown> = Token<T> | InjectionToken<T>;
7
+ /**
8
+ * The parameter list a factory receives for an `inject` list: each token is
9
+ * mapped to the type it resolves to, so `[DB, Clock]` gives `[Db, Clock]`.
10
+ * Untyped string/symbol tokens map to `unknown`, and a non-tuple
11
+ * `readonly ProviderToken[]` gives `unknown[]`, as before.
12
+ */
13
+ export type InjectedDependencies<Deps extends readonly ProviderToken[]> = {
14
+ -readonly [K in keyof Deps]: Deps[K] extends ProviderToken<infer U> ? U : never;
15
+ };
16
+ /** A factory whose parameters are typed from its `inject` list. */
17
+ export type InjectedFactory<T, Deps extends readonly ProviderToken[]> = (...dependencies: InjectedDependencies<Deps>) => T;
7
18
  export interface ClassProvider<T> {
8
19
  readonly useClass: Constructor<T>;
9
20
  /**
@@ -55,11 +66,19 @@ export declare function hasInjectedDependencies<T = unknown>(provider: Provider<
55
66
  readonly inject: readonly ProviderToken[];
56
67
  };
57
68
  export declare function classProvider<T>(useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassProvider<T>;
58
- export declare function factoryProvider<T>(useFactory: (...dependencies: unknown[]) => T, inject?: readonly ProviderToken[]): FactoryProvider<T>;
69
+ /**
70
+ * Builds a factory provider. The factory's parameters are inferred from the
71
+ * `inject` tokens, in order.
72
+ */
73
+ export declare function factoryProvider<T, const Deps extends readonly ProviderToken[] = readonly []>(useFactory: InjectedFactory<T, Deps>, inject?: Deps): FactoryProvider<T>;
59
74
  export declare function valueProvider<T>(useValue: T): ValueProvider<T>;
60
75
  export declare function existingProvider<T>(useExisting: ProviderToken<T>): ExistingProvider<T>;
61
76
  export declare function provideClass<T>(provide: ProviderToken<T>, useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassRegistration<T>;
62
- export declare function provideFactory<T>(provide: ProviderToken<T>, useFactory: (...dependencies: unknown[]) => T, inject?: readonly ProviderToken[]): FactoryRegistration<T>;
77
+ /**
78
+ * Builds a factory registration. The factory's parameters are inferred from
79
+ * the `inject` tokens, in order.
80
+ */
81
+ export declare function provideFactory<T, const Deps extends readonly ProviderToken[] = readonly []>(provide: ProviderToken<T>, useFactory: InjectedFactory<T, Deps>, inject?: Deps): FactoryRegistration<T>;
63
82
  export declare function provideValue<T>(provide: ProviderToken<T>, useValue: T): ValueRegistration<T>;
64
83
  export declare function provideExisting<T>(provide: ProviderToken<T>, useExisting: ProviderToken<T>): ExistingRegistration<T>;
65
84
  export declare function getProviderToken<T>(provider: ContainerProvider<T>): ProviderToken<T> | undefined;
@@ -25,8 +25,15 @@ export function hasInjectedDependencies(provider) {
25
25
  export function classProvider(useClass, inject = []) {
26
26
  return Object.freeze({ useClass, inject: Object.freeze([...inject]) });
27
27
  }
28
+ /**
29
+ * Builds a factory provider. The factory's parameters are inferred from the
30
+ * `inject` tokens, in order.
31
+ */
28
32
  export function factoryProvider(useFactory, inject = []) {
29
- return Object.freeze({ useFactory, inject: Object.freeze([...inject]) });
33
+ return Object.freeze({
34
+ useFactory: useFactory,
35
+ inject: Object.freeze([...inject]),
36
+ });
30
37
  }
31
38
  export function valueProvider(useValue) {
32
39
  return Object.freeze({ useValue });
@@ -41,10 +48,14 @@ export function provideClass(provide, useClass, inject = []) {
41
48
  inject: Object.freeze([...inject]),
42
49
  });
43
50
  }
51
+ /**
52
+ * Builds a factory registration. The factory's parameters are inferred from
53
+ * the `inject` tokens, in order.
54
+ */
44
55
  export function provideFactory(provide, useFactory, inject = []) {
45
56
  return Object.freeze({
46
57
  provide,
47
- useFactory,
58
+ useFactory: useFactory,
48
59
  inject: Object.freeze([...inject]),
49
60
  });
50
61
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @zudojs/container/containerResolution/containerResolution.autoRegister
3
+ *
4
+ * Guards for `autoRegisterClasses`: resolving an unregistered class builds
5
+ * it with no arguments, which is only correct for a constructor that
6
+ * declares none.
7
+ */
8
+ /**
9
+ * Whether an unregistered token may be auto-registered: a class whose
10
+ * constructor declares no required parameters (`Class.length === 0`).
11
+ *
12
+ * Parameters with a default value do not count, so
13
+ * `constructor(value = 7)` still qualifies.
14
+ */
15
+ export declare function isAutoRegistrable(token: unknown): boolean;
16
+ /**
17
+ * Throws when a class cannot be auto-registered because its constructor
18
+ * declares parameters. Auto-registration has no inject list, so building
19
+ * it would pass `undefined` for every dependency.
20
+ *
21
+ * @throws {RegistrationNotFoundError} naming the class and how to
22
+ * register it.
23
+ */
24
+ export declare function assertAutoRegistrable(token: {
25
+ readonly length: number;
26
+ }): void;
27
+ //# sourceMappingURL=containerResolution.autoRegister.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @zudojs/container/containerResolution/containerResolution.autoRegister
3
+ *
4
+ * Guards for `autoRegisterClasses`: resolving an unregistered class builds
5
+ * it with no arguments, which is only correct for a constructor that
6
+ * declares none.
7
+ */
8
+ import { RegistrationNotFoundError } from "@zudojs/errors";
9
+ import { describeToken } from "../containerToken/containerToken.type.js";
10
+ /**
11
+ * Whether an unregistered token may be auto-registered: a class whose
12
+ * constructor declares no required parameters (`Class.length === 0`).
13
+ *
14
+ * Parameters with a default value do not count, so
15
+ * `constructor(value = 7)` still qualifies.
16
+ */
17
+ export function isAutoRegistrable(token) {
18
+ return typeof token === "function" && token.length === 0;
19
+ }
20
+ /**
21
+ * Throws when a class cannot be auto-registered because its constructor
22
+ * declares parameters. Auto-registration has no inject list, so building
23
+ * it would pass `undefined` for every dependency.
24
+ *
25
+ * @throws {RegistrationNotFoundError} naming the class and how to
26
+ * register it.
27
+ */
28
+ export function assertAutoRegistrable(token) {
29
+ if (isAutoRegistrable(token))
30
+ return;
31
+ const name = describeToken(token);
32
+ const count = token.length;
33
+ throw new RegistrationNotFoundError(name, `Cannot auto-register class "${name}": its constructor declares ` +
34
+ `${count} parameter${count === 1 ? "" : "s"} and no inject list is ` +
35
+ `registered, so it would be built with undefined dependencies. ` +
36
+ `Register it with container.registerClass(${name}, ${name}, ` +
37
+ `{ inject: [/* one token per parameter */] }) or ` +
38
+ `container.registerFactory(${name}, (...deps) => new ${name}(...deps), [/* tokens */]).`);
39
+ }
40
+ //# sourceMappingURL=containerResolution.autoRegister.js.map
@@ -35,6 +35,7 @@ import { unwrapToken } from "../containerToken/containerToken.type.js";
35
35
  import { CircularDependencyError, ContainerError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
36
36
  import { AsyncProviderError, CaptiveDependencyError, DependencyResolutionError, MaxResolutionDepthError, ScopedResolutionError, } from "./containerResolution.error.js";
37
37
  import { describeToken } from "../containerToken/containerToken.type.js";
38
+ import { assertAutoRegistrable, isAutoRegistrable, } from "./containerResolution.autoRegister.js";
38
39
  import { DependentIndex } from "./containerResolution.dependents.js";
39
40
  /**
40
41
  * Scope cache that falls back to its parent scope's cache for lookups while
@@ -124,6 +125,7 @@ export class ContainerResolver {
124
125
  let registration = this.registry.get(token);
125
126
  if (!registration) {
126
127
  if (state.autoRegisterClasses && typeof token === "function") {
128
+ assertAutoRegistrable(token);
127
129
  if (state.allowRegistration) {
128
130
  registration = this.registry.register(token, { useClass: token }, { scope: Scope.TRANSIENT });
129
131
  }
@@ -214,7 +216,7 @@ export class ContainerResolver {
214
216
  if (isExistingProvider(provider)) {
215
217
  const target = unwrapToken(provider.useExisting);
216
218
  if (!this.registry.has(target) &&
217
- !(state.autoRegisterClasses && typeof target === "function")) {
219
+ !(state.autoRegisterClasses && isAutoRegistrable(target))) {
218
220
  throw new ContainerError(`useExisting target "${describeToken(target)}" for token ` +
219
221
  `"${describeToken(token)}" is not registered.`, { token: describeToken(token) });
220
222
  }
@@ -283,7 +285,7 @@ export class ContainerResolver {
283
285
  }
284
286
  canResolve(token, autoRegisterClasses = true) {
285
287
  const t = unwrapToken(token);
286
- return (this.registry.has(t) || (autoRegisterClasses && typeof t === "function"));
288
+ return (this.registry.has(t) || (autoRegisterClasses && isAutoRegistrable(t)));
287
289
  }
288
290
  handleRegistryChange(event) {
289
291
  switch (event.operation) {
@@ -44,5 +44,13 @@ export declare function isInjectionToken<T = unknown>(value: unknown): value is
44
44
  export declare function isConstructorToken<T = unknown>(token: Token<T>): token is Constructor<T>;
45
45
  export declare function isSymbolToken<T = unknown>(token: Token<T>): token is symbol;
46
46
  export declare function isStringToken<T = unknown>(token: Token<T>): token is string;
47
+ /**
48
+ * Returns a human-readable name for a token, used in error messages and as
49
+ * the default registration name.
50
+ *
51
+ * A symbol token (including every `createToken`/`createGlobalToken` token)
52
+ * is shown by its description, so errors read `MissingService` rather than
53
+ * `Symbol(MissingService)`. A symbol without a description is `Symbol()`.
54
+ */
47
55
  export declare function describeToken<T>(token: Token<T> | InjectionToken<T>): string;
48
56
  //# sourceMappingURL=containerToken.type.d.ts.map
@@ -48,14 +48,20 @@ export function isSymbolToken(token) {
48
48
  export function isStringToken(token) {
49
49
  return typeof token === "string";
50
50
  }
51
+ /**
52
+ * Returns a human-readable name for a token, used in error messages and as
53
+ * the default registration name.
54
+ *
55
+ * A symbol token (including every `createToken`/`createGlobalToken` token)
56
+ * is shown by its description, so errors read `MissingService` rather than
57
+ * `Symbol(MissingService)`. A symbol without a description is `Symbol()`.
58
+ */
51
59
  export function describeToken(token) {
52
60
  const resolved = unwrapToken(token);
53
61
  if (typeof resolved === "string")
54
62
  return resolved;
55
63
  if (typeof resolved === "symbol")
56
- return resolved.description
57
- ? `Symbol(${resolved.description})`
58
- : "Symbol()";
64
+ return resolved.description ? resolved.description : "Symbol()";
59
65
  if (typeof resolved === "function")
60
66
  return resolved.name || "AnonymousConstructor";
61
67
  return "UnknownToken";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/container",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Token-based dependency injection container for managing application dependencies and service lifetimes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,11 +18,11 @@
18
18
  "!dist/.tsbuildinfo"
19
19
  ],
20
20
  "dependencies": {
21
- "@zudojs/errors": "1.2.0"
21
+ "@zudojs/errors": "1.3.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "7.0.2",
25
- "vitest": "^4.1.11"
25
+ "vitest": "^5.0.1"
26
26
  },
27
27
  "license": "MIT",
28
28
  "author": {
@@ -41,7 +41,7 @@
41
41
  "dependency-injection",
42
42
  "container"
43
43
  ],
44
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
44
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-container",
45
45
  "bugs": {
46
46
  "url": "https://github.com/oyinlola-tech/zudo/issues"
47
47
  },