di-craft 0.0.9 → 0.0.11

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
@@ -1,18 +1,375 @@
1
1
  # di-craft
2
2
 
3
- A tiny TypeScript dependency injection container.
3
+ A tiny, type-safe dependency injection container for TypeScript.
4
+
5
+ ## Contents
6
+
7
+ - [Quick start](#quick-start)
8
+ - [Philosophy](#philosophy)
9
+ - [Features](#features)
10
+ - [Install](#install)
11
+ - [Core concepts](#core-concepts)
12
+ - [Tokens](#tokens)
13
+ - [Providers](#providers)
14
+ - [Container](#container)
15
+ - [Scopes](#scopes)
16
+ - [Disposal](#disposal)
17
+ - [Child containers](#child-containers)
18
+ - [Cycle detection](#cycle-detection)
19
+ - [Example: per-request container](#example-per-request-container)
20
+ - [Error handling](#error-handling)
21
+ - [API reference](#api-reference)
22
+ - [License](#license)
23
+
24
+ ## Quick start
25
+
26
+ Declare typed tokens, describe how each one is built with a provider, then create
27
+ a container and resolve from it. Dependencies are wired explicitly through the
28
+ `deps` map and resolved for you.
29
+
30
+ ```ts
31
+ import {
32
+ createContainer,
33
+ createToken,
34
+ provideFactory,
35
+ provideValue,
36
+ type Provider,
37
+ } from "di-craft";
38
+
39
+ const CONFIG = createToken<Config>("config");
40
+ const LOGGER = createToken<Logger>("logger");
41
+ const USERS = createToken<UserService>("users");
42
+
43
+ const providers: Provider[] = [
44
+ provideValue(CONFIG, loadConfig()),
45
+ provideFactory(LOGGER, {
46
+ deps: { config: CONFIG },
47
+ useFactory: ({ config }) => new Logger(config.level),
48
+ }),
49
+ provideFactory(USERS, {
50
+ deps: { logger: LOGGER },
51
+ useFactory: ({ logger }) => new UserService(logger),
52
+ }),
53
+ ];
54
+
55
+ const container = createContainer(providers);
56
+
57
+ const users = container.get(USERS); // UserService, fully typed
58
+ ```
59
+
60
+ ## Philosophy
61
+
62
+ Dependency injection without the magic — no decorators, no `reflect-metadata`, no
63
+ framework coupling. You work with just **tokens**, **providers**, a **container**,
64
+ **scopes**, and **cycle detection**.
4
65
 
5
66
  ## Features
6
67
 
7
68
  - Zero runtime dependencies
8
- - No decorators
9
- - No reflect-metadata
10
- - Type-safe tokens
11
- - Explicit factories
12
- - Singleton and transient scopes
69
+ - Type-safe tokens and factories
70
+ - Singleton, transient, and scoped lifetimes
71
+ - Hierarchical child containers
72
+ - Deterministic disposal with `onDispose` hooks
73
+ - Circular dependency detection
74
+ - Tree-shakable, tiny bundle size
75
+ - Ships both ESM and CommonJS builds
13
76
 
14
77
  ## Install
15
78
 
16
79
  ```bash
17
80
  bun add di-craft
81
+ npm install di-craft
82
+ pnpm add di-craft
83
+ yarn add di-craft
84
+ ```
85
+
86
+ Requires Node.js `>= 20`.
87
+
88
+ ## Core concepts
89
+
90
+ ### Tokens
91
+
92
+ A token is a unique, type-carrying key. Identity is based on an internal `symbol`, **not** on the name — two tokens with the same name are still different.
93
+
94
+ ```ts
95
+ const PORT = createToken<number>("port");
96
+
97
+ PORT.name; // "port" — used only for error messages
98
+ ```
99
+
100
+ The type argument flows everywhere: providers must produce a matching value, and `container.get(PORT)` returns `number`.
101
+
102
+ ### Providers
103
+
104
+ A provider tells the container how to produce the value for a token.
105
+
106
+ `provideValue` — register an existing value:
107
+
108
+ ```ts
109
+ provideValue(PORT, 3000);
110
+ ```
111
+
112
+ `provideFactory` — build the value lazily, with optional dependencies and scope:
113
+
114
+ ```ts
115
+ provideFactory(HTTP, {
116
+ deps: { config: CONFIG }, // optional, keyed map of tokens
117
+ scope: "singleton", // optional, defaults to "singleton"
118
+ useFactory: ({ config }) => new HttpClient(config.apiUrl),
119
+ });
120
+ ```
121
+
122
+ The keys in `deps` become the keys of the object passed to `useFactory`, each resolved to its token's type.
123
+
124
+ ### Container
125
+
126
+ The container holds your providers and resolves values on demand. Create one
127
+ from a list of providers (all optional), then add more, check, resolve, and
128
+ dispose:
129
+
130
+ - `register(provider, options?)` — add a provider at any time.
131
+ - `has(token)` — whether a provider for the token is registered.
132
+ - `get(token)` — resolve the value, building and caching it as its scope dictates.
133
+ - `dispose()` — run `onDispose` hooks and release resolved instances.
134
+
135
+ ```ts
136
+ const container = createContainer(providers); // providers are optional
137
+
138
+ container.register(provideValue(PORT, 3000)); // register more at any time
139
+ container.has(PORT); // true
140
+ container.get(PORT); // 3000
141
+ await container.dispose(); // release resolved singletons
142
+ ```
143
+
144
+ Registering the same token twice throws `DuplicateProviderError`. To replace an
145
+ existing provider on purpose (handy for tests, mocks, and environment-specific
146
+ overrides), pass `{ allowOverride: true }`:
147
+
148
+ ```ts
149
+ container.register(provideValue(API, fakeApi), { allowOverride: true });
150
+ ```
151
+
152
+ Overriding a token whose value was already resolved as a singleton drops the
153
+ cached instance, so the next `get` rebuilds it from the new provider.
154
+
155
+ ### Scopes
156
+
157
+ | Scope | Behavior |
158
+ | ---------------------- | ---------------------------------------------------------------- |
159
+ | `singleton` (default) | The factory runs once; the same instance is returned every time. |
160
+ | `transient` | The factory runs on every `get`, producing a fresh instance. |
161
+ | `scoped` | One instance per container. In a child container each child gets its own instance, while the provider can still be declared once on the parent. |
162
+
163
+ Use the `Scopes` helper for autocompletion, or pass the plain string — both work:
164
+
165
+ ```ts
166
+ import { Scopes, provideFactory } from "di-craft";
167
+
168
+ provideFactory(ID, {
169
+ scope: Scopes.Transient, // or scope: "transient"
170
+ useFactory: () => crypto.randomUUID(),
171
+ });
172
+
173
+ container.get(ID) !== container.get(ID); // true
174
+ ```
175
+
176
+ A transient provider that depends on a singleton still reuses the shared singleton instance.
177
+
178
+ ### Disposal
179
+
180
+ Factory providers can declare an `onDispose` hook to release resources (database
181
+ pools, sockets, timers, subscriptions). Calling `container.dispose()` runs the
182
+ hooks for every resolved singleton and clears the cache:
183
+
184
+ ```ts
185
+ const DB = createToken<Pool>("db");
186
+
187
+ const container = createContainer([
188
+ provideFactory(DB, {
189
+ useFactory: () => createPool(url),
190
+ onDispose: (pool) => pool.end(), // may be sync or async
191
+ }),
192
+ ]);
193
+
194
+ container.get(DB);
195
+
196
+ await container.dispose(); // awaits async hooks, then clears instances
18
197
  ```
198
+
199
+ Details:
200
+
201
+ - Hooks run in reverse creation order (dependents before their dependencies).
202
+ - `dispose()` returns a promise and awaits async hooks.
203
+ - It is idempotent — calling it again is a no-op.
204
+ - Only resolved singletons are disposed; transient and never-resolved instances are not tracked.
205
+
206
+ ### Child containers
207
+
208
+ `createChildContainer(parent, providers?)` creates a child that inherits
209
+ everything from its parent but can add or override providers locally. This is the
210
+ typical pattern for per-request isolation on a server: shared services live on
211
+ the root, request-specific values live on a short-lived child.
212
+
213
+ ```ts
214
+ const root = createContainer([
215
+ provideFactory(LOGGER, { useFactory: () => console }), // singleton, shared
216
+ provideFactory(HANDLER, {
217
+ scope: Scopes.Scoped, // one instance per child
218
+ deps: { request: REQUEST },
219
+ useFactory: ({ request }) => createHandler(request),
220
+ }),
221
+ ]);
222
+
223
+ function handle(request: Request) {
224
+ const child = createChildContainer(root, [provideValue(REQUEST, request)]);
225
+
226
+ child.get(LOGGER); // same logger as the root and every other child
227
+ child.get(HANDLER); // a fresh handler, unique to this child
228
+
229
+ return child.dispose(); // release only this child's instances
230
+ }
231
+ ```
232
+
233
+ How resolution works across the chain:
234
+
235
+ - A token is looked up in the child first, then walks up to the parent.
236
+ - `singleton` is cached on the container that **owns** the provider, so it is shared by the whole subtree.
237
+ - `scoped` is cached on the **requesting** child, so each child gets its own instance — even when the provider is declared once on the parent.
238
+ - A `scoped` provider resolves its dependencies from the requesting child, so it can depend on values registered only in that child (like `REQUEST`).
239
+ - `dispose()` only releases the container it is called on; it does not cascade to parents or children.
240
+
241
+ ### Cycle detection
242
+
243
+ If providers form a dependency cycle, resolution throws `CircularDependencyError` with the full path instead of overflowing the stack.
244
+
245
+ ```ts
246
+ // A -> B -> A
247
+ container.get(A); // throws: Circular dependency detected: A -> B -> A
248
+ ```
249
+
250
+ ## Example: per-request container
251
+
252
+ A common server pattern: build one **root** container at startup with the
253
+ long-lived singletons (a database pool, clients, config), then fork a
254
+ short-lived **child** per request for request-specific state. `scoped` providers
255
+ give each request its own instance, and `dispose()` releases them once the
256
+ response is sent.
257
+
258
+ ```ts
259
+ import Fastify, { type FastifyRequest } from "fastify";
260
+ import {
261
+ type Container,
262
+ createContainer,
263
+ createChildContainer,
264
+ createToken,
265
+ provideValue,
266
+ provideFactory,
267
+ Scopes,
268
+ } from "di-craft";
269
+
270
+ const DB = createToken<Pool>("db");
271
+ const REQUEST = createToken<FastifyRequest>("request");
272
+ const REQUEST_ID = createToken<string>("request-id");
273
+ const USERS = createToken<UserService>("users");
274
+
275
+ const app = Fastify({ logger: true });
276
+
277
+ // Built once, shared by every request.
278
+ const root = createContainer([
279
+ provideFactory(DB, {
280
+ useFactory: () => createPool(process.env.DATABASE_URL),
281
+ onDispose: (pool) => pool.end(), // closed only on shutdown
282
+ }),
283
+ provideFactory(REQUEST_ID, {
284
+ scope: Scopes.Scoped, // one id per request
285
+ deps: { request: REQUEST }, // resolves the value registered on the child
286
+ useFactory: ({ request }) => request.id, // Fastify's built-in request id
287
+ }),
288
+ provideFactory(USERS, {
289
+ scope: Scopes.Scoped, // one service per request...
290
+ deps: { db: DB }, // ...but reuses the shared pool
291
+ useFactory: ({ db }) => new UserService(db),
292
+ }),
293
+ ]);
294
+
295
+ // Keep each request's child container without patching Fastify's types.
296
+ const containers = new WeakMap<FastifyRequest, Container>();
297
+
298
+ app.addHook("onRequest", async (request) => {
299
+ // A fresh child per request, seeded with the request object.
300
+ containers.set(request, createChildContainer(root, [provideValue(REQUEST, request)]));
301
+ });
302
+
303
+ app.addHook("onResponse", async (request) => {
304
+ // Release this request's scoped instances once the response is sent.
305
+ await containers.get(request)?.dispose();
306
+ });
307
+
308
+ app.get<{ Params: { id: string } }>("/users/:id", async (request) => {
309
+ const container = containers.get(request)!;
310
+ const users = container.get(USERS);
311
+ const id = container.get(REQUEST_ID); // unique to this request
312
+
313
+ return users.findById(request.params.id, { traceId: id });
314
+ });
315
+
316
+ // On shutdown, dispose the root to close the shared pool.
317
+ app.addHook("onClose", async () => {
318
+ await root.dispose();
319
+ });
320
+
321
+ await app.listen({ port: 3000 });
322
+ ```
323
+
324
+ What each scope buys you here:
325
+
326
+ - `DB` is a **singleton** — created once on the root and reused by every request, so you don't open a new pool per call.
327
+ - `REQUEST_ID` and `USERS` are **scoped** — a new instance per child, so each request gets isolated state even though the providers are declared once on the root.
328
+ - `REQUEST` is a per-request **value** registered on the child, and the scoped `REQUEST_ID` resolves it from that same child.
329
+ - The child's `dispose()` releases only that request's scoped instances; the shared pool stays open until `root.dispose()` runs on `onClose`.
330
+
331
+ ## Error handling
332
+
333
+ All errors extend the shared `DiError` base class, so you can catch any container error with a single check:
334
+
335
+ ```ts
336
+ import { DiError, MissingProviderError } from "di-craft";
337
+
338
+ try {
339
+ container.get(SOME_TOKEN);
340
+ } catch (error) {
341
+ if (error instanceof MissingProviderError) {
342
+ // a specific failure
343
+ }
344
+
345
+ if (error instanceof DiError) {
346
+ // any di-craft error
347
+ }
348
+ }
349
+ ```
350
+
351
+ | Error | Thrown when |
352
+ | -------------------------- | -------------------------------------------------------- |
353
+ | `MissingProviderError` | A token is resolved but no provider is registered. |
354
+ | `DuplicateProviderError` | A token is registered more than once. |
355
+ | `CircularDependencyError` | Providers form a dependency cycle. |
356
+ | `InvalidDependencyError` | A declared dependency token is missing/undefined. |
357
+
358
+ ## API reference
359
+
360
+ | Export | Description |
361
+ | ----------------------- | ---------------------------------------------------------- |
362
+ | `createToken<T>(name)` | Create a unique, typed token. |
363
+ | `provideValue(token, value)` | Provider that returns an existing value. |
364
+ | `provideFactory(token, options)` | Provider that builds a value via a factory. |
365
+ | `createContainer(providers?)` | Create a container, optionally seeded with providers. |
366
+ | `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
367
+ | `Scopes` | Object of scope values (`Scopes.Singleton`, `Scopes.Transient`, `Scopes.Scoped`). |
368
+
369
+ Exported types: `Container`, `Token`, `Provider`, `ValueProvider`, `FactoryProvider`, `Scope`, `DisposeHook`, `RegisterOptions`.
370
+
371
+ Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`, `CircularDependencyError`, `InvalidDependencyError`.
372
+
373
+ ## License
374
+
375
+ [MIT](./LICENSE)
package/dist/index.cjs CHANGED
@@ -1,4 +1,221 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/error/error.ts
3
+ var DiError = class extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "DiError";
7
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
8
+ }
9
+ };
10
+ //#endregion
11
+ //#region src/registry/errors.ts
12
+ var DuplicateProviderError = class extends DiError {
13
+ constructor(tokenName) {
14
+ super(`Provider for token "${tokenName}" is already registered`);
15
+ this.name = "DuplicateProviderError";
16
+ }
17
+ };
18
+ //#endregion
19
+ //#region src/registry/registry.ts
20
+ var RegistryClass = class {
21
+ providers = /* @__PURE__ */ new Map();
22
+ register(provider, options) {
23
+ if (!options?.allowOverride && this.providers.has(provider.provide.id)) throw new DuplicateProviderError(provider.provide.name);
24
+ this.providers.set(provider.provide.id, provider);
25
+ }
26
+ get(token) {
27
+ return this.providers.get(token.id);
28
+ }
29
+ has(token) {
30
+ return this.providers.has(token.id);
31
+ }
32
+ };
33
+ const createRegistry = () => new RegistryClass();
34
+ //#endregion
35
+ //#region src/resolver/errors.ts
36
+ var MissingProviderError = class extends DiError {
37
+ constructor(tokenName) {
38
+ super(`Provider for token "${tokenName}" is not registered`);
39
+ this.name = "MissingProviderError";
40
+ }
41
+ };
42
+ var InvalidDependencyError = class extends DiError {
43
+ constructor(dependencyKey) {
44
+ super(`Invalid dependency "${dependencyKey}"`);
45
+ this.name = "InvalidDependencyError";
46
+ }
47
+ };
48
+ var CircularDependencyError = class extends DiError {
49
+ constructor(tokenNames) {
50
+ super(`Circular dependency detected: ${tokenNames.join(" -> ")}`);
51
+ this.name = "CircularDependencyError";
52
+ }
53
+ };
54
+ //#endregion
55
+ //#region src/scope/scope.ts
56
+ const Scopes = {
57
+ Singleton: "singleton",
58
+ Transient: "transient",
59
+ Scoped: "scoped"
60
+ };
61
+ //#endregion
62
+ //#region src/provider/provider.ts
63
+ const provideValue = (token, useValue) => ({
64
+ provide: token,
65
+ useValue
66
+ });
67
+ const provideFactory = (token, options) => {
68
+ return {
69
+ provide: token,
70
+ useFactory: options.useFactory,
71
+ scope: options.scope ?? Scopes.Singleton,
72
+ ...options.deps ? { deps: options.deps } : {},
73
+ ...options.onDispose ? { onDispose: options.onDispose } : {}
74
+ };
75
+ };
76
+ const isValueProvider = (provider) => {
77
+ return "useValue" in provider;
78
+ };
79
+ const isFactoryProvider = (provider) => {
80
+ return "useFactory" in provider;
81
+ };
82
+ //#endregion
83
+ //#region src/store/store.ts
84
+ var StoreClass = class {
85
+ instances = /* @__PURE__ */ new Map();
86
+ get(token) {
87
+ return this.instances.get(token.id);
88
+ }
89
+ set(token, record) {
90
+ this.instances.set(token.id, record);
91
+ }
92
+ delete(token) {
93
+ this.instances.delete(token.id);
94
+ }
95
+ async dispose() {
96
+ const records = [...this.instances.values()].reverse();
97
+ this.instances.clear();
98
+ for (const record of records) if (record.onDispose) await record.onDispose(record.value);
99
+ }
100
+ };
101
+ const createStore = () => new StoreClass();
102
+ //#endregion
103
+ //#region src/resolver/context.ts
104
+ var ResolutionContext = class {
105
+ resolving = /* @__PURE__ */ new Set();
106
+ path = [];
107
+ enter(token) {
108
+ if (this.resolving.has(token.id)) {
109
+ const cycleStartIndex = this.path.findIndex((pathToken) => pathToken.id === token.id);
110
+ throw new CircularDependencyError([...this.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
111
+ }
112
+ this.resolving.add(token.id);
113
+ this.path.push(token);
114
+ }
115
+ exit(token) {
116
+ this.path.pop();
117
+ this.resolving.delete(token.id);
118
+ }
119
+ };
120
+ //#endregion
121
+ //#region src/resolver/resolver.ts
122
+ var ResolverClass = class {
123
+ registry;
124
+ store = createStore();
125
+ parent;
126
+ constructor(registry, parent) {
127
+ this.registry = registry;
128
+ this.parent = parent;
129
+ }
130
+ resolve(token) {
131
+ return this.resolveToken(token, new ResolutionContext());
132
+ }
133
+ invalidate(token) {
134
+ this.store.delete(token);
135
+ }
136
+ dispose() {
137
+ return this.store.dispose();
138
+ }
139
+ resolveToken(token, context) {
140
+ const owner = this.findOwner(token);
141
+ if (!owner) throw new MissingProviderError(token.name);
142
+ const provider = owner.registry.get(token);
143
+ if (!provider) throw new MissingProviderError(token.name);
144
+ if (isValueProvider(provider)) return provider.useValue;
145
+ if (isFactoryProvider(provider)) {
146
+ const host = this.selectHost(provider.scope, owner);
147
+ if (host) {
148
+ const cached = host.store.get(token);
149
+ if (cached) return cached.value;
150
+ }
151
+ context.enter(token);
152
+ try {
153
+ const deps = (host ?? this).resolveDeps(provider.deps, context);
154
+ const value = provider.useFactory(deps);
155
+ if (host) host.store.set(token, {
156
+ value,
157
+ ...provider.onDispose ? { onDispose: provider.onDispose } : {}
158
+ });
159
+ return value;
160
+ } finally {
161
+ context.exit(token);
162
+ }
163
+ }
164
+ throw new MissingProviderError(token.name);
165
+ }
166
+ findOwner(token) {
167
+ let resolver = this;
168
+ while (resolver) {
169
+ if (resolver.registry.has(token)) return resolver;
170
+ resolver = resolver.parent;
171
+ }
172
+ }
173
+ selectHost(scope, owner) {
174
+ if (scope === Scopes.Transient) return;
175
+ if (scope === Scopes.Scoped) return this;
176
+ return owner;
177
+ }
178
+ resolveDeps(deps, context) {
179
+ if (!deps) return {};
180
+ const resolvedDeps = {};
181
+ for (const key of Object.keys(deps)) {
182
+ const token = deps[key];
183
+ if (token === void 0) throw new InvalidDependencyError(String(key));
184
+ resolvedDeps[key] = this.resolveToken(token, context);
185
+ }
186
+ return resolvedDeps;
187
+ }
188
+ };
189
+ const createResolver = (registry, parent) => new ResolverClass(registry, parent);
190
+ //#endregion
191
+ //#region src/container/container.ts
192
+ var ContainerClass = class {
193
+ registry;
194
+ resolver;
195
+ parent;
196
+ constructor(providers = [], parent) {
197
+ this.parent = parent;
198
+ this.registry = createRegistry();
199
+ for (const provider of providers) this.registry.register(provider);
200
+ this.resolver = createResolver(this.registry, parent?.resolver);
201
+ }
202
+ register(provider, options) {
203
+ this.registry.register(provider, options);
204
+ if (options?.allowOverride) this.resolver.invalidate(provider.provide);
205
+ }
206
+ get(token) {
207
+ return this.resolver.resolve(token);
208
+ }
209
+ has(token) {
210
+ return this.registry.has(token) || (this.parent?.has(token) ?? false);
211
+ }
212
+ dispose() {
213
+ return this.resolver.dispose();
214
+ }
215
+ };
216
+ const createContainer = (providers = []) => new ContainerClass(providers);
217
+ const createChildContainer = (parent, providers = []) => new ContainerClass(providers, parent);
218
+ //#endregion
2
219
  //#region src/token/token.ts
3
220
  var TokenClass = class {
4
221
  name;
@@ -10,4 +227,14 @@ var TokenClass = class {
10
227
  };
11
228
  const createToken = (name) => new TokenClass(name);
12
229
  //#endregion
230
+ exports.CircularDependencyError = CircularDependencyError;
231
+ exports.DiError = DiError;
232
+ exports.DuplicateProviderError = DuplicateProviderError;
233
+ exports.InvalidDependencyError = InvalidDependencyError;
234
+ exports.MissingProviderError = MissingProviderError;
235
+ exports.Scopes = Scopes;
236
+ exports.createChildContainer = createChildContainer;
237
+ exports.createContainer = createContainer;
13
238
  exports.createToken = createToken;
239
+ exports.provideFactory = provideFactory;
240
+ exports.provideValue = provideValue;
package/dist/index.d.cts CHANGED
@@ -1,3 +1,11 @@
1
+ //#region src/scope/scope.d.ts
2
+ declare const Scopes: {
3
+ readonly Singleton: "singleton";
4
+ readonly Transient: "transient";
5
+ readonly Scoped: "scoped";
6
+ };
7
+ type Scope = (typeof Scopes)[keyof typeof Scopes];
8
+ //#endregion
1
9
  //#region src/token/types.d.ts
2
10
  type Token<T> = {
3
11
  readonly id: symbol;
@@ -8,4 +16,71 @@ type Token<T> = {
8
16
  //#region src/token/token.d.ts
9
17
  declare const createToken: <T>(name: string) => Token<T>;
10
18
  //#endregion
11
- export { type Token, createToken };
19
+ //#region src/provider/types.d.ts
20
+ type DepsMap = Record<string, Token<unknown>>;
21
+ type TokenValue<TToken> = TToken extends Token<infer TValue> ? TValue : never;
22
+ type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: TokenValue<TDeps[TKey]> };
23
+ type Factory<T, TDeps extends DepsMap> = (deps: ResolveDeps<TDeps>) => T;
24
+ type DisposeHook<T> = (instance: T) => void | Promise<void>;
25
+ type ValueProvider<T> = {
26
+ readonly provide: Token<T>;
27
+ readonly useValue: T;
28
+ };
29
+ type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
30
+ readonly provide: Token<T>;
31
+ readonly deps?: TDeps;
32
+ readonly scope?: Scope;
33
+ readonly useFactory: Factory<T, TDeps>;
34
+ readonly onDispose?: DisposeHook<T>;
35
+ };
36
+ type AnyFactoryProvider = FactoryProvider<any, any>;
37
+ type Provider = ValueProvider<unknown> | AnyFactoryProvider;
38
+ //#endregion
39
+ //#region src/provider/provider.d.ts
40
+ declare const provideValue: <T>(token: Token<T>, useValue: T) => ValueProvider<T>;
41
+ declare const provideFactory: <T, TDeps extends DepsMap = Record<never, never>>(token: Token<T>, options: {
42
+ readonly deps?: TDeps;
43
+ readonly scope?: Scope;
44
+ readonly useFactory: Factory<T, TDeps>;
45
+ readonly onDispose?: DisposeHook<T>;
46
+ }) => FactoryProvider<T, TDeps>;
47
+ //#endregion
48
+ //#region src/error/error.d.ts
49
+ declare class DiError extends Error {
50
+ constructor(message: string);
51
+ }
52
+ //#endregion
53
+ //#region src/registry/errors.d.ts
54
+ declare class DuplicateProviderError extends DiError {
55
+ constructor(tokenName: string);
56
+ }
57
+ //#endregion
58
+ //#region src/registry/types.d.ts
59
+ type RegisterOptions = {
60
+ readonly allowOverride?: boolean;
61
+ };
62
+ //#endregion
63
+ //#region src/container/types.d.ts
64
+ type Container = {
65
+ register(provider: Provider, options?: RegisterOptions): void;
66
+ get<T>(token: Token<T>): T;
67
+ has(token: Token<unknown>): boolean;
68
+ dispose(): Promise<void>;
69
+ };
70
+ //#endregion
71
+ //#region src/container/container.d.ts
72
+ declare const createContainer: (providers?: readonly Provider[]) => Container;
73
+ declare const createChildContainer: (parent: Container, providers?: readonly Provider[]) => Container;
74
+ //#endregion
75
+ //#region src/resolver/errors.d.ts
76
+ declare class MissingProviderError extends DiError {
77
+ constructor(tokenName: string);
78
+ }
79
+ declare class InvalidDependencyError extends DiError {
80
+ constructor(dependencyKey: string);
81
+ }
82
+ declare class CircularDependencyError extends DiError {
83
+ constructor(tokenNames: string[]);
84
+ }
85
+ //#endregion
86
+ export { CircularDependencyError, type Container, DiError, type DisposeHook, DuplicateProviderError, type FactoryProvider, InvalidDependencyError, MissingProviderError, type Provider, type RegisterOptions, type Scope, Scopes, type Token, type ValueProvider, createChildContainer, createContainer, createToken, provideFactory, provideValue };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,11 @@
1
+ //#region src/scope/scope.d.ts
2
+ declare const Scopes: {
3
+ readonly Singleton: "singleton";
4
+ readonly Transient: "transient";
5
+ readonly Scoped: "scoped";
6
+ };
7
+ type Scope = (typeof Scopes)[keyof typeof Scopes];
8
+ //#endregion
1
9
  //#region src/token/types.d.ts
2
10
  type Token<T> = {
3
11
  readonly id: symbol;
@@ -8,4 +16,71 @@ type Token<T> = {
8
16
  //#region src/token/token.d.ts
9
17
  declare const createToken: <T>(name: string) => Token<T>;
10
18
  //#endregion
11
- export { type Token, createToken };
19
+ //#region src/provider/types.d.ts
20
+ type DepsMap = Record<string, Token<unknown>>;
21
+ type TokenValue<TToken> = TToken extends Token<infer TValue> ? TValue : never;
22
+ type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: TokenValue<TDeps[TKey]> };
23
+ type Factory<T, TDeps extends DepsMap> = (deps: ResolveDeps<TDeps>) => T;
24
+ type DisposeHook<T> = (instance: T) => void | Promise<void>;
25
+ type ValueProvider<T> = {
26
+ readonly provide: Token<T>;
27
+ readonly useValue: T;
28
+ };
29
+ type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
30
+ readonly provide: Token<T>;
31
+ readonly deps?: TDeps;
32
+ readonly scope?: Scope;
33
+ readonly useFactory: Factory<T, TDeps>;
34
+ readonly onDispose?: DisposeHook<T>;
35
+ };
36
+ type AnyFactoryProvider = FactoryProvider<any, any>;
37
+ type Provider = ValueProvider<unknown> | AnyFactoryProvider;
38
+ //#endregion
39
+ //#region src/provider/provider.d.ts
40
+ declare const provideValue: <T>(token: Token<T>, useValue: T) => ValueProvider<T>;
41
+ declare const provideFactory: <T, TDeps extends DepsMap = Record<never, never>>(token: Token<T>, options: {
42
+ readonly deps?: TDeps;
43
+ readonly scope?: Scope;
44
+ readonly useFactory: Factory<T, TDeps>;
45
+ readonly onDispose?: DisposeHook<T>;
46
+ }) => FactoryProvider<T, TDeps>;
47
+ //#endregion
48
+ //#region src/error/error.d.ts
49
+ declare class DiError extends Error {
50
+ constructor(message: string);
51
+ }
52
+ //#endregion
53
+ //#region src/registry/errors.d.ts
54
+ declare class DuplicateProviderError extends DiError {
55
+ constructor(tokenName: string);
56
+ }
57
+ //#endregion
58
+ //#region src/registry/types.d.ts
59
+ type RegisterOptions = {
60
+ readonly allowOverride?: boolean;
61
+ };
62
+ //#endregion
63
+ //#region src/container/types.d.ts
64
+ type Container = {
65
+ register(provider: Provider, options?: RegisterOptions): void;
66
+ get<T>(token: Token<T>): T;
67
+ has(token: Token<unknown>): boolean;
68
+ dispose(): Promise<void>;
69
+ };
70
+ //#endregion
71
+ //#region src/container/container.d.ts
72
+ declare const createContainer: (providers?: readonly Provider[]) => Container;
73
+ declare const createChildContainer: (parent: Container, providers?: readonly Provider[]) => Container;
74
+ //#endregion
75
+ //#region src/resolver/errors.d.ts
76
+ declare class MissingProviderError extends DiError {
77
+ constructor(tokenName: string);
78
+ }
79
+ declare class InvalidDependencyError extends DiError {
80
+ constructor(dependencyKey: string);
81
+ }
82
+ declare class CircularDependencyError extends DiError {
83
+ constructor(tokenNames: string[]);
84
+ }
85
+ //#endregion
86
+ export { CircularDependencyError, type Container, DiError, type DisposeHook, DuplicateProviderError, type FactoryProvider, InvalidDependencyError, MissingProviderError, type Provider, type RegisterOptions, type Scope, Scopes, type Token, type ValueProvider, createChildContainer, createContainer, createToken, provideFactory, provideValue };
package/dist/index.mjs CHANGED
@@ -1,3 +1,220 @@
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
+ //#endregion
10
+ //#region src/registry/errors.ts
11
+ var DuplicateProviderError = class extends DiError {
12
+ constructor(tokenName) {
13
+ super(`Provider for token "${tokenName}" is already registered`);
14
+ this.name = "DuplicateProviderError";
15
+ }
16
+ };
17
+ //#endregion
18
+ //#region src/registry/registry.ts
19
+ var RegistryClass = class {
20
+ providers = /* @__PURE__ */ new Map();
21
+ register(provider, options) {
22
+ if (!options?.allowOverride && this.providers.has(provider.provide.id)) throw new DuplicateProviderError(provider.provide.name);
23
+ this.providers.set(provider.provide.id, provider);
24
+ }
25
+ get(token) {
26
+ return this.providers.get(token.id);
27
+ }
28
+ has(token) {
29
+ return this.providers.has(token.id);
30
+ }
31
+ };
32
+ const createRegistry = () => new RegistryClass();
33
+ //#endregion
34
+ //#region src/resolver/errors.ts
35
+ var MissingProviderError = class extends DiError {
36
+ constructor(tokenName) {
37
+ super(`Provider for token "${tokenName}" is not registered`);
38
+ this.name = "MissingProviderError";
39
+ }
40
+ };
41
+ var InvalidDependencyError = class extends DiError {
42
+ constructor(dependencyKey) {
43
+ super(`Invalid dependency "${dependencyKey}"`);
44
+ this.name = "InvalidDependencyError";
45
+ }
46
+ };
47
+ var CircularDependencyError = class extends DiError {
48
+ constructor(tokenNames) {
49
+ super(`Circular dependency detected: ${tokenNames.join(" -> ")}`);
50
+ this.name = "CircularDependencyError";
51
+ }
52
+ };
53
+ //#endregion
54
+ //#region src/scope/scope.ts
55
+ const Scopes = {
56
+ Singleton: "singleton",
57
+ Transient: "transient",
58
+ Scoped: "scoped"
59
+ };
60
+ //#endregion
61
+ //#region src/provider/provider.ts
62
+ const provideValue = (token, useValue) => ({
63
+ provide: token,
64
+ useValue
65
+ });
66
+ const provideFactory = (token, options) => {
67
+ return {
68
+ provide: token,
69
+ useFactory: options.useFactory,
70
+ scope: options.scope ?? Scopes.Singleton,
71
+ ...options.deps ? { deps: options.deps } : {},
72
+ ...options.onDispose ? { onDispose: options.onDispose } : {}
73
+ };
74
+ };
75
+ const isValueProvider = (provider) => {
76
+ return "useValue" in provider;
77
+ };
78
+ const isFactoryProvider = (provider) => {
79
+ return "useFactory" in provider;
80
+ };
81
+ //#endregion
82
+ //#region src/store/store.ts
83
+ var StoreClass = class {
84
+ instances = /* @__PURE__ */ new Map();
85
+ get(token) {
86
+ return this.instances.get(token.id);
87
+ }
88
+ set(token, record) {
89
+ this.instances.set(token.id, record);
90
+ }
91
+ delete(token) {
92
+ this.instances.delete(token.id);
93
+ }
94
+ async dispose() {
95
+ const records = [...this.instances.values()].reverse();
96
+ this.instances.clear();
97
+ for (const record of records) if (record.onDispose) await record.onDispose(record.value);
98
+ }
99
+ };
100
+ const createStore = () => new StoreClass();
101
+ //#endregion
102
+ //#region src/resolver/context.ts
103
+ var ResolutionContext = class {
104
+ resolving = /* @__PURE__ */ new Set();
105
+ path = [];
106
+ enter(token) {
107
+ if (this.resolving.has(token.id)) {
108
+ const cycleStartIndex = this.path.findIndex((pathToken) => pathToken.id === token.id);
109
+ throw new CircularDependencyError([...this.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
110
+ }
111
+ this.resolving.add(token.id);
112
+ this.path.push(token);
113
+ }
114
+ exit(token) {
115
+ this.path.pop();
116
+ this.resolving.delete(token.id);
117
+ }
118
+ };
119
+ //#endregion
120
+ //#region src/resolver/resolver.ts
121
+ var ResolverClass = class {
122
+ registry;
123
+ store = createStore();
124
+ parent;
125
+ constructor(registry, parent) {
126
+ this.registry = registry;
127
+ this.parent = parent;
128
+ }
129
+ resolve(token) {
130
+ return this.resolveToken(token, new ResolutionContext());
131
+ }
132
+ invalidate(token) {
133
+ this.store.delete(token);
134
+ }
135
+ dispose() {
136
+ return this.store.dispose();
137
+ }
138
+ resolveToken(token, context) {
139
+ const owner = this.findOwner(token);
140
+ if (!owner) throw new MissingProviderError(token.name);
141
+ const provider = owner.registry.get(token);
142
+ if (!provider) throw new MissingProviderError(token.name);
143
+ if (isValueProvider(provider)) return provider.useValue;
144
+ if (isFactoryProvider(provider)) {
145
+ const host = this.selectHost(provider.scope, owner);
146
+ if (host) {
147
+ const cached = host.store.get(token);
148
+ if (cached) return cached.value;
149
+ }
150
+ context.enter(token);
151
+ try {
152
+ const deps = (host ?? this).resolveDeps(provider.deps, context);
153
+ const value = provider.useFactory(deps);
154
+ if (host) host.store.set(token, {
155
+ value,
156
+ ...provider.onDispose ? { onDispose: provider.onDispose } : {}
157
+ });
158
+ return value;
159
+ } finally {
160
+ context.exit(token);
161
+ }
162
+ }
163
+ throw new MissingProviderError(token.name);
164
+ }
165
+ findOwner(token) {
166
+ let resolver = this;
167
+ while (resolver) {
168
+ if (resolver.registry.has(token)) return resolver;
169
+ resolver = resolver.parent;
170
+ }
171
+ }
172
+ selectHost(scope, owner) {
173
+ if (scope === Scopes.Transient) return;
174
+ if (scope === Scopes.Scoped) return this;
175
+ return owner;
176
+ }
177
+ resolveDeps(deps, context) {
178
+ if (!deps) return {};
179
+ const resolvedDeps = {};
180
+ for (const key of Object.keys(deps)) {
181
+ const token = deps[key];
182
+ if (token === void 0) throw new InvalidDependencyError(String(key));
183
+ resolvedDeps[key] = this.resolveToken(token, context);
184
+ }
185
+ return resolvedDeps;
186
+ }
187
+ };
188
+ const createResolver = (registry, parent) => new ResolverClass(registry, parent);
189
+ //#endregion
190
+ //#region src/container/container.ts
191
+ var ContainerClass = class {
192
+ registry;
193
+ resolver;
194
+ parent;
195
+ constructor(providers = [], parent) {
196
+ this.parent = parent;
197
+ this.registry = createRegistry();
198
+ for (const provider of providers) this.registry.register(provider);
199
+ this.resolver = createResolver(this.registry, parent?.resolver);
200
+ }
201
+ register(provider, options) {
202
+ this.registry.register(provider, options);
203
+ if (options?.allowOverride) this.resolver.invalidate(provider.provide);
204
+ }
205
+ get(token) {
206
+ return this.resolver.resolve(token);
207
+ }
208
+ has(token) {
209
+ return this.registry.has(token) || (this.parent?.has(token) ?? false);
210
+ }
211
+ dispose() {
212
+ return this.resolver.dispose();
213
+ }
214
+ };
215
+ const createContainer = (providers = []) => new ContainerClass(providers);
216
+ const createChildContainer = (parent, providers = []) => new ContainerClass(providers, parent);
217
+ //#endregion
1
218
  //#region src/token/token.ts
2
219
  var TokenClass = class {
3
220
  name;
@@ -9,4 +226,4 @@ var TokenClass = class {
9
226
  };
10
227
  const createToken = (name) => new TokenClass(name);
11
228
  //#endregion
12
- export { createToken };
229
+ export { CircularDependencyError, DiError, DuplicateProviderError, InvalidDependencyError, MissingProviderError, Scopes, createChildContainer, createContainer, createToken, provideFactory, provideValue };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "di-craft",
3
- "version": "0.0.9",
4
- "description": "A tiny TypeScript dependency injection container",
3
+ "version": "0.0.11",
4
+ "description": "A tiny, type-safe dependency injection container for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Egor Bezmen",