@praxisjs/di 0.2.3 → 1.1.0
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/CHANGELOG.md +65 -0
- package/dist/__tests__/container.test.js +108 -3
- package/dist/__tests__/container.test.js.map +1 -1
- package/dist/__tests__/decorators.test.js +322 -100
- package/dist/__tests__/decorators.test.js.map +1 -1
- package/dist/container.d.ts +2 -2
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +43 -24
- package/dist/container.js.map +1 -1
- package/dist/decorators.d.ts +16 -4
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +83 -45
- package/dist/decorators.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/container.test.ts +106 -1
- package/src/__tests__/decorators.test.ts +204 -114
- package/src/container.ts +59 -25
- package/src/decorators.ts +99 -55
- package/src/index.ts +2 -3
|
@@ -1,191 +1,281 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
3
|
|
|
4
|
-
import { Container, token } from "../container";
|
|
4
|
+
import { Container, container, token } from "../container";
|
|
5
5
|
import {
|
|
6
6
|
Injectable,
|
|
7
7
|
Inject,
|
|
8
8
|
InjectContainer,
|
|
9
|
-
|
|
10
|
-
createScope,
|
|
9
|
+
Scope,
|
|
11
10
|
} from "../decorators";
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
function makeFieldCtx(name: string) {
|
|
13
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
14
|
+
return {
|
|
15
|
+
ctx: {
|
|
16
|
+
name,
|
|
17
|
+
kind: "field" as const,
|
|
18
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
19
|
+
} as ClassFieldDecoratorContext,
|
|
20
|
+
run(instance: unknown) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── @Injectable ───────────────────────────────────────────────────────────────
|
|
14
25
|
|
|
15
26
|
describe("Injectable", () => {
|
|
27
|
+
it("with no arguments — defaults to singleton scope", () => {
|
|
28
|
+
class SingletonByDefault {}
|
|
29
|
+
Injectable()(SingletonByDefault, {} as ClassDecoratorContext);
|
|
30
|
+
const a = container.resolve(SingletonByDefault);
|
|
31
|
+
const b = container.resolve(SingletonByDefault);
|
|
32
|
+
expect(a).toBe(b);
|
|
33
|
+
});
|
|
34
|
+
|
|
16
35
|
it("registers the class in the global container", () => {
|
|
17
|
-
// We test via useService resolving without throwing
|
|
18
36
|
class MyService {
|
|
19
|
-
greet() {
|
|
20
|
-
return "hello";
|
|
21
|
-
}
|
|
37
|
+
greet() { return "hello"; }
|
|
22
38
|
}
|
|
23
39
|
Injectable()(MyService, {} as ClassDecoratorContext);
|
|
24
|
-
|
|
25
|
-
expect(
|
|
26
|
-
expect(instance.greet()).toBe("hello");
|
|
40
|
+
expect(container.resolve(MyService)).toBeInstanceOf(MyService);
|
|
41
|
+
expect(container.resolve(MyService).greet()).toBe("hello");
|
|
27
42
|
});
|
|
28
43
|
|
|
29
|
-
it("registers with transient scope", () => {
|
|
44
|
+
it("registers with transient scope — each resolve returns a new instance", () => {
|
|
30
45
|
class TmpService {}
|
|
31
46
|
Injectable({ scope: "transient" })(TmpService, {} as ClassDecoratorContext);
|
|
32
|
-
|
|
33
|
-
const b = useService(TmpService);
|
|
34
|
-
expect(a).not.toBe(b);
|
|
47
|
+
expect(container.resolve(TmpService)).not.toBe(container.resolve(TmpService));
|
|
35
48
|
});
|
|
36
49
|
});
|
|
37
50
|
|
|
38
|
-
// ── Inject
|
|
51
|
+
// ── @Inject ───────────────────────────────────────────────────────────────────
|
|
39
52
|
|
|
40
53
|
describe("Inject", () => {
|
|
41
54
|
it("injects a registered service via property", () => {
|
|
42
55
|
class Logger {
|
|
43
|
-
log(msg: string) {
|
|
44
|
-
return msg;
|
|
45
|
-
}
|
|
56
|
+
log(msg: string) { return msg; }
|
|
46
57
|
}
|
|
47
58
|
Injectable()(Logger, {} as ClassDecoratorContext);
|
|
48
59
|
|
|
49
|
-
const
|
|
50
|
-
const ctx = {
|
|
51
|
-
name: "logger",
|
|
52
|
-
kind: "field" as const,
|
|
53
|
-
addInitializer(fn: (this: unknown) => void) {
|
|
54
|
-
initializers.push(fn);
|
|
55
|
-
},
|
|
56
|
-
} as ClassFieldDecoratorContext;
|
|
57
|
-
|
|
60
|
+
const { ctx, run } = makeFieldCtx("logger");
|
|
58
61
|
Inject(Logger)(undefined, ctx);
|
|
59
|
-
|
|
60
62
|
const instance: Record<string, unknown> = {};
|
|
61
|
-
|
|
62
|
-
fn.call(instance);
|
|
63
|
-
});
|
|
64
|
-
|
|
63
|
+
run(instance);
|
|
65
64
|
expect(instance.logger).toBeInstanceOf(Logger);
|
|
65
|
+
// second access covers the cache-hit branch (line 62: !cache.has(this) === false)
|
|
66
|
+
expect(instance.logger).toBe(instance.logger);
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
it("throws a descriptive error when dep is not registered", () => {
|
|
69
70
|
class UnknownDep {}
|
|
70
|
-
|
|
71
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
72
|
-
const ctx = {
|
|
73
|
-
name: "dep",
|
|
74
|
-
kind: "field" as const,
|
|
75
|
-
addInitializer(fn: (this: unknown) => void) {
|
|
76
|
-
initializers.push(fn);
|
|
77
|
-
},
|
|
78
|
-
} as ClassFieldDecoratorContext;
|
|
79
|
-
|
|
71
|
+
const { ctx, run } = makeFieldCtx("dep");
|
|
80
72
|
Inject(UnknownDep)(undefined, ctx);
|
|
81
|
-
|
|
82
|
-
class MyClass {
|
|
83
|
-
constructor() {}
|
|
84
|
-
}
|
|
73
|
+
class MyClass {}
|
|
85
74
|
const instance = new MyClass() as Record<string, unknown>;
|
|
86
|
-
|
|
87
|
-
fn.call(instance);
|
|
88
|
-
});
|
|
89
|
-
|
|
75
|
+
run(instance);
|
|
90
76
|
expect(() => instance.dep).toThrow("[Inject]");
|
|
91
77
|
});
|
|
92
78
|
|
|
93
79
|
it("warns on direct assignment in non-production", () => {
|
|
94
80
|
class Dep2 {}
|
|
95
81
|
Injectable()(Dep2, {} as ClassDecoratorContext);
|
|
96
|
-
|
|
97
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
98
|
-
const ctx = {
|
|
99
|
-
name: "dep2",
|
|
100
|
-
kind: "field" as const,
|
|
101
|
-
addInitializer(fn: (this: unknown) => void) {
|
|
102
|
-
initializers.push(fn);
|
|
103
|
-
},
|
|
104
|
-
} as ClassFieldDecoratorContext;
|
|
105
|
-
|
|
82
|
+
const { ctx, run } = makeFieldCtx("dep2");
|
|
106
83
|
Inject(Dep2)(undefined, ctx);
|
|
107
84
|
const instance: Record<string, unknown> = {};
|
|
108
|
-
|
|
109
|
-
fn.call(instance);
|
|
110
|
-
});
|
|
111
|
-
|
|
85
|
+
run(instance);
|
|
112
86
|
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
113
87
|
instance.dep2 = "override";
|
|
114
88
|
expect(spy).toHaveBeenCalledWith(expect.stringContaining("[Inject]"));
|
|
115
89
|
spy.mockRestore();
|
|
116
90
|
});
|
|
91
|
+
|
|
92
|
+
it("returns cached value on second access (cache hit)", () => {
|
|
93
|
+
class CachedService {
|
|
94
|
+
id = Math.random();
|
|
95
|
+
}
|
|
96
|
+
Injectable()(CachedService, {} as ClassDecoratorContext);
|
|
97
|
+
|
|
98
|
+
const { ctx, run } = makeFieldCtx("svc");
|
|
99
|
+
Inject(CachedService)(undefined, ctx);
|
|
100
|
+
const instance: Record<string, unknown> = {};
|
|
101
|
+
run(instance);
|
|
102
|
+
|
|
103
|
+
const first = instance.svc;
|
|
104
|
+
const second = instance.svc;
|
|
105
|
+
expect(first).toBe(second); // same cached instance
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("does not warn on direct assignment in production mode", () => {
|
|
109
|
+
class ProdDep {}
|
|
110
|
+
Injectable()(ProdDep, {} as ClassDecoratorContext);
|
|
111
|
+
const { ctx, run } = makeFieldCtx("dep");
|
|
112
|
+
Inject(ProdDep)(undefined, ctx);
|
|
113
|
+
const instance: Record<string, unknown> = {};
|
|
114
|
+
run(instance);
|
|
115
|
+
|
|
116
|
+
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
117
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
118
|
+
instance.dep = "override";
|
|
119
|
+
expect(spy).not.toHaveBeenCalled();
|
|
120
|
+
|
|
121
|
+
vi.unstubAllEnvs();
|
|
122
|
+
spy.mockRestore();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("includes the Token description in the error when resolution fails", () => {
|
|
126
|
+
const MY_TOKEN = token<string>("MY_SERVICE");
|
|
127
|
+
const { ctx, run } = makeFieldCtx("svc");
|
|
128
|
+
Inject(MY_TOKEN)(undefined, ctx);
|
|
129
|
+
class Consumer {}
|
|
130
|
+
const instance = new Consumer() as Record<string, unknown>;
|
|
131
|
+
run(instance);
|
|
132
|
+
expect(() => instance.svc).toThrow("MY_SERVICE");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("@Inject on an anonymous class — error message uses token description instead of empty class name", () => {
|
|
136
|
+
const ANON_TOKEN = token<string>("ANON_TOKEN");
|
|
137
|
+
const { ctx, run } = makeFieldCtx("val");
|
|
138
|
+
Inject(ANON_TOKEN)(undefined, ctx);
|
|
139
|
+
const instance: Record<string, unknown> = {};
|
|
140
|
+
run(instance);
|
|
141
|
+
expect(() => instance.val).toThrow("ANON_TOKEN");
|
|
142
|
+
});
|
|
117
143
|
});
|
|
118
144
|
|
|
119
|
-
// ── InjectContainer
|
|
145
|
+
// ── @InjectContainer ──────────────────────────────────────────────────────────
|
|
120
146
|
|
|
121
147
|
describe("InjectContainer", () => {
|
|
122
|
-
it("injects the global container
|
|
123
|
-
const
|
|
124
|
-
const ctx = {
|
|
125
|
-
name: "container",
|
|
126
|
-
kind: "field" as const,
|
|
127
|
-
addInitializer(fn: (this: unknown) => void) {
|
|
128
|
-
initializers.push(fn);
|
|
129
|
-
},
|
|
130
|
-
} as ClassFieldDecoratorContext;
|
|
131
|
-
|
|
148
|
+
it("injects the global container on an unscoped class", () => {
|
|
149
|
+
const { ctx, run } = makeFieldCtx("c");
|
|
132
150
|
InjectContainer()(undefined, ctx);
|
|
133
151
|
const instance: Record<string, unknown> = {};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
run(instance);
|
|
153
|
+
expect(instance.c).toBeInstanceOf(Container);
|
|
154
|
+
expect(instance.c).toBe(container);
|
|
155
|
+
});
|
|
137
156
|
|
|
138
|
-
|
|
157
|
+
it("called multiple times on same instance — returns same container instance (cached)", () => {
|
|
158
|
+
const { ctx, run } = makeFieldCtx("c");
|
|
159
|
+
InjectContainer()(undefined, ctx);
|
|
160
|
+
const instance: Record<string, unknown> = {};
|
|
161
|
+
run(instance);
|
|
162
|
+
const first = instance.c;
|
|
163
|
+
const second = instance.c;
|
|
164
|
+
expect(first).toBe(second);
|
|
139
165
|
});
|
|
140
166
|
});
|
|
141
167
|
|
|
142
|
-
// ──
|
|
168
|
+
// ── @Scope ────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
describe("Scope", () => {
|
|
171
|
+
it("creates a child container per instance", () => {
|
|
172
|
+
@Scope()
|
|
173
|
+
class Widget {}
|
|
174
|
+
const { ctx, run } = makeFieldCtx("c");
|
|
175
|
+
InjectContainer()(undefined, ctx);
|
|
176
|
+
|
|
177
|
+
const a = new Widget() as Record<string, unknown>;
|
|
178
|
+
run(a);
|
|
179
|
+
const b = new Widget() as Record<string, unknown>;
|
|
180
|
+
run(b);
|
|
181
|
+
|
|
182
|
+
expect(a.c).toBeInstanceOf(Container);
|
|
183
|
+
expect(b.c).toBeInstanceOf(Container);
|
|
184
|
+
expect(a.c).not.toBe(b.c); // each instance gets its own scope
|
|
185
|
+
expect(a.c).not.toBe(container); // it's a child, not the root
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("services registered in configure are resolvable within the scope", () => {
|
|
189
|
+
const DB_URL = token<string>("DB_URL");
|
|
190
|
+
|
|
191
|
+
@Scope((c) => { c.registerValue(DB_URL, "postgres://localhost"); })
|
|
192
|
+
class DataModule {}
|
|
143
193
|
|
|
144
|
-
|
|
145
|
-
|
|
194
|
+
const { ctx, run } = makeFieldCtx("db");
|
|
195
|
+
Inject(DB_URL)(undefined, ctx);
|
|
196
|
+
|
|
197
|
+
const instance = new DataModule() as Record<string, unknown>;
|
|
198
|
+
run(instance);
|
|
199
|
+
|
|
200
|
+
expect(instance.db).toBe("postgres://localhost");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("@Inject resolves from the scoped container, not the global one", () => {
|
|
204
|
+
class ScopedRepo {}
|
|
205
|
+
// NOT registered in global container
|
|
146
206
|
const VERSION = token<string>("VERSION");
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
207
|
+
|
|
208
|
+
@Scope((c) => {
|
|
209
|
+
c.register(ScopedRepo);
|
|
210
|
+
c.registerValue(VERSION, "2.0");
|
|
211
|
+
})
|
|
212
|
+
class App {}
|
|
213
|
+
|
|
214
|
+
const { ctx: repoCtx, run: runRepo } = makeFieldCtx("repo");
|
|
215
|
+
Inject(ScopedRepo)(undefined, repoCtx);
|
|
216
|
+
const { ctx: verCtx, run: runVer } = makeFieldCtx("version");
|
|
217
|
+
Inject(VERSION)(undefined, verCtx);
|
|
218
|
+
|
|
219
|
+
const instance = new App() as Record<string, unknown>;
|
|
220
|
+
runRepo(instance);
|
|
221
|
+
runVer(instance);
|
|
222
|
+
|
|
223
|
+
expect(instance.repo).toBeInstanceOf(ScopedRepo);
|
|
224
|
+
expect(instance.version).toBe("2.0");
|
|
150
225
|
});
|
|
151
|
-
});
|
|
152
226
|
|
|
153
|
-
|
|
227
|
+
it("@InjectContainer returns the scoped child container", () => {
|
|
228
|
+
@Scope((c) => { c.registerValue(token<number>("X"), 42); })
|
|
229
|
+
class Service {}
|
|
230
|
+
|
|
231
|
+
const { ctx, run } = makeFieldCtx("container");
|
|
232
|
+
InjectContainer()(undefined, ctx);
|
|
233
|
+
|
|
234
|
+
const instance = new Service() as Record<string, unknown>;
|
|
235
|
+
run(instance);
|
|
154
236
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const child = createScope();
|
|
158
|
-
expect(child).toBeInstanceOf(Container);
|
|
237
|
+
expect(instance.container).toBeInstanceOf(Container);
|
|
238
|
+
expect(instance.container).not.toBe(container);
|
|
159
239
|
});
|
|
160
240
|
|
|
161
|
-
it("
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
|
|
241
|
+
it("child container inherits registrations from the parent", () => {
|
|
242
|
+
class GlobalService {}
|
|
243
|
+
Injectable()(GlobalService, {} as ClassDecoratorContext);
|
|
244
|
+
|
|
245
|
+
@Scope()
|
|
246
|
+
class Child {}
|
|
247
|
+
|
|
248
|
+
const { ctx, run } = makeFieldCtx("svc");
|
|
249
|
+
Inject(GlobalService)(undefined, ctx);
|
|
250
|
+
const instance = new Child() as Record<string, unknown>;
|
|
251
|
+
run(instance);
|
|
252
|
+
|
|
253
|
+
expect(instance.svc).toBeInstanceOf(GlobalService);
|
|
167
254
|
});
|
|
168
|
-
});
|
|
169
255
|
|
|
170
|
-
|
|
256
|
+
it("@Scope on child class that inherits from @Scope parent — child gets own container", () => {
|
|
257
|
+
const PARENT_TOKEN = token<string>("PARENT_VAL");
|
|
258
|
+
const CHILD_TOKEN = token<string>("CHILD_VAL");
|
|
171
259
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const MY_TOKEN = token<string>("MY_SERVICE");
|
|
260
|
+
@Scope((c) => { c.registerValue(PARENT_TOKEN, "parent"); })
|
|
261
|
+
class Parent {}
|
|
175
262
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
name: "svc",
|
|
179
|
-
kind: "field" as const,
|
|
180
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
181
|
-
} as ClassFieldDecoratorContext;
|
|
263
|
+
@Scope((c) => { c.registerValue(CHILD_TOKEN, "child"); })
|
|
264
|
+
class Child extends (Parent as unknown as new () => object) {}
|
|
182
265
|
|
|
183
|
-
|
|
266
|
+
const { ctx: ctxC, run: runC } = makeFieldCtx("c");
|
|
267
|
+
InjectContainer()(undefined, ctxC);
|
|
184
268
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
initializers.forEach((fn) => { fn.call(instance); });
|
|
269
|
+
const parentInstance = new Parent() as Record<string, unknown>;
|
|
270
|
+
runC(parentInstance);
|
|
188
271
|
|
|
189
|
-
|
|
272
|
+
const childInstance = new Child() as Record<string, unknown>;
|
|
273
|
+
runC(childInstance);
|
|
274
|
+
|
|
275
|
+
expect(parentInstance.c).toBeInstanceOf(Container);
|
|
276
|
+
expect(childInstance.c).toBeInstanceOf(Container);
|
|
277
|
+
// child has its own scoped container, different from the parent's
|
|
278
|
+
expect(childInstance.c).not.toBe(parentInstance.c);
|
|
279
|
+
expect(childInstance.c).not.toBe(container);
|
|
190
280
|
});
|
|
191
281
|
});
|
package/src/container.ts
CHANGED
|
@@ -10,7 +10,8 @@ export interface ServiceDescriptor {
|
|
|
10
10
|
instance?: unknown;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
export type Constructor<T = unknown> = new (...args: any[]) => T;
|
|
14
15
|
|
|
15
16
|
export class Token<_T> {
|
|
16
17
|
readonly description: string;
|
|
@@ -58,38 +59,70 @@ export class Container {
|
|
|
58
59
|
return this;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
resolve<T>(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
62
|
+
resolve<T>(
|
|
63
|
+
target: Constructor<T> | Token<T>,
|
|
64
|
+
resolutionStack = new Set<Constructor | Token<unknown>>(),
|
|
65
|
+
): T {
|
|
66
|
+
const key = target as Constructor | Token<unknown>;
|
|
67
|
+
|
|
68
|
+
if (resolutionStack.has(key)) {
|
|
69
|
+
const chain = [...resolutionStack, key]
|
|
70
|
+
.map((k) =>
|
|
71
|
+
k instanceof Token ? k.toString() : (k).name,
|
|
72
|
+
)
|
|
73
|
+
.join(" → ");
|
|
74
|
+
throw new Error(`[DI] Circular dependency detected: ${chain}`);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
resolutionStack.add(key);
|
|
78
|
+
try {
|
|
79
|
+
const entry = this.services.get(key);
|
|
80
|
+
|
|
81
|
+
if (target instanceof Token) {
|
|
82
|
+
if (entry !== undefined) return entry as T;
|
|
83
|
+
if (this.parent) return this.parent.resolve(target);
|
|
84
|
+
throw new Error(`[DI] Token not registered: ${target.toString()}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!entry) {
|
|
88
|
+
if (this.parent) return this.parent.resolve(target);
|
|
89
|
+
throw new Error(
|
|
90
|
+
`[DI] Service not registered: ${(target as Constructor).name}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const descriptor = entry as ServiceDescriptor;
|
|
95
|
+
|
|
96
|
+
if (descriptor.scope === "singleton") {
|
|
97
|
+
descriptor.instance ??= this.instantiate(descriptor.target, resolutionStack);
|
|
98
|
+
return descriptor.instance as T;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this.instantiate(descriptor.target, resolutionStack) as T;
|
|
102
|
+
} finally {
|
|
103
|
+
resolutionStack.delete(key);
|
|
82
104
|
}
|
|
83
|
-
|
|
84
|
-
return this.instantiate(descriptor.target) as T;
|
|
85
105
|
}
|
|
86
106
|
|
|
87
|
-
private instantiate<T>(
|
|
107
|
+
private instantiate<T>(
|
|
108
|
+
target: Constructor<T>,
|
|
109
|
+
resolutionStack = new Set<Constructor | Token<unknown>>(),
|
|
110
|
+
): T {
|
|
88
111
|
const deps: Array<Constructor | Token<unknown>> =
|
|
89
112
|
(Reflect.getMetadata("di:inject", target) as Array<Constructor | Token<unknown>> | undefined) ?? [];
|
|
90
113
|
|
|
91
|
-
const resolvedDeps = deps.map((dep) =>
|
|
92
|
-
|
|
114
|
+
const resolvedDeps = deps.map((dep) =>
|
|
115
|
+
this.resolve(dep as Constructor, resolutionStack),
|
|
116
|
+
);
|
|
117
|
+
let instance: T;
|
|
118
|
+
try {
|
|
119
|
+
instance = new target(...resolvedDeps);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err instanceof Error && err.message.startsWith("[DI]")) throw err;
|
|
122
|
+
throw new Error(
|
|
123
|
+
`[DI] Failed to instantiate "${target.name}": ${(err as Error).message}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
93
126
|
|
|
94
127
|
const rawPropInjections: unknown = Reflect.getMetadata("di:props", target.prototype as object);
|
|
95
128
|
const propInjections =
|
|
@@ -98,6 +131,7 @@ export class Container {
|
|
|
98
131
|
for (const [prop, dep] of propInjections) {
|
|
99
132
|
(instance as Record<string, unknown>)[prop] = this.resolve(
|
|
100
133
|
dep as Constructor,
|
|
134
|
+
resolutionStack,
|
|
101
135
|
);
|
|
102
136
|
}
|
|
103
137
|
|