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 +363 -6
- package/dist/index.cjs +227 -0
- package/dist/index.d.cts +76 -1
- package/dist/index.d.mts +76 -1
- package/dist/index.mjs +218 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,18 +1,375 @@
|
|
|
1
1
|
# di-craft
|
|
2
2
|
|
|
3
|
-
A tiny
|
|
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
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "A tiny
|
|
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",
|