@praxisjs/di 0.2.3 → 1.0.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.
@@ -1,7 +1,19 @@
1
1
  import { type Container, Token, type Constructor, type InjectableOptions } from "./container";
2
2
  export declare function Injectable(options?: InjectableOptions): (value: Constructor, _context: ClassDecoratorContext) => void;
3
- export declare function Inject<T>(dep: Constructor<T> | Token<T>): (_value: undefined, context: ClassFieldDecoratorContext) => void;
4
- export declare function InjectContainer(): (_value: undefined, context: ClassFieldDecoratorContext) => void;
5
- export declare function useService<T>(dep: Constructor<T> | Token<T>): T;
6
- export declare function createScope(configure?: (c: Container) => void): Container;
3
+ /**
4
+ * Creates a child DI container scoped to each instance of the decorated class.
5
+ * Services registered in the configure callback are available within the scope,
6
+ * and @Inject / @InjectContainer will resolve from it automatically.
7
+ *
8
+ * @Scope((c) => {
9
+ * c.register(UserRepository);
10
+ * c.registerValue(AuthToken, 'abc');
11
+ * })
12
+ * class UserModule extends StatefulComponent {
13
+ * @Inject(UserRepository) repo!: UserRepository;
14
+ * }
15
+ */
16
+ export declare function Scope(configure?: (c: Container) => void): (cls: new (...args: any[]) => any, _ctx: ClassDecoratorContext) => any;
17
+ export declare function Inject<T>(dep: Constructor<T> | Token<T>): (_value: undefined, context: ClassFieldDecoratorContext<any>) => void;
18
+ export declare function InjectContainer(): (_value: undefined, context: ClassFieldDecoratorContext<any>) => void;
7
19
  //# sourceMappingURL=decorators.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,EAAE,KAAK,WAAW,EAAE,KAAK,iBAAiB,EAAG,MAAM,aAAa,CAAC;AAE1G,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,IACvC,OAAO,WAAW,EAAE,UAAU,qBAAqB,KAAG,IAAI,CAG5E;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IACrC,QAAQ,SAAS,EAAE,SAAS,0BAA0B,KAAG,IAAI,CAsC/E;AAED,wBAAgB,eAAe,KACZ,QAAQ,SAAS,EAAE,SAAS,0BAA0B,KAAG,IAAI,CAW/E;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAE/D;AAED,wBAAgB,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,KAAK,IAAI,GAAG,SAAS,CAIzE"}
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,EAAE,KAAK,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAWzG,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,IACvC,OAAO,WAAW,EAAE,UAAU,qBAAqB,KAAG,IAAI,CAG5E;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,KAAK,IAAI,IAEtC,KAAK,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,qBAAqB,KAAG,GAAG,CAapF;AAID,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAsCrC,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,CAAC,GAAG,CAAC,KAAK,IAAI,CACvF;AAID,wBAAgB,eAAe,IAaZ,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,CAAC,GAAG,CAAC,KAAK,IAAI,CACvF"}
@@ -1,60 +1,94 @@
1
+ import { createFieldDecorator } from "@praxisjs/decorators";
1
2
  import { container, Token } from "./container";
3
+ // Maps instances to their scoped child containers (set by @Scope).
4
+ const scopedContainers = new WeakMap();
5
+ function resolveFrom(instance) {
6
+ return scopedContainers.get(instance) ?? container;
7
+ }
8
+ // ── @Injectable ───────────────────────────────────────────────────────────────
2
9
  export function Injectable(options = {}) {
3
10
  return function (value, _context) {
4
11
  container.register(value, options);
5
12
  };
6
13
  }
14
+ // ── @Scope ────────────────────────────────────────────────────────────────────
15
+ /**
16
+ * Creates a child DI container scoped to each instance of the decorated class.
17
+ * Services registered in the configure callback are available within the scope,
18
+ * and @Inject / @InjectContainer will resolve from it automatically.
19
+ *
20
+ * @Scope((c) => {
21
+ * c.register(UserRepository);
22
+ * c.registerValue(AuthToken, 'abc');
23
+ * })
24
+ * class UserModule extends StatefulComponent {
25
+ * @Inject(UserRepository) repo!: UserRepository;
26
+ * }
27
+ */
28
+ export function Scope(configure) {
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ return function (cls, _ctx) {
31
+ return class extends cls {
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ constructor(...args) {
34
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
35
+ super(...args);
36
+ const child = container.createChild();
37
+ configure?.(child);
38
+ scopedContainers.set(this, child);
39
+ }
40
+ };
41
+ };
42
+ }
43
+ // ── @Inject ───────────────────────────────────────────────────────────────────
7
44
  export function Inject(dep) {
8
- return function (_value, context) {
9
- const cache = new WeakMap();
10
- const propertyKey = String(context.name);
11
- context.addInitializer(function () {
12
- Object.defineProperty(this, context.name, {
13
- get() {
14
- if (!cache.has(this)) {
15
- let resolved;
16
- try {
17
- resolved = container.resolve(dep);
45
+ const cache = new WeakMap();
46
+ return createFieldDecorator({
47
+ bind(_instance, name, _initialValue) {
48
+ return {
49
+ descriptor: {
50
+ get() {
51
+ if (!cache.has(this)) {
52
+ const c = resolveFrom(this);
53
+ let resolved;
54
+ try {
55
+ resolved = c.resolve(dep);
56
+ }
57
+ catch (err) {
58
+ throw new Error(`[Inject] Failed to resolve "${dep instanceof Token
59
+ ? dep.toString()
60
+ : dep.name}" in "${this.constructor.name}.${name}": ${err.message}`);
61
+ }
62
+ cache.set(this, resolved);
18
63
  }
19
- catch (err) {
20
- throw new Error(`[Inject] Failed to resolve "${dep instanceof Token
21
- ? dep.toString()
22
- : dep.name}" in "${this.constructor.name}.${propertyKey}": ${err.message}`);
64
+ return cache.get(this);
65
+ },
66
+ set(_value) {
67
+ if (process.env.NODE_ENV !== "production") {
68
+ console.warn(`[Inject] "${name}" is managed by the DI container and cannot be assigned directly.`);
23
69
  }
24
- cache.set(this, resolved);
25
- }
26
- return cache.get(this);
27
- },
28
- set(_value) {
29
- if (process.env.NODE_ENV !== "production") {
30
- console.warn(`[Inject] "${propertyKey}" is managed by the DI container and cannot be assigned directly.`);
31
- }
70
+ },
32
71
  },
33
- enumerable: true,
34
- configurable: true,
35
- });
36
- });
37
- };
72
+ };
73
+ },
74
+ // DI decorators work on any class, not just StatefulComponent
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ });
38
77
  }
78
+ // ── @InjectContainer ──────────────────────────────────────────────────────────
39
79
  export function InjectContainer() {
40
- return function (_value, context) {
41
- context.addInitializer(function () {
42
- Object.defineProperty(this, context.name, {
43
- get() {
44
- return container;
80
+ return createFieldDecorator({
81
+ bind(_instance, _name, _initialValue) {
82
+ return {
83
+ descriptor: {
84
+ get() {
85
+ return resolveFrom(this);
86
+ },
45
87
  },
46
- enumerable: true,
47
- configurable: true,
48
- });
49
- });
50
- };
51
- }
52
- export function useService(dep) {
53
- return container.resolve(dep);
54
- }
55
- export function createScope(configure) {
56
- const child = container.createChild();
57
- configure?.(child);
58
- return child;
88
+ };
89
+ },
90
+ // DI decorators work on any class, not just StatefulComponent
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ });
59
93
  }
60
94
  //# sourceMappingURL=decorators.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,KAAK,EAA6C,MAAM,aAAa,CAAC;AAE1G,MAAM,UAAU,UAAU,CAAC,UAA6B,EAAE;IACxD,OAAO,UAAU,KAAkB,EAAE,QAA+B;QAClE,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,MAAM,CAAI,GAA8B;IACtD,OAAO,UAAU,MAAiB,EAAE,OAAmC;QACrE,MAAM,KAAK,GAAG,IAAI,OAAO,EAAa,CAAC;QACvC,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAEzC,OAAO,CAAC,cAAc,CAAC;YACrB,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;gBACxC,GAAG;oBACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;wBACrB,IAAI,QAAW,CAAC;wBAChB,IAAI,CAAC;4BACH,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,GAAqB,CAAC,CAAC;wBACtD,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,MAAM,IAAI,KAAK,CACb,+BACE,GAAG,YAAY,KAAK;gCAClB,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;gCAChB,CAAC,CAAE,GAAmB,CAAC,IAC3B,SAAU,IAA0C,CAAC,WAAW,CAAC,IAAI,IAAI,WAAW,MAAO,GAAa,CAAC,OAAO,EAAE,CACnH,CAAC;wBACJ,CAAC;wBACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;oBAC5B,CAAC;oBACD,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAM,CAAC;gBAC9B,CAAC;gBAED,GAAG,CAAC,MAAe;oBACjB,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;wBAC1C,OAAO,CAAC,IAAI,CACV,aAAa,WAAW,mEAAmE,CAC5F,CAAC;oBACJ,CAAC;gBACH,CAAC;gBAED,UAAU,EAAE,IAAI;gBAChB,YAAY,EAAE,IAAI;aACnB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,MAAiB,EAAE,OAAmC;QACrE,OAAO,CAAC,cAAc,CAAC;YACrB,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;gBACxC,GAAG;oBACD,OAAO,SAAS,CAAC;gBACnB,CAAC;gBACD,UAAU,EAAE,IAAI;gBAChB,YAAY,EAAE,IAAI;aACnB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAI,GAA8B;IAC1D,OAAO,SAAS,CAAC,OAAO,CAAC,GAAqB,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,SAAkC;IAC5D,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;IACtC,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC;IACnB,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import "reflect-metadata";
2
- export { Injectable, Inject, InjectContainer, useService, createScope, } from "./decorators";
2
+ export { Injectable, Inject, InjectContainer, Scope, } from "./decorators";
3
3
  export { Container, container, Token, token } from "./container";
4
- export type { Scope, InjectableOptions, Constructor, ServiceDescriptor, } from "./container";
4
+ export type { Scope as ScopeType, InjectableOptions, Constructor, ServiceDescriptor, } from "./container";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,UAAU,EACV,MAAM,EACN,eAAe,EACf,UAAU,EACV,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EACV,KAAK,EACL,iBAAiB,EACjB,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,UAAU,EACV,MAAM,EACN,eAAe,EACf,KAAK,GACN,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EACV,KAAK,IAAI,SAAS,EAClB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import "reflect-metadata";
2
- export { Injectable, Inject, InjectContainer, useService, createScope, } from "./decorators";
2
+ export { Injectable, Inject, InjectContainer, Scope, } from "./decorators";
3
3
  export { Container, container, Token, token } from "./container";
4
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,UAAU,EACV,MAAM,EACN,eAAe,EACf,UAAU,EACV,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,UAAU,EACV,MAAM,EACN,eAAe,EACf,KAAK,GACN,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/di",
3
- "version": "0.2.3",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "reflect-metadata": "^0.2.2",
15
- "@praxisjs/core": "0.4.2"
15
+ "@praxisjs/core": "1.0.0",
16
+ "@praxisjs/decorators": "0.5.0"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@types/node": "^25.3.0",
@@ -133,6 +133,26 @@ describe("Container", () => {
133
133
  expect(grandchild.resolve(URL_TOKEN)).toBe("https://grandparent.com");
134
134
  });
135
135
 
136
+ it("resolves constructor injection from di:inject metadata", () => {
137
+ class Engine {}
138
+ class Car {
139
+ constructor(public engine: Engine) {}
140
+ }
141
+
142
+ const c = new Container();
143
+ c.register(Engine);
144
+ c.register(Car);
145
+
146
+ // Simulate what a @Inject constructor param decorator would set
147
+ Reflect.defineMetadata("di:inject", [Engine], Car);
148
+
149
+ const car = c.resolve(Car);
150
+ expect(car).toBeInstanceOf(Car);
151
+ expect(car.engine).toBeInstanceOf(Engine);
152
+
153
+ Reflect.deleteMetadata("di:inject", Car);
154
+ });
155
+
136
156
  it("registering the same class twice replaces the previous registration", () => {
137
157
  class Counter {
138
158
  value = Math.random();
@@ -1,191 +1,228 @@
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
- useService,
10
- createScope,
9
+ Scope,
11
10
  } from "../decorators";
12
11
 
13
- // ── Injectable ────────────────────────────────────────────────────────────────
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", () => {
16
27
  it("registers the class in the global container", () => {
17
- // We test via useService resolving without throwing
18
28
  class MyService {
19
- greet() {
20
- return "hello";
21
- }
29
+ greet() { return "hello"; }
22
30
  }
23
31
  Injectable()(MyService, {} as ClassDecoratorContext);
24
- const instance = useService(MyService);
25
- expect(instance).toBeInstanceOf(MyService);
26
- expect(instance.greet()).toBe("hello");
32
+ expect(container.resolve(MyService)).toBeInstanceOf(MyService);
33
+ expect(container.resolve(MyService).greet()).toBe("hello");
27
34
  });
28
35
 
29
- it("registers with transient scope", () => {
36
+ it("registers with transient scope — each resolve returns a new instance", () => {
30
37
  class TmpService {}
31
38
  Injectable({ scope: "transient" })(TmpService, {} as ClassDecoratorContext);
32
- const a = useService(TmpService);
33
- const b = useService(TmpService);
34
- expect(a).not.toBe(b);
39
+ expect(container.resolve(TmpService)).not.toBe(container.resolve(TmpService));
35
40
  });
36
41
  });
37
42
 
38
- // ── Inject ────────────────────────────────────────────────────────────────────
43
+ // ── @Inject ───────────────────────────────────────────────────────────────────
39
44
 
40
45
  describe("Inject", () => {
41
46
  it("injects a registered service via property", () => {
42
47
  class Logger {
43
- log(msg: string) {
44
- return msg;
45
- }
48
+ log(msg: string) { return msg; }
46
49
  }
47
50
  Injectable()(Logger, {} as ClassDecoratorContext);
48
51
 
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
-
52
+ const { ctx, run } = makeFieldCtx("logger");
58
53
  Inject(Logger)(undefined, ctx);
59
-
60
54
  const instance: Record<string, unknown> = {};
61
- initializers.forEach((fn) => {
62
- fn.call(instance);
63
- });
64
-
55
+ run(instance);
65
56
  expect(instance.logger).toBeInstanceOf(Logger);
57
+ // second access covers the cache-hit branch (line 62: !cache.has(this) === false)
58
+ expect(instance.logger).toBe(instance.logger);
66
59
  });
67
60
 
68
61
  it("throws a descriptive error when dep is not registered", () => {
69
62
  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
-
63
+ const { ctx, run } = makeFieldCtx("dep");
80
64
  Inject(UnknownDep)(undefined, ctx);
81
-
82
- class MyClass {
83
- constructor() {}
84
- }
65
+ class MyClass {}
85
66
  const instance = new MyClass() as Record<string, unknown>;
86
- initializers.forEach((fn) => {
87
- fn.call(instance);
88
- });
89
-
67
+ run(instance);
90
68
  expect(() => instance.dep).toThrow("[Inject]");
91
69
  });
92
70
 
93
71
  it("warns on direct assignment in non-production", () => {
94
72
  class Dep2 {}
95
73
  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
-
74
+ const { ctx, run } = makeFieldCtx("dep2");
106
75
  Inject(Dep2)(undefined, ctx);
107
76
  const instance: Record<string, unknown> = {};
108
- initializers.forEach((fn) => {
109
- fn.call(instance);
110
- });
111
-
77
+ run(instance);
112
78
  const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
113
79
  instance.dep2 = "override";
114
80
  expect(spy).toHaveBeenCalledWith(expect.stringContaining("[Inject]"));
115
81
  spy.mockRestore();
116
82
  });
117
- });
118
83
 
119
- // ── InjectContainer ───────────────────────────────────────────────────────────
84
+ it("returns cached value on second access (cache hit)", () => {
85
+ class CachedService {
86
+ id = Math.random();
87
+ }
88
+ Injectable()(CachedService, {} as ClassDecoratorContext);
120
89
 
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;
90
+ const { ctx, run } = makeFieldCtx("svc");
91
+ Inject(CachedService)(undefined, ctx);
92
+ const instance: Record<string, unknown> = {};
93
+ run(instance);
131
94
 
132
- InjectContainer()(undefined, ctx);
95
+ const first = instance.svc;
96
+ const second = instance.svc;
97
+ expect(first).toBe(second); // same cached instance
98
+ });
99
+
100
+ it("does not warn on direct assignment in production mode", () => {
101
+ class ProdDep {}
102
+ Injectable()(ProdDep, {} as ClassDecoratorContext);
103
+ const { ctx, run } = makeFieldCtx("dep");
104
+ Inject(ProdDep)(undefined, ctx);
133
105
  const instance: Record<string, unknown> = {};
134
- initializers.forEach((fn) => {
135
- fn.call(instance);
136
- });
106
+ run(instance);
137
107
 
138
- expect(instance.container).toBeInstanceOf(Container);
108
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
109
+ vi.stubEnv("NODE_ENV", "production");
110
+ instance.dep = "override";
111
+ expect(spy).not.toHaveBeenCalled();
112
+
113
+ vi.unstubAllEnvs();
114
+ spy.mockRestore();
115
+ });
116
+
117
+ it("includes the Token description in the error when resolution fails", () => {
118
+ const MY_TOKEN = token<string>("MY_SERVICE");
119
+ const { ctx, run } = makeFieldCtx("svc");
120
+ Inject(MY_TOKEN)(undefined, ctx);
121
+ class Consumer {}
122
+ const instance = new Consumer() as Record<string, unknown>;
123
+ run(instance);
124
+ expect(() => instance.svc).toThrow("MY_SERVICE");
139
125
  });
140
126
  });
141
127
 
142
- // ── useService ────────────────────────────────────────────────────────────────
128
+ // ── @InjectContainer ──────────────────────────────────────────────────────────
143
129
 
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");
130
+ describe("InjectContainer", () => {
131
+ it("injects the global container on an unscoped class", () => {
132
+ const { ctx, run } = makeFieldCtx("c");
133
+ InjectContainer()(undefined, ctx);
134
+ const instance: Record<string, unknown> = {};
135
+ run(instance);
136
+ expect(instance.c).toBeInstanceOf(Container);
137
+ expect(instance.c).toBe(container);
150
138
  });
151
139
  });
152
140
 
153
- // ── createScope ───────────────────────────────────────────────────────────────
141
+ // ── @Scope ────────────────────────────────────────────────────────────────────
142
+
143
+ describe("Scope", () => {
144
+ it("creates a child container per instance", () => {
145
+ @Scope()
146
+ class Widget {}
147
+ const { ctx, run } = makeFieldCtx("c");
148
+ InjectContainer()(undefined, ctx);
149
+
150
+ const a = new Widget() as Record<string, unknown>;
151
+ run(a);
152
+ const b = new Widget() as Record<string, unknown>;
153
+ run(b);
154
154
 
155
- describe("createScope", () => {
156
- it("creates a child container without configure fn", () => {
157
- const child = createScope();
158
- expect(child).toBeInstanceOf(Container);
155
+ expect(a.c).toBeInstanceOf(Container);
156
+ expect(b.c).toBeInstanceOf(Container);
157
+ expect(a.c).not.toBe(b.c); // each instance gets its own scope
158
+ expect(a.c).not.toBe(container); // it's a child, not the root
159
159
  });
160
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");
161
+ it("services registered in configure are resolvable within the scope", () => {
162
+ const DB_URL = token<string>("DB_URL");
163
+
164
+ @Scope((c) => { c.registerValue(DB_URL, "postgres://localhost"); })
165
+ class DataModule {}
166
+
167
+ const { ctx, run } = makeFieldCtx("db");
168
+ Inject(DB_URL)(undefined, ctx);
169
+
170
+ const instance = new DataModule() as Record<string, unknown>;
171
+ run(instance);
172
+
173
+ expect(instance.db).toBe("postgres://localhost");
167
174
  });
168
- });
169
175
 
170
- // ── Inject — Token in error message ──────────────────────────────────────────
176
+ it("@Inject resolves from the scoped container, not the global one", () => {
177
+ class ScopedRepo {}
178
+ // NOT registered in global container
179
+ const VERSION = token<string>("VERSION");
171
180
 
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");
181
+ @Scope((c) => {
182
+ c.register(ScopedRepo);
183
+ c.registerValue(VERSION, "2.0");
184
+ })
185
+ class App {}
175
186
 
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;
187
+ const { ctx: repoCtx, run: runRepo } = makeFieldCtx("repo");
188
+ Inject(ScopedRepo)(undefined, repoCtx);
189
+ const { ctx: verCtx, run: runVer } = makeFieldCtx("version");
190
+ Inject(VERSION)(undefined, verCtx);
182
191
 
183
- Inject(MY_TOKEN)(undefined, ctx);
192
+ const instance = new App() as Record<string, unknown>;
193
+ runRepo(instance);
194
+ runVer(instance);
184
195
 
185
- class Consumer { constructor() {} }
186
- const instance = new Consumer() as Record<string, unknown>;
187
- initializers.forEach((fn) => { fn.call(instance); });
196
+ expect(instance.repo).toBeInstanceOf(ScopedRepo);
197
+ expect(instance.version).toBe("2.0");
198
+ });
188
199
 
189
- expect(() => instance.svc).toThrow("MY_SERVICE");
200
+ it("@InjectContainer returns the scoped child container", () => {
201
+ @Scope((c) => { c.registerValue(token<number>("X"), 42); })
202
+ class Service {}
203
+
204
+ const { ctx, run } = makeFieldCtx("container");
205
+ InjectContainer()(undefined, ctx);
206
+
207
+ const instance = new Service() as Record<string, unknown>;
208
+ run(instance);
209
+
210
+ expect(instance.container).toBeInstanceOf(Container);
211
+ expect(instance.container).not.toBe(container);
212
+ });
213
+
214
+ it("child container inherits registrations from the parent", () => {
215
+ class GlobalService {}
216
+ Injectable()(GlobalService, {} as ClassDecoratorContext);
217
+
218
+ @Scope()
219
+ class Child {}
220
+
221
+ const { ctx, run } = makeFieldCtx("svc");
222
+ Inject(GlobalService)(undefined, ctx);
223
+ const instance = new Child() as Record<string, unknown>;
224
+ run(instance);
225
+
226
+ expect(instance.svc).toBeInstanceOf(GlobalService);
190
227
  });
191
228
  });
package/src/container.ts CHANGED
@@ -10,7 +10,8 @@ export interface ServiceDescriptor {
10
10
  instance?: unknown;
11
11
  }
12
12
 
13
- export type Constructor<T = unknown> = new (...args: unknown[]) => T;
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;