@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.
- package/CHANGELOG.md +49 -0
- package/dist/__tests__/container.test.js +24 -3
- package/dist/__tests__/container.test.js.map +1 -1
- package/dist/__tests__/decorators.test.js +247 -100
- package/dist/__tests__/decorators.test.js.map +1 -1
- package/dist/container.d.ts +1 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +1 -3
- 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 +79 -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 +20 -0
- package/src/__tests__/decorators.test.ts +153 -116
- package/src/container.ts +2 -1
- package/src/decorators.ts +94 -55
- package/src/index.ts +2 -3
package/dist/decorators.d.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/decorators.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
return createFieldDecorator({
|
|
81
|
+
bind(_instance, _name, _initialValue) {
|
|
82
|
+
return {
|
|
83
|
+
descriptor: {
|
|
84
|
+
get() {
|
|
85
|
+
return resolveFrom(this);
|
|
86
|
+
},
|
|
45
87
|
},
|
|
46
|
-
|
|
47
|
-
|
|
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
|
package/dist/decorators.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,KAAK,
|
|
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,
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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", () => {
|
|
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
|
-
|
|
25
|
-
expect(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
fn.call(instance);
|
|
136
|
-
});
|
|
106
|
+
run(instance);
|
|
137
107
|
|
|
138
|
-
|
|
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
|
-
// ──
|
|
128
|
+
// ── @InjectContainer ──────────────────────────────────────────────────────────
|
|
143
129
|
|
|
144
|
-
describe("
|
|
145
|
-
it("
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
expect(
|
|
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("
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
@Scope((c) => {
|
|
182
|
+
c.register(ScopedRepo);
|
|
183
|
+
c.registerValue(VERSION, "2.0");
|
|
184
|
+
})
|
|
185
|
+
class App {}
|
|
175
186
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
192
|
+
const instance = new App() as Record<string, unknown>;
|
|
193
|
+
runRepo(instance);
|
|
194
|
+
runVer(instance);
|
|
184
195
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
expect(instance.repo).toBeInstanceOf(ScopedRepo);
|
|
197
|
+
expect(instance.version).toBe("2.0");
|
|
198
|
+
});
|
|
188
199
|
|
|
189
|
-
|
|
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
|
-
|
|
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;
|