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 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 the magic — no decorators, no `reflect-metadata`, no
84
- framework coupling. You work with just **tokens**, **providers**, a **container**,
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
- //#region src/error/error.d.ts
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/scope/scope.d.ts
12
- declare const Scopes: {
13
- readonly Singleton: "singleton";
14
- readonly Transient: "transient";
15
- readonly Scoped: "scoped";
16
- };
17
- type Scope = (typeof Scopes)[keyof typeof Scopes];
18
- //#endregion
19
- //#region src/token/types.d.ts
20
- type Token<T> = {
21
- readonly id: symbol;
22
- readonly name: string;
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/types.d.ts
30
- type OptionalDependency<T> = {
31
- readonly token: Token<T>;
32
- readonly optional: true;
33
- };
34
- type Dependency<T> = Token<T> | OptionalDependency<T>;
35
- type DepsMap = Record<string, Dependency<unknown>>;
36
- type DependencyValue<TDep> = TDep extends OptionalDependency<infer T> ? T | undefined : TDep extends Token<infer T> ? T : never;
37
- type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: DependencyValue<TDeps[TKey]> };
38
- type Factory<T, TDeps extends DepsMap> = (deps: ResolveDeps<TDeps>) => T;
39
- type DisposeHook<T> = (instance: T) => void | Promise<void>;
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/registry/types.d.ts
70
- type RegisterOptions = {
71
- readonly allowOverride?: boolean;
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/container/types.d.ts
75
- type Container = {
76
- register(provider: Provider, options?: RegisterOptions): void;
77
- get<T>(token: Token<T>): T;
78
- get<T>(dependency: OptionalDependency<T>): T | undefined;
79
- has(token: Token<unknown>): boolean;
80
- dispose(): Promise<void>;
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/container/container.d.ts
84
- declare const createContainer: (providers?: readonly Provider[]) => Container;
85
- declare const createChildContainer: (parent: Container, providers?: readonly Provider[]) => Container;
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
- //#region src/error/error.ts
2
- var DiError = class extends Error {
3
- constructor(message) {
4
- super(message);
5
- this.name = "DiError";
6
- if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
7
- }
8
- };
9
-
10
- //#endregion
11
- //#region src/provider/errors.ts
12
- var InvalidProviderError = class extends DiError {
13
- constructor(message) {
14
- super(message);
15
- this.name = "InvalidProviderError";
16
- }
17
- };
18
-
19
- //#endregion
20
- //#region src/scope/scope.ts
21
- const Scopes = {
22
- Singleton: "singleton",
23
- Transient: "transient",
24
- Scoped: "scoped"
25
- };
26
-
27
- //#endregion
28
- //#region src/provider/provider.ts
29
- const provideValue = (token, useValue) => ({
30
- provide: token,
31
- useValue
32
- });
33
- const provideFactory = (token, options) => {
34
- if (options.scope === Scopes.Transient && options.onDispose) throw new InvalidProviderError(`onDispose is not supported for transient providers (token "${token.name}"): transient instances are not tracked, so the hook would never run.`);
35
- return {
36
- provide: token,
37
- useFactory: options.useFactory,
38
- scope: options.scope ?? Scopes.Singleton,
39
- ...options.deps ? { deps: options.deps } : {},
40
- ...options.onDispose ? { onDispose: options.onDispose } : {}
41
- };
42
- };
43
- const optional = (token) => ({
44
- token,
45
- optional: true
46
- });
47
- const isOptionalDependency = (dependency) => {
48
- return dependency.optional === true;
49
- };
50
- const isValueProvider = (provider) => {
51
- return "useValue" in provider;
52
- };
53
-
54
- //#endregion
55
- //#region src/registry/errors.ts
56
- var DuplicateProviderError = class extends DiError {
57
- constructor(tokenName) {
58
- super(`Provider for token "${tokenName}" is already registered`);
59
- this.name = "DuplicateProviderError";
60
- }
61
- };
62
-
63
- //#endregion
64
- //#region src/registry/registry.ts
65
- var RegistryClass = class {
66
- providers = /* @__PURE__ */ new Map();
67
- register(provider, options) {
68
- const existed = this.providers.has(provider.provide.id);
69
- if (existed && !options?.allowOverride) throw new DuplicateProviderError(provider.provide.name);
70
- this.providers.set(provider.provide.id, provider);
71
- return existed;
72
- }
73
- get(token) {
74
- return this.providers.get(token.id);
75
- }
76
- has(token) {
77
- return this.providers.has(token.id);
78
- }
79
- };
80
- const createRegistry = () => new RegistryClass();
81
-
82
- //#endregion
83
- //#region src/resolver/errors.ts
84
- var MissingProviderError = class extends DiError {
85
- constructor(tokenName) {
86
- super(`Provider for token "${tokenName}" is not registered`);
87
- this.name = "MissingProviderError";
88
- }
89
- };
90
- var InvalidDependencyError = class extends DiError {
91
- constructor(dependencyKey) {
92
- super(`Invalid dependency "${dependencyKey}"`);
93
- this.name = "InvalidDependencyError";
94
- }
95
- };
96
- var CircularDependencyError = class extends DiError {
97
- constructor(tokenNames) {
98
- super(`Circular dependency detected: ${tokenNames.join(" -> ")}`);
99
- this.name = "CircularDependencyError";
100
- }
101
- };
102
-
103
- //#endregion
104
- //#region src/store/store.ts
105
- var StoreClass = class {
106
- instances = /* @__PURE__ */ new Map();
107
- get(token) {
108
- return this.instances.get(token.id);
109
- }
110
- set(token, record) {
111
- this.instances.set(token.id, record);
112
- }
113
- delete(token) {
114
- this.instances.delete(token.id);
115
- }
116
- async dispose() {
117
- const records = [...this.instances.values()].reverse();
118
- this.instances.clear();
119
- for (const record of records) if (record.onDispose) await record.onDispose(record.value);
120
- }
121
- };
122
- const createStore = () => new StoreClass();
123
-
124
- //#endregion
125
- //#region src/resolver/context.ts
126
- var ResolutionContext = class {
127
- resolving = /* @__PURE__ */ new Set();
128
- path = [];
129
- enter(token) {
130
- if (this.resolving.has(token.id)) {
131
- const cycleStartIndex = this.path.findIndex((pathToken) => pathToken.id === token.id);
132
- throw new CircularDependencyError([...this.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
133
- }
134
- this.resolving.add(token.id);
135
- this.path.push(token);
136
- }
137
- exit(token) {
138
- this.path.pop();
139
- this.resolving.delete(token.id);
140
- }
141
- };
142
-
143
- //#endregion
144
- //#region src/resolver/resolver.ts
145
- const lifetimeRank = (scope) => scope === Scopes.Scoped ? 1 : scope === Scopes.Transient ? 2 : 0;
146
- var ResolverClass = class {
147
- registry;
148
- store = createStore();
149
- parent;
150
- constructor(registry, parent) {
151
- this.registry = registry;
152
- this.parent = parent;
153
- }
154
- resolve(token) {
155
- return this.resolveToken(token, void 0);
156
- }
157
- resolveOptional(token) {
158
- return this.lookupOptional(token, void 0);
159
- }
160
- invalidate(token) {
161
- this.store.delete(token);
162
- }
163
- hasDisposableInstance(token) {
164
- return this.store.get(token)?.onDispose !== void 0;
165
- }
166
- dispose() {
167
- return this.store.dispose();
168
- }
169
- resolveToken(token, context, consumerScope, consumerName) {
170
- let owner = this;
171
- let provider;
172
- while (owner) {
173
- provider = owner.registry.get(token);
174
- if (provider) break;
175
- owner = owner.parent;
176
- }
177
- if (!provider || !owner) throw new MissingProviderError(token.name);
178
- if (isValueProvider(provider)) return provider.useValue;
179
- if (consumerScope !== void 0 && lifetimeRank(provider.scope) > lifetimeRank(consumerScope)) throw new InvalidProviderError(`"${consumerName}" (${consumerScope}) cannot depend on "${token.name}" (${provider.scope ?? Scopes.Singleton}): a longer-lived provider would capture a shorter-lived one. Widen the dependency's scope or narrow the consumer's.`);
180
- const host = this.selectHost(provider.scope, owner);
181
- if (host) {
182
- const cached = host.store.get(token);
183
- if (cached) return cached.value;
184
- }
185
- const ctx = context ?? new ResolutionContext();
186
- ctx.enter(token);
187
- try {
188
- const deps = (host ?? this).resolveDeps(provider.deps, ctx, provider.scope, token.name);
189
- const value = provider.useFactory(deps);
190
- if (host) host.store.set(token, {
191
- value,
192
- ...provider.onDispose ? { onDispose: provider.onDispose } : {}
193
- });
194
- return value;
195
- } finally {
196
- ctx.exit(token);
197
- }
198
- }
199
- selectHost(scope, owner) {
200
- if (scope === Scopes.Transient) return;
201
- if (scope === Scopes.Scoped) return this;
202
- return owner;
203
- }
204
- resolveDeps(deps, context, consumerScope, consumerName) {
205
- if (!deps) return {};
206
- const resolvedDeps = {};
207
- for (const key of Object.keys(deps)) {
208
- const dependency = deps[key];
209
- if (dependency === void 0) throw new InvalidDependencyError(String(key));
210
- resolvedDeps[key] = isOptionalDependency(dependency) ? this.lookupOptional(dependency.token, context, consumerScope, consumerName) : this.resolveToken(dependency, context, consumerScope, consumerName);
211
- }
212
- return resolvedDeps;
213
- }
214
- lookupOptional(token, context, consumerScope, consumerName) {
215
- let owner = this;
216
- while (owner) {
217
- if (owner.registry.has(token)) return this.resolveToken(token, context, consumerScope, consumerName);
218
- owner = owner.parent;
219
- }
220
- }
221
- };
222
- const createResolver = (registry, parent) => new ResolverClass(registry, parent);
223
-
224
- //#endregion
225
- //#region src/container/container.ts
226
- var ContainerClass = class {
227
- registry;
228
- resolver;
229
- parent;
230
- constructor(providers = [], parent) {
231
- this.parent = parent;
232
- this.registry = createRegistry();
233
- for (const provider of providers) this.registry.register(provider);
234
- this.resolver = createResolver(this.registry, parent?.resolver);
235
- }
236
- register(provider, options) {
237
- if (options?.allowOverride && this.resolver.hasDisposableInstance(provider.provide)) throw new InvalidProviderError(`Cannot override token "${provider.provide.name}": its instance was already created and has an onDispose hook. Dispose the container before replacing it.`);
238
- if (this.registry.register(provider, options)) this.resolver.invalidate(provider.provide);
239
- }
240
- get(dependency) {
241
- if (isOptionalDependency(dependency)) return this.resolver.resolveOptional(dependency.token);
242
- return this.resolver.resolve(dependency);
243
- }
244
- has(token) {
245
- return this.registry.has(token) || (this.parent?.has(token) ?? false);
246
- }
247
- dispose() {
248
- return this.resolver.dispose();
249
- }
250
- };
251
- const createContainer = (providers = []) => new ContainerClass(providers);
252
- const createChildContainer = (parent, providers = []) => new ContainerClass(providers, parent);
253
-
254
- //#endregion
255
- //#region src/token/token.ts
256
- var TokenClass = class {
257
- name;
258
- id;
259
- constructor(name) {
260
- this.name = name;
261
- this.id = Symbol(name);
262
- }
263
- };
264
- const createToken = (name) => new TokenClass(name);
265
-
266
- //#endregion
267
- export { CircularDependencyError, DiError, DuplicateProviderError, InvalidDependencyError, InvalidProviderError, MissingProviderError, Scopes, createChildContainer, createContainer, createToken, optional, provideFactory, provideValue };
1
+ 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.17",
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": [