@resolid/di 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,26 @@
1
- # Resolid: DI Container Package
1
+ # TypeScript Dependency Injection Container
2
2
 
3
3
  ![GitHub License](https://img.shields.io/github/license/resolid/framework)
4
4
  ![NPM Version](https://img.shields.io/npm/v/%40resolid/di)
5
5
 
6
6
  <b>[Documentation](https://www.resolid.tech/docs/di)</b> | [Framework Bundle](https://github.com/resolid/framework)
7
7
 
8
- ## TypeScript Dependency Injection Container
9
-
10
8
  A lightweight, fully-typed Dependency Injection (DI) container for TypeScript.
11
- Supports singleton & transient scopes, lazy resolution, and disposable resources. Fully functional with async factories
9
+ Supports singleton & transient scopes, lazy resolution, optional dependencies, and disposable resources. Fully functional with async factories
12
10
  and avoids circular dependency issues.
13
11
 
14
- ### Features
12
+ ## Features
15
13
 
16
14
  - Fully typed with TypeScript, no any.
17
15
  - Supports singleton and transient scopes.
18
- - Lazy resolution for async dependencies.
16
+ - Lazy resolution for dependencies.
17
+ - Optional dependency resolution.
19
18
  - Detects circular dependencies.
20
19
  - Handles disposable instances with dispose() support.
21
- - Fully async-compatible.
20
+ - Fully async-compatible with inject() and injectAsync() functions.
21
+ - Context-aware injection with InjectionContext.
22
22
 
23
- ### Installation
23
+ ## Installation
24
24
 
25
25
  ```shell
26
26
  pnpm add @resolid/di
@@ -32,125 +32,237 @@ yarn add @resolid/di
32
32
  bun add @resolid/di
33
33
  ```
34
34
 
35
- ### Basic Usage
35
+ ## Usage
36
36
 
37
- ```js
38
- import { createContainer } from "@resolid/di";
37
+ ### Basic
39
38
 
40
- const VALUE = "VALUE";
41
- const FUNCTION = "FUNCTION";
42
- const FACTORY = "FACTORY";
39
+ ```typescript
40
+ import { Container, inject } from "@resolid/di";
43
41
 
44
- const container = createContainer([
45
- { name: VALUE, value: "Hello World" },
46
- {
47
- name: FUNCTION,
48
- callable: () => {
49
- return "Hello World from callable";
50
- },
51
- },
52
- {
53
- name: FACTORY,
54
- factory: async ({ resolver, config }) => {
55
- const value = await resolver.resolve(VALUE);
56
-
57
- return value + " " + config.message || "from factory!";
58
- },
59
- /**
60
- * Scope of a binding.
61
- * singleton (default): Only one instance is created and shared for all resolves.
62
- * transient: A new instance is created on each resolve.
63
- */
64
- scope: "singleton",
65
- /**
66
- * Optional configuration object passed to a factory.
67
- * Type is inferred from the factory definition.
68
- * Used to provide parameters for instance creation.
69
- */
70
- config: { message: " from factory with config" },
71
- },
72
- ]);
42
+ class LogService {
43
+ log(message: string): void {
44
+ console.log(`[LOG]: ${message}`);
45
+ }
46
+ }
47
+
48
+ class UserService {
49
+ private logger: LogService;
50
+
51
+ constructor(logger: LogService) {
52
+ this.logger = logger;
53
+ }
73
54
 
74
- const valueResult = await container.resolve(VALUE);
75
- console.log(valueResult); // Output: "Hello World"
55
+ createUser(name: string): void {
56
+ this.logger.log(`Creating user: ${name}`);
57
+ }
58
+ }
59
+
60
+ const container = new Container();
61
+
62
+ container.add({
63
+ token: LogService,
64
+ factory: () => new LogService(),
65
+ });
76
66
 
77
- const callable = await container.resolve(FUNCTION);
78
- const callableResult = callable();
79
- console.log(callableResult); // Output: "Hello World from callable"
67
+ container.add({
68
+ token: UserService,
69
+ factory: () => new UserService(inject(LogService)),
70
+ });
80
71
 
81
- const factoryResult = await container.resolve(FACTORY);
82
- console.log(factoryResult); // Output: "Hello World from factory with config"
72
+ const userService = container.get(USER_SERVICE);
73
+
74
+ userService.createUser("John Doe"); // Output: [LOG]: Creating user: John Doe
83
75
  ```
84
76
 
85
- ### Lazy Resolve
77
+ ### Scopes
78
+
79
+ The container supports two scopes:
86
80
 
87
- You can create bindings that are resolved lazily, useful for circular dependencies or heavy computations:
81
+ - **singleton** (default): Only one instance is created and shared for all resolutions.
82
+ - **transient**: A new instance is created on each resolution.
88
83
 
89
- ```js
90
- const LAZY_A = "LAZY_A";
91
- const LAZY_B = "LAZY_B";
84
+ ```typescript
85
+ container.add({
86
+ token: LogService,
87
+ factory: () => new LogService(),
88
+ scope: "singleton", // Default scope
89
+ });
90
+
91
+ container.add({
92
+ token: Symbol("REQUEST_ID"),
93
+ factory: () => Math.random(),
94
+ scope: "transient", // New instance each time
95
+ });
96
+ ```
92
97
 
93
- const container = createContainer([
94
- {
95
- name: LAZY_A,
96
- factory: ({ resolver }) => {
97
- const b = resolver.lazyResolve(LAZY_B);
98
- return `A depends on ${b.value}`;
99
- },
98
+ ### Asynchronous Dependencies
99
+
100
+ ```typescript
101
+ import { Container, injectAsync } from "@resolid/di";
102
+
103
+ class AsyncDatabaseService {
104
+ async connect(): Promise<void> {
105
+ await new Promise((resolve) => setTimeout(resolve, 100));
106
+ console.log("Database connected");
107
+ }
108
+ }
109
+
110
+ class AsyncUserService {
111
+ private db: AsyncDatabaseService;
112
+
113
+ constructor(db: AsyncDatabaseService) {
114
+ this.db = db;
115
+ }
116
+
117
+ async getUser(id: number): Promise<string> {
118
+ return `User ${id}`;
119
+ }
120
+ }
121
+
122
+ const container = new Container();
123
+
124
+ container.add({
125
+ token: AsyncDatabaseService,
126
+ factory: async () => {
127
+ const db = new AsyncDatabaseService();
128
+ await db.connect();
129
+ return db;
100
130
  },
101
- {
102
- name: LAZY_B,
103
- factory: ({ resolver }) => {
104
- const a = resolver.lazyResolve(LAZY_A);
105
- return `B depends on ${a.value}`;
106
- },
131
+ async: true,
132
+ });
133
+
134
+ container.add({
135
+ token: AsyncUserService,
136
+ factory: async () => {
137
+ const db = await injectAsync(AsyncDatabaseService);
138
+ return new AsyncUserService(db);
107
139
  },
108
- ]);
140
+ async: true,
141
+ });
142
+
143
+ // Resolve async services
144
+ const userService = await container.getAsync(AsyncUserService);
145
+ const user = await userService.getUser(1);
146
+
147
+ console.log(user); // Output: User 1
148
+ ```
109
149
 
110
- // Accessing value after construction is safe
111
- await container.resolve(LAZY_A);
112
- await container.resolve(LAZY_B);
150
+ ### Optional Dependencies
151
+
152
+ ```typescript
153
+ import { Container, inject } from "@resolid/di";
154
+
155
+ class AnalyticsService {
156
+ private logger?: LogService;
157
+
158
+ constructor(logger?: LogService) {
159
+ this.logger = logger;
160
+ }
161
+
162
+ track(event: string): void {
163
+ if (this.logger) {
164
+ this.logger.log(`Analytics: ${event}`);
165
+ } else {
166
+ console.log(`Analytics (no logger): ${event}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ const container = new Container();
172
+
173
+ // Logger is optional
174
+ container.add({
175
+ token: AnalyticsService,
176
+ factory: () => new AnalyticsService(inject(LOG_SERVICE, { optional: true })),
177
+ });
178
+
179
+ const analytics = container.get(AnalyticsService);
180
+ analytics.track("page_view"); // Output: Analytics (no logger): page_view
113
181
  ```
114
182
 
115
- > Note: Access `lazyResolve.value` **only after all bindings are constructed**, otherwise it will throw an error.
183
+ ### Lazy Resolution
184
+
185
+ ```typescript
186
+ import { Container, inject } from "@resolid/di";
187
+
188
+ class ReportService {
189
+ private getLogger: () => LogService;
190
+
191
+ constructor(private getLogger: () => LogService) {
192
+ this.getLogger = getLogger;
193
+ }
194
+
195
+ generateReport(): void {
196
+ const logger = this.getLogger();
197
+ logger.log("Report generated");
198
+ }
199
+ }
200
+
201
+ const container = new Container();
202
+
203
+ container.add({
204
+ token: LogService,
205
+ factory: () => new LogService(),
206
+ });
207
+
208
+ container.add({
209
+ token: ReportService,
210
+ factory: () => new ReportService(inject(LogService, { lazy: true })),
211
+ });
212
+
213
+ const reportService = container.get(ReportService);
214
+ reportService.generateReport(); // Output: [LOG]: Report generated
215
+ ```
116
216
 
117
217
  ### Circular Dependency Detection
118
218
 
119
- ```js
120
- const A = "A";
121
- const B = "B";
219
+ ```typescript
220
+ class ApiService {
221
+ constructor(private auth: AuthService) {}
222
+ }
122
223
 
123
- const container = createContainer([
124
- {
125
- name: A,
126
- factory: ({ resolver }) => resolver.resolve(B),
127
- },
128
- {
129
- name: B,
130
- factory: ({ resolver }) => resolver.resolve(A),
131
- },
132
- ]);
224
+ class AuthService {
225
+ constructor(private api: ApiService) {}
226
+ }
227
+
228
+ const container = new Container();
229
+
230
+ container.add({
231
+ token: ApiService,
232
+ factory: () => new ApiService(inject(AuthService)),
233
+ });
133
234
 
134
- await container.resolve(A);
135
- // Throws Error: Circular dependency detected: A -> B -> A
235
+ container.add({
236
+ token: AuthService,
237
+ factory: () => new AuthService(inject(ApiService)),
238
+ });
239
+
240
+ container.get(ApiService); // Throws: Circular dependency detected ApiService -> AuthService -> ApiService
136
241
  ```
137
242
 
138
- ### Dispose
243
+ ### Disposable Resources
244
+
245
+ ```typescript
246
+ import { Container } from "@resolid/di";
139
247
 
140
- ```js
141
- class Resource {
142
- async dispose() {
143
- console.log("Resource disposed");
248
+ class DatabaseConnection {
249
+ async dispose(): Promise<void> {
250
+ console.log("Database connection closed");
144
251
  }
145
252
  }
146
253
 
147
- const RESOURCE = "RESOURCE";
254
+ const container = new Container();
255
+
256
+ container.add({
257
+ token: DatabaseConnection,
258
+ factory: async () => new DatabaseConnection(),
259
+ async: true,
260
+ });
148
261
 
149
- const container = createContainer([{ name: RESOURCE, value: new Resource() }]);
262
+ const db = await container.getAsync(DatabaseConnection);
150
263
 
151
264
  // Dispose all singleton instances
152
- await container.dispose();
153
- // Output: "Resource disposed"
265
+ await container.dispose(); // Output: Database connection closed
154
266
  ```
155
267
 
156
268
  ## License
package/dist/index.d.ts CHANGED
@@ -1,45 +1,121 @@
1
- //#region src/index.d.ts
2
- type Resolve = <T>(name: string) => Promise<T>;
3
- type LazyResolve = <T = unknown>(name: string) => Readonly<{
4
- value: T;
5
- }>;
6
- type Scope = "singleton" | "transient";
7
- type Resolver = {
8
- resolve: Resolve;
9
- lazyResolve: LazyResolve;
10
- };
11
- type FactoryConfig = Record<string, unknown>;
12
- type BindingDefinition<T$1 = unknown, C extends FactoryConfig = FactoryConfig> = {
13
- name: string;
14
- value: T$1;
15
- callable?: never;
16
- factory?: never;
17
- scope?: never;
18
- config?: never;
19
- } | {
1
+ //#region src/shared/index.d.ts
2
+ type Token<T = unknown> = symbol | (new (...args: any[]) => T) | {
3
+ prototype: T;
20
4
  name: string;
21
- callable: (...args: unknown[]) => T$1;
22
- value?: never;
23
- factory?: never;
24
- scope?: never;
25
- config?: never;
26
- } | {
27
- name: string;
28
- factory: (options: {
29
- resolver: Resolver;
30
- config?: C;
31
- }) => T$1 | Promise<T$1>;
32
- scope?: Scope;
33
- config?: C;
34
- value?: never;
35
- callable?: never;
36
5
  };
37
- type Disposable = {
6
+ interface Disposable {
38
7
  dispose: () => Promise<void> | void;
8
+ }
9
+ interface Resolver {
10
+ get: (<T>(token: Token<T>) => T) & (<T>(token: Token<T>, options: {
11
+ optional: true;
12
+ }) => T | undefined) & (<T>(token: Token<T>, options: {
13
+ lazy: true;
14
+ }) => () => T) & (<T>(token: Token<T>, options: {
15
+ lazy: true;
16
+ optional: true;
17
+ }) => () => T | undefined) & (<T>(token: Token<T>, options: {
18
+ lazy?: false;
19
+ optional?: boolean;
20
+ }) => T | undefined) & (<T>(token: Token<T>, options?: {
21
+ lazy?: boolean;
22
+ optional?: boolean;
23
+ }) => T | undefined | (() => T | undefined));
24
+ getAsync: (<T>(token: Token<T>) => Promise<T>) & (<T>(token: Token<T>, options: {
25
+ optional: true;
26
+ }) => Promise<T | undefined>) & (<T>(token: Token<T>, options: {
27
+ lazy: true;
28
+ }) => () => Promise<T>) & (<T>(token: Token<T>, options: {
29
+ lazy: true;
30
+ optional: true;
31
+ }) => () => Promise<T | undefined>) & (<T>(token: Token<T>, options: {
32
+ lazy?: false;
33
+ optional?: boolean;
34
+ }) => Promise<T | undefined>) & (<T>(token: Token<T>, options?: {
35
+ lazy?: boolean;
36
+ optional?: boolean;
37
+ }) => Promise<T | undefined> | (() => Promise<T | undefined>));
38
+ }
39
+ //#endregion
40
+ //#region src/container/index.d.ts
41
+ type Scope = "singleton" | "transient";
42
+ type SyncProvider<T = unknown> = {
43
+ token: Token<T>;
44
+ factory: (resolver: Resolver) => T;
45
+ async?: false;
46
+ scope?: Scope;
39
47
  };
40
- type Container = {
41
- resolve: Resolve;
42
- } & Disposable;
43
- declare const createContainer: (definitions: readonly BindingDefinition[]) => Container;
48
+ type AsyncProvider<T = unknown> = {
49
+ token: Token<T>;
50
+ factory: (resolver: Resolver) => Promise<T>;
51
+ async?: true;
52
+ scope?: Scope;
53
+ };
54
+ type Provider<T = unknown> = SyncProvider<T> | AsyncProvider<T>;
55
+ declare class Container implements Resolver, Disposable {
56
+ private readonly _providers;
57
+ private readonly _singletons;
58
+ private readonly _constructing;
59
+ private _checkCircularDependency;
60
+ private _resolveProvide;
61
+ private _resolve;
62
+ private _resolveAsync;
63
+ add(provider: Provider): void;
64
+ get<T>(token: Token<T>): T;
65
+ get<T>(token: Token<T>, options: {
66
+ optional: true;
67
+ }): T | undefined;
68
+ get<T>(token: Token<T>, options: {
69
+ lazy: true;
70
+ }): () => T;
71
+ get<T>(token: Token<T>, options: {
72
+ lazy: true;
73
+ optional: true;
74
+ }): () => T | undefined;
75
+ get<T>(token: Token<T>, options: {
76
+ lazy?: false;
77
+ optional?: boolean;
78
+ }): T | undefined;
79
+ getAsync<T>(token: Token<T>): Promise<T>;
80
+ getAsync<T>(token: Token<T>, options: {
81
+ optional: true;
82
+ }): Promise<T | undefined>;
83
+ getAsync<T>(token: Token<T>, options: {
84
+ lazy: true;
85
+ }): () => Promise<T>;
86
+ getAsync<T>(token: Token<T>, options: {
87
+ lazy: true;
88
+ optional: true;
89
+ }): () => Promise<T | undefined>;
90
+ getAsync<T>(token: Token<T>, options: {
91
+ lazy?: false;
92
+ optional?: boolean;
93
+ }): Promise<T | undefined>;
94
+ dispose(): Promise<void>;
95
+ }
96
+ //#endregion
97
+ //#region src/inject/index.d.ts
98
+ declare function inject<T>(token: Token<T>): T;
99
+ declare function inject<T>(token: Token<T>, options: {
100
+ optional: true;
101
+ }): T | undefined;
102
+ declare function inject<T>(token: Token<T>, options: {
103
+ lazy: true;
104
+ }): () => T;
105
+ declare function inject<T>(token: Token<T>, options: {
106
+ lazy: true;
107
+ optional: true;
108
+ }): () => T | undefined;
109
+ declare function injectAsync<T>(token: Token<T>): Promise<T>;
110
+ declare function injectAsync<T>(token: Token<T>, options: {
111
+ optional: true;
112
+ }): Promise<T | undefined>;
113
+ declare function injectAsync<T>(token: Token<T>, options: {
114
+ lazy: true;
115
+ }): () => Promise<T>;
116
+ declare function injectAsync<T>(token: Token<T>, options: {
117
+ lazy: true;
118
+ optional: true;
119
+ }): () => Promise<T | undefined>;
44
120
  //#endregion
45
- export { BindingDefinition, Container, FactoryConfig, Resolver, Scope, createContainer };
121
+ export { Container, Provider, type Resolver, Scope, type Token, inject, injectAsync };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- const e=e=>{let n=new t;return n.register(e),{resolve:e=>n.resolve(e),dispose:()=>n.dispose()}};var t=class e{_bindings;_singletons;_constructing;_lazyResolveQueue=[];constructor(e,t,n){this._bindings=e?._bindings??new Map,this._singletons=e?._singletons??new Map,this._constructing=t?[...n??e?._constructing??[],t]:[]}register(e){for(let t of e){let e=t.callable?()=>t.callable:t.factory?(e,n)=>t.factory({resolver:{resolve:e,lazyResolve:n},config:t.config}):()=>t.value;this._bindings.set(t.name,{factory:e,scope:t.scope??`singleton`,config:t.config})}}async _resolveBinding(t,n){let r=this._bindings.get(t);if(!r)throw Error(`No binding found for ${t}.`);let i=r.scope===`singleton`;if(i&&this._singletons.has(t))return this._singletons.get(t);let a=new e(this,t,n),o=r.factory(e=>a.resolve(e),e=>a.lazyResolve(e)),s=o instanceof Promise?await o:o;return i&&this._singletons.set(t,s),await a.dequeueLazyResolves(),s}async dequeueLazyResolves(){for(let e of this._lazyResolveQueue)try{e.resolve(await this._resolveBinding(e.name,[]))}catch(t){throw Error(`Failed to resolve lazy binding ${e.name}: ${t instanceof Error?t.message:String(t)}`)}}async resolve(e){if(this._constructing.includes(e))throw Error(`Circular dependency detected: ${[...this._constructing,e].join(` -> `)}`);return this._resolveBinding(e)}lazyResolve(e){let t={resolved:!1};return this._lazyResolveQueue.push({name:e,resolve:e=>{t.value=e,t.resolved=!0}}),{get value(){if(!t.resolved)throw Error(`Lazy binding is not yet resolved. Avoid accessing it before container construction finishes.`);return t.value}}}async dispose(){let e=0,t=``;for(let[n,r]of this._singletons)if(typeof r.dispose==`function`)try{await r.dispose()}catch(r){e++,t+=`${n}: ${r instanceof Error?r.message:r}; `}if(this._singletons.clear(),e>0)throw Error(`Failed to dispose ${e} binding(s):\n${t.slice(0,-2)}`)}};export{e as createContainer};
1
+ var e=class extends Error{constructor(){super(`inject() / injectAsync() must be called within a injection context`)}},t=class t{static contextStack=[];resolver;constructor(e){this.resolver=e}static current(){let t=this.contextStack[this.contextStack.length-1];if(t===void 0)throw new e;return t}run(e){t.contextStack.push(this);try{return e(this.resolver)}finally{t.contextStack.pop()}}async runAsync(e){t.contextStack.push(this);try{return await e(this.resolver)}finally{t.contextStack.pop()}}};function n(e){return typeof e==`symbol`?e.description??String(e):e.name}var r=class{_providers=new Map;_singletons=new Map;_constructing=[];_checkCircularDependency(e){if(this._constructing.includes(e))throw Error(`Circular dependency detected ${[...this._constructing,e].map(n).join(` -> `)}`)}_resolveProvide(e,t){let r=this._providers.get(e);if(r===void 0&&!t)throw Error(`No provider found for ${n(e)}`);return r}_resolve(e,r){this._checkCircularDependency(e);let i=this._resolveProvide(e,r);if(i===void 0)return;if(i.async)throw Error(`Provider for ${n(e)} is async, please use injectAsync()`);let a=i.scope!==`transient`;if(a&&this._singletons.has(e))return this._singletons.get(e);this._constructing.push(e);try{let n=new t(this).run(()=>i.factory(this));return a&&this._singletons.set(e,n),n}finally{this._constructing.pop()}}async _resolveAsync(e,n){this._checkCircularDependency(e);let r=this._resolveProvide(e,n);if(r===void 0)return;let i=r.scope!==`transient`;if(i&&this._singletons.has(e))return this._singletons.get(e);this._constructing.push(e);try{let n=await new t(this).runAsync(async()=>await r.factory(this));return i&&this._singletons.set(e,n),n}finally{this._constructing.pop()}}add(e){this._providers.set(e.token,e)}get(e,t){return t?.lazy??!1?()=>this.get(e,{...t,lazy:!1}):this._resolve(e,t?.optional??!1)}getAsync(e,t){return t?.lazy??!1?()=>this.getAsync(e,{...t,lazy:!1}):this._resolveAsync(e,t?.optional??!1)}async dispose(){let e=0,t=``;for(let[r,i]of this._singletons)if(typeof i.dispose==`function`)try{await i.dispose()}catch(i){e++,t+=`${n(r)}: ${i instanceof Error?i.message:i}; `}if(this._singletons.clear(),e>0)throw Error(`Failed to dispose ${e} provider(s):\n${t.slice(0,-2)}`)}};function i(n,r){try{return t.current().run(e=>e.get(n,r))}catch(t){if(t instanceof e&&r?.optional==1)return;throw t}}function a(n,r){try{return r?.lazy?t.current().run(e=>e.getAsync(n,{...r,lazy:!0})):t.current().runAsync(e=>e.getAsync(n,{...r,lazy:!1}))}catch(t){return t instanceof e&&r?.optional===!0?r?.lazy?()=>Promise.resolve(void 0):Promise.resolve(void 0):Promise.reject(t)}}export{r as Container,i as inject,a as injectAsync};
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@resolid/di",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
- "description": "The Resolid DI Container package.",
5
+ "description": "TypeScript Dependency Injection Container",
6
6
  "keywords": [
7
7
  "resolid",
8
8
  "container",
@@ -36,10 +36,10 @@
36
36
  "dist"
37
37
  ],
38
38
  "devDependencies": {
39
- "@vitest/coverage-v8": "beta",
40
- "tsdown": "^0.15.8",
39
+ "@vitest/coverage-v8": "^4.0.4",
40
+ "tsdown": "^0.15.11",
41
41
  "typescript": "^5.9.3",
42
- "vitest": "beta"
42
+ "vitest": "^4.0.4"
43
43
  },
44
44
  "engines": {
45
45
  "node": "^20.19.0 || ^22.13.0 || >=24"