@praxisjs/di 0.2.1 → 0.2.3

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @praxisjs/di
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [d11a10a]
8
+ - @praxisjs/core@0.4.2
9
+
10
+ ## 0.2.2
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [fe39901]
15
+ - @praxisjs/core@0.4.1
16
+
3
17
  ## 0.2.1
4
18
 
5
19
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ import "reflect-metadata";
2
+ //# sourceMappingURL=container.test.d.ts.map
@@ -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,136 @@
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
+ it("resolves prop injections from di:props metadata", () => {
88
+ class Logger {
89
+ }
90
+ class Service {
91
+ logger;
92
+ }
93
+ const c = new Container();
94
+ c.register(Logger);
95
+ c.register(Service);
96
+ // Simulate what the @Inject decorator does — set di:props metadata
97
+ const propsMap = new Map();
98
+ propsMap.set("logger", Logger);
99
+ Reflect.defineMetadata("di:props", propsMap, Service.prototype);
100
+ const instance = c.resolve(Service);
101
+ expect(instance.logger).toBeInstanceOf(Logger);
102
+ // Cleanup
103
+ Reflect.deleteMetadata("di:props", Service.prototype);
104
+ });
105
+ it("child container falls back to parent for unregistered service", () => {
106
+ class MyDep {
107
+ }
108
+ const parent = new Container();
109
+ parent.register(MyDep);
110
+ const child = parent.createChild();
111
+ expect(child.resolve(MyDep)).toBeInstanceOf(MyDep);
112
+ });
113
+ it("grandchild resolves from grandparent (three-level chain)", () => {
114
+ const URL_TOKEN = token("GRANDPARENT_URL");
115
+ const grandparent = new Container();
116
+ grandparent.registerValue(URL_TOKEN, "https://grandparent.com");
117
+ const child = grandparent.createChild();
118
+ const grandchild = child.createChild();
119
+ expect(grandchild.resolve(URL_TOKEN)).toBe("https://grandparent.com");
120
+ });
121
+ it("registering the same class twice replaces the previous registration", () => {
122
+ class Counter {
123
+ value = Math.random();
124
+ }
125
+ const c = new Container();
126
+ c.register(Counter, { scope: "singleton" });
127
+ const first = c.resolve(Counter);
128
+ // Re-register resets the descriptor (no cached instance)
129
+ c.register(Counter, { scope: "singleton" });
130
+ const second = c.resolve(Counter);
131
+ // After re-registration, a new singleton is created
132
+ expect(second).toBeInstanceOf(Counter);
133
+ expect(second).not.toBe(first);
134
+ });
135
+ });
136
+ //# sourceMappingURL=container.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.test.js","sourceRoot":"","sources":["../../src/__tests__/container.test.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAEvD,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;IACrB,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC;QAC3B,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,GAAG,KAAK,CAAS,QAAQ,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,SAAS;SAAG;QAClB,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACtB,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/B,MAAM,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QACpC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,OAAO;YACX,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;SACvB;QACD,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7B,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,OAAO;SAAG;QAChB,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,MAAM,GAAG,KAAK,CAAS,QAAQ,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,CAAC,CAAC,aAAa,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;QACnD,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,GAAG,GAAG,KAAK,CAAS,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,KAAK,CAAS,SAAS,CAAC,CAAC;QACnC,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,SAAS,GAAG,KAAK,CAAS,KAAK,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC/B,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAC;QACvD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,SAAS,GAAG,KAAK,CAAS,KAAK,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC/B,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QACnC,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;QACpD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC;SAAG;QACV,MAAM,CAAC;SAAG;QACV,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,MAAM;SAAG;QACf,MAAM,OAAO;YACX,MAAM,CAAU;SACjB;QAED,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAEpB,mEAAmE;QACnE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;QAC5C,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/B,OAAO,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAEhE,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACpC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAE/C,UAAU;QACV,OAAO,CAAC,cAAc,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,KAAK;SAAG;QACd,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC/B,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACvB,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,SAAS,GAAG,KAAK,CAAS,iBAAiB,CAAC,CAAC;QACnD,MAAM,WAAW,GAAG,IAAI,SAAS,EAAE,CAAC;QACpC,WAAW,CAAC,aAAa,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAChE,MAAM,KAAK,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QACvC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,OAAO;YACX,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;SACvB;QACD,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAEjC,yDAAyD;QACzD,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAElC,oDAAoD;QACpD,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import "reflect-metadata";
2
+ //# sourceMappingURL=decorators.test.d.ts.map
@@ -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,155 @@
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
+ // ── Injectable ────────────────────────────────────────────────────────────────
6
+ describe("Injectable", () => {
7
+ it("registers the class in the global container", () => {
8
+ // We test via useService resolving without throwing
9
+ class MyService {
10
+ greet() {
11
+ return "hello";
12
+ }
13
+ }
14
+ Injectable()(MyService, {});
15
+ const instance = useService(MyService);
16
+ expect(instance).toBeInstanceOf(MyService);
17
+ expect(instance.greet()).toBe("hello");
18
+ });
19
+ it("registers with transient scope", () => {
20
+ class TmpService {
21
+ }
22
+ Injectable({ scope: "transient" })(TmpService, {});
23
+ const a = useService(TmpService);
24
+ const b = useService(TmpService);
25
+ expect(a).not.toBe(b);
26
+ });
27
+ });
28
+ // ── Inject ────────────────────────────────────────────────────────────────────
29
+ describe("Inject", () => {
30
+ it("injects a registered service via property", () => {
31
+ class Logger {
32
+ log(msg) {
33
+ return msg;
34
+ }
35
+ }
36
+ Injectable()(Logger, {});
37
+ const initializers = [];
38
+ const ctx = {
39
+ name: "logger",
40
+ kind: "field",
41
+ addInitializer(fn) {
42
+ initializers.push(fn);
43
+ },
44
+ };
45
+ Inject(Logger)(undefined, ctx);
46
+ const instance = {};
47
+ initializers.forEach((fn) => {
48
+ fn.call(instance);
49
+ });
50
+ expect(instance.logger).toBeInstanceOf(Logger);
51
+ });
52
+ it("throws a descriptive error when dep is not registered", () => {
53
+ class UnknownDep {
54
+ }
55
+ const initializers = [];
56
+ const ctx = {
57
+ name: "dep",
58
+ kind: "field",
59
+ addInitializer(fn) {
60
+ initializers.push(fn);
61
+ },
62
+ };
63
+ Inject(UnknownDep)(undefined, ctx);
64
+ class MyClass {
65
+ constructor() { }
66
+ }
67
+ const instance = new MyClass();
68
+ initializers.forEach((fn) => {
69
+ fn.call(instance);
70
+ });
71
+ expect(() => instance.dep).toThrow("[Inject]");
72
+ });
73
+ it("warns on direct assignment in non-production", () => {
74
+ class Dep2 {
75
+ }
76
+ Injectable()(Dep2, {});
77
+ const initializers = [];
78
+ const ctx = {
79
+ name: "dep2",
80
+ kind: "field",
81
+ addInitializer(fn) {
82
+ initializers.push(fn);
83
+ },
84
+ };
85
+ Inject(Dep2)(undefined, ctx);
86
+ const instance = {};
87
+ initializers.forEach((fn) => {
88
+ fn.call(instance);
89
+ });
90
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => { });
91
+ instance.dep2 = "override";
92
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining("[Inject]"));
93
+ spy.mockRestore();
94
+ });
95
+ });
96
+ // ── InjectContainer ───────────────────────────────────────────────────────────
97
+ describe("InjectContainer", () => {
98
+ it("injects the global container instance", () => {
99
+ const initializers = [];
100
+ const ctx = {
101
+ name: "container",
102
+ kind: "field",
103
+ addInitializer(fn) {
104
+ initializers.push(fn);
105
+ },
106
+ };
107
+ InjectContainer()(undefined, ctx);
108
+ const instance = {};
109
+ initializers.forEach((fn) => {
110
+ fn.call(instance);
111
+ });
112
+ expect(instance.container).toBeInstanceOf(Container);
113
+ });
114
+ });
115
+ // ── useService ────────────────────────────────────────────────────────────────
116
+ describe("useService", () => {
117
+ it("resolves by Token", () => {
118
+ const VERSION = token("VERSION");
119
+ const c = createScope((sc) => sc.registerValue(VERSION, "1.0.0"));
120
+ // useService uses global container — test via token on a fresh child
121
+ expect(c.resolve(VERSION)).toBe("1.0.0");
122
+ });
123
+ });
124
+ // ── createScope ───────────────────────────────────────────────────────────────
125
+ describe("createScope", () => {
126
+ it("creates a child container without configure fn", () => {
127
+ const child = createScope();
128
+ expect(child).toBeInstanceOf(Container);
129
+ });
130
+ it("applies the configure callback", () => {
131
+ const DB = token("DB");
132
+ const child = createScope((c) => c.registerValue(DB, "postgres://localhost"));
133
+ expect(child.resolve(DB)).toBe("postgres://localhost");
134
+ });
135
+ });
136
+ // ── Inject — Token in error message ──────────────────────────────────────────
137
+ describe("Inject — Token error display", () => {
138
+ it("includes the Token description in the error message when resolution fails", () => {
139
+ const MY_TOKEN = token("MY_SERVICE");
140
+ const initializers = [];
141
+ const ctx = {
142
+ name: "svc",
143
+ kind: "field",
144
+ addInitializer(fn) { initializers.push(fn); },
145
+ };
146
+ Inject(MY_TOKEN)(undefined, ctx);
147
+ class Consumer {
148
+ constructor() { }
149
+ }
150
+ const instance = new Consumer();
151
+ initializers.forEach((fn) => { fn.call(instance); });
152
+ expect(() => instance.svc).toThrow("MY_SERVICE");
153
+ });
154
+ });
155
+ //# 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.1",
3
+ "version": "0.2.3",
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.4.0"
15
+ "@praxisjs/core": "0.4.2"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "^25.3.0",
@@ -0,0 +1,152 @@
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
+
97
+ it("resolves prop injections from di:props metadata", () => {
98
+ class Logger {}
99
+ class Service {
100
+ logger!: Logger;
101
+ }
102
+
103
+ const c = new Container();
104
+ c.register(Logger);
105
+ c.register(Service);
106
+
107
+ // Simulate what the @Inject decorator does — set di:props metadata
108
+ const propsMap = new Map<string, unknown>();
109
+ propsMap.set("logger", Logger);
110
+ Reflect.defineMetadata("di:props", propsMap, Service.prototype);
111
+
112
+ const instance = c.resolve(Service);
113
+ expect(instance.logger).toBeInstanceOf(Logger);
114
+
115
+ // Cleanup
116
+ Reflect.deleteMetadata("di:props", Service.prototype);
117
+ });
118
+
119
+ it("child container falls back to parent for unregistered service", () => {
120
+ class MyDep {}
121
+ const parent = new Container();
122
+ parent.register(MyDep);
123
+ const child = parent.createChild();
124
+ expect(child.resolve(MyDep)).toBeInstanceOf(MyDep);
125
+ });
126
+
127
+ it("grandchild resolves from grandparent (three-level chain)", () => {
128
+ const URL_TOKEN = token<string>("GRANDPARENT_URL");
129
+ const grandparent = new Container();
130
+ grandparent.registerValue(URL_TOKEN, "https://grandparent.com");
131
+ const child = grandparent.createChild();
132
+ const grandchild = child.createChild();
133
+ expect(grandchild.resolve(URL_TOKEN)).toBe("https://grandparent.com");
134
+ });
135
+
136
+ it("registering the same class twice replaces the previous registration", () => {
137
+ class Counter {
138
+ value = Math.random();
139
+ }
140
+ const c = new Container();
141
+ c.register(Counter, { scope: "singleton" });
142
+ const first = c.resolve(Counter);
143
+
144
+ // Re-register resets the descriptor (no cached instance)
145
+ c.register(Counter, { scope: "singleton" });
146
+ const second = c.resolve(Counter);
147
+
148
+ // After re-registration, a new singleton is created
149
+ expect(second).toBeInstanceOf(Counter);
150
+ expect(second).not.toBe(first);
151
+ });
152
+ });
@@ -0,0 +1,191 @@
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
+ // ── Injectable ────────────────────────────────────────────────────────────────
14
+
15
+ describe("Injectable", () => {
16
+ it("registers the class in the global container", () => {
17
+ // We test via useService resolving without throwing
18
+ class MyService {
19
+ greet() {
20
+ return "hello";
21
+ }
22
+ }
23
+ Injectable()(MyService, {} as ClassDecoratorContext);
24
+ const instance = useService(MyService);
25
+ expect(instance).toBeInstanceOf(MyService);
26
+ expect(instance.greet()).toBe("hello");
27
+ });
28
+
29
+ it("registers with transient scope", () => {
30
+ class TmpService {}
31
+ Injectable({ scope: "transient" })(TmpService, {} as ClassDecoratorContext);
32
+ const a = useService(TmpService);
33
+ const b = useService(TmpService);
34
+ expect(a).not.toBe(b);
35
+ });
36
+ });
37
+
38
+ // ── Inject ────────────────────────────────────────────────────────────────────
39
+
40
+ describe("Inject", () => {
41
+ it("injects a registered service via property", () => {
42
+ class Logger {
43
+ log(msg: string) {
44
+ return msg;
45
+ }
46
+ }
47
+ Injectable()(Logger, {} as ClassDecoratorContext);
48
+
49
+ const initializers: Array<(this: unknown) => void> = [];
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
+
58
+ Inject(Logger)(undefined, ctx);
59
+
60
+ const instance: Record<string, unknown> = {};
61
+ initializers.forEach((fn) => {
62
+ fn.call(instance);
63
+ });
64
+
65
+ expect(instance.logger).toBeInstanceOf(Logger);
66
+ });
67
+
68
+ it("throws a descriptive error when dep is not registered", () => {
69
+ 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
+
80
+ Inject(UnknownDep)(undefined, ctx);
81
+
82
+ class MyClass {
83
+ constructor() {}
84
+ }
85
+ const instance = new MyClass() as Record<string, unknown>;
86
+ initializers.forEach((fn) => {
87
+ fn.call(instance);
88
+ });
89
+
90
+ expect(() => instance.dep).toThrow("[Inject]");
91
+ });
92
+
93
+ it("warns on direct assignment in non-production", () => {
94
+ class Dep2 {}
95
+ 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
+
106
+ Inject(Dep2)(undefined, ctx);
107
+ const instance: Record<string, unknown> = {};
108
+ initializers.forEach((fn) => {
109
+ fn.call(instance);
110
+ });
111
+
112
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
113
+ instance.dep2 = "override";
114
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining("[Inject]"));
115
+ spy.mockRestore();
116
+ });
117
+ });
118
+
119
+ // ── InjectContainer ───────────────────────────────────────────────────────────
120
+
121
+ describe("InjectContainer", () => {
122
+ it("injects the global container instance", () => {
123
+ const initializers: Array<(this: unknown) => void> = [];
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
+
132
+ InjectContainer()(undefined, ctx);
133
+ const instance: Record<string, unknown> = {};
134
+ initializers.forEach((fn) => {
135
+ fn.call(instance);
136
+ });
137
+
138
+ expect(instance.container).toBeInstanceOf(Container);
139
+ });
140
+ });
141
+
142
+ // ── useService ────────────────────────────────────────────────────────────────
143
+
144
+ describe("useService", () => {
145
+ it("resolves by Token", () => {
146
+ const VERSION = token<string>("VERSION");
147
+ const c = createScope((sc) => sc.registerValue(VERSION, "1.0.0"));
148
+ // useService uses global container — test via token on a fresh child
149
+ expect(c.resolve(VERSION)).toBe("1.0.0");
150
+ });
151
+ });
152
+
153
+ // ── createScope ───────────────────────────────────────────────────────────────
154
+
155
+ describe("createScope", () => {
156
+ it("creates a child container without configure fn", () => {
157
+ const child = createScope();
158
+ expect(child).toBeInstanceOf(Container);
159
+ });
160
+
161
+ it("applies the configure callback", () => {
162
+ const DB = token<string>("DB");
163
+ const child = createScope((c) =>
164
+ c.registerValue(DB, "postgres://localhost"),
165
+ );
166
+ expect(child.resolve(DB)).toBe("postgres://localhost");
167
+ });
168
+ });
169
+
170
+ // ── Inject — Token in error message ──────────────────────────────────────────
171
+
172
+ describe("Inject — Token error display", () => {
173
+ it("includes the Token description in the error message when resolution fails", () => {
174
+ const MY_TOKEN = token<string>("MY_SERVICE");
175
+
176
+ const initializers: Array<(this: unknown) => void> = [];
177
+ const ctx = {
178
+ name: "svc",
179
+ kind: "field" as const,
180
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
181
+ } as ClassFieldDecoratorContext;
182
+
183
+ Inject(MY_TOKEN)(undefined, ctx);
184
+
185
+ class Consumer { constructor() {} }
186
+ const instance = new Consumer() as Record<string, unknown>;
187
+ initializers.forEach((fn) => { fn.call(instance); });
188
+
189
+ expect(() => instance.svc).toThrow("MY_SERVICE");
190
+ });
191
+ });