@codefast/di 0.3.16-canary.2 → 0.4.0-canary.4
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 +16 -0
- package/README.md +5 -17
- package/dist/binding.d.mts +24 -24
- package/dist/constraints.d.mts +3 -3
- package/dist/container.d.mts +3 -3
- package/dist/container.mjs +89 -157
- package/dist/decorators/inject.d.mts +3 -2
- package/dist/decorators/inject.mjs +14 -41
- package/dist/decorators/injectable.mjs +4 -12
- package/dist/decorators/lifecycle-decorators.mjs +13 -74
- package/dist/dependency-graph.d.mts +1 -1
- package/dist/dependency-graph.mjs +6 -4
- package/dist/graph-adapters/cytoscape.d.mts +12 -12
- package/dist/graph-adapters/cytoscape.mjs +7 -7
- package/dist/graph-adapters/reactflow.d.mts +15 -15
- package/dist/graph-adapters/reactflow.mjs +6 -9
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +1 -1
- package/dist/inspector.d.mts +3 -2
- package/dist/inspector.mjs +7 -10
- package/dist/lifecycle.mjs +2 -12
- package/dist/metadata/metadata-keys.d.mts +8 -33
- package/dist/metadata/metadata-keys.mjs +8 -21
- package/dist/metadata/metadata-types.d.mts +5 -0
- package/dist/metadata/symbol-metadata-reader.d.mts +1 -0
- package/dist/metadata/symbol-metadata-reader.mjs +11 -33
- package/dist/module.mjs +1 -1
- package/dist/registry.mjs +3 -22
- package/dist/resolve-options.d.mts +2 -2
- package/dist/resolve-options.mjs +10 -8
- package/dist/resolver.d.mts +6 -1
- package/dist/resolver.mjs +15 -21
- package/dist/types.d.mts +10 -4
- package/package.json +40 -14
- package/src/binding-scope.ts +26 -0
- package/src/binding-select.ts +158 -0
- package/src/binding.ts +277 -0
- package/src/constraints.ts +121 -0
- package/src/constructor-type.ts +19 -0
- package/src/container.ts +1135 -0
- package/src/decorators/inject.ts +222 -0
- package/src/decorators/injectable.ts +85 -0
- package/src/decorators/lifecycle-decorators.ts +51 -0
- package/src/dependency-graph.ts +116 -0
- package/src/environment.ts +207 -0
- package/src/errors.ts +260 -0
- package/src/graph-adapters/cytoscape.ts +64 -0
- package/src/graph-adapters/dot.ts +22 -0
- package/src/graph-adapters/reactflow.ts +58 -0
- package/src/index.ts +101 -0
- package/src/inspector.ts +125 -0
- package/src/lifecycle.ts +217 -0
- package/src/metadata/metadata-keys.ts +25 -0
- package/src/metadata/metadata-reader-token.ts +8 -0
- package/src/metadata/metadata-types.ts +51 -0
- package/src/metadata/symbol-metadata-reader.ts +45 -0
- package/src/module.ts +93 -0
- package/src/registry.ts +232 -0
- package/src/resolve-options.ts +42 -0
- package/src/resolver.ts +1609 -0
- package/src/scope.ts +77 -0
- package/src/token.ts +40 -0
- package/src/types.ts +123 -0
- package/dist/graph-adapters/types.d.mts +0 -2
- package/dist/graph-adapters/types.mjs +0 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { getActiveContainer } from "#/environment";
|
|
2
|
+
import { InternalError, MissingContainerContextError } from "#/errors";
|
|
3
|
+
import { INJECT_ACCESSOR_KEY } from "#/metadata/metadata-keys";
|
|
4
|
+
import { injectionSlotToResolveOptions } from "#/resolve-options";
|
|
5
|
+
import type { Token } from "#/token";
|
|
6
|
+
import type { BindingTag, Constructor } from "#/types";
|
|
7
|
+
|
|
8
|
+
// ── InjectionDescriptor ───────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @since 0.3.16-canary.0
|
|
12
|
+
*/
|
|
13
|
+
export interface InjectOptions {
|
|
14
|
+
name?: string;
|
|
15
|
+
tags?: ReadonlyArray<BindingTag>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @since 0.3.16-canary.0
|
|
20
|
+
*/
|
|
21
|
+
export interface InjectionDescriptor<Value = unknown> {
|
|
22
|
+
readonly token: Token<Value> | Constructor<Value>;
|
|
23
|
+
readonly optional: boolean;
|
|
24
|
+
readonly multi: boolean;
|
|
25
|
+
readonly name?: string;
|
|
26
|
+
readonly tags?: ReadonlyArray<BindingTag>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @since 0.3.16-canary.0
|
|
31
|
+
*/
|
|
32
|
+
export type InjectableDependency<Value = unknown> = Token<Value> | Constructor<Value> | InjectionDescriptor<Value>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @since 0.3.16-canary.0
|
|
36
|
+
*/
|
|
37
|
+
export function isInjectionDescriptor(value: unknown): value is InjectionDescriptor {
|
|
38
|
+
if (value === null || value === undefined) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const type = typeof value;
|
|
42
|
+
// inject() returns a function (dual-role), so must check both object and function
|
|
43
|
+
if (type !== "object" && type !== "function") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return (
|
|
47
|
+
"token" in (value as object) &&
|
|
48
|
+
"optional" in (value as object) &&
|
|
49
|
+
"multi" in (value as object) &&
|
|
50
|
+
typeof (value as InjectionDescriptor).optional === "boolean" &&
|
|
51
|
+
typeof (value as InjectionDescriptor).multi === "boolean"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @since 0.3.16-canary.0
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeToDescriptor(dependency: InjectableDependency): InjectionDescriptor {
|
|
59
|
+
if (isInjectionDescriptor(dependency)) {
|
|
60
|
+
return materializeInjectionDescriptor(dependency);
|
|
61
|
+
}
|
|
62
|
+
return { token: dependency as Token<unknown> | Constructor, optional: false, multi: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Dual-role `inject()` values are functions: [[Function]].name must not be treated as a DI slot name.
|
|
67
|
+
* Only enumerable own `name` / `tags` from `Object.defineProperties` are real injection options.
|
|
68
|
+
*/
|
|
69
|
+
function materializeInjectionDescriptor(dependency: InjectionDescriptor): InjectionDescriptor {
|
|
70
|
+
if (typeof dependency !== "function") {
|
|
71
|
+
return dependency;
|
|
72
|
+
}
|
|
73
|
+
const dualRole = dependency as InjectionDescriptor & ((...args: Array<unknown>) => unknown);
|
|
74
|
+
const base: Pick<InjectionDescriptor, "token" | "optional" | "multi"> = {
|
|
75
|
+
token: dualRole.token,
|
|
76
|
+
optional: dualRole.optional,
|
|
77
|
+
multi: dualRole.multi,
|
|
78
|
+
};
|
|
79
|
+
const nameDesc = Object.getOwnPropertyDescriptor(dualRole, "name");
|
|
80
|
+
const tagsDesc = Object.getOwnPropertyDescriptor(dualRole, "tags");
|
|
81
|
+
const explicitName = nameDesc?.enumerable === true && typeof nameDesc.value === "string" ? nameDesc.value : undefined;
|
|
82
|
+
const explicitTags = tagsDesc?.enumerable === true ? tagsDesc.value : undefined;
|
|
83
|
+
|
|
84
|
+
if (explicitName !== undefined && explicitTags !== undefined) {
|
|
85
|
+
return {
|
|
86
|
+
...base,
|
|
87
|
+
name: explicitName,
|
|
88
|
+
tags: explicitTags as NonNullable<InjectionDescriptor["tags"]>,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (explicitName !== undefined) {
|
|
92
|
+
return { ...base, name: explicitName };
|
|
93
|
+
}
|
|
94
|
+
if (explicitTags !== undefined) {
|
|
95
|
+
return { ...base, tags: explicitTags as NonNullable<InjectionDescriptor["tags"]> };
|
|
96
|
+
}
|
|
97
|
+
return base;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function withOptions<DescValue>(
|
|
101
|
+
base: Pick<InjectionDescriptor<DescValue>, "token" | "optional" | "multi">,
|
|
102
|
+
options: InjectOptions | undefined,
|
|
103
|
+
): InjectionDescriptor<DescValue> {
|
|
104
|
+
if (options?.name !== undefined && options.tags !== undefined) {
|
|
105
|
+
return { ...base, name: options.name, tags: options.tags };
|
|
106
|
+
}
|
|
107
|
+
if (options?.name !== undefined) {
|
|
108
|
+
return { ...base, name: options.name };
|
|
109
|
+
}
|
|
110
|
+
if (options?.tags !== undefined) {
|
|
111
|
+
return { ...base, tags: options.tags };
|
|
112
|
+
}
|
|
113
|
+
return base;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildInjectionDescriptor<const Value>(
|
|
117
|
+
token: Token<Value> | Constructor<Value>,
|
|
118
|
+
options?: InjectOptions,
|
|
119
|
+
): InjectionDescriptor<Value> {
|
|
120
|
+
return withOptions({ token, optional: false, multi: false }, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── inject() — dual-role ──────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
type ClassAccessorDecorator<This, Value> = (
|
|
126
|
+
target: ClassAccessorDecoratorTarget<This, Value>,
|
|
127
|
+
context: ClassAccessorDecoratorContext<This, Value>,
|
|
128
|
+
) => ClassAccessorDecoratorResult<This, Value> | void;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @since 0.3.16-canary.0
|
|
132
|
+
*/
|
|
133
|
+
export function inject<const Value>(
|
|
134
|
+
token: Token<Value> | Constructor<Value>,
|
|
135
|
+
options?: InjectOptions,
|
|
136
|
+
): InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value> {
|
|
137
|
+
const descriptor = buildInjectionDescriptor(token, options);
|
|
138
|
+
|
|
139
|
+
const decoratorFn = (
|
|
140
|
+
_target: ClassAccessorDecoratorTarget<unknown, Value>,
|
|
141
|
+
context: ClassAccessorDecoratorContext<unknown, Value>,
|
|
142
|
+
): ClassAccessorDecoratorResult<unknown, Value> => {
|
|
143
|
+
if (context.static) {
|
|
144
|
+
throw new InternalError(
|
|
145
|
+
"@inject() on static accessors is not supported; only instance accessors participate in runWithContainer-based property injection.",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const meta = context.metadata as Record<string | symbol, unknown>;
|
|
149
|
+
if (!Array.isArray(meta[INJECT_ACCESSOR_KEY])) {
|
|
150
|
+
meta[INJECT_ACCESSOR_KEY] = [];
|
|
151
|
+
}
|
|
152
|
+
(meta[INJECT_ACCESSOR_KEY] as Array<{ key: string | symbol; descriptor: InjectionDescriptor }>).push({
|
|
153
|
+
key: context.name,
|
|
154
|
+
descriptor,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
context.addInitializer(function (this: unknown) {
|
|
158
|
+
const container = getActiveContainer();
|
|
159
|
+
if (container === undefined) {
|
|
160
|
+
throw new MissingContainerContextError(String(context.name));
|
|
161
|
+
}
|
|
162
|
+
const hint =
|
|
163
|
+
options === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: injectionSlotToResolveOptions({
|
|
166
|
+
...(options.name !== undefined ? { name: options.name } : {}),
|
|
167
|
+
...(options.tags !== undefined ? { tags: options.tags } : {}),
|
|
168
|
+
});
|
|
169
|
+
const value = descriptor.optional ? container.resolveOptional(token, hint) : container.resolve(token, hint);
|
|
170
|
+
context.access.set(this, value as Value);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return {};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Use defineProperties to handle read-only `name` property of functions
|
|
177
|
+
const props: PropertyDescriptorMap = {};
|
|
178
|
+
for (const key of Object.keys(descriptor) as Array<keyof typeof descriptor>) {
|
|
179
|
+
props[key] = { value: descriptor[key], writable: true, enumerable: true, configurable: true };
|
|
180
|
+
}
|
|
181
|
+
Object.defineProperties(decoratorFn, props);
|
|
182
|
+
|
|
183
|
+
return decoratorFn as unknown as InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── optional() ────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @since 0.3.16-canary.0
|
|
190
|
+
*/
|
|
191
|
+
export function optional<const Value>(
|
|
192
|
+
token: Token<Value> | Constructor<Value>,
|
|
193
|
+
options?: InjectOptions,
|
|
194
|
+
): InjectionDescriptor<Value | undefined> {
|
|
195
|
+
return withOptions(
|
|
196
|
+
{
|
|
197
|
+
token: token as Token<Value | undefined> | Constructor<Value | undefined>,
|
|
198
|
+
optional: true,
|
|
199
|
+
multi: false,
|
|
200
|
+
},
|
|
201
|
+
options,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── injectAll() ───────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @since 0.3.16-canary.0
|
|
209
|
+
*/
|
|
210
|
+
export function injectAll<const Value>(
|
|
211
|
+
token: Token<Value> | Constructor<Value>,
|
|
212
|
+
options?: InjectOptions,
|
|
213
|
+
): InjectionDescriptor<Array<Value>> {
|
|
214
|
+
return withOptions(
|
|
215
|
+
{
|
|
216
|
+
token: token as Token<Array<Value>> | Constructor<Array<Value>>,
|
|
217
|
+
optional: false,
|
|
218
|
+
multi: true,
|
|
219
|
+
},
|
|
220
|
+
options,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { InjectableDependency } from "#/decorators/inject";
|
|
2
|
+
import { normalizeToDescriptor } from "#/decorators/inject";
|
|
3
|
+
import { INJECTABLE_KEY } from "#/metadata/metadata-keys";
|
|
4
|
+
import type { ParamMetadata } from "#/metadata/metadata-types";
|
|
5
|
+
import type { BindingScope, Constructor } from "#/types";
|
|
6
|
+
|
|
7
|
+
// ── AutoRegisterRegistry ──────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @since 0.3.16-canary.0
|
|
11
|
+
*/
|
|
12
|
+
export interface AutoRegisterRegistry {
|
|
13
|
+
register(target: Constructor, scope: BindingScope): void;
|
|
14
|
+
entries(): ReadonlyArray<{ target: Constructor; scope: BindingScope }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @since 0.3.16-canary.0
|
|
19
|
+
*/
|
|
20
|
+
export function createAutoRegisterRegistry(): AutoRegisterRegistry {
|
|
21
|
+
const _registeredEntries: Array<{ target: Constructor; scope: BindingScope }> = [];
|
|
22
|
+
return {
|
|
23
|
+
register(target: Constructor, scope: BindingScope): void {
|
|
24
|
+
_registeredEntries.push({ target, scope });
|
|
25
|
+
},
|
|
26
|
+
entries(): ReadonlyArray<{ target: Constructor; scope: BindingScope }> {
|
|
27
|
+
return _registeredEntries;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── InjectableOptions ─────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @since 0.3.16-canary.0
|
|
36
|
+
*/
|
|
37
|
+
export interface InjectableOptions {
|
|
38
|
+
autoRegister?: AutoRegisterRegistry;
|
|
39
|
+
scope?: BindingScope;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── @injectable() ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @since 0.3.16-canary.0
|
|
46
|
+
*/
|
|
47
|
+
export function injectable(
|
|
48
|
+
deps?: ReadonlyArray<InjectableDependency>,
|
|
49
|
+
options?: InjectableOptions,
|
|
50
|
+
): (target: unknown, context: ClassDecoratorContext) => void {
|
|
51
|
+
return function (target: unknown, context: ClassDecoratorContext): void {
|
|
52
|
+
const parameterMetadataList: Array<ParamMetadata> = (deps ?? []).map((dependency, index) => {
|
|
53
|
+
const descriptor = normalizeToDescriptor(dependency);
|
|
54
|
+
const baseParameterMetadata: Pick<ParamMetadata, "index" | "token" | "optional" | "multi"> = {
|
|
55
|
+
index,
|
|
56
|
+
token: descriptor.token,
|
|
57
|
+
optional: descriptor.optional,
|
|
58
|
+
multi: descriptor.multi,
|
|
59
|
+
};
|
|
60
|
+
if (descriptor.name !== undefined && descriptor.tags !== undefined) {
|
|
61
|
+
return { ...baseParameterMetadata, name: descriptor.name, tags: descriptor.tags };
|
|
62
|
+
}
|
|
63
|
+
if (descriptor.name !== undefined) {
|
|
64
|
+
return { ...baseParameterMetadata, name: descriptor.name };
|
|
65
|
+
}
|
|
66
|
+
if (descriptor.tags !== undefined) {
|
|
67
|
+
return { ...baseParameterMetadata, tags: descriptor.tags };
|
|
68
|
+
}
|
|
69
|
+
return baseParameterMetadata;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Field decorators run before the class decorator — accessor @inject entries are
|
|
73
|
+
// already on context.metadata by the time this runs.
|
|
74
|
+
(context.metadata as Record<string | symbol, unknown>)[INJECTABLE_KEY] = {
|
|
75
|
+
params: parameterMetadataList,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (options?.autoRegister !== undefined) {
|
|
79
|
+
const scope: BindingScope = options.scope ?? "transient";
|
|
80
|
+
options.autoRegister.register(target as Constructor, scope);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type { InjectableDependency };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { InternalError } from "#/errors";
|
|
2
|
+
import { LIFECYCLE_KEY } from "#/metadata/metadata-keys";
|
|
3
|
+
import type { MutableLifecycleMetadata } from "#/metadata/metadata-types";
|
|
4
|
+
|
|
5
|
+
function appendUniqueMethod(
|
|
6
|
+
metadata: MutableLifecycleMetadata,
|
|
7
|
+
phase: "postConstruct" | "preDestroy",
|
|
8
|
+
methodName: string,
|
|
9
|
+
): void {
|
|
10
|
+
if (!metadata[phase].includes(methodName)) {
|
|
11
|
+
metadata[phase].push(methodName);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @since 0.3.16-canary.0
|
|
17
|
+
*/
|
|
18
|
+
export function postConstruct(): (target: unknown, context: ClassMethodDecoratorContext) => void {
|
|
19
|
+
return function (target: unknown, context: ClassMethodDecoratorContext): void {
|
|
20
|
+
if (context.static) {
|
|
21
|
+
throw new InternalError(
|
|
22
|
+
"@postConstruct() applies to instance methods only; static methods are not invoked during instance lifecycle.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const methodName = String(context.name);
|
|
26
|
+
const meta = context.metadata as Record<string | symbol, unknown>;
|
|
27
|
+
if (!meta[LIFECYCLE_KEY]) {
|
|
28
|
+
meta[LIFECYCLE_KEY] = { postConstruct: [], preDestroy: [] };
|
|
29
|
+
}
|
|
30
|
+
appendUniqueMethod(meta[LIFECYCLE_KEY] as MutableLifecycleMetadata, "postConstruct", methodName);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @since 0.3.16-canary.0
|
|
36
|
+
*/
|
|
37
|
+
export function preDestroy(): (target: unknown, context: ClassMethodDecoratorContext) => void {
|
|
38
|
+
return function (target: unknown, context: ClassMethodDecoratorContext): void {
|
|
39
|
+
if (context.static) {
|
|
40
|
+
throw new InternalError(
|
|
41
|
+
"@preDestroy() applies to instance methods only; static methods are not invoked during instance teardown.",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const methodName = String(context.name);
|
|
45
|
+
const meta = context.metadata as Record<string | symbol, unknown>;
|
|
46
|
+
if (!meta[LIFECYCLE_KEY]) {
|
|
47
|
+
meta[LIFECYCLE_KEY] = { postConstruct: [], preDestroy: [] };
|
|
48
|
+
}
|
|
49
|
+
appendUniqueMethod(meta[LIFECYCLE_KEY] as MutableLifecycleMetadata, "preDestroy", methodName);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { effectiveBindingScope } from "#/binding-scope";
|
|
2
|
+
import type { MetadataReader } from "#/metadata/metadata-types";
|
|
3
|
+
import type { BindingRegistry } from "#/registry";
|
|
4
|
+
import { tokenName } from "#/token";
|
|
5
|
+
import type { BindingScope, Constructor } from "#/types";
|
|
6
|
+
|
|
7
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @since 0.3.16-canary.0
|
|
11
|
+
*/
|
|
12
|
+
export interface GraphNode {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly tokenName: string;
|
|
15
|
+
readonly kind: string;
|
|
16
|
+
readonly scope: BindingScope;
|
|
17
|
+
readonly fromParent: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @since 0.3.16-canary.0
|
|
22
|
+
*/
|
|
23
|
+
export interface GraphEdge {
|
|
24
|
+
readonly from: string;
|
|
25
|
+
readonly to: string;
|
|
26
|
+
readonly label?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @since 0.3.16-canary.0
|
|
31
|
+
*/
|
|
32
|
+
export interface ContainerGraphJson {
|
|
33
|
+
readonly nodes: ReadonlyArray<GraphNode>;
|
|
34
|
+
readonly edges: ReadonlyArray<GraphEdge>;
|
|
35
|
+
readonly includesParent: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @since 0.3.16-canary.0
|
|
40
|
+
*/
|
|
41
|
+
export interface GraphOptions {
|
|
42
|
+
readonly includeParent?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Builder ───────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @since 0.3.16-canary.0
|
|
49
|
+
*/
|
|
50
|
+
export function buildDependencyGraph(
|
|
51
|
+
registry: BindingRegistry,
|
|
52
|
+
metadataReader: MetadataReader,
|
|
53
|
+
options: GraphOptions | undefined,
|
|
54
|
+
parentRegistry?: BindingRegistry,
|
|
55
|
+
): ContainerGraphJson {
|
|
56
|
+
const nodes: Array<GraphNode> = [];
|
|
57
|
+
const edges: Array<GraphEdge> = [];
|
|
58
|
+
const includesParent = options?.includeParent === true;
|
|
59
|
+
|
|
60
|
+
const addBindings = (sourceRegistry: BindingRegistry, fromParent: boolean): void => {
|
|
61
|
+
for (const binding of sourceRegistry.allBindings()) {
|
|
62
|
+
const scope = effectiveBindingScope(binding);
|
|
63
|
+
nodes.push({
|
|
64
|
+
id: binding.id,
|
|
65
|
+
tokenName: tokenName(binding.token),
|
|
66
|
+
kind: binding.kind,
|
|
67
|
+
scope,
|
|
68
|
+
fromParent,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Build edges
|
|
72
|
+
if (binding.kind === "class") {
|
|
73
|
+
const meta = metadataReader.getConstructorMetadata(binding.target as Constructor);
|
|
74
|
+
if (meta !== undefined) {
|
|
75
|
+
for (let index = 0; index < meta.params.length; index += 1) {
|
|
76
|
+
const param = meta.params[index]!;
|
|
77
|
+
const dependencyBinding = sourceRegistry.getAll(param.token as Constructor)[0];
|
|
78
|
+
if (dependencyBinding !== undefined) {
|
|
79
|
+
edges.push({
|
|
80
|
+
from: binding.id,
|
|
81
|
+
to: dependencyBinding.id,
|
|
82
|
+
label: `[${index}]`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else if (binding.kind === "resolved" || binding.kind === "resolved-async") {
|
|
88
|
+
for (let index = 0; index < binding.deps.length; index += 1) {
|
|
89
|
+
const dependency = binding.deps[index]!;
|
|
90
|
+
const dependencyBindings = sourceRegistry.getAll(dependency.token as Constructor);
|
|
91
|
+
if (dependencyBindings.length > 0 && dependencyBindings[0] !== undefined) {
|
|
92
|
+
const label =
|
|
93
|
+
dependency.name !== undefined
|
|
94
|
+
? `name:${dependency.name}`
|
|
95
|
+
: dependency.tags !== undefined && dependency.tags.length > 0
|
|
96
|
+
? `tag:${dependency.tags[0]?.[0]}=${String(dependency.tags[0]?.[1])}`
|
|
97
|
+
: `[${index}]`;
|
|
98
|
+
edges.push({ from: binding.id, to: dependencyBindings[0].id, label });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else if (binding.kind === "alias") {
|
|
102
|
+
const targetBindings = sourceRegistry.getAll(binding.target as Constructor);
|
|
103
|
+
if (targetBindings.length > 0 && targetBindings[0] !== undefined) {
|
|
104
|
+
edges.push({ from: binding.id, to: targetBindings[0].id, label: "alias" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
addBindings(registry, false);
|
|
111
|
+
if (includesParent && parentRegistry !== undefined) {
|
|
112
|
+
addBindings(parentRegistry, true);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { nodes, edges, includesParent };
|
|
116
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Container } from "#/container";
|
|
2
|
+
import type { Token } from "#/token";
|
|
3
|
+
import type {
|
|
4
|
+
BindingIdentifier,
|
|
5
|
+
BindingKind,
|
|
6
|
+
BindingScope,
|
|
7
|
+
ConstraintContext,
|
|
8
|
+
Constructor,
|
|
9
|
+
ResolutionFrame,
|
|
10
|
+
ResolutionContext,
|
|
11
|
+
ResolveOptions,
|
|
12
|
+
} from "#/types";
|
|
13
|
+
|
|
14
|
+
// ── Active container ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
let _activeContainer: Container | undefined;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @since 0.3.16-canary.0
|
|
20
|
+
*/
|
|
21
|
+
export function runWithContainer<Result>(container: Container, fn: () => Result): Result {
|
|
22
|
+
const prev = _activeContainer;
|
|
23
|
+
_activeContainer = container;
|
|
24
|
+
try {
|
|
25
|
+
return fn();
|
|
26
|
+
} finally {
|
|
27
|
+
_activeContainer = prev;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @since 0.3.16-canary.0
|
|
33
|
+
*/
|
|
34
|
+
export function getActiveContainer(): Container | undefined {
|
|
35
|
+
return _activeContainer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── ResolutionContext implementation ──────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @since 0.3.16-canary.0
|
|
42
|
+
*/
|
|
43
|
+
export interface ResolverCallbacks {
|
|
44
|
+
resolveFromContext<const Value>(
|
|
45
|
+
token: Token<Value> | Constructor<Value>,
|
|
46
|
+
resolutionPath: Array<string>,
|
|
47
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
48
|
+
): Value;
|
|
49
|
+
resolve<const Value>(
|
|
50
|
+
token: Token<Value> | Constructor<Value>,
|
|
51
|
+
hint: ResolveOptions | undefined,
|
|
52
|
+
resolutionPath: Array<string>,
|
|
53
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
54
|
+
): Value;
|
|
55
|
+
resolveAsyncFromContext<const Value>(
|
|
56
|
+
token: Token<Value> | Constructor<Value>,
|
|
57
|
+
resolutionPath: Array<string>,
|
|
58
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
59
|
+
): Promise<Value>;
|
|
60
|
+
resolveAsync<const Value>(
|
|
61
|
+
token: Token<Value> | Constructor<Value>,
|
|
62
|
+
hint: ResolveOptions | undefined,
|
|
63
|
+
resolutionPath: Array<string>,
|
|
64
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
65
|
+
): Promise<Value>;
|
|
66
|
+
resolveOptional<const Value>(
|
|
67
|
+
token: Token<Value> | Constructor<Value>,
|
|
68
|
+
hint: ResolveOptions | undefined,
|
|
69
|
+
resolutionPath: Array<string>,
|
|
70
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
71
|
+
): Value | undefined;
|
|
72
|
+
resolveOptionalAsync<const Value>(
|
|
73
|
+
token: Token<Value> | Constructor<Value>,
|
|
74
|
+
hint: ResolveOptions | undefined,
|
|
75
|
+
resolutionPath: Array<string>,
|
|
76
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
77
|
+
): Promise<Value | undefined>;
|
|
78
|
+
resolveAll<const Value>(
|
|
79
|
+
token: Token<Value> | Constructor<Value>,
|
|
80
|
+
hint: ResolveOptions | undefined,
|
|
81
|
+
resolutionPath: Array<string>,
|
|
82
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
83
|
+
): Array<Value>;
|
|
84
|
+
resolveAllAsync<const Value>(
|
|
85
|
+
token: Token<Value> | Constructor<Value>,
|
|
86
|
+
hint: ResolveOptions | undefined,
|
|
87
|
+
resolutionPath: Array<string>,
|
|
88
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
89
|
+
): Promise<Array<Value>>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @since 0.3.16-canary.0
|
|
94
|
+
*/
|
|
95
|
+
export class DefaultResolutionContext implements ResolutionContext {
|
|
96
|
+
private _resolver: ResolverCallbacks;
|
|
97
|
+
private _resolutionPath: Array<string>;
|
|
98
|
+
private _resolutionStack: Array<ResolutionFrame>;
|
|
99
|
+
private _currentHint: ResolveOptions | undefined;
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
resolver: ResolverCallbacks,
|
|
103
|
+
resolutionPath: Array<string>,
|
|
104
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
105
|
+
currentHint: ResolveOptions | undefined,
|
|
106
|
+
) {
|
|
107
|
+
this._resolver = resolver;
|
|
108
|
+
this._resolutionPath = resolutionPath;
|
|
109
|
+
this._resolutionStack = resolutionStack;
|
|
110
|
+
this._currentHint = currentHint;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private _graph: ConstraintContext | undefined;
|
|
114
|
+
|
|
115
|
+
get graph(): ConstraintContext {
|
|
116
|
+
if (this._graph === undefined) {
|
|
117
|
+
this._graph = new DefaultConstraintContext(this._resolutionPath, this._resolutionStack, this._currentHint);
|
|
118
|
+
}
|
|
119
|
+
return this._graph;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
reset(
|
|
123
|
+
resolver: ResolverCallbacks,
|
|
124
|
+
resolutionPath: Array<string>,
|
|
125
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
126
|
+
currentHint: ResolveOptions | undefined,
|
|
127
|
+
): void {
|
|
128
|
+
this._resolver = resolver;
|
|
129
|
+
this._resolutionPath = resolutionPath;
|
|
130
|
+
this._resolutionStack = resolutionStack;
|
|
131
|
+
this._currentHint = currentHint;
|
|
132
|
+
this._graph = undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resolve<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Value {
|
|
136
|
+
if (hint === undefined) {
|
|
137
|
+
return this._resolver.resolveFromContext(token, this._resolutionPath, this._resolutionStack);
|
|
138
|
+
}
|
|
139
|
+
return this._resolver.resolve(token, hint, this._resolutionPath, this._resolutionStack);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Promise<Value> {
|
|
143
|
+
if (hint === undefined) {
|
|
144
|
+
return this._resolver.resolveAsyncFromContext(token, this._resolutionPath, this._resolutionStack);
|
|
145
|
+
}
|
|
146
|
+
return this._resolver.resolveAsync(token, hint, this._resolutionPath, this._resolutionStack);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Value | undefined {
|
|
150
|
+
return this._resolver.resolveOptional(token, hint, this._resolutionPath, this._resolutionStack);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
resolveOptionalAsync<const Value>(
|
|
154
|
+
token: Token<Value> | Constructor<Value>,
|
|
155
|
+
hint?: ResolveOptions,
|
|
156
|
+
): Promise<Value | undefined> {
|
|
157
|
+
return this._resolver.resolveOptionalAsync(token, hint, this._resolutionPath, this._resolutionStack);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Array<Value> {
|
|
161
|
+
return this._resolver.resolveAll(token, hint, this._resolutionPath, this._resolutionStack);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
resolveAllAsync<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Promise<Array<Value>> {
|
|
165
|
+
return this._resolver.resolveAllAsync(token, hint, this._resolutionPath, this._resolutionStack);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class DefaultConstraintContext implements ConstraintContext {
|
|
170
|
+
readonly resolutionPath: ReadonlyArray<string>;
|
|
171
|
+
readonly resolutionStack: ReadonlyArray<ResolutionFrame>;
|
|
172
|
+
readonly parent: ResolutionFrame | undefined;
|
|
173
|
+
readonly currentResolveHint: ResolveOptions | undefined;
|
|
174
|
+
|
|
175
|
+
constructor(
|
|
176
|
+
resolutionPath: ReadonlyArray<string>,
|
|
177
|
+
resolutionStack: ReadonlyArray<ResolutionFrame>,
|
|
178
|
+
currentResolveHint: ResolveOptions | undefined,
|
|
179
|
+
) {
|
|
180
|
+
this.resolutionPath = resolutionPath;
|
|
181
|
+
this.resolutionStack = resolutionStack;
|
|
182
|
+
this.parent = resolutionStack.at(-1);
|
|
183
|
+
this.currentResolveHint = currentResolveHint;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private _ancestors: ReadonlyArray<ResolutionFrame> | undefined;
|
|
187
|
+
|
|
188
|
+
get ancestors(): ReadonlyArray<ResolutionFrame> {
|
|
189
|
+
if (this._ancestors === undefined) {
|
|
190
|
+
this._ancestors = this.resolutionStack.length > 1 ? this.resolutionStack.slice(0, -1) : [];
|
|
191
|
+
}
|
|
192
|
+
return this._ancestors;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @since 0.3.16-canary.0
|
|
198
|
+
*/
|
|
199
|
+
export function buildResolutionFrame(
|
|
200
|
+
tokenName: string,
|
|
201
|
+
scope: BindingScope,
|
|
202
|
+
bindingId: BindingIdentifier,
|
|
203
|
+
kind: BindingKind,
|
|
204
|
+
slot: { name: string | undefined; tags: ReadonlyArray<readonly [string, unknown]> },
|
|
205
|
+
): ResolutionFrame {
|
|
206
|
+
return { tokenName, scope, bindingId, kind, slot };
|
|
207
|
+
}
|