di-craft 0.0.17 → 0.0.19
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 +194 -7
- package/dist/container-C6GcrdNn.mjs +1 -0
- package/dist/hydration-BJ-gWd38.d.mts +174 -0
- package/dist/hydration-DsCFyhuJ.mjs +1 -0
- package/dist/index.d.mts +144 -61
- package/dist/index.mjs +1 -267
- package/dist/next/client.d.mts +2 -0
- package/dist/next/client.mjs +1 -0
- package/dist/next/server.d.mts +33 -0
- package/dist/next/server.mjs +1 -0
- package/dist/types-BzLfq9VA.d.mts +140 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -17,10 +17,6 @@
|
|
|
17
17
|
</a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
-
> [!NOTE]
|
|
21
|
-
> This README was generated with a bit of AI help — don't believe everything you see 🙂
|
|
22
|
-
|
|
23
|
-
|
|
24
20
|
## Contents
|
|
25
21
|
|
|
26
22
|
- [Quick start](#quick-start)
|
|
@@ -30,6 +26,7 @@
|
|
|
30
26
|
- [Core concepts](#core-concepts)
|
|
31
27
|
- [Tokens](#tokens)
|
|
32
28
|
- [Providers](#providers)
|
|
29
|
+
- [Annotation-based class providers](#annotation-based-class-providers)
|
|
33
30
|
- [Optional dependencies](#optional-dependencies)
|
|
34
31
|
- [Container](#container)
|
|
35
32
|
- [Scopes](#scopes)
|
|
@@ -37,6 +34,8 @@
|
|
|
37
34
|
- [Child containers](#child-containers)
|
|
38
35
|
- [Cycle detection](#cycle-detection)
|
|
39
36
|
- [Async dependencies](#async-dependencies)
|
|
37
|
+
- [Adapters](#adapters)
|
|
38
|
+
- [Next.js App Router](#nextjs-app-router)
|
|
40
39
|
- [Dependency injection vs service location](#dependency-injection-vs-service-location)
|
|
41
40
|
- [Error handling](#error-handling)
|
|
42
41
|
- [API reference](#api-reference)
|
|
@@ -80,14 +79,17 @@ const users = container.get(USERS); // UserService, fully typed
|
|
|
80
79
|
|
|
81
80
|
## Philosophy
|
|
82
81
|
|
|
83
|
-
Dependency injection without
|
|
84
|
-
framework coupling. You work with just **tokens**,
|
|
85
|
-
**scopes**, and **cycle detection**.
|
|
82
|
+
Dependency injection without hidden magic — no `reflect-metadata`, no runtime
|
|
83
|
+
type guessing, and no framework coupling. You work with just **tokens**,
|
|
84
|
+
**providers**, a **container**, **scopes**, and **cycle detection**. Standard
|
|
85
|
+
JavaScript decorators are available as optional sugar for class providers, but
|
|
86
|
+
they still use explicit tokens.
|
|
86
87
|
|
|
87
88
|
## Features
|
|
88
89
|
|
|
89
90
|
- Zero runtime dependencies
|
|
90
91
|
- Type-safe tokens and factories
|
|
92
|
+
- Optional `@Injectable` annotation for class providers
|
|
91
93
|
- Optional dependencies via `optional()`
|
|
92
94
|
- Singleton, transient, and scoped lifetimes
|
|
93
95
|
- Hierarchical child containers
|
|
@@ -147,6 +149,63 @@ provideFactory(HTTP, {
|
|
|
147
149
|
The keys in `deps` become the keys of the object passed to `useFactory`, each
|
|
148
150
|
resolved to its token's type.
|
|
149
151
|
|
|
152
|
+
### Annotation-based class providers
|
|
153
|
+
|
|
154
|
+
If you write services as classes, you can attach provider metadata to the class
|
|
155
|
+
with standard JavaScript decorators, then turn the class into a normal provider
|
|
156
|
+
with `provideInjectable`.
|
|
157
|
+
|
|
158
|
+
`@Injectable(options)` marks a class as a provider for `options.token`.
|
|
159
|
+
`deps` is an ordered list of constructor dependencies. `scope` and
|
|
160
|
+
`onDispose` behave exactly like they do in `provideFactory`.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import {
|
|
164
|
+
Injectable,
|
|
165
|
+
Scopes,
|
|
166
|
+
createContainer,
|
|
167
|
+
createToken,
|
|
168
|
+
optional,
|
|
169
|
+
provideInjectable,
|
|
170
|
+
provideValue,
|
|
171
|
+
} from "di-craft";
|
|
172
|
+
|
|
173
|
+
const CONFIG = createToken<Config>("config");
|
|
174
|
+
const LOGGER = createToken<Logger>("logger");
|
|
175
|
+
const USERS = createToken<UserService>("users");
|
|
176
|
+
|
|
177
|
+
@Injectable({
|
|
178
|
+
token: USERS,
|
|
179
|
+
deps: [LOGGER, optional(CONFIG)],
|
|
180
|
+
scope: Scopes.Scoped,
|
|
181
|
+
})
|
|
182
|
+
class UserService {
|
|
183
|
+
private readonly logger: Logger;
|
|
184
|
+
private readonly config: Config | undefined;
|
|
185
|
+
|
|
186
|
+
constructor(logger: Logger, config: Config | undefined) {
|
|
187
|
+
this.logger = logger;
|
|
188
|
+
this.config = config;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const container = createContainer([
|
|
193
|
+
provideValue(LOGGER, new Logger()),
|
|
194
|
+
provideInjectable(UserService),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
const users = container.get(USERS); // UserService
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`@Injectable` is the only annotation needed for class injection. It produces a
|
|
201
|
+
regular factory provider internally, so all existing container behavior still
|
|
202
|
+
applies: optional dependencies, scopes, child containers, disposal hooks,
|
|
203
|
+
overrides, and cycle detection.
|
|
204
|
+
|
|
205
|
+
di-craft does not use `reflect-metadata` or parameter decorators. Constructor
|
|
206
|
+
types are erased by JavaScript at runtime, so dependency tokens stay explicit
|
|
207
|
+
instead of being guessed from TypeScript types.
|
|
208
|
+
|
|
150
209
|
### Optional dependencies
|
|
151
210
|
|
|
152
211
|
Wrap a token with `optional` to mark a dependency as not required. When no
|
|
@@ -376,6 +435,125 @@ provideFactory(POOL, {
|
|
|
376
435
|
});
|
|
377
436
|
```
|
|
378
437
|
|
|
438
|
+
## Adapters
|
|
439
|
+
|
|
440
|
+
Adapters are optional framework integrations built around the core container.
|
|
441
|
+
They live behind subpath exports, so the root import stays framework-agnostic:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { createContainer } from "di-craft"; // core only
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Next.js App Router
|
|
448
|
+
|
|
449
|
+
The Next adapter helps connect di-craft to the App Router request lifecycle
|
|
450
|
+
without making React or Next.js part of the core import.
|
|
451
|
+
|
|
452
|
+
Use `di-craft/next/server` from a server-only composition file. Pass React's
|
|
453
|
+
`cache` function so React/Next owns request memoization while di-craft owns only
|
|
454
|
+
the dependency graph:
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
// app/di.server.ts
|
|
458
|
+
import "server-only";
|
|
459
|
+
import { cache } from "react";
|
|
460
|
+
import { provideValue } from "di-craft";
|
|
461
|
+
import { createNextDi } from "di-craft/next/server";
|
|
462
|
+
|
|
463
|
+
export const {
|
|
464
|
+
getRequestContainer,
|
|
465
|
+
getRootContainer,
|
|
466
|
+
runWithRequestContainer,
|
|
467
|
+
} = createNextDi({
|
|
468
|
+
cache,
|
|
469
|
+
providers,
|
|
470
|
+
requestProviders: () => [
|
|
471
|
+
provideValue(REQUEST_ID, crypto.randomUUID()),
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Then resolve dependencies in Server Components at the composition edge:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
import { getRequestContainer } from "./di.server";
|
|
480
|
+
|
|
481
|
+
export default async function Page() {
|
|
482
|
+
const users = getRequestContainer().get(USERS_SERVICE);
|
|
483
|
+
|
|
484
|
+
return <UsersView users={await users.list()} />;
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Next.js does not expose a general "RSC render is finished" hook, so the adapter
|
|
489
|
+
does not pretend it can automatically dispose a cached Server Component request
|
|
490
|
+
container. If you own the lifecycle, for example in a Route Handler, Server
|
|
491
|
+
Action, test, or job, use `runWithRequestContainer`. It creates a fresh child
|
|
492
|
+
container and disposes it in a `finally` block:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
import { runWithRequestContainer } from "./di.server";
|
|
496
|
+
|
|
497
|
+
export async function GET() {
|
|
498
|
+
return runWithRequestContainer({
|
|
499
|
+
run: async (container) => {
|
|
500
|
+
const users = await container.get(USERS_SERVICE).list();
|
|
501
|
+
|
|
502
|
+
return Response.json(users);
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
State hydration is explicit. The server reads serializable snapshots with
|
|
509
|
+
`di-craft/next/server`; the client restores them with `di-craft/next/client`.
|
|
510
|
+
The DI container itself is never hydrated.
|
|
511
|
+
|
|
512
|
+
Import boundary-specific runtime helpers from their own subpath:
|
|
513
|
+
|
|
514
|
+
- `di-craft/next/server` — `createNextDi`, `dehydrate`, server-only adapter types.
|
|
515
|
+
- `di-craft/next/client` — `hydrate`, client-boundary hydration types.
|
|
516
|
+
- Shared hydration contracts like `Hydratable`, `HydrationSchema`, and
|
|
517
|
+
`HydrationSnapshot` are exported from both subpaths for convenience.
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
import {
|
|
521
|
+
dehydrate,
|
|
522
|
+
type Hydratable,
|
|
523
|
+
type HydrationSchema,
|
|
524
|
+
} from "di-craft/next/server";
|
|
525
|
+
|
|
526
|
+
class UserState implements Hydratable<UserSnapshot> {
|
|
527
|
+
dehydrate(): UserSnapshot {
|
|
528
|
+
return { users: this.users };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
hydrate(snapshot: UserSnapshot): void {
|
|
532
|
+
this.users = snapshot.users;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const hydration = {
|
|
537
|
+
user: USER_STATE,
|
|
538
|
+
} satisfies HydrationSchema;
|
|
539
|
+
const snapshot = dehydrate({
|
|
540
|
+
container: getRequestContainer(),
|
|
541
|
+
schema: hydration,
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
"use client";
|
|
547
|
+
|
|
548
|
+
import { hydrate } from "di-craft/next/client";
|
|
549
|
+
|
|
550
|
+
hydrate({
|
|
551
|
+
container: clientContainer,
|
|
552
|
+
schema: hydration,
|
|
553
|
+
snapshot,
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
379
557
|
## Dependency injection vs service location
|
|
380
558
|
|
|
381
559
|
di-craft is built for **dependency injection**: dependencies are declared up
|
|
@@ -451,6 +629,8 @@ try {
|
|
|
451
629
|
| `createToken<T>(name)` | Create a unique, typed token. |
|
|
452
630
|
| `provideValue(token, value)` | Provider that returns an existing value. |
|
|
453
631
|
| `provideFactory(token, options)` | Provider that builds a value via a factory. |
|
|
632
|
+
| `@Injectable(options)` | Mark a class as a token-backed injectable provider. |
|
|
633
|
+
| `provideInjectable(class)` | Create a factory provider from an injectable class. |
|
|
454
634
|
| `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
|
|
455
635
|
| `createContainer(providers?)` | Create a container, optionally seeded with providers. |
|
|
456
636
|
| `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
|
|
@@ -463,6 +643,13 @@ Exported types: `Container`, `Token`, `Provider`, `ValueProvider`,
|
|
|
463
643
|
Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`,
|
|
464
644
|
`CircularDependencyError`, `InvalidDependencyError`, `InvalidProviderError`.
|
|
465
645
|
|
|
646
|
+
Subpath exports:
|
|
647
|
+
|
|
648
|
+
| Export | Description |
|
|
649
|
+
| --------------------------- | ------------------------------------------------------- |
|
|
650
|
+
| `di-craft/next/server` | Next.js server adapter for request-scoped containers. |
|
|
651
|
+
| `di-craft/next/client` | Client-boundary helpers for restoring state snapshots. |
|
|
652
|
+
|
|
466
653
|
## License
|
|
467
654
|
|
|
468
655
|
[MIT](./LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
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;var c=class extends e{constructor(e){super(`Provider for token "${e}" is already registered`),this.name=`DuplicateProviderError`}},l=class{providers=new Map;register(e,t){let n=this.providers.has(e.provide.id);if(n&&!t?.allowOverride)throw new c(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 u=()=>new l;var d=class extends e{constructor(e){super(`Provider for token "${e}" is not registered`),this.name=`MissingProviderError`}},f=class extends e{constructor(e){super(`Invalid dependency "${e}"`),this.name=`InvalidDependencyError`}},p=class extends e{constructor(e){super(`Circular dependency detected: ${e.join(` -> `)}`),this.name=`CircularDependencyError`}},m=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 h=()=>new m;var g=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 p([...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 _=e=>e===n.Scoped?1:e===n.Transient?2:0;var v=class{registry;store=h();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 d(e.name);if(s(c))return c.useValue;if(i!==void 0&&_(c.scope)>_(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 g;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 f(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 y=(e,t)=>new v(e,t);var b=class{registry;resolver;parent;constructor(e=[],t){this.parent=t,this.registry=u();for(let t of e)this.registry.register(t);this.resolver=y(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 x=(e=[])=>new b(e),S=(e,t=[])=>new b(t,e);export{d as a,i as c,t as d,e as f,f as i,r as l,x as n,c as o,p as r,a as s,S as t,n as u};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { d as Token, l as Provider, t as Container } from "./types-BzLfq9VA.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/next/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Minimal shape of React's `cache` function needed by the adapter.
|
|
6
|
+
*
|
|
7
|
+
* Passing it in keeps React out of di-craft's dependency graph while still
|
|
8
|
+
* letting React/Next own the current request cache.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { cache } from "react";
|
|
13
|
+
*
|
|
14
|
+
* createNextDi({ cache, providers });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
type RequestCache = <T>(factory: () => T) => () => T;
|
|
18
|
+
/**
|
|
19
|
+
* Options for `createNextDi`.
|
|
20
|
+
*/
|
|
21
|
+
type CreateNextDiOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* Providers registered once in the root server container.
|
|
24
|
+
*/
|
|
25
|
+
readonly providers?: readonly Provider[];
|
|
26
|
+
/**
|
|
27
|
+
* React request cache, imported from `react` in a Next server file.
|
|
28
|
+
*/
|
|
29
|
+
readonly cache: RequestCache;
|
|
30
|
+
/**
|
|
31
|
+
* Providers registered in each request-scoped child container.
|
|
32
|
+
*/
|
|
33
|
+
readonly requestProviders?: () => readonly Provider[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Options for running work inside a fresh request-scoped child container.
|
|
37
|
+
*/
|
|
38
|
+
type RunWithRequestContainerOptions<TResult> = {
|
|
39
|
+
/**
|
|
40
|
+
* Extra providers registered only for this manual request container.
|
|
41
|
+
*/
|
|
42
|
+
readonly providers?: readonly Provider[];
|
|
43
|
+
/**
|
|
44
|
+
* Work that owns the request lifecycle. The container is disposed after this
|
|
45
|
+
* callback settles.
|
|
46
|
+
*/
|
|
47
|
+
readonly run: (container: Container) => TResult | Promise<TResult>;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Next.js server adapter instance created by `createNextDi`.
|
|
51
|
+
*/
|
|
52
|
+
type NextDiAdapter = {
|
|
53
|
+
/**
|
|
54
|
+
* Returns the long-lived root server container.
|
|
55
|
+
*/
|
|
56
|
+
readonly getRootContainer: () => Container;
|
|
57
|
+
/**
|
|
58
|
+
* Returns the current request-scoped child container.
|
|
59
|
+
*/
|
|
60
|
+
readonly getRequestContainer: () => Container;
|
|
61
|
+
/**
|
|
62
|
+
* Creates a fresh child container manually, useful in route handlers or tests.
|
|
63
|
+
*/
|
|
64
|
+
readonly createRequestContainer: (providers?: readonly Provider[]) => Container;
|
|
65
|
+
/**
|
|
66
|
+
* Runs work inside a fresh child container and disposes it in a `finally`
|
|
67
|
+
* block. Use this in Route Handlers, Server Actions, or tests where the
|
|
68
|
+
* request lifecycle is explicit.
|
|
69
|
+
*/
|
|
70
|
+
readonly runWithRequestContainer: <TResult>(options: RunWithRequestContainerOptions<TResult>) => Promise<Awaited<TResult>>;
|
|
71
|
+
/**
|
|
72
|
+
* Disposes cached instances owned by the root container.
|
|
73
|
+
*/
|
|
74
|
+
readonly disposeRootContainer: () => Promise<void>;
|
|
75
|
+
};
|
|
76
|
+
type SerializablePrimitive = string | number | boolean | null;
|
|
77
|
+
/**
|
|
78
|
+
* JSON-like value that can safely cross a Server Component boundary.
|
|
79
|
+
*/
|
|
80
|
+
type Serializable = SerializablePrimitive | readonly Serializable[] | {
|
|
81
|
+
readonly [key: string]: Serializable;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Entity that can expose and restore a serializable state snapshot.
|
|
85
|
+
*
|
|
86
|
+
* The snapshot, not the DI container, is what crosses the RSC/client boundary.
|
|
87
|
+
*/
|
|
88
|
+
type Hydratable<TSnapshot extends Serializable> = {
|
|
89
|
+
/**
|
|
90
|
+
* Returns the minimal serializable state needed by the client.
|
|
91
|
+
*/
|
|
92
|
+
dehydrate(): TSnapshot;
|
|
93
|
+
/**
|
|
94
|
+
* Restores state from a previously serialized snapshot.
|
|
95
|
+
*/
|
|
96
|
+
hydrate(snapshot: TSnapshot): void;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Named hydratable tokens that define a client-boundary snapshot shape.
|
|
100
|
+
*
|
|
101
|
+
* Use it with `satisfies` to validate the schema without losing literal keys or
|
|
102
|
+
* token-specific snapshot inference.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* const hydration = {
|
|
107
|
+
* user: USER_STATE,
|
|
108
|
+
* } satisfies HydrationSchema;
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
type HydrationSchema = Readonly<Record<string, Token<Hydratable<Serializable>>>>;
|
|
112
|
+
/**
|
|
113
|
+
* Snapshot object inferred from a hydration schema.
|
|
114
|
+
*/
|
|
115
|
+
type HydrationSnapshot<TSchema extends HydrationSchema> = { readonly [TKey in keyof TSchema]: TSchema[TKey] extends Token<Hydratable<infer TSnapshot>> ? TSnapshot : never };
|
|
116
|
+
/**
|
|
117
|
+
* Options for reading a serializable snapshot from a container.
|
|
118
|
+
*/
|
|
119
|
+
type DehydrateOptions<TSchema extends HydrationSchema> = {
|
|
120
|
+
/**
|
|
121
|
+
* Container that owns the hydratable state entities.
|
|
122
|
+
*/
|
|
123
|
+
readonly container: Container;
|
|
124
|
+
/**
|
|
125
|
+
* Hydratable tokens keyed by snapshot field name.
|
|
126
|
+
*/
|
|
127
|
+
readonly schema: TSchema;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Options for restoring a serializable snapshot into a container.
|
|
131
|
+
*/
|
|
132
|
+
type HydrateOptions<TSchema extends HydrationSchema> = DehydrateOptions<TSchema> & {
|
|
133
|
+
/**
|
|
134
|
+
* Serializable state produced by `dehydrate` for the same schema.
|
|
135
|
+
*/
|
|
136
|
+
readonly snapshot: HydrationSnapshot<TSchema>;
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/adapters/next/hydration.d.ts
|
|
140
|
+
/**
|
|
141
|
+
* Reads serializable snapshots from hydratable entities declared in a schema.
|
|
142
|
+
*
|
|
143
|
+
* Use this on the server side before crossing into a Client Component. Only the
|
|
144
|
+
* returned snapshot should cross the boundary, not the container or service
|
|
145
|
+
* instances themselves.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const hydration = { user: USER_STATE };
|
|
150
|
+
* const snapshot = dehydrate({
|
|
151
|
+
* container: getRequestContainer(),
|
|
152
|
+
* schema: hydration,
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare const dehydrate: <const TSchema extends HydrationSchema>(options: DehydrateOptions<TSchema>) => HydrationSnapshot<TSchema>;
|
|
157
|
+
/**
|
|
158
|
+
* Restores serializable snapshots into hydratable entities declared in a schema.
|
|
159
|
+
*
|
|
160
|
+
* Use this with a client-safe container and the snapshot received through a
|
|
161
|
+
* Client Component prop.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* hydrate({
|
|
166
|
+
* container: clientContainer,
|
|
167
|
+
* schema: hydration,
|
|
168
|
+
* snapshot,
|
|
169
|
+
* });
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare const hydrate: <const TSchema extends HydrationSchema>(options: HydrateOptions<TSchema>) => void;
|
|
173
|
+
//#endregion
|
|
174
|
+
export { Hydratable as a, HydrationSnapshot as c, RunWithRequestContainerOptions as d, Serializable as f, DehydrateOptions as i, NextDiAdapter as l, hydrate as n, HydrateOptions as o, SerializablePrimitive as p, CreateNextDiOptions as r, HydrationSchema as s, dehydrate as t, RequestCache as u };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=e=>{let t={};for(let n of Object.keys(e.schema)){let r=e.schema[n],i=r===void 0,a=String(n);if(i)throw Error(`Missing hydration token for key "${a}".`);t[a]=e.container.get(r).dehydrate()}return t},t=e=>{let t=e.snapshot;for(let n of Object.keys(e.schema)){let r=e.schema[n],i=String(n),a=t[i],o=r===void 0,s=a===void 0;if(o)throw Error(`Missing hydration token for key "${i}".`);if(s)throw Error(`Missing hydration snapshot for key "${i}".`);e.container.get(r).hydrate(a)}};export{t as n,e as t};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,98 +1,181 @@
|
|
|
1
|
-
|
|
1
|
+
import { a as DisposeHook, c as OptionalDependency, d as Token, f as Scope, i as DepsMap, l as Provider, n as RegisterOptions, o as Factory, p as Scopes, r as Dependency, s as FactoryProvider, t as Container, u as ValueProvider } from "./types-BzLfq9VA.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/error/error.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Base class for all di-craft runtime errors.
|
|
6
|
+
*/
|
|
2
7
|
declare class DiError extends Error {
|
|
3
8
|
constructor(message: string);
|
|
4
9
|
}
|
|
5
10
|
//#endregion
|
|
6
|
-
//#region src/provider/errors.d.ts
|
|
11
|
+
//#region src/core/provider/errors.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* Error thrown when a provider configuration cannot be used safely.
|
|
14
|
+
*/
|
|
7
15
|
declare class InvalidProviderError extends DiError {
|
|
8
16
|
constructor(message: string);
|
|
9
17
|
}
|
|
10
18
|
//#endregion
|
|
11
|
-
//#region src/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
readonly __type?: T;
|
|
24
|
-
};
|
|
25
|
-
//#endregion
|
|
26
|
-
//#region src/token/token.d.ts
|
|
19
|
+
//#region src/core/token/token.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Creates a unique typed token.
|
|
22
|
+
*
|
|
23
|
+
* The `name` is used only for diagnostics. Token identity is unique per call,
|
|
24
|
+
* so two tokens with the same name do not collide.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const CONFIG = createToken<Config>("config");
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
27
31
|
declare const createToken: <T>(name: string) => Token<T>;
|
|
28
32
|
//#endregion
|
|
29
|
-
//#region src/provider/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
type ValueProvider<T> = {
|
|
41
|
-
readonly provide: Token<T>;
|
|
42
|
-
readonly useValue: T;
|
|
43
|
-
};
|
|
44
|
-
type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
|
|
45
|
-
readonly provide: Token<T>;
|
|
46
|
-
readonly deps?: TDeps;
|
|
47
|
-
readonly scope?: Scope;
|
|
48
|
-
readonly useFactory: Factory<T, TDeps>;
|
|
49
|
-
readonly onDispose?: DisposeHook<T>;
|
|
50
|
-
};
|
|
51
|
-
type AnyFactoryProvider = FactoryProvider<any, any>;
|
|
52
|
-
type Provider = ValueProvider<unknown> | AnyFactoryProvider;
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/provider/provider.d.ts
|
|
33
|
+
//#region src/core/provider/provider.d.ts
|
|
34
|
+
/**
|
|
35
|
+
* Creates a provider for an already constructed value.
|
|
36
|
+
*
|
|
37
|
+
* The value must match the token type.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* provideValue(PORT, 3000);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
55
44
|
declare const provideValue: <T>(token: Token<T>, useValue: T) => ValueProvider<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Creates a provider that lazily builds a value.
|
|
47
|
+
*
|
|
48
|
+
* Dependencies declared in `deps` become the object passed to `useFactory`.
|
|
49
|
+
* The factory return type must match the token type.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* provideFactory(HTTP, {
|
|
54
|
+
* deps: { config: CONFIG },
|
|
55
|
+
* useFactory: ({ config }) => new HttpClient(config.apiUrl),
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
56
59
|
declare const provideFactory: <T, TDeps extends DepsMap = Record<never, never>>(token: Token<T>, options: {
|
|
57
60
|
readonly deps?: TDeps;
|
|
58
61
|
readonly scope?: Scope;
|
|
59
62
|
readonly useFactory: Factory<T, TDeps>;
|
|
60
63
|
readonly onDispose?: DisposeHook<T>;
|
|
61
64
|
}) => FactoryProvider<T, TDeps>;
|
|
65
|
+
/**
|
|
66
|
+
* Marks a token as optional.
|
|
67
|
+
*
|
|
68
|
+
* Optional dependencies resolve to `undefined` instead of throwing when no
|
|
69
|
+
* provider exists in the container chain. They can be used in factory `deps`
|
|
70
|
+
* or passed directly to `container.get`.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const logger = container.get(optional(LOGGER));
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
62
77
|
declare const optional: <T>(token: Token<T>) => OptionalDependency<T>;
|
|
63
78
|
//#endregion
|
|
64
|
-
//#region src/registry/errors.d.ts
|
|
79
|
+
//#region src/core/registry/errors.d.ts
|
|
80
|
+
/**
|
|
81
|
+
* Error thrown when registering a provider for an already registered token.
|
|
82
|
+
*/
|
|
65
83
|
declare class DuplicateProviderError extends DiError {
|
|
66
84
|
constructor(tokenName: string);
|
|
67
85
|
}
|
|
68
86
|
//#endregion
|
|
69
|
-
//#region src/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
//#region src/core/container/container.d.ts
|
|
88
|
+
/**
|
|
89
|
+
* Creates a root container with optional initial providers.
|
|
90
|
+
*
|
|
91
|
+
* Root containers own singleton instances for providers registered in them.
|
|
92
|
+
*/
|
|
93
|
+
declare const createContainer: (providers?: readonly Provider[]) => Container;
|
|
94
|
+
/**
|
|
95
|
+
* Creates a child container that can resolve providers from its parent.
|
|
96
|
+
*
|
|
97
|
+
* Child containers may register their own providers while still reusing parent
|
|
98
|
+
* providers. Scoped providers create one cached instance per resolving child.
|
|
99
|
+
*/
|
|
100
|
+
declare const createChildContainer: (parent: Container, providers?: readonly Provider[]) => Container;
|
|
73
101
|
//#endregion
|
|
74
|
-
//#region src/
|
|
75
|
-
type
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
//#region src/core/annotation/types.d.ts
|
|
103
|
+
type InjectableDeps = readonly Dependency<unknown>[];
|
|
104
|
+
type DependencyValue<TDependency> = TDependency extends OptionalDependency<infer T> ? T | undefined : TDependency extends Token<infer T> ? T : never;
|
|
105
|
+
type ResolveDependencyTuple<TDeps extends InjectableDeps> = { -readonly [TKey in keyof TDeps]: DependencyValue<TDeps[TKey]> };
|
|
106
|
+
/**
|
|
107
|
+
* Constructor type whose parameters match an injectable dependency tuple.
|
|
108
|
+
*/
|
|
109
|
+
type InjectableConstructor<T, TDeps extends InjectableDeps> = new (...args: ResolveDependencyTuple<TDeps>) => T;
|
|
110
|
+
/**
|
|
111
|
+
* Class that can be passed to `provideInjectable`.
|
|
112
|
+
*
|
|
113
|
+
* `never[]` keeps classes with required constructor parameters assignable
|
|
114
|
+
* without claiming arbitrary constructor arguments are valid.
|
|
115
|
+
*/
|
|
116
|
+
type InjectableClass<T = unknown> = new (...args: never[]) => T;
|
|
117
|
+
/**
|
|
118
|
+
* Options stored by `@Injectable` and converted into a factory provider.
|
|
119
|
+
*/
|
|
120
|
+
type InjectableOptions<T, TDeps extends InjectableDeps = InjectableDeps> = {
|
|
121
|
+
/**
|
|
122
|
+
* Token provided by the decorated class.
|
|
123
|
+
*/
|
|
124
|
+
readonly token: Token<T>;
|
|
125
|
+
/**
|
|
126
|
+
* Ordered constructor dependencies.
|
|
127
|
+
*
|
|
128
|
+
* Each item is resolved and passed to the constructor at the same index.
|
|
129
|
+
*/
|
|
130
|
+
readonly deps?: TDeps;
|
|
131
|
+
/**
|
|
132
|
+
* Provider lifetime.
|
|
133
|
+
*
|
|
134
|
+
* Defaults to `Scopes.Singleton`.
|
|
135
|
+
*/
|
|
136
|
+
readonly scope?: Scope;
|
|
137
|
+
/**
|
|
138
|
+
* Cleanup hook for cached singleton and scoped instances.
|
|
139
|
+
*
|
|
140
|
+
* Not supported for transient providers.
|
|
141
|
+
*/
|
|
142
|
+
readonly onDispose?: DisposeHook<T>;
|
|
81
143
|
};
|
|
82
144
|
//#endregion
|
|
83
|
-
//#region src/
|
|
84
|
-
|
|
85
|
-
|
|
145
|
+
//#region src/core/annotation/annotation.d.ts
|
|
146
|
+
type InjectableDecorator<TTarget> = (target: TTarget, context: ClassDecoratorContext) => void;
|
|
147
|
+
type InjectableOptionsWithoutDeps<T> = Omit<InjectableOptions<T, readonly []>, "deps">;
|
|
148
|
+
declare function Injectable<T, const TDeps extends readonly Dependency<unknown>[]>(options: InjectableOptions<T, TDeps> & {
|
|
149
|
+
readonly deps: TDeps;
|
|
150
|
+
}): InjectableDecorator<InjectableConstructor<T, TDeps>>;
|
|
151
|
+
declare function Injectable<T>(options: InjectableOptionsWithoutDeps<T>): InjectableDecorator<InjectableConstructor<T, readonly []>>;
|
|
152
|
+
/**
|
|
153
|
+
* Creates a factory provider from a class marked with `@Injectable`.
|
|
154
|
+
*
|
|
155
|
+
* The returned provider is a normal di-craft factory provider, so scopes,
|
|
156
|
+
* optional dependencies, disposal hooks, overrides, and cycle detection behave
|
|
157
|
+
* the same as with `provideFactory`.
|
|
158
|
+
*/
|
|
159
|
+
declare function provideInjectable<T>(target: InjectableClass<T>): FactoryProvider<T, DepsMap>;
|
|
86
160
|
//#endregion
|
|
87
|
-
//#region src/resolver/errors.d.ts
|
|
161
|
+
//#region src/core/resolver/errors.d.ts
|
|
162
|
+
/**
|
|
163
|
+
* Error thrown when resolving a token without a registered provider.
|
|
164
|
+
*/
|
|
88
165
|
declare class MissingProviderError extends DiError {
|
|
89
166
|
constructor(tokenName: string);
|
|
90
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Error thrown when a dependency descriptor is invalid.
|
|
170
|
+
*/
|
|
91
171
|
declare class InvalidDependencyError extends DiError {
|
|
92
172
|
constructor(dependencyKey: string);
|
|
93
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Error thrown when provider dependencies form a cycle.
|
|
176
|
+
*/
|
|
94
177
|
declare class CircularDependencyError extends DiError {
|
|
95
178
|
constructor(tokenNames: string[]);
|
|
96
179
|
}
|
|
97
180
|
//#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 };
|
|
181
|
+
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
|
+
import{a as e,c as t,d as n,f as r,i,l as a,n as o,o as s,r as c,s as l,t as u,u as d}from"./container-C6GcrdNn.mjs";const f=e=>{if(e.length!==0)return Object.fromEntries(e.entries())},p=new WeakMap,m=e=>p.get(e),h=e=>{p.set(e.target,e.metadata)},g=e=>{let{context:t,target:r}=e;if(!(t.kind===`class`&&typeof r==`function`))throw new n(`@Injectable can only decorate classes`)};function _(e){return(t,n)=>{g({target:t,context:n}),h({target:t,metadata:e})}}function v(e){let r=m(e);if(!r)throw new n(`Class "${e.name||`<anonymous>`}" is not marked as injectable. Add @Injectable({ token: TOKEN }) before calling provideInjectable().`);let{deps:i=[],onDispose:a,scope:o,token:s}=r,c=f(i),l=e;return t(s,{useFactory:e=>new l(...i.map((t,n)=>e[String(n)])),...c?{deps:c}:{},...o?{scope:o}:{},...a?{onDispose:a}:{}})}var y=class{name;id;constructor(e){this.name=e,this.id=Symbol(e)}};const b=e=>new y(e);export{c as CircularDependencyError,r as DiError,s as DuplicateProviderError,_ as Injectable,i as InvalidDependencyError,n as InvalidProviderError,e as MissingProviderError,d as Scopes,u as createChildContainer,o as createContainer,b as createToken,l as optional,t as provideFactory,v as provideInjectable,a as provideValue};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Hydratable, c as HydrationSnapshot, f as Serializable, n as hydrate, o as HydrateOptions, p as SerializablePrimitive, s as HydrationSchema } from "../hydration-BJ-gWd38.mjs";
|
|
2
|
+
export { type Hydratable, type HydrateOptions, type HydrationSchema, type HydrationSnapshot, type Serializable, type SerializablePrimitive, hydrate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e}from"../hydration-DsCFyhuJ.mjs";export{e as hydrate};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { a as Hydratable, c as HydrationSnapshot, d as RunWithRequestContainerOptions, f as Serializable, i as DehydrateOptions, l as NextDiAdapter, p as SerializablePrimitive, r as CreateNextDiOptions, s as HydrationSchema, t as dehydrate, u as RequestCache } from "../hydration-BJ-gWd38.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/next/server.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates a server adapter for Next.js App Router and React Server Components.
|
|
6
|
+
*
|
|
7
|
+
* The root container is created once. `getRequestContainer` creates a child
|
|
8
|
+
* container through the provided React request cache, so repeated calls inside
|
|
9
|
+
* one RSC request resolve through the same scoped dependency graph.
|
|
10
|
+
*
|
|
11
|
+
* Import this helper from a server-only composition file. The adapter does not
|
|
12
|
+
* import React itself; pass `cache` from `react` so React/Next owns request
|
|
13
|
+
* memoization.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import "server-only";
|
|
18
|
+
* import { cache } from "react";
|
|
19
|
+
* import { createNextDi } from "di-craft/next/server";
|
|
20
|
+
*
|
|
21
|
+
* export const { getRequestContainer } = createNextDi({
|
|
22
|
+
* cache,
|
|
23
|
+
* providers,
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare const createNextDi: ({
|
|
28
|
+
cache,
|
|
29
|
+
providers,
|
|
30
|
+
requestProviders
|
|
31
|
+
}: CreateNextDiOptions) => NextDiAdapter;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type CreateNextDiOptions, type DehydrateOptions, type Hydratable, type HydrationSchema, type HydrationSnapshot, type NextDiAdapter, type RequestCache, type RunWithRequestContainerOptions, type Serializable, type SerializablePrimitive, createNextDi, dehydrate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e,t}from"../container-C6GcrdNn.mjs";import{t as n}from"../hydration-DsCFyhuJ.mjs";const r=()=>{if(`window`in globalThis)throw Error(`di-craft/next/server can only be used in a server runtime.`)},i=({cache:n,providers:i=[],requestProviders:a})=>{r();let o=e(i),s=(e=[])=>t(o,[...a?.()??[],...e]);return{getRootContainer:()=>o,getRequestContainer:n(()=>s()),createRequestContainer:s,runWithRequestContainer:async({providers:e=[],run:t})=>{let n=s(e);try{return await t(n)}finally{await n.dispose()}},disposeRootContainer:()=>o.dispose()}};export{i as createNextDi,n as dehydrate};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//#region src/core/scope/scope.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Built-in provider lifetimes.
|
|
4
|
+
*/
|
|
5
|
+
declare const Scopes: {
|
|
6
|
+
readonly Singleton: "singleton";
|
|
7
|
+
readonly Transient: "transient";
|
|
8
|
+
readonly Scoped: "scoped";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Provider lifetime.
|
|
12
|
+
*
|
|
13
|
+
* - `singleton`: one cached instance in the container that owns the provider.
|
|
14
|
+
* - `scoped`: one cached instance in the container that resolves the provider.
|
|
15
|
+
* - `transient`: a new instance for every resolution.
|
|
16
|
+
*/
|
|
17
|
+
type Scope = (typeof Scopes)[keyof typeof Scopes];
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/core/token/types.d.ts
|
|
20
|
+
/** A unique, typed key used to register and resolve a dependency. */
|
|
21
|
+
type Token<T> = {
|
|
22
|
+
/**
|
|
23
|
+
* Runtime identity of the token.
|
|
24
|
+
*
|
|
25
|
+
* Tokens with the same name still have different identities.
|
|
26
|
+
*/
|
|
27
|
+
readonly id: symbol;
|
|
28
|
+
/**
|
|
29
|
+
* Human-readable name used in error messages and diagnostics.
|
|
30
|
+
*/
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly __type?: T;
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/core/provider/types.d.ts
|
|
36
|
+
/**
|
|
37
|
+
* A token wrapped as an optional dependency.
|
|
38
|
+
*
|
|
39
|
+
* Optional dependencies resolve to `undefined` when no provider is registered
|
|
40
|
+
* in the container chain.
|
|
41
|
+
*/
|
|
42
|
+
type OptionalDependency<T> = {
|
|
43
|
+
readonly token: Token<T>;
|
|
44
|
+
readonly optional: true;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A dependency accepted by factory providers and `container.get`.
|
|
48
|
+
*/
|
|
49
|
+
type Dependency<T> = Token<T> | OptionalDependency<T>;
|
|
50
|
+
type DepsMap = Record<string, Dependency<unknown>>;
|
|
51
|
+
type DependencyValue<TDep> = TDep extends OptionalDependency<infer T> ? T | undefined : TDep extends Token<infer T> ? T : never;
|
|
52
|
+
type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: DependencyValue<TDeps[TKey]> };
|
|
53
|
+
type Factory<T, TDeps extends DepsMap> = (deps: ResolveDeps<TDeps>) => T;
|
|
54
|
+
/**
|
|
55
|
+
* Cleanup hook called for a cached instance during container disposal.
|
|
56
|
+
*
|
|
57
|
+
* Transient providers cannot use disposal hooks because their instances are not
|
|
58
|
+
* cached or tracked by the container.
|
|
59
|
+
*/
|
|
60
|
+
type DisposeHook<T> = (instance: T) => void | Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Provider that resolves a token to an existing value.
|
|
63
|
+
*/
|
|
64
|
+
type ValueProvider<T> = {
|
|
65
|
+
readonly provide: Token<T>;
|
|
66
|
+
readonly useValue: T;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Provider that lazily creates a token value from typed dependencies.
|
|
70
|
+
*
|
|
71
|
+
* `deps` keys become the properties passed to `useFactory`, `scope` controls
|
|
72
|
+
* caching lifetime, and `onDispose` runs for cached singleton/scoped instances.
|
|
73
|
+
*/
|
|
74
|
+
type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
|
|
75
|
+
readonly provide: Token<T>;
|
|
76
|
+
readonly deps?: TDeps;
|
|
77
|
+
readonly scope?: Scope;
|
|
78
|
+
readonly useFactory: Factory<T, TDeps>;
|
|
79
|
+
readonly onDispose?: DisposeHook<T>;
|
|
80
|
+
};
|
|
81
|
+
type AnyFactoryProvider = FactoryProvider<any, any>;
|
|
82
|
+
/**
|
|
83
|
+
* Provider accepted by `createContainer` and `container.register`.
|
|
84
|
+
*/
|
|
85
|
+
type Provider = ValueProvider<unknown> | AnyFactoryProvider;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/core/registry/types.d.ts
|
|
88
|
+
/**
|
|
89
|
+
* Options for provider registration.
|
|
90
|
+
*/
|
|
91
|
+
type RegisterOptions = {
|
|
92
|
+
/**
|
|
93
|
+
* Replace an existing provider for the same token.
|
|
94
|
+
*
|
|
95
|
+
* Overriding an already resolved disposable singleton is rejected to avoid
|
|
96
|
+
* dropping resources without running their cleanup hook.
|
|
97
|
+
*/
|
|
98
|
+
readonly allowOverride?: boolean;
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/core/container/types.d.ts
|
|
102
|
+
/**
|
|
103
|
+
* Container that stores providers and resolves typed dependencies.
|
|
104
|
+
*/
|
|
105
|
+
type Container = {
|
|
106
|
+
/**
|
|
107
|
+
* Registers a provider in this container.
|
|
108
|
+
*
|
|
109
|
+
* Pass `{ allowOverride: true }` to intentionally replace an existing
|
|
110
|
+
* provider for the same token.
|
|
111
|
+
*/
|
|
112
|
+
register(provider: Provider, options?: RegisterOptions): void;
|
|
113
|
+
/**
|
|
114
|
+
* Resolves a required token.
|
|
115
|
+
*
|
|
116
|
+
* Throws `MissingProviderError` when no provider exists in this container or
|
|
117
|
+
* any parent container.
|
|
118
|
+
*/
|
|
119
|
+
get<T>(token: Token<T>): T;
|
|
120
|
+
/**
|
|
121
|
+
* Resolves an optional dependency.
|
|
122
|
+
*
|
|
123
|
+
* Returns `undefined` when no provider exists in this container or any parent
|
|
124
|
+
* container.
|
|
125
|
+
*/
|
|
126
|
+
get<T>(dependency: OptionalDependency<T>): T | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Checks whether this container or one of its parents has a provider.
|
|
129
|
+
*/
|
|
130
|
+
has(token: Token<unknown>): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Disposes cached instances owned by this container.
|
|
133
|
+
*
|
|
134
|
+
* Disposal hooks run in reverse creation order. Calling `dispose` more than
|
|
135
|
+
* once is safe.
|
|
136
|
+
*/
|
|
137
|
+
dispose(): Promise<void>;
|
|
138
|
+
};
|
|
139
|
+
//#endregion
|
|
140
|
+
export { DisposeHook as a, OptionalDependency as c, Token as d, Scope as f, DepsMap as i, Provider as l, RegisterOptions as n, Factory as o, Scopes as p, Dependency as r, FactoryProvider as s, Container as t, ValueProvider as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "di-craft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "A tiny, type-safe dependency injection container for TypeScript",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
"types": "./dist/index.d.mts",
|
|
21
21
|
"default": "./dist/index.mjs"
|
|
22
22
|
},
|
|
23
|
+
"./next/client": {
|
|
24
|
+
"types": "./dist/next/client.d.mts",
|
|
25
|
+
"default": "./dist/next/client.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./next/server": {
|
|
28
|
+
"types": "./dist/next/server.d.mts",
|
|
29
|
+
"default": "./dist/next/server.mjs"
|
|
30
|
+
},
|
|
23
31
|
"./package.json": "./package.json"
|
|
24
32
|
},
|
|
25
33
|
"files": [
|