@praxisjs/di 0.2.0 → 0.2.2
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 +14 -0
- package/dist/__tests__/container.test.d.ts +2 -0
- package/dist/__tests__/container.test.d.ts.map +1 -0
- package/dist/__tests__/container.test.js +88 -0
- package/dist/__tests__/container.test.js.map +1 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +140 -0
- package/dist/__tests__/decorators.test.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/container.test.ts +96 -0
- package/src/__tests__/decorators.test.ts +173 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @praxisjs/di
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [fe39901]
|
|
8
|
+
- @praxisjs/core@0.4.1
|
|
9
|
+
|
|
10
|
+
## 0.2.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [f52354d]
|
|
15
|
+
- @praxisjs/core@0.4.0
|
|
16
|
+
|
|
3
17
|
## 0.2.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/container.test.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { Container, Token, token } from "../container";
|
|
4
|
+
describe("Token", () => {
|
|
5
|
+
it("stores description", () => {
|
|
6
|
+
const t = new Token("MY_SERVICE");
|
|
7
|
+
expect(t.description).toBe("MY_SERVICE");
|
|
8
|
+
});
|
|
9
|
+
it("toString includes description", () => {
|
|
10
|
+
const t = new Token("FOO");
|
|
11
|
+
expect(t.toString()).toBe("Token(FOO)");
|
|
12
|
+
});
|
|
13
|
+
it("token() factory creates a Token", () => {
|
|
14
|
+
const t = token("DB_URL");
|
|
15
|
+
expect(t).toBeInstanceOf(Token);
|
|
16
|
+
expect(t.description).toBe("DB_URL");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe("Container", () => {
|
|
20
|
+
it("registers and resolves a singleton service", () => {
|
|
21
|
+
class MyService {
|
|
22
|
+
}
|
|
23
|
+
const c = new Container();
|
|
24
|
+
c.register(MyService);
|
|
25
|
+
const a = c.resolve(MyService);
|
|
26
|
+
const b = c.resolve(MyService);
|
|
27
|
+
expect(a).toBeInstanceOf(MyService);
|
|
28
|
+
expect(a).toBe(b); // singleton
|
|
29
|
+
});
|
|
30
|
+
it("creates new instance for transient scope", () => {
|
|
31
|
+
class Counter {
|
|
32
|
+
value = Math.random();
|
|
33
|
+
}
|
|
34
|
+
const c = new Container();
|
|
35
|
+
c.register(Counter, { scope: "transient" });
|
|
36
|
+
const a = c.resolve(Counter);
|
|
37
|
+
const b = c.resolve(Counter);
|
|
38
|
+
expect(a).not.toBe(b);
|
|
39
|
+
});
|
|
40
|
+
it("throws when resolving unregistered service", () => {
|
|
41
|
+
class Unknown {
|
|
42
|
+
}
|
|
43
|
+
const c = new Container();
|
|
44
|
+
expect(() => c.resolve(Unknown)).toThrow("[DI] Service not registered");
|
|
45
|
+
});
|
|
46
|
+
it("registerValue stores a value by token", () => {
|
|
47
|
+
const DB_URL = token("DB_URL");
|
|
48
|
+
const c = new Container();
|
|
49
|
+
c.registerValue(DB_URL, "postgres://localhost/db");
|
|
50
|
+
expect(c.resolve(DB_URL)).toBe("postgres://localhost/db");
|
|
51
|
+
});
|
|
52
|
+
it("registerFactory computes value via factory", () => {
|
|
53
|
+
const MAX = token("MAX");
|
|
54
|
+
const c = new Container();
|
|
55
|
+
c.registerFactory(MAX, () => 100);
|
|
56
|
+
expect(c.resolve(MAX)).toBe(100);
|
|
57
|
+
});
|
|
58
|
+
it("throws for unregistered token", () => {
|
|
59
|
+
const t = token("MISSING");
|
|
60
|
+
const c = new Container();
|
|
61
|
+
expect(() => c.resolve(t)).toThrow("[DI] Token not registered");
|
|
62
|
+
});
|
|
63
|
+
it("child container resolves from parent", () => {
|
|
64
|
+
const URL_TOKEN = token("URL");
|
|
65
|
+
const parent = new Container();
|
|
66
|
+
parent.registerValue(URL_TOKEN, "https://example.com");
|
|
67
|
+
const child = parent.createChild();
|
|
68
|
+
expect(child.resolve(URL_TOKEN)).toBe("https://example.com");
|
|
69
|
+
});
|
|
70
|
+
it("child container can override parent registration", () => {
|
|
71
|
+
const URL_TOKEN = token("URL");
|
|
72
|
+
const parent = new Container();
|
|
73
|
+
parent.registerValue(URL_TOKEN, "https://parent.com");
|
|
74
|
+
const child = parent.createChild();
|
|
75
|
+
child.registerValue(URL_TOKEN, "https://child.com");
|
|
76
|
+
expect(child.resolve(URL_TOKEN)).toBe("https://child.com");
|
|
77
|
+
expect(parent.resolve(URL_TOKEN)).toBe("https://parent.com");
|
|
78
|
+
});
|
|
79
|
+
it("register() is chainable", () => {
|
|
80
|
+
class A {
|
|
81
|
+
}
|
|
82
|
+
class B {
|
|
83
|
+
}
|
|
84
|
+
const c = new Container();
|
|
85
|
+
expect(c.register(A).register(B)).toBe(c);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=container.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container.test.js","sourceRoot":"","sources":["../../src/__tests__/container.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { Container, token } from "../container";
|
|
4
|
+
import { Injectable, Inject, InjectContainer, useService, createScope, } from "../decorators";
|
|
5
|
+
// Each test group uses a fresh container to avoid state pollution
|
|
6
|
+
function makeScope() {
|
|
7
|
+
return new Container();
|
|
8
|
+
}
|
|
9
|
+
// ── Injectable ────────────────────────────────────────────────────────────────
|
|
10
|
+
describe("Injectable", () => {
|
|
11
|
+
it("registers the class in the global container", () => {
|
|
12
|
+
// We test via useService resolving without throwing
|
|
13
|
+
class MyService {
|
|
14
|
+
greet() {
|
|
15
|
+
return "hello";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
Injectable()(MyService, {});
|
|
19
|
+
const instance = useService(MyService);
|
|
20
|
+
expect(instance).toBeInstanceOf(MyService);
|
|
21
|
+
expect(instance.greet()).toBe("hello");
|
|
22
|
+
});
|
|
23
|
+
it("registers with transient scope", () => {
|
|
24
|
+
class TmpService {
|
|
25
|
+
}
|
|
26
|
+
Injectable({ scope: "transient" })(TmpService, {});
|
|
27
|
+
const a = useService(TmpService);
|
|
28
|
+
const b = useService(TmpService);
|
|
29
|
+
expect(a).not.toBe(b);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
// ── Inject ────────────────────────────────────────────────────────────────────
|
|
33
|
+
describe("Inject", () => {
|
|
34
|
+
it("injects a registered service via property", () => {
|
|
35
|
+
class Logger {
|
|
36
|
+
log(msg) {
|
|
37
|
+
return msg;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
Injectable()(Logger, {});
|
|
41
|
+
const initializers = [];
|
|
42
|
+
const ctx = {
|
|
43
|
+
name: "logger",
|
|
44
|
+
kind: "field",
|
|
45
|
+
addInitializer(fn) {
|
|
46
|
+
initializers.push(fn);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
Inject(Logger)(undefined, ctx);
|
|
50
|
+
const instance = {};
|
|
51
|
+
initializers.forEach((fn) => {
|
|
52
|
+
fn.call(instance);
|
|
53
|
+
});
|
|
54
|
+
expect(instance.logger).toBeInstanceOf(Logger);
|
|
55
|
+
});
|
|
56
|
+
it("throws a descriptive error when dep is not registered", () => {
|
|
57
|
+
class UnknownDep {
|
|
58
|
+
}
|
|
59
|
+
const initializers = [];
|
|
60
|
+
const ctx = {
|
|
61
|
+
name: "dep",
|
|
62
|
+
kind: "field",
|
|
63
|
+
addInitializer(fn) {
|
|
64
|
+
initializers.push(fn);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
Inject(UnknownDep)(undefined, ctx);
|
|
68
|
+
class MyClass {
|
|
69
|
+
constructor() { }
|
|
70
|
+
}
|
|
71
|
+
const instance = new MyClass();
|
|
72
|
+
initializers.forEach((fn) => {
|
|
73
|
+
fn.call(instance);
|
|
74
|
+
});
|
|
75
|
+
expect(() => instance.dep).toThrow("[Inject]");
|
|
76
|
+
});
|
|
77
|
+
it("warns on direct assignment in non-production", () => {
|
|
78
|
+
class Dep2 {
|
|
79
|
+
}
|
|
80
|
+
Injectable()(Dep2, {});
|
|
81
|
+
const initializers = [];
|
|
82
|
+
const ctx = {
|
|
83
|
+
name: "dep2",
|
|
84
|
+
kind: "field",
|
|
85
|
+
addInitializer(fn) {
|
|
86
|
+
initializers.push(fn);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
Inject(Dep2)(undefined, ctx);
|
|
90
|
+
const instance = {};
|
|
91
|
+
initializers.forEach((fn) => {
|
|
92
|
+
fn.call(instance);
|
|
93
|
+
});
|
|
94
|
+
const spy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
95
|
+
instance.dep2 = "override";
|
|
96
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining("[Inject]"));
|
|
97
|
+
spy.mockRestore();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
// ── InjectContainer ───────────────────────────────────────────────────────────
|
|
101
|
+
describe("InjectContainer", () => {
|
|
102
|
+
it("injects the global container instance", () => {
|
|
103
|
+
const initializers = [];
|
|
104
|
+
const ctx = {
|
|
105
|
+
name: "container",
|
|
106
|
+
kind: "field",
|
|
107
|
+
addInitializer(fn) {
|
|
108
|
+
initializers.push(fn);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
InjectContainer()(undefined, ctx);
|
|
112
|
+
const instance = {};
|
|
113
|
+
initializers.forEach((fn) => {
|
|
114
|
+
fn.call(instance);
|
|
115
|
+
});
|
|
116
|
+
expect(instance.container).toBeInstanceOf(Container);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// ── useService ────────────────────────────────────────────────────────────────
|
|
120
|
+
describe("useService", () => {
|
|
121
|
+
it("resolves by Token", () => {
|
|
122
|
+
const VERSION = token("VERSION");
|
|
123
|
+
const c = createScope((sc) => sc.registerValue(VERSION, "1.0.0"));
|
|
124
|
+
// useService uses global container — test via token on a fresh child
|
|
125
|
+
expect(c.resolve(VERSION)).toBe("1.0.0");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ── createScope ───────────────────────────────────────────────────────────────
|
|
129
|
+
describe("createScope", () => {
|
|
130
|
+
it("creates a child container without configure fn", () => {
|
|
131
|
+
const child = createScope();
|
|
132
|
+
expect(child).toBeInstanceOf(Container);
|
|
133
|
+
});
|
|
134
|
+
it("applies the configure callback", () => {
|
|
135
|
+
const DB = token("DB");
|
|
136
|
+
const child = createScope((c) => c.registerValue(DB, "postgres://localhost"));
|
|
137
|
+
expect(child.resolve(DB)).toBe("postgres://localhost");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
//# sourceMappingURL=decorators.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.test.js","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/di",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"reflect-metadata": "^0.2.2",
|
|
15
|
-
"@praxisjs/core": "0.
|
|
15
|
+
"@praxisjs/core": "0.4.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/node": "^25.3.0",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { Container, Token, token } from "../container";
|
|
5
|
+
|
|
6
|
+
describe("Token", () => {
|
|
7
|
+
it("stores description", () => {
|
|
8
|
+
const t = new Token("MY_SERVICE");
|
|
9
|
+
expect(t.description).toBe("MY_SERVICE");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("toString includes description", () => {
|
|
13
|
+
const t = new Token("FOO");
|
|
14
|
+
expect(t.toString()).toBe("Token(FOO)");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("token() factory creates a Token", () => {
|
|
18
|
+
const t = token<string>("DB_URL");
|
|
19
|
+
expect(t).toBeInstanceOf(Token);
|
|
20
|
+
expect(t.description).toBe("DB_URL");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("Container", () => {
|
|
25
|
+
it("registers and resolves a singleton service", () => {
|
|
26
|
+
class MyService {}
|
|
27
|
+
const c = new Container();
|
|
28
|
+
c.register(MyService);
|
|
29
|
+
const a = c.resolve(MyService);
|
|
30
|
+
const b = c.resolve(MyService);
|
|
31
|
+
expect(a).toBeInstanceOf(MyService);
|
|
32
|
+
expect(a).toBe(b); // singleton
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("creates new instance for transient scope", () => {
|
|
36
|
+
class Counter {
|
|
37
|
+
value = Math.random();
|
|
38
|
+
}
|
|
39
|
+
const c = new Container();
|
|
40
|
+
c.register(Counter, { scope: "transient" });
|
|
41
|
+
const a = c.resolve(Counter);
|
|
42
|
+
const b = c.resolve(Counter);
|
|
43
|
+
expect(a).not.toBe(b);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("throws when resolving unregistered service", () => {
|
|
47
|
+
class Unknown {}
|
|
48
|
+
const c = new Container();
|
|
49
|
+
expect(() => c.resolve(Unknown)).toThrow("[DI] Service not registered");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("registerValue stores a value by token", () => {
|
|
53
|
+
const DB_URL = token<string>("DB_URL");
|
|
54
|
+
const c = new Container();
|
|
55
|
+
c.registerValue(DB_URL, "postgres://localhost/db");
|
|
56
|
+
expect(c.resolve(DB_URL)).toBe("postgres://localhost/db");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("registerFactory computes value via factory", () => {
|
|
60
|
+
const MAX = token<number>("MAX");
|
|
61
|
+
const c = new Container();
|
|
62
|
+
c.registerFactory(MAX, () => 100);
|
|
63
|
+
expect(c.resolve(MAX)).toBe(100);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("throws for unregistered token", () => {
|
|
67
|
+
const t = token<string>("MISSING");
|
|
68
|
+
const c = new Container();
|
|
69
|
+
expect(() => c.resolve(t)).toThrow("[DI] Token not registered");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("child container resolves from parent", () => {
|
|
73
|
+
const URL_TOKEN = token<string>("URL");
|
|
74
|
+
const parent = new Container();
|
|
75
|
+
parent.registerValue(URL_TOKEN, "https://example.com");
|
|
76
|
+
const child = parent.createChild();
|
|
77
|
+
expect(child.resolve(URL_TOKEN)).toBe("https://example.com");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("child container can override parent registration", () => {
|
|
81
|
+
const URL_TOKEN = token<string>("URL");
|
|
82
|
+
const parent = new Container();
|
|
83
|
+
parent.registerValue(URL_TOKEN, "https://parent.com");
|
|
84
|
+
const child = parent.createChild();
|
|
85
|
+
child.registerValue(URL_TOKEN, "https://child.com");
|
|
86
|
+
expect(child.resolve(URL_TOKEN)).toBe("https://child.com");
|
|
87
|
+
expect(parent.resolve(URL_TOKEN)).toBe("https://parent.com");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("register() is chainable", () => {
|
|
91
|
+
class A {}
|
|
92
|
+
class B {}
|
|
93
|
+
const c = new Container();
|
|
94
|
+
expect(c.register(A).register(B)).toBe(c);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { Container, token } from "../container";
|
|
5
|
+
import {
|
|
6
|
+
Injectable,
|
|
7
|
+
Inject,
|
|
8
|
+
InjectContainer,
|
|
9
|
+
useService,
|
|
10
|
+
createScope,
|
|
11
|
+
} from "../decorators";
|
|
12
|
+
|
|
13
|
+
// Each test group uses a fresh container to avoid state pollution
|
|
14
|
+
function makeScope() {
|
|
15
|
+
return new Container();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Injectable ────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe("Injectable", () => {
|
|
21
|
+
it("registers the class in the global container", () => {
|
|
22
|
+
// We test via useService resolving without throwing
|
|
23
|
+
class MyService {
|
|
24
|
+
greet() {
|
|
25
|
+
return "hello";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
Injectable()(MyService, {} as ClassDecoratorContext);
|
|
29
|
+
const instance = useService(MyService);
|
|
30
|
+
expect(instance).toBeInstanceOf(MyService);
|
|
31
|
+
expect(instance.greet()).toBe("hello");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("registers with transient scope", () => {
|
|
35
|
+
class TmpService {}
|
|
36
|
+
Injectable({ scope: "transient" })(TmpService, {} as ClassDecoratorContext);
|
|
37
|
+
const a = useService(TmpService);
|
|
38
|
+
const b = useService(TmpService);
|
|
39
|
+
expect(a).not.toBe(b);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── Inject ────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("Inject", () => {
|
|
46
|
+
it("injects a registered service via property", () => {
|
|
47
|
+
class Logger {
|
|
48
|
+
log(msg: string) {
|
|
49
|
+
return msg;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
Injectable()(Logger, {} as ClassDecoratorContext);
|
|
53
|
+
|
|
54
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
55
|
+
const ctx = {
|
|
56
|
+
name: "logger",
|
|
57
|
+
kind: "field" as const,
|
|
58
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
59
|
+
initializers.push(fn);
|
|
60
|
+
},
|
|
61
|
+
} as ClassFieldDecoratorContext;
|
|
62
|
+
|
|
63
|
+
Inject(Logger)(undefined, ctx);
|
|
64
|
+
|
|
65
|
+
const instance: Record<string, unknown> = {};
|
|
66
|
+
initializers.forEach((fn) => {
|
|
67
|
+
fn.call(instance);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(instance.logger).toBeInstanceOf(Logger);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("throws a descriptive error when dep is not registered", () => {
|
|
74
|
+
class UnknownDep {}
|
|
75
|
+
|
|
76
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
77
|
+
const ctx = {
|
|
78
|
+
name: "dep",
|
|
79
|
+
kind: "field" as const,
|
|
80
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
81
|
+
initializers.push(fn);
|
|
82
|
+
},
|
|
83
|
+
} as ClassFieldDecoratorContext;
|
|
84
|
+
|
|
85
|
+
Inject(UnknownDep)(undefined, ctx);
|
|
86
|
+
|
|
87
|
+
class MyClass {
|
|
88
|
+
constructor() {}
|
|
89
|
+
}
|
|
90
|
+
const instance = new MyClass() as Record<string, unknown>;
|
|
91
|
+
initializers.forEach((fn) => {
|
|
92
|
+
fn.call(instance);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(() => instance.dep).toThrow("[Inject]");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("warns on direct assignment in non-production", () => {
|
|
99
|
+
class Dep2 {}
|
|
100
|
+
Injectable()(Dep2, {} as ClassDecoratorContext);
|
|
101
|
+
|
|
102
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
103
|
+
const ctx = {
|
|
104
|
+
name: "dep2",
|
|
105
|
+
kind: "field" as const,
|
|
106
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
107
|
+
initializers.push(fn);
|
|
108
|
+
},
|
|
109
|
+
} as ClassFieldDecoratorContext;
|
|
110
|
+
|
|
111
|
+
Inject(Dep2)(undefined, ctx);
|
|
112
|
+
const instance: Record<string, unknown> = {};
|
|
113
|
+
initializers.forEach((fn) => {
|
|
114
|
+
fn.call(instance);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
118
|
+
instance.dep2 = "override";
|
|
119
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining("[Inject]"));
|
|
120
|
+
spy.mockRestore();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── InjectContainer ───────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
describe("InjectContainer", () => {
|
|
127
|
+
it("injects the global container instance", () => {
|
|
128
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
129
|
+
const ctx = {
|
|
130
|
+
name: "container",
|
|
131
|
+
kind: "field" as const,
|
|
132
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
133
|
+
initializers.push(fn);
|
|
134
|
+
},
|
|
135
|
+
} as ClassFieldDecoratorContext;
|
|
136
|
+
|
|
137
|
+
InjectContainer()(undefined, ctx);
|
|
138
|
+
const instance: Record<string, unknown> = {};
|
|
139
|
+
initializers.forEach((fn) => {
|
|
140
|
+
fn.call(instance);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(instance.container).toBeInstanceOf(Container);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── useService ────────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
describe("useService", () => {
|
|
150
|
+
it("resolves by Token", () => {
|
|
151
|
+
const VERSION = token<string>("VERSION");
|
|
152
|
+
const c = createScope((sc) => sc.registerValue(VERSION, "1.0.0"));
|
|
153
|
+
// useService uses global container — test via token on a fresh child
|
|
154
|
+
expect(c.resolve(VERSION)).toBe("1.0.0");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── createScope ───────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe("createScope", () => {
|
|
161
|
+
it("creates a child container without configure fn", () => {
|
|
162
|
+
const child = createScope();
|
|
163
|
+
expect(child).toBeInstanceOf(Container);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("applies the configure callback", () => {
|
|
167
|
+
const DB = token<string>("DB");
|
|
168
|
+
const child = createScope((c) =>
|
|
169
|
+
c.registerValue(DB, "postgres://localhost"),
|
|
170
|
+
);
|
|
171
|
+
expect(child.resolve(DB)).toBe("postgres://localhost");
|
|
172
|
+
});
|
|
173
|
+
});
|