di-craft 0.0.17 → 0.0.18

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
@@ -30,6 +30,7 @@
30
30
  - [Core concepts](#core-concepts)
31
31
  - [Tokens](#tokens)
32
32
  - [Providers](#providers)
33
+ - [Annotation-based class providers](#annotation-based-class-providers)
33
34
  - [Optional dependencies](#optional-dependencies)
34
35
  - [Container](#container)
35
36
  - [Scopes](#scopes)
@@ -80,14 +81,17 @@ const users = container.get(USERS); // UserService, fully typed
80
81
 
81
82
  ## Philosophy
82
83
 
83
- Dependency injection without the magic — no decorators, no `reflect-metadata`, no
84
- framework coupling. You work with just **tokens**, **providers**, a **container**,
85
- **scopes**, and **cycle detection**.
84
+ Dependency injection without hidden magic — no `reflect-metadata`, no runtime
85
+ type guessing, and no framework coupling. You work with just **tokens**,
86
+ **providers**, a **container**, **scopes**, and **cycle detection**. Standard
87
+ JavaScript decorators are available as optional sugar for class providers, but
88
+ they still use explicit tokens.
86
89
 
87
90
  ## Features
88
91
 
89
92
  - Zero runtime dependencies
90
93
  - Type-safe tokens and factories
94
+ - Optional `@Injectable` annotation for class providers
91
95
  - Optional dependencies via `optional()`
92
96
  - Singleton, transient, and scoped lifetimes
93
97
  - Hierarchical child containers
@@ -147,6 +151,63 @@ provideFactory(HTTP, {
147
151
  The keys in `deps` become the keys of the object passed to `useFactory`, each
148
152
  resolved to its token's type.
149
153
 
154
+ ### Annotation-based class providers
155
+
156
+ If you write services as classes, you can attach provider metadata to the class
157
+ with standard JavaScript decorators, then turn the class into a normal provider
158
+ with `provideInjectable`.
159
+
160
+ `@Injectable(options)` marks a class as a provider for `options.token`.
161
+ `deps` is an ordered list of constructor dependencies. `scope` and
162
+ `onDispose` behave exactly like they do in `provideFactory`.
163
+
164
+ ```ts
165
+ import {
166
+ Injectable,
167
+ Scopes,
168
+ createContainer,
169
+ createToken,
170
+ optional,
171
+ provideInjectable,
172
+ provideValue,
173
+ } from "di-craft";
174
+
175
+ const CONFIG = createToken<Config>("config");
176
+ const LOGGER = createToken<Logger>("logger");
177
+ const USERS = createToken<UserService>("users");
178
+
179
+ @Injectable({
180
+ token: USERS,
181
+ deps: [LOGGER, optional(CONFIG)],
182
+ scope: Scopes.Scoped,
183
+ })
184
+ class UserService {
185
+ private readonly logger: Logger;
186
+ private readonly config: Config | undefined;
187
+
188
+ constructor(logger: Logger, config: Config | undefined) {
189
+ this.logger = logger;
190
+ this.config = config;
191
+ }
192
+ }
193
+
194
+ const container = createContainer([
195
+ provideValue(LOGGER, new Logger()),
196
+ provideInjectable(UserService),
197
+ ]);
198
+
199
+ const users = container.get(USERS); // UserService
200
+ ```
201
+
202
+ `@Injectable` is the only annotation needed for class injection. It produces a
203
+ regular factory provider internally, so all existing container behavior still
204
+ applies: optional dependencies, scopes, child containers, disposal hooks,
205
+ overrides, and cycle detection.
206
+
207
+ di-craft does not use `reflect-metadata` or parameter decorators. Constructor
208
+ types are erased by JavaScript at runtime, so dependency tokens stay explicit
209
+ instead of being guessed from TypeScript types.
210
+
150
211
  ### Optional dependencies
151
212
 
152
213
  Wrap a token with `optional` to mark a dependency as not required. When no
@@ -451,6 +512,8 @@ try {
451
512
  | `createToken<T>(name)` | Create a unique, typed token. |
452
513
  | `provideValue(token, value)` | Provider that returns an existing value. |
453
514
  | `provideFactory(token, options)` | Provider that builds a value via a factory. |
515
+ | `@Injectable(options)` | Mark a class as a token-backed injectable provider. |
516
+ | `provideInjectable(class)` | Create a factory provider from an injectable class. |
454
517
  | `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
455
518
  | `createContainer(providers?)` | Create a container, optionally seeded with providers. |
456
519
  | `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
package/dist/index.d.mts CHANGED
@@ -1,46 +1,106 @@
1
1
  //#region src/error/error.d.ts
2
+ /**
3
+ * Base class for all di-craft runtime errors.
4
+ */
2
5
  declare class DiError extends Error {
3
6
  constructor(message: string);
4
7
  }
5
8
  //#endregion
6
9
  //#region src/provider/errors.d.ts
10
+ /**
11
+ * Error thrown when a provider configuration cannot be used safely.
12
+ */
7
13
  declare class InvalidProviderError extends DiError {
8
14
  constructor(message: string);
9
15
  }
10
16
  //#endregion
11
17
  //#region src/scope/scope.d.ts
18
+ /**
19
+ * Built-in provider lifetimes.
20
+ */
12
21
  declare const Scopes: {
13
22
  readonly Singleton: "singleton";
14
23
  readonly Transient: "transient";
15
24
  readonly Scoped: "scoped";
16
25
  };
26
+ /**
27
+ * Provider lifetime.
28
+ *
29
+ * - `singleton`: one cached instance in the container that owns the provider.
30
+ * - `scoped`: one cached instance in the container that resolves the provider.
31
+ * - `transient`: a new instance for every resolution.
32
+ */
17
33
  type Scope = (typeof Scopes)[keyof typeof Scopes];
18
34
  //#endregion
19
35
  //#region src/token/types.d.ts
36
+ /** A unique, typed key used to register and resolve a dependency. */
20
37
  type Token<T> = {
38
+ /**
39
+ * Runtime identity of the token.
40
+ *
41
+ * Tokens with the same name still have different identities.
42
+ */
21
43
  readonly id: symbol;
44
+ /**
45
+ * Human-readable name used in error messages and diagnostics.
46
+ */
22
47
  readonly name: string;
23
48
  readonly __type?: T;
24
49
  };
25
50
  //#endregion
26
51
  //#region src/token/token.d.ts
52
+ /**
53
+ * Creates a unique typed token.
54
+ *
55
+ * The `name` is used only for diagnostics. Token identity is unique per call,
56
+ * so two tokens with the same name do not collide.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const CONFIG = createToken<Config>("config");
61
+ * ```
62
+ */
27
63
  declare const createToken: <T>(name: string) => Token<T>;
28
64
  //#endregion
29
65
  //#region src/provider/types.d.ts
66
+ /**
67
+ * A token wrapped as an optional dependency.
68
+ *
69
+ * Optional dependencies resolve to `undefined` when no provider is registered
70
+ * in the container chain.
71
+ */
30
72
  type OptionalDependency<T> = {
31
73
  readonly token: Token<T>;
32
74
  readonly optional: true;
33
75
  };
76
+ /**
77
+ * A dependency accepted by factory providers and `container.get`.
78
+ */
34
79
  type Dependency<T> = Token<T> | OptionalDependency<T>;
35
80
  type DepsMap = Record<string, Dependency<unknown>>;
36
- type DependencyValue<TDep> = TDep extends OptionalDependency<infer T> ? T | undefined : TDep extends Token<infer T> ? T : never;
37
- type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: DependencyValue<TDeps[TKey]> };
81
+ type DependencyValue$1<TDep> = TDep extends OptionalDependency<infer T> ? T | undefined : TDep extends Token<infer T> ? T : never;
82
+ type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: DependencyValue$1<TDeps[TKey]> };
38
83
  type Factory<T, TDeps extends DepsMap> = (deps: ResolveDeps<TDeps>) => T;
84
+ /**
85
+ * Cleanup hook called for a cached instance during container disposal.
86
+ *
87
+ * Transient providers cannot use disposal hooks because their instances are not
88
+ * cached or tracked by the container.
89
+ */
39
90
  type DisposeHook<T> = (instance: T) => void | Promise<void>;
91
+ /**
92
+ * Provider that resolves a token to an existing value.
93
+ */
40
94
  type ValueProvider<T> = {
41
95
  readonly provide: Token<T>;
42
96
  readonly useValue: T;
43
97
  };
98
+ /**
99
+ * Provider that lazily creates a token value from typed dependencies.
100
+ *
101
+ * `deps` keys become the properties passed to `useFactory`, `scope` controls
102
+ * caching lifetime, and `onDispose` runs for cached singleton/scoped instances.
103
+ */
44
104
  type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
45
105
  readonly provide: Token<T>;
46
106
  readonly deps?: TDeps;
@@ -49,50 +109,210 @@ type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
49
109
  readonly onDispose?: DisposeHook<T>;
50
110
  };
51
111
  type AnyFactoryProvider = FactoryProvider<any, any>;
112
+ /**
113
+ * Provider accepted by `createContainer` and `container.register`.
114
+ */
52
115
  type Provider = ValueProvider<unknown> | AnyFactoryProvider;
53
116
  //#endregion
54
117
  //#region src/provider/provider.d.ts
118
+ /**
119
+ * Creates a provider for an already constructed value.
120
+ *
121
+ * The value must match the token type.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * provideValue(PORT, 3000);
126
+ * ```
127
+ */
55
128
  declare const provideValue: <T>(token: Token<T>, useValue: T) => ValueProvider<T>;
129
+ /**
130
+ * Creates a provider that lazily builds a value.
131
+ *
132
+ * Dependencies declared in `deps` become the object passed to `useFactory`.
133
+ * The factory return type must match the token type.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * provideFactory(HTTP, {
138
+ * deps: { config: CONFIG },
139
+ * useFactory: ({ config }) => new HttpClient(config.apiUrl),
140
+ * });
141
+ * ```
142
+ */
56
143
  declare const provideFactory: <T, TDeps extends DepsMap = Record<never, never>>(token: Token<T>, options: {
57
144
  readonly deps?: TDeps;
58
145
  readonly scope?: Scope;
59
146
  readonly useFactory: Factory<T, TDeps>;
60
147
  readonly onDispose?: DisposeHook<T>;
61
148
  }) => FactoryProvider<T, TDeps>;
149
+ /**
150
+ * Marks a token as optional.
151
+ *
152
+ * Optional dependencies resolve to `undefined` instead of throwing when no
153
+ * provider exists in the container chain. They can be used in factory `deps`
154
+ * or passed directly to `container.get`.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const logger = container.get(optional(LOGGER));
159
+ * ```
160
+ */
62
161
  declare const optional: <T>(token: Token<T>) => OptionalDependency<T>;
63
162
  //#endregion
163
+ //#region src/annotation/types.d.ts
164
+ type InjectableDeps = readonly Dependency<unknown>[];
165
+ type DependencyValue<TDependency> = TDependency extends OptionalDependency<infer T> ? T | undefined : TDependency extends Token<infer T> ? T : never;
166
+ type ResolveDependencyTuple<TDeps extends InjectableDeps> = { -readonly [TKey in keyof TDeps]: DependencyValue<TDeps[TKey]> };
167
+ /**
168
+ * Constructor type whose parameters match an injectable dependency tuple.
169
+ */
170
+ type InjectableConstructor<T, TDeps extends InjectableDeps> = new (...args: ResolveDependencyTuple<TDeps>) => T;
171
+ /**
172
+ * Class that can be passed to `provideInjectable`.
173
+ *
174
+ * `never[]` keeps classes with required constructor parameters assignable
175
+ * without claiming arbitrary constructor arguments are valid.
176
+ */
177
+ type InjectableClass<T = unknown> = new (...args: never[]) => T;
178
+ /**
179
+ * Options stored by `@Injectable` and converted into a factory provider.
180
+ */
181
+ type InjectableOptions<T, TDeps extends InjectableDeps = InjectableDeps> = {
182
+ /**
183
+ * Token provided by the decorated class.
184
+ */
185
+ readonly token: Token<T>;
186
+ /**
187
+ * Ordered constructor dependencies.
188
+ *
189
+ * Each item is resolved and passed to the constructor at the same index.
190
+ */
191
+ readonly deps?: TDeps;
192
+ /**
193
+ * Provider lifetime.
194
+ *
195
+ * Defaults to `Scopes.Singleton`.
196
+ */
197
+ readonly scope?: Scope;
198
+ /**
199
+ * Cleanup hook for cached singleton and scoped instances.
200
+ *
201
+ * Not supported for transient providers.
202
+ */
203
+ readonly onDispose?: DisposeHook<T>;
204
+ };
205
+ //#endregion
206
+ //#region src/annotation/annotation.d.ts
207
+ type InjectableDecorator<TTarget> = (target: TTarget, context: ClassDecoratorContext) => void;
208
+ type InjectableOptionsWithoutDeps<T> = Omit<InjectableOptions<T, readonly []>, "deps">;
209
+ declare function Injectable<T, const TDeps extends readonly Dependency<unknown>[]>(options: InjectableOptions<T, TDeps> & {
210
+ readonly deps: TDeps;
211
+ }): InjectableDecorator<InjectableConstructor<T, TDeps>>;
212
+ declare function Injectable<T>(options: InjectableOptionsWithoutDeps<T>): InjectableDecorator<InjectableConstructor<T, readonly []>>;
213
+ /**
214
+ * Creates a factory provider from a class marked with `@Injectable`.
215
+ *
216
+ * The returned provider is a normal di-craft factory provider, so scopes,
217
+ * optional dependencies, disposal hooks, overrides, and cycle detection behave
218
+ * the same as with `provideFactory`.
219
+ */
220
+ declare function provideInjectable<T>(target: InjectableClass<T>): FactoryProvider<T, DepsMap>;
221
+ //#endregion
64
222
  //#region src/registry/errors.d.ts
223
+ /**
224
+ * Error thrown when registering a provider for an already registered token.
225
+ */
65
226
  declare class DuplicateProviderError extends DiError {
66
227
  constructor(tokenName: string);
67
228
  }
68
229
  //#endregion
69
230
  //#region src/registry/types.d.ts
231
+ /**
232
+ * Options for provider registration.
233
+ */
70
234
  type RegisterOptions = {
235
+ /**
236
+ * Replace an existing provider for the same token.
237
+ *
238
+ * Overriding an already resolved disposable singleton is rejected to avoid
239
+ * dropping resources without running their cleanup hook.
240
+ */
71
241
  readonly allowOverride?: boolean;
72
242
  };
73
243
  //#endregion
74
244
  //#region src/container/types.d.ts
245
+ /**
246
+ * Container that stores providers and resolves typed dependencies.
247
+ */
75
248
  type Container = {
249
+ /**
250
+ * Registers a provider in this container.
251
+ *
252
+ * Pass `{ allowOverride: true }` to intentionally replace an existing
253
+ * provider for the same token.
254
+ */
76
255
  register(provider: Provider, options?: RegisterOptions): void;
256
+ /**
257
+ * Resolves a required token.
258
+ *
259
+ * Throws `MissingProviderError` when no provider exists in this container or
260
+ * any parent container.
261
+ */
77
262
  get<T>(token: Token<T>): T;
263
+ /**
264
+ * Resolves an optional dependency.
265
+ *
266
+ * Returns `undefined` when no provider exists in this container or any parent
267
+ * container.
268
+ */
78
269
  get<T>(dependency: OptionalDependency<T>): T | undefined;
270
+ /**
271
+ * Checks whether this container or one of its parents has a provider.
272
+ */
79
273
  has(token: Token<unknown>): boolean;
274
+ /**
275
+ * Disposes cached instances owned by this container.
276
+ *
277
+ * Disposal hooks run in reverse creation order. Calling `dispose` more than
278
+ * once is safe.
279
+ */
80
280
  dispose(): Promise<void>;
81
281
  };
82
282
  //#endregion
83
283
  //#region src/container/container.d.ts
284
+ /**
285
+ * Creates a root container with optional initial providers.
286
+ *
287
+ * Root containers own singleton instances for providers registered in them.
288
+ */
84
289
  declare const createContainer: (providers?: readonly Provider[]) => Container;
290
+ /**
291
+ * Creates a child container that can resolve providers from its parent.
292
+ *
293
+ * Child containers may register their own providers while still reusing parent
294
+ * providers. Scoped providers create one cached instance per resolving child.
295
+ */
85
296
  declare const createChildContainer: (parent: Container, providers?: readonly Provider[]) => Container;
86
297
  //#endregion
87
298
  //#region src/resolver/errors.d.ts
299
+ /**
300
+ * Error thrown when resolving a token without a registered provider.
301
+ */
88
302
  declare class MissingProviderError extends DiError {
89
303
  constructor(tokenName: string);
90
304
  }
305
+ /**
306
+ * Error thrown when a dependency descriptor is invalid.
307
+ */
91
308
  declare class InvalidDependencyError extends DiError {
92
309
  constructor(dependencyKey: string);
93
310
  }
311
+ /**
312
+ * Error thrown when provider dependencies form a cycle.
313
+ */
94
314
  declare class CircularDependencyError extends DiError {
95
315
  constructor(tokenNames: string[]);
96
316
  }
97
317
  //#endregion
98
- export { CircularDependencyError, type Container, type Dependency, DiError, type DisposeHook, DuplicateProviderError, type FactoryProvider, InvalidDependencyError, InvalidProviderError, MissingProviderError, type OptionalDependency, type Provider, type RegisterOptions, type Scope, Scopes, type Token, type ValueProvider, createChildContainer, createContainer, createToken, optional, provideFactory, provideValue };
318
+ export { CircularDependencyError, type Container, type Dependency, DiError, type DisposeHook, DuplicateProviderError, type FactoryProvider, Injectable, InvalidDependencyError, InvalidProviderError, MissingProviderError, type OptionalDependency, type Provider, type RegisterOptions, type Scope, Scopes, type Token, type ValueProvider, createChildContainer, createContainer, createToken, optional, provideFactory, provideInjectable, provideValue };
package/dist/index.mjs CHANGED
@@ -1,267 +1 @@
1
- //#region src/error/error.ts
2
- var DiError = class extends Error {
3
- constructor(message) {
4
- super(message);
5
- this.name = "DiError";
6
- if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
7
- }
8
- };
9
-
10
- //#endregion
11
- //#region src/provider/errors.ts
12
- var InvalidProviderError = class extends DiError {
13
- constructor(message) {
14
- super(message);
15
- this.name = "InvalidProviderError";
16
- }
17
- };
18
-
19
- //#endregion
20
- //#region src/scope/scope.ts
21
- const Scopes = {
22
- Singleton: "singleton",
23
- Transient: "transient",
24
- Scoped: "scoped"
25
- };
26
-
27
- //#endregion
28
- //#region src/provider/provider.ts
29
- const provideValue = (token, useValue) => ({
30
- provide: token,
31
- useValue
32
- });
33
- const provideFactory = (token, options) => {
34
- if (options.scope === Scopes.Transient && options.onDispose) throw new InvalidProviderError(`onDispose is not supported for transient providers (token "${token.name}"): transient instances are not tracked, so the hook would never run.`);
35
- return {
36
- provide: token,
37
- useFactory: options.useFactory,
38
- scope: options.scope ?? Scopes.Singleton,
39
- ...options.deps ? { deps: options.deps } : {},
40
- ...options.onDispose ? { onDispose: options.onDispose } : {}
41
- };
42
- };
43
- const optional = (token) => ({
44
- token,
45
- optional: true
46
- });
47
- const isOptionalDependency = (dependency) => {
48
- return dependency.optional === true;
49
- };
50
- const isValueProvider = (provider) => {
51
- return "useValue" in provider;
52
- };
53
-
54
- //#endregion
55
- //#region src/registry/errors.ts
56
- var DuplicateProviderError = class extends DiError {
57
- constructor(tokenName) {
58
- super(`Provider for token "${tokenName}" is already registered`);
59
- this.name = "DuplicateProviderError";
60
- }
61
- };
62
-
63
- //#endregion
64
- //#region src/registry/registry.ts
65
- var RegistryClass = class {
66
- providers = /* @__PURE__ */ new Map();
67
- register(provider, options) {
68
- const existed = this.providers.has(provider.provide.id);
69
- if (existed && !options?.allowOverride) throw new DuplicateProviderError(provider.provide.name);
70
- this.providers.set(provider.provide.id, provider);
71
- return existed;
72
- }
73
- get(token) {
74
- return this.providers.get(token.id);
75
- }
76
- has(token) {
77
- return this.providers.has(token.id);
78
- }
79
- };
80
- const createRegistry = () => new RegistryClass();
81
-
82
- //#endregion
83
- //#region src/resolver/errors.ts
84
- var MissingProviderError = class extends DiError {
85
- constructor(tokenName) {
86
- super(`Provider for token "${tokenName}" is not registered`);
87
- this.name = "MissingProviderError";
88
- }
89
- };
90
- var InvalidDependencyError = class extends DiError {
91
- constructor(dependencyKey) {
92
- super(`Invalid dependency "${dependencyKey}"`);
93
- this.name = "InvalidDependencyError";
94
- }
95
- };
96
- var CircularDependencyError = class extends DiError {
97
- constructor(tokenNames) {
98
- super(`Circular dependency detected: ${tokenNames.join(" -> ")}`);
99
- this.name = "CircularDependencyError";
100
- }
101
- };
102
-
103
- //#endregion
104
- //#region src/store/store.ts
105
- var StoreClass = class {
106
- instances = /* @__PURE__ */ new Map();
107
- get(token) {
108
- return this.instances.get(token.id);
109
- }
110
- set(token, record) {
111
- this.instances.set(token.id, record);
112
- }
113
- delete(token) {
114
- this.instances.delete(token.id);
115
- }
116
- async dispose() {
117
- const records = [...this.instances.values()].reverse();
118
- this.instances.clear();
119
- for (const record of records) if (record.onDispose) await record.onDispose(record.value);
120
- }
121
- };
122
- const createStore = () => new StoreClass();
123
-
124
- //#endregion
125
- //#region src/resolver/context.ts
126
- var ResolutionContext = class {
127
- resolving = /* @__PURE__ */ new Set();
128
- path = [];
129
- enter(token) {
130
- if (this.resolving.has(token.id)) {
131
- const cycleStartIndex = this.path.findIndex((pathToken) => pathToken.id === token.id);
132
- throw new CircularDependencyError([...this.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
133
- }
134
- this.resolving.add(token.id);
135
- this.path.push(token);
136
- }
137
- exit(token) {
138
- this.path.pop();
139
- this.resolving.delete(token.id);
140
- }
141
- };
142
-
143
- //#endregion
144
- //#region src/resolver/resolver.ts
145
- const lifetimeRank = (scope) => scope === Scopes.Scoped ? 1 : scope === Scopes.Transient ? 2 : 0;
146
- var ResolverClass = class {
147
- registry;
148
- store = createStore();
149
- parent;
150
- constructor(registry, parent) {
151
- this.registry = registry;
152
- this.parent = parent;
153
- }
154
- resolve(token) {
155
- return this.resolveToken(token, void 0);
156
- }
157
- resolveOptional(token) {
158
- return this.lookupOptional(token, void 0);
159
- }
160
- invalidate(token) {
161
- this.store.delete(token);
162
- }
163
- hasDisposableInstance(token) {
164
- return this.store.get(token)?.onDispose !== void 0;
165
- }
166
- dispose() {
167
- return this.store.dispose();
168
- }
169
- resolveToken(token, context, consumerScope, consumerName) {
170
- let owner = this;
171
- let provider;
172
- while (owner) {
173
- provider = owner.registry.get(token);
174
- if (provider) break;
175
- owner = owner.parent;
176
- }
177
- if (!provider || !owner) throw new MissingProviderError(token.name);
178
- if (isValueProvider(provider)) return provider.useValue;
179
- if (consumerScope !== void 0 && lifetimeRank(provider.scope) > lifetimeRank(consumerScope)) throw new InvalidProviderError(`"${consumerName}" (${consumerScope}) cannot depend on "${token.name}" (${provider.scope ?? Scopes.Singleton}): a longer-lived provider would capture a shorter-lived one. Widen the dependency's scope or narrow the consumer's.`);
180
- const host = this.selectHost(provider.scope, owner);
181
- if (host) {
182
- const cached = host.store.get(token);
183
- if (cached) return cached.value;
184
- }
185
- const ctx = context ?? new ResolutionContext();
186
- ctx.enter(token);
187
- try {
188
- const deps = (host ?? this).resolveDeps(provider.deps, ctx, provider.scope, token.name);
189
- const value = provider.useFactory(deps);
190
- if (host) host.store.set(token, {
191
- value,
192
- ...provider.onDispose ? { onDispose: provider.onDispose } : {}
193
- });
194
- return value;
195
- } finally {
196
- ctx.exit(token);
197
- }
198
- }
199
- selectHost(scope, owner) {
200
- if (scope === Scopes.Transient) return;
201
- if (scope === Scopes.Scoped) return this;
202
- return owner;
203
- }
204
- resolveDeps(deps, context, consumerScope, consumerName) {
205
- if (!deps) return {};
206
- const resolvedDeps = {};
207
- for (const key of Object.keys(deps)) {
208
- const dependency = deps[key];
209
- if (dependency === void 0) throw new InvalidDependencyError(String(key));
210
- resolvedDeps[key] = isOptionalDependency(dependency) ? this.lookupOptional(dependency.token, context, consumerScope, consumerName) : this.resolveToken(dependency, context, consumerScope, consumerName);
211
- }
212
- return resolvedDeps;
213
- }
214
- lookupOptional(token, context, consumerScope, consumerName) {
215
- let owner = this;
216
- while (owner) {
217
- if (owner.registry.has(token)) return this.resolveToken(token, context, consumerScope, consumerName);
218
- owner = owner.parent;
219
- }
220
- }
221
- };
222
- const createResolver = (registry, parent) => new ResolverClass(registry, parent);
223
-
224
- //#endregion
225
- //#region src/container/container.ts
226
- var ContainerClass = class {
227
- registry;
228
- resolver;
229
- parent;
230
- constructor(providers = [], parent) {
231
- this.parent = parent;
232
- this.registry = createRegistry();
233
- for (const provider of providers) this.registry.register(provider);
234
- this.resolver = createResolver(this.registry, parent?.resolver);
235
- }
236
- register(provider, options) {
237
- if (options?.allowOverride && this.resolver.hasDisposableInstance(provider.provide)) throw new InvalidProviderError(`Cannot override token "${provider.provide.name}": its instance was already created and has an onDispose hook. Dispose the container before replacing it.`);
238
- if (this.registry.register(provider, options)) this.resolver.invalidate(provider.provide);
239
- }
240
- get(dependency) {
241
- if (isOptionalDependency(dependency)) return this.resolver.resolveOptional(dependency.token);
242
- return this.resolver.resolve(dependency);
243
- }
244
- has(token) {
245
- return this.registry.has(token) || (this.parent?.has(token) ?? false);
246
- }
247
- dispose() {
248
- return this.resolver.dispose();
249
- }
250
- };
251
- const createContainer = (providers = []) => new ContainerClass(providers);
252
- const createChildContainer = (parent, providers = []) => new ContainerClass(providers, parent);
253
-
254
- //#endregion
255
- //#region src/token/token.ts
256
- var TokenClass = class {
257
- name;
258
- id;
259
- constructor(name) {
260
- this.name = name;
261
- this.id = Symbol(name);
262
- }
263
- };
264
- const createToken = (name) => new TokenClass(name);
265
-
266
- //#endregion
267
- export { CircularDependencyError, DiError, DuplicateProviderError, InvalidDependencyError, InvalidProviderError, MissingProviderError, Scopes, createChildContainer, createContainer, createToken, optional, provideFactory, provideValue };
1
+ var e=class extends Error{constructor(e){super(e),this.name=`DiError`,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},t=class extends e{constructor(e){super(e),this.name=`InvalidProviderError`}};const n={Singleton:`singleton`,Transient:`transient`,Scoped:`scoped`},r=(e,t)=>({provide:e,useValue:t}),i=(e,r)=>{if(r.scope===n.Transient&&r.onDispose)throw new t(`onDispose is not supported for transient providers (token "${e.name}"): transient instances are not tracked, so the hook would never run.`);return{provide:e,useFactory:r.useFactory,scope:r.scope??n.Singleton,...r.deps?{deps:r.deps}:{},...r.onDispose?{onDispose:r.onDispose}:{}}},a=e=>({token:e,optional:!0}),o=e=>e.optional===!0,s=e=>`useValue`in e,c=e=>{if(e.length!==0)return Object.fromEntries(e.entries())},l=new WeakMap,u=e=>l.get(e),d=e=>{l.set(e.target,e.metadata)},f=e=>{let{context:n,target:r}=e;if(!(n.kind===`class`&&typeof r==`function`))throw new t(`@Injectable can only decorate classes`)};function p(e){return(t,n)=>{f({target:t,context:n}),d({target:t,metadata:e})}}function m(e){let n=u(e);if(!n)throw new t(`Class "${e.name||`<anonymous>`}" is not marked as injectable. Add @Injectable({ token: TOKEN }) before calling provideInjectable().`);let{deps:r=[],onDispose:a,scope:o,token:s}=n,l=c(r),d=e;return i(s,{useFactory:e=>new d(...r.map((t,n)=>e[String(n)])),...l?{deps:l}:{},...o?{scope:o}:{},...a?{onDispose:a}:{}})}var h=class extends e{constructor(e){super(`Provider for token "${e}" is already registered`),this.name=`DuplicateProviderError`}},g=class{providers=new Map;register(e,t){let n=this.providers.has(e.provide.id);if(n&&!t?.allowOverride)throw new h(e.provide.name);return this.providers.set(e.provide.id,e),n}get(e){return this.providers.get(e.id)}has(e){return this.providers.has(e.id)}};const _=()=>new g;var v=class extends e{constructor(e){super(`Provider for token "${e}" is not registered`),this.name=`MissingProviderError`}},y=class extends e{constructor(e){super(`Invalid dependency "${e}"`),this.name=`InvalidDependencyError`}},b=class extends e{constructor(e){super(`Circular dependency detected: ${e.join(` -> `)}`),this.name=`CircularDependencyError`}},x=class{instances=new Map;get(e){return this.instances.get(e.id)}set(e,t){this.instances.set(e.id,t)}delete(e){this.instances.delete(e.id)}async dispose(){let e=[...this.instances.values()].reverse();this.instances.clear();for(let t of e)t.onDispose&&await t.onDispose(t.value)}};const S=()=>new x;var C=class{resolving=new Set;path=[];enter(e){if(this.resolving.has(e.id)){let t=this.path.findIndex(t=>t.id===e.id);throw new b([...this.path.slice(t),e].map(e=>e.name))}this.resolving.add(e.id),this.path.push(e)}exit(e){this.path.pop(),this.resolving.delete(e.id)}};const w=e=>e===n.Scoped?1:e===n.Transient?2:0;var T=class{registry;store=S();parent;constructor(e,t){this.registry=e,this.parent=t}resolve(e){return this.resolveToken(e,void 0)}resolveOptional(e){return this.lookupOptional(e,void 0)}invalidate(e){this.store.delete(e)}hasDisposableInstance(e){return this.store.get(e)?.onDispose!==void 0}dispose(){return this.store.dispose()}resolveToken(e,r,i,a){let o=this,c;for(;o&&(c=o.registry.get(e),!c);)o=o.parent;if(!c||!o)throw new v(e.name);if(s(c))return c.useValue;if(i!==void 0&&w(c.scope)>w(i))throw new t(`"${a}" (${i}) cannot depend on "${e.name}" (${c.scope??n.Singleton}): a longer-lived provider would capture a shorter-lived one. Widen the dependency's scope or narrow the consumer's.`);let l=this.selectHost(c.scope,o);if(l){let t=l.store.get(e);if(t)return t.value}let u=r??new C;u.enter(e);try{let t=(l??this).resolveDeps(c.deps,u,c.scope,e.name),n=c.useFactory(t);return l&&l.store.set(e,{value:n,...c.onDispose?{onDispose:c.onDispose}:{}}),n}finally{u.exit(e)}}selectHost(e,t){if(e!==n.Transient)return e===n.Scoped?this:t}resolveDeps(e,t,n,r){if(!e)return{};let i={};for(let a of Object.keys(e)){let s=e[a];if(s===void 0)throw new y(String(a));i[a]=o(s)?this.lookupOptional(s.token,t,n,r):this.resolveToken(s,t,n,r)}return i}lookupOptional(e,t,n,r){let i=this;for(;i;){if(i.registry.has(e))return this.resolveToken(e,t,n,r);i=i.parent}}};const E=(e,t)=>new T(e,t);var D=class{registry;resolver;parent;constructor(e=[],t){this.parent=t,this.registry=_();for(let t of e)this.registry.register(t);this.resolver=E(this.registry,t?.resolver)}register(e,n){if(n?.allowOverride&&this.resolver.hasDisposableInstance(e.provide))throw new t(`Cannot override token "${e.provide.name}": its instance was already created and has an onDispose hook. Dispose the container before replacing it.`);this.registry.register(e,n)&&this.resolver.invalidate(e.provide)}get(e){return o(e)?this.resolver.resolveOptional(e.token):this.resolver.resolve(e)}has(e){return this.registry.has(e)||(this.parent?.has(e)??!1)}dispose(){return this.resolver.dispose()}};const O=(e=[])=>new D(e),k=(e,t=[])=>new D(t,e);var A=class{name;id;constructor(e){this.name=e,this.id=Symbol(e)}};const j=e=>new A(e);export{b as CircularDependencyError,e as DiError,h as DuplicateProviderError,p as Injectable,y as InvalidDependencyError,t as InvalidProviderError,v as MissingProviderError,n as Scopes,k as createChildContainer,O as createContainer,j as createToken,a as optional,i as provideFactory,m as provideInjectable,r as provideValue};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "di-craft",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "A tiny, type-safe dependency injection container for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": {