@zudojs/container 1.1.2 → 1.2.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.
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
 
@@ -144,9 +162,13 @@ container.register(API, {
144
162
  });
145
163
  ```
146
164
 
147
- Classes without an `inject` list are constructed with zero arguments. There
148
- is no reflection/decorator magic — dependencies are exactly the tokens you
149
- list, resolved in order.
165
+ Classes without an `inject` list are constructed with zero arguments, so
166
+ only a constructor that declares no parameters may omit it (parameters with
167
+ a default value do not count). Registering such a class without `inject`
168
+ is allowed, but resolving it throws a `ProviderResolutionError` naming the
169
+ class and the `inject: [...]` fix, instead of building it with `undefined`
170
+ dependencies. There is no reflection/decorator magic — dependencies are
171
+ exactly the tokens you list, resolved in order.
150
172
 
151
173
  ## Duplicate registrations
152
174
 
@@ -185,7 +207,13 @@ const container = createContainer({
185
207
  registers it on the fly as `TRANSIENT`. When registrations are frozen the
186
208
  class is instantiated _ephemerally_ without being registered. Set to
187
209
  `false` to require explicit registration (`canResolve`/`resolveOptional`
188
- respect this).
210
+ respect this). Only a class whose constructor declares no required
211
+ parameters (`Class.length === 0`) is auto-registered: auto-registration
212
+ has no inject list, so `resolve(NeedsDep)` for
213
+ `constructor(dep: Dep)` throws `RegistrationNotFoundError` naming the
214
+ class and telling you to register it with an `inject` list, instead of
215
+ building it with `dep = undefined`. Parameters with defaults do not
216
+ count.
189
217
  - `detectCircularDependencies` — circular chains throw
190
218
  `CircularDependencyError` with the full chain. When disabled,
191
219
  `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,42 @@
1
+ /**
2
+ * @zudojs/container/containerResolution/containerResolution.autoRegister
3
+ *
4
+ * Guards against building a class with no arguments when its constructor
5
+ * declares some: an unregistered class under `autoRegisterClasses`, and a
6
+ * class registration with no `inject` list.
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
+ /**
28
+ * Throws when a class registration has no `inject` list but its constructor
29
+ * declares parameters. Without one the class is built with no arguments,
30
+ * so every dependency would be `undefined`. Registering it is allowed;
31
+ * resolving it is what fails.
32
+ *
33
+ * Parameters with a default value do not count (`Function.length` stops at
34
+ * the first one).
35
+ *
36
+ * @throws {ProviderResolutionError} naming the class and the `inject` fix.
37
+ */
38
+ export declare function assertClassInjectable(token: string, ctor: {
39
+ readonly length: number;
40
+ readonly name: string;
41
+ }, inject: readonly unknown[]): void;
42
+ //# sourceMappingURL=containerResolution.autoRegister.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @zudojs/container/containerResolution/containerResolution.autoRegister
3
+ *
4
+ * Guards against building a class with no arguments when its constructor
5
+ * declares some: an unregistered class under `autoRegisterClasses`, and a
6
+ * class registration with no `inject` list.
7
+ */
8
+ import { ProviderResolutionError, 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
+ /**
41
+ * Throws when a class registration has no `inject` list but its constructor
42
+ * declares parameters. Without one the class is built with no arguments,
43
+ * so every dependency would be `undefined`. Registering it is allowed;
44
+ * resolving it is what fails.
45
+ *
46
+ * Parameters with a default value do not count (`Function.length` stops at
47
+ * the first one).
48
+ *
49
+ * @throws {ProviderResolutionError} naming the class and the `inject` fix.
50
+ */
51
+ export function assertClassInjectable(token, ctor, inject) {
52
+ const count = ctor.length;
53
+ if (inject.length > 0 || count === 0)
54
+ return;
55
+ const name = ctor.name || "anonymous class";
56
+ throw new ProviderResolutionError(token, `Cannot build class "${name}" for token "${token}": its constructor ` +
57
+ `declares ${count} parameter${count === 1 ? "" : "s"} and the ` +
58
+ `registration has no inject list, so it would be built with undefined ` +
59
+ `dependencies. Register it with container.registerClass(${token}, ` +
60
+ `${name}, { inject: [/* one token per parameter */] }).`);
61
+ }
62
+ //# 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, assertClassInjectable, 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
  }
@@ -237,6 +239,7 @@ export class ContainerResolver {
237
239
  }
238
240
  if (isClassProvider(provider)) {
239
241
  const deps = provider.inject ?? [];
242
+ assertClassInjectable(describeToken(token), provider.useClass, deps);
240
243
  const args = deps.map((d) => this.resolveInternal(unwrapToken(d), state, singletonAncestor)
241
244
  .value);
242
245
  const ctor = provider.useClass;
@@ -283,7 +286,7 @@ export class ContainerResolver {
283
286
  }
284
287
  canResolve(token, autoRegisterClasses = true) {
285
288
  const t = unwrapToken(token);
286
- return (this.registry.has(t) || (autoRegisterClasses && typeof t === "function"));
289
+ return (this.registry.has(t) || (autoRegisterClasses && isAutoRegistrable(t)));
287
290
  }
288
291
  handleRegistryChange(event) {
289
292
  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.1",
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
  },