di-craft 0.0.16 → 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 +72 -5
- package/dist/index.d.mts +223 -3
- package/dist/index.mjs +1 -267
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<
|
|
13
|
-
|
|
12
|
+
<a href="https://www.npmjs.com/package/di-craft">
|
|
13
|
+
<img
|
|
14
|
+
alt="npm version and bundle size"
|
|
15
|
+
src="https://shieldcn.dev/group/npm/di-craft+bundlephobia/minzip/di-craft.svg?variant=secondary"
|
|
16
|
+
/>
|
|
17
|
+
</a>
|
|
14
18
|
</p>
|
|
15
19
|
|
|
16
20
|
> [!NOTE]
|
|
@@ -26,6 +30,7 @@
|
|
|
26
30
|
- [Core concepts](#core-concepts)
|
|
27
31
|
- [Tokens](#tokens)
|
|
28
32
|
- [Providers](#providers)
|
|
33
|
+
- [Annotation-based class providers](#annotation-based-class-providers)
|
|
29
34
|
- [Optional dependencies](#optional-dependencies)
|
|
30
35
|
- [Container](#container)
|
|
31
36
|
- [Scopes](#scopes)
|
|
@@ -76,14 +81,17 @@ const users = container.get(USERS); // UserService, fully typed
|
|
|
76
81
|
|
|
77
82
|
## Philosophy
|
|
78
83
|
|
|
79
|
-
Dependency injection without
|
|
80
|
-
framework coupling. You work with just **tokens**,
|
|
81
|
-
**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.
|
|
82
89
|
|
|
83
90
|
## Features
|
|
84
91
|
|
|
85
92
|
- Zero runtime dependencies
|
|
86
93
|
- Type-safe tokens and factories
|
|
94
|
+
- Optional `@Injectable` annotation for class providers
|
|
87
95
|
- Optional dependencies via `optional()`
|
|
88
96
|
- Singleton, transient, and scoped lifetimes
|
|
89
97
|
- Hierarchical child containers
|
|
@@ -143,6 +151,63 @@ provideFactory(HTTP, {
|
|
|
143
151
|
The keys in `deps` become the keys of the object passed to `useFactory`, each
|
|
144
152
|
resolved to its token's type.
|
|
145
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
|
+
|
|
146
211
|
### Optional dependencies
|
|
147
212
|
|
|
148
213
|
Wrap a token with `optional` to mark a dependency as not required. When no
|
|
@@ -447,6 +512,8 @@ try {
|
|
|
447
512
|
| `createToken<T>(name)` | Create a unique, typed token. |
|
|
448
513
|
| `provideValue(token, value)` | Provider that returns an existing value. |
|
|
449
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. |
|
|
450
517
|
| `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
|
|
451
518
|
| `createContainer(providers?)` | Create a container, optionally seeded with providers. |
|
|
452
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "A tiny, type-safe dependency injection container for TypeScript",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -41,14 +41,18 @@
|
|
|
41
41
|
"pre-commit": "bun run lint && bun run typecheck"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
|
-
"dependency-injection",
|
|
45
44
|
"di",
|
|
45
|
+
"di container",
|
|
46
|
+
"dependency-injection",
|
|
47
|
+
"dependency injection",
|
|
46
48
|
"ioc",
|
|
47
|
-
"inversion
|
|
49
|
+
"inversion of control",
|
|
50
|
+
"ioc container",
|
|
48
51
|
"typescript",
|
|
49
|
-
"type-safe",
|
|
50
52
|
"container",
|
|
51
|
-
"
|
|
53
|
+
"zero dependencies",
|
|
54
|
+
"lightweight",
|
|
55
|
+
"tiny",
|
|
52
56
|
"tree-shakable"
|
|
53
57
|
],
|
|
54
58
|
"homepage": "https://github.com/bezmen-e/di-craft",
|