@codefast/di 0.3.13-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 +59 -0
- package/LICENSE +21 -0
- package/README.md +572 -0
- package/dist/binding-select.d.mts +22 -0
- package/dist/binding-select.mjs +50 -0
- package/dist/binding.d.mts +219 -0
- package/dist/binding.mjs +240 -0
- package/dist/constraints.d.mts +18 -0
- package/dist/constraints.mjs +24 -0
- package/dist/container.d.mts +82 -0
- package/dist/container.mjs +406 -0
- package/dist/decorators/inject.d.mts +24 -0
- package/dist/decorators/inject.mjs +69 -0
- package/dist/decorators/injectable.d.mts +40 -0
- package/dist/decorators/injectable.mjs +62 -0
- package/dist/decorators/lifecycle-decorators.d.mts +13 -0
- package/dist/decorators/lifecycle-decorators.mjs +34 -0
- package/dist/dependency-graph.d.mts +35 -0
- package/dist/dependency-graph.mjs +126 -0
- package/dist/environment.d.mts +14 -0
- package/dist/environment.mjs +20 -0
- package/dist/errors.d.mts +100 -0
- package/dist/errors.mjs +152 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +8 -0
- package/dist/inspector.d.mts +76 -0
- package/dist/inspector.mjs +247 -0
- package/dist/lifecycle.d.mts +34 -0
- package/dist/lifecycle.mjs +83 -0
- package/dist/metadata/metadata-keys.d.mts +17 -0
- package/dist/metadata/metadata-keys.mjs +19 -0
- package/dist/metadata/metadata-types.d.mts +55 -0
- package/dist/metadata/metadata-types.mjs +1 -0
- package/dist/metadata/param-registry.d.mts +16 -0
- package/dist/metadata/param-registry.mjs +25 -0
- package/dist/metadata/symbol-metadata-reader.d.mts +15 -0
- package/dist/metadata/symbol-metadata-reader.mjs +32 -0
- package/dist/module.d.mts +60 -0
- package/dist/module.mjs +57 -0
- package/dist/registry.d.mts +38 -0
- package/dist/registry.mjs +65 -0
- package/dist/resolver.d.mts +102 -0
- package/dist/resolver.mjs +361 -0
- package/dist/scope-validation.d.mts +20 -0
- package/dist/scope-validation.mjs +34 -0
- package/dist/scope.d.mts +80 -0
- package/dist/scope.mjs +185 -0
- package/dist/token.d.mts +20 -0
- package/dist/token.mjs +9 -0
- package/package.json +157 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { AsyncModuleLoadError, CircularDependencyError, InternalError } from "./errors.mjs";
|
|
2
|
+
import { BindingBuilder } from "./binding.mjs";
|
|
3
|
+
import { getAutoRegistered } from "./decorators/injectable.mjs";
|
|
4
|
+
import { SymbolMetadataReader } from "./metadata/symbol-metadata-reader.mjs";
|
|
5
|
+
import { ContainerInspector } from "./inspector.mjs";
|
|
6
|
+
import { AsyncModule } from "./module.mjs";
|
|
7
|
+
import { BindingRegistry } from "./registry.mjs";
|
|
8
|
+
import { DependencyResolver } from "./resolver.mjs";
|
|
9
|
+
import { validateScopeRules } from "./scope-validation.mjs";
|
|
10
|
+
import { ScopeManager } from "./scope.mjs";
|
|
11
|
+
import { isDevelopmentOrTestEnvironment } from "./environment.mjs";
|
|
12
|
+
//#region src/container.ts
|
|
13
|
+
function resolveHintForBinding(binding) {
|
|
14
|
+
if (binding.bindingName !== void 0) return { name: binding.bindingName };
|
|
15
|
+
for (const [tagKey, tagValue] of binding.tags) return { tag: [tagKey, tagValue] };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Default IoC container: registry + scoped caches + synchronous / asynchronous resolution.
|
|
19
|
+
* @internal Implementation of {@link Container}; not part of the public package contract.
|
|
20
|
+
*/
|
|
21
|
+
var DefaultContainer = class DefaultContainer {
|
|
22
|
+
syncModuleStack = [];
|
|
23
|
+
asyncModuleStack = [];
|
|
24
|
+
loadedModules = /* @__PURE__ */ new Map();
|
|
25
|
+
devValidationRan = false;
|
|
26
|
+
constructor(ownRegistry, ownScopeManager, parent, resolver, metadataReader) {
|
|
27
|
+
this.ownRegistry = ownRegistry;
|
|
28
|
+
this.ownScopeManager = ownScopeManager;
|
|
29
|
+
this.parent = parent;
|
|
30
|
+
this.resolver = resolver;
|
|
31
|
+
this.metadataReader = metadataReader;
|
|
32
|
+
}
|
|
33
|
+
static create() {
|
|
34
|
+
const ownRegistry = new BindingRegistry();
|
|
35
|
+
const ownScopeManager = ScopeManager.createRoot();
|
|
36
|
+
const metadataReader = new SymbolMetadataReader();
|
|
37
|
+
const holder = { current: void 0 };
|
|
38
|
+
const container = new DefaultContainer(ownRegistry, ownScopeManager, void 0, new DependencyResolver({
|
|
39
|
+
lookup: (token) => {
|
|
40
|
+
const current = holder.current;
|
|
41
|
+
if (current === void 0) throw new InternalError("container is not initialized");
|
|
42
|
+
return current.lookupBindings(token);
|
|
43
|
+
},
|
|
44
|
+
scopeManager: ownScopeManager,
|
|
45
|
+
metadataReader
|
|
46
|
+
}), metadataReader);
|
|
47
|
+
holder.current = container;
|
|
48
|
+
return container;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Starts a fluent binding registered on this container when {@link BindingBuilder.build} runs.
|
|
52
|
+
*/
|
|
53
|
+
bind(token) {
|
|
54
|
+
return new BindingBuilder(token, void 0, {
|
|
55
|
+
register: (built) => {
|
|
56
|
+
this.invalidateDevValidationState();
|
|
57
|
+
this.ownRegistry.add(token, built);
|
|
58
|
+
},
|
|
59
|
+
update: (built) => {
|
|
60
|
+
this.invalidateDevValidationState();
|
|
61
|
+
this.ownRegistry.replaceById(built.id, built);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
has(token, hint) {
|
|
66
|
+
const list = this.lookupBindings(token);
|
|
67
|
+
if (list === void 0 || list.length === 0) return false;
|
|
68
|
+
if (hint === void 0) return true;
|
|
69
|
+
return list.some((binding) => {
|
|
70
|
+
if (hint.name !== void 0 && binding.bindingName !== hint.name) return false;
|
|
71
|
+
if (hint.tag !== void 0) {
|
|
72
|
+
const tag = hint.tag;
|
|
73
|
+
if (binding.tags.get(tag[0]) !== tag[1]) return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
unbind(tokenOrId) {
|
|
79
|
+
this.invalidateDevValidationState();
|
|
80
|
+
if (typeof tokenOrId === "string") {
|
|
81
|
+
this.ownScopeManager.releaseByBindingId(tokenOrId);
|
|
82
|
+
this.ownRegistry.removeById(tokenOrId);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const owned = this.ownRegistry.get(tokenOrId);
|
|
86
|
+
if (owned !== void 0) for (const binding of owned) this.ownScopeManager.releaseBinding(binding);
|
|
87
|
+
this.ownRegistry.remove(tokenOrId);
|
|
88
|
+
}
|
|
89
|
+
async unbindAsync(tokenOrId) {
|
|
90
|
+
this.invalidateDevValidationState();
|
|
91
|
+
if (typeof tokenOrId === "string") {
|
|
92
|
+
await this.ownScopeManager.releaseByBindingIdAsync(tokenOrId);
|
|
93
|
+
this.ownRegistry.removeById(tokenOrId);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const owned = this.ownRegistry.get(tokenOrId);
|
|
97
|
+
if (owned !== void 0) for (const binding of owned) await this.ownScopeManager.releaseBindingAsync(binding);
|
|
98
|
+
this.ownRegistry.remove(tokenOrId);
|
|
99
|
+
}
|
|
100
|
+
rebind(token) {
|
|
101
|
+
this.invalidateDevValidationState();
|
|
102
|
+
const owned = this.ownRegistry.get(token);
|
|
103
|
+
if (owned !== void 0) for (const binding of owned) this.ownScopeManager.releaseBinding(binding);
|
|
104
|
+
this.ownRegistry.remove(token);
|
|
105
|
+
return this.bind(token);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Registers bindings from synchronous modules. Re-loading a module already present on this
|
|
109
|
+
* container is a no-op (deduplication, spec §7.3 Phase 3). When {@link isDevelopmentOrTestEnvironment}
|
|
110
|
+
* is true, runs {@link validate} at most once after the registry changes.
|
|
111
|
+
*/
|
|
112
|
+
load(...modules) {
|
|
113
|
+
this.invalidateDevValidationState();
|
|
114
|
+
for (const syncModule of modules) {
|
|
115
|
+
if (syncModule instanceof AsyncModule) throw new AsyncModuleLoadError(syncModule.name);
|
|
116
|
+
this.ensureSyncModuleLoaded(syncModule);
|
|
117
|
+
}
|
|
118
|
+
this.maybeRunDevValidationOnce();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Like {@link load} but allows async modules. Re-loading is deduplicated the same way.
|
|
122
|
+
*/
|
|
123
|
+
async loadAsync(...modules) {
|
|
124
|
+
this.invalidateDevValidationState();
|
|
125
|
+
for (const moduleOrAsync of modules) if (moduleOrAsync instanceof AsyncModule) await this.ensureAsyncModuleLoaded(moduleOrAsync);
|
|
126
|
+
else this.ensureSyncModuleLoaded(moduleOrAsync);
|
|
127
|
+
this.maybeRunDevValidationOnce();
|
|
128
|
+
}
|
|
129
|
+
unload(...modules) {
|
|
130
|
+
this.invalidateDevValidationState();
|
|
131
|
+
for (const module of modules) {
|
|
132
|
+
const ownedBindingIds = this.loadedModules.get(module);
|
|
133
|
+
if (ownedBindingIds === void 0) throw new InternalError(`Module "${module.name}" is not loaded on this container.`);
|
|
134
|
+
for (const bindingId of [...ownedBindingIds].reverse()) {
|
|
135
|
+
this.ownScopeManager.releaseByBindingId(bindingId);
|
|
136
|
+
this.ownRegistry.removeById(bindingId);
|
|
137
|
+
}
|
|
138
|
+
this.loadedModules.delete(module);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async unloadAsync(...modules) {
|
|
142
|
+
this.invalidateDevValidationState();
|
|
143
|
+
for (const module of modules) {
|
|
144
|
+
const ownedBindingIds = this.loadedModules.get(module);
|
|
145
|
+
if (ownedBindingIds === void 0) throw new InternalError(`Module "${module.name}" is not loaded on this container.`);
|
|
146
|
+
for (const bindingId of [...ownedBindingIds].reverse()) {
|
|
147
|
+
await this.ownScopeManager.releaseByBindingIdAsync(bindingId);
|
|
148
|
+
this.ownRegistry.removeById(bindingId);
|
|
149
|
+
}
|
|
150
|
+
this.loadedModules.delete(module);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolves synchronously. Captive dependency (singleton holding scoped/transient) throws
|
|
155
|
+
* {@link ScopeViolationError} with binding ids and full resolution path. In dev/test, the first
|
|
156
|
+
* successful resolution also triggers a one-time static {@link validate} when not in production.
|
|
157
|
+
*/
|
|
158
|
+
resolve(key, hint) {
|
|
159
|
+
try {
|
|
160
|
+
return this.resolver.resolveRoot(key, hint);
|
|
161
|
+
} finally {
|
|
162
|
+
this.maybeRunDevValidationOnce();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Async variant of {@link resolve}; same dev/test validation and runtime scope enforcement. */
|
|
166
|
+
resolveAsync(key, hint) {
|
|
167
|
+
return this.resolver.resolveAsyncRoot(key, hint).finally(() => {
|
|
168
|
+
this.maybeRunDevValidationOnce();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
resolveOptional(key, hint) {
|
|
172
|
+
try {
|
|
173
|
+
return this.resolver.resolveOptionalRoot(key, hint);
|
|
174
|
+
} finally {
|
|
175
|
+
this.maybeRunDevValidationOnce();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
resolveAll(key, hint) {
|
|
179
|
+
try {
|
|
180
|
+
return this.resolver.resolveAllRoot(key, hint);
|
|
181
|
+
} finally {
|
|
182
|
+
this.maybeRunDevValidationOnce();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Async variant of {@link resolveAll}: safe for multi-bindings that mix sync and async factories
|
|
187
|
+
* (spec §5.2).
|
|
188
|
+
*/
|
|
189
|
+
resolveAllAsync(key, hint) {
|
|
190
|
+
return this.resolver.resolveAllAsyncRoot(key, hint).finally(() => {
|
|
191
|
+
this.maybeRunDevValidationOnce();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Eagerly resolves every registered `singleton` binding, including `async-dynamic` factories.
|
|
196
|
+
*/
|
|
197
|
+
async initializeAsync() {
|
|
198
|
+
for (const registryKey of this.collectAllRegistryKeysInHierarchy()) {
|
|
199
|
+
const list = this.lookupBindings(registryKey);
|
|
200
|
+
if (list === void 0) continue;
|
|
201
|
+
for (const binding of list) {
|
|
202
|
+
if (binding.scope !== "singleton") continue;
|
|
203
|
+
const hint = resolveHintForBinding(binding);
|
|
204
|
+
await this.resolveAsync(registryKey, hint);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
invalidateDevValidationState() {
|
|
209
|
+
this.devValidationRan = false;
|
|
210
|
+
}
|
|
211
|
+
maybeRunDevValidationOnce() {
|
|
212
|
+
if (!isDevelopmentOrTestEnvironment()) return;
|
|
213
|
+
if (this.devValidationRan) return;
|
|
214
|
+
this.devValidationRan = true;
|
|
215
|
+
this.validate();
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Validates static dependency scope rules: a singleton must not depend on scoped/transient
|
|
219
|
+
* class/factory/alias targets. Constant dependencies are always allowed.
|
|
220
|
+
*/
|
|
221
|
+
validate() {
|
|
222
|
+
validateScopeRules({
|
|
223
|
+
collectAllRegistryKeys: () => this.collectAllRegistryKeysInHierarchy(),
|
|
224
|
+
lookupBindings: (registryKey) => this.lookupBindings(registryKey),
|
|
225
|
+
getMetadataReader: () => this.metadataReader
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Dev/debug snapshot of registered bindings and activation status (design spec §5.5).
|
|
230
|
+
*/
|
|
231
|
+
inspect() {
|
|
232
|
+
return this.createInspector().getSnapshot();
|
|
233
|
+
}
|
|
234
|
+
generateDependencyGraph(options) {
|
|
235
|
+
const inspector = this.createInspector();
|
|
236
|
+
if (options?.format === "json") return inspector.generateDependencyGraph(options);
|
|
237
|
+
return inspector.generateDotGraph(options);
|
|
238
|
+
}
|
|
239
|
+
loadAutoRegistered() {
|
|
240
|
+
const entries = getAutoRegistered();
|
|
241
|
+
let count = 0;
|
|
242
|
+
for (const { implementationClass, scope } of entries) {
|
|
243
|
+
const builder = this.bind(implementationClass).toSelf();
|
|
244
|
+
switch (scope) {
|
|
245
|
+
case "singleton":
|
|
246
|
+
builder.singleton();
|
|
247
|
+
break;
|
|
248
|
+
case "scoped":
|
|
249
|
+
builder.scoped();
|
|
250
|
+
break;
|
|
251
|
+
default: builder.transient();
|
|
252
|
+
}
|
|
253
|
+
count++;
|
|
254
|
+
}
|
|
255
|
+
return count;
|
|
256
|
+
}
|
|
257
|
+
[Symbol.dispose]() {
|
|
258
|
+
throw new InternalError("Container disposal is async. Use `await using container = Container.create()` or call `await container.dispose()` instead of `using`.");
|
|
259
|
+
}
|
|
260
|
+
createInspector() {
|
|
261
|
+
return new ContainerInspector({
|
|
262
|
+
collectAllRegistryKeys: () => this.collectAllRegistryKeysInHierarchy(),
|
|
263
|
+
lookupBindings: (token) => this.lookupBindings(token),
|
|
264
|
+
isBindingCached: (binding) => this.ownScopeManager.isBindingCached(binding),
|
|
265
|
+
metadataReader: this.metadataReader
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
collectAllRegistryKeysInHierarchy() {
|
|
269
|
+
const keys = /* @__PURE__ */ new Set();
|
|
270
|
+
this.accumulateRegistryKeysFromHierarchy(keys, this);
|
|
271
|
+
return [...keys];
|
|
272
|
+
}
|
|
273
|
+
accumulateRegistryKeysFromHierarchy(keys, container) {
|
|
274
|
+
if (container === void 0) return;
|
|
275
|
+
for (const entry of container.ownRegistry.listEntries()) keys.add(entry.key);
|
|
276
|
+
this.accumulateRegistryKeysFromHierarchy(keys, container.parent);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Child inherits parent bindings via lookup fallback and shares singleton instances,
|
|
280
|
+
* but receives an isolated scoped cache.
|
|
281
|
+
*/
|
|
282
|
+
createChild() {
|
|
283
|
+
const childRegistry = new BindingRegistry();
|
|
284
|
+
const childScope = this.ownScopeManager.createChildScope();
|
|
285
|
+
const holder = { current: void 0 };
|
|
286
|
+
const resolver = new DependencyResolver({
|
|
287
|
+
lookup: (token) => {
|
|
288
|
+
const current = holder.current;
|
|
289
|
+
if (current === void 0) throw new InternalError("child container is not initialized");
|
|
290
|
+
return current.lookupBindings(token);
|
|
291
|
+
},
|
|
292
|
+
scopeManager: childScope,
|
|
293
|
+
metadataReader: this.metadataReader
|
|
294
|
+
});
|
|
295
|
+
const child = new DefaultContainer(childRegistry, childScope, this, resolver, this.metadataReader);
|
|
296
|
+
holder.current = child;
|
|
297
|
+
return child;
|
|
298
|
+
}
|
|
299
|
+
lookupBindings(token) {
|
|
300
|
+
const own = this.ownRegistry.get(token);
|
|
301
|
+
if (own !== void 0 && own.length > 0) return own;
|
|
302
|
+
return this.parent?.lookupBindings(token);
|
|
303
|
+
}
|
|
304
|
+
async dispose() {
|
|
305
|
+
await this.ownScopeManager.disposeAsync();
|
|
306
|
+
}
|
|
307
|
+
[Symbol.asyncDispose]() {
|
|
308
|
+
return this.dispose();
|
|
309
|
+
}
|
|
310
|
+
bindForModule(owner) {
|
|
311
|
+
return (token) => new BindingBuilder(token, owner.name, {
|
|
312
|
+
register: (built) => {
|
|
313
|
+
this.invalidateDevValidationState();
|
|
314
|
+
this.ownRegistry.replaceKeyLastWins(token, built, (removed) => {
|
|
315
|
+
this.ownScopeManager.releaseBinding(removed);
|
|
316
|
+
});
|
|
317
|
+
this.recordBindingForModule(owner, built.id);
|
|
318
|
+
},
|
|
319
|
+
update: (built) => {
|
|
320
|
+
this.invalidateDevValidationState();
|
|
321
|
+
this.ownRegistry.replaceById(built.id, built);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
recordBindingForModule(owner, id) {
|
|
326
|
+
const list = this.loadedModules.get(owner);
|
|
327
|
+
if (list === void 0) {
|
|
328
|
+
this.loadedModules.set(owner, [id]);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
list.push(id);
|
|
332
|
+
}
|
|
333
|
+
createSyncModuleBuilder(module) {
|
|
334
|
+
return {
|
|
335
|
+
import: (...deps) => {
|
|
336
|
+
for (const dep of deps) {
|
|
337
|
+
if (dep instanceof AsyncModule) throw new InternalError(`Module "${module.name}" cannot synchronously import async module "${dep.name}".`);
|
|
338
|
+
this.ensureSyncModuleLoaded(dep);
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
bind: this.bindForModule(module)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
createAsyncModuleBuilder(module) {
|
|
345
|
+
const pendingImports = [];
|
|
346
|
+
return {
|
|
347
|
+
moduleBuilder: {
|
|
348
|
+
import: (...deps) => {
|
|
349
|
+
for (const dep of deps) if (dep instanceof AsyncModule) pendingImports.push(this.ensureAsyncModuleLoaded(dep));
|
|
350
|
+
else this.ensureSyncModuleLoaded(dep);
|
|
351
|
+
},
|
|
352
|
+
bind: this.bindForModule(module)
|
|
353
|
+
},
|
|
354
|
+
awaitImports: async () => {
|
|
355
|
+
if (pendingImports.length === 0) return;
|
|
356
|
+
await Promise.all(pendingImports);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
ensureSyncModuleLoaded(module) {
|
|
361
|
+
if (this.loadedModules.has(module)) return;
|
|
362
|
+
if (this.syncModuleStack.includes(module)) throw new CircularDependencyError([...this.syncModuleStack.map((stackedModule) => stackedModule.name), module.name]);
|
|
363
|
+
this.syncModuleStack.push(module);
|
|
364
|
+
try {
|
|
365
|
+
this.loadedModules.set(module, []);
|
|
366
|
+
const moduleBuilder = this.createSyncModuleBuilder(module);
|
|
367
|
+
module.runSyncSetup(moduleBuilder);
|
|
368
|
+
} finally {
|
|
369
|
+
this.syncModuleStack.pop();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async ensureAsyncModuleLoaded(asyncModule) {
|
|
373
|
+
if (this.loadedModules.has(asyncModule)) return;
|
|
374
|
+
if (this.asyncModuleStack.includes(asyncModule)) throw new CircularDependencyError([...this.asyncModuleStack.map((stackedAsyncModule) => stackedAsyncModule.name), asyncModule.name]);
|
|
375
|
+
this.asyncModuleStack.push(asyncModule);
|
|
376
|
+
try {
|
|
377
|
+
this.loadedModules.set(asyncModule, []);
|
|
378
|
+
const { moduleBuilder, awaitImports } = this.createAsyncModuleBuilder(asyncModule);
|
|
379
|
+
await asyncModule.runAsyncSetup(moduleBuilder);
|
|
380
|
+
await awaitImports();
|
|
381
|
+
} finally {
|
|
382
|
+
this.asyncModuleStack.pop();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
let Container;
|
|
387
|
+
(function(_Container) {
|
|
388
|
+
function create() {
|
|
389
|
+
return DefaultContainer.create();
|
|
390
|
+
}
|
|
391
|
+
_Container.create = create;
|
|
392
|
+
function fromModules(...modules) {
|
|
393
|
+
const container = DefaultContainer.create();
|
|
394
|
+
container.load(...modules);
|
|
395
|
+
return container;
|
|
396
|
+
}
|
|
397
|
+
_Container.fromModules = fromModules;
|
|
398
|
+
async function fromModulesAsync(...modules) {
|
|
399
|
+
const container = DefaultContainer.create();
|
|
400
|
+
await container.loadAsync(...modules);
|
|
401
|
+
return container;
|
|
402
|
+
}
|
|
403
|
+
_Container.fromModulesAsync = fromModulesAsync;
|
|
404
|
+
})(Container || (Container = {}));
|
|
405
|
+
//#endregion
|
|
406
|
+
export { Container };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Token } from "../token.mjs";
|
|
2
|
+
import { Constructor, ResolveHint } from "../binding.mjs";
|
|
3
|
+
import { InjectionDescriptor } from "../metadata/metadata-types.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/decorators/inject.d.ts
|
|
6
|
+
/** Options forwarded to the container when resolving an injected dependency. */
|
|
7
|
+
type InjectOptions = ResolveHint;
|
|
8
|
+
/**
|
|
9
|
+
* Creates an {@link InjectionDescriptor} for use in an `@injectable(deps)` array, or as an
|
|
10
|
+
* `accessor` field decorator for post-construction property injection.
|
|
11
|
+
*
|
|
12
|
+
* As a deps-array entry: `@injectable([inject(Logger, { name: 'file' })])`
|
|
13
|
+
* As an accessor decorator: `@inject(Logger) accessor logger!: LoggerService`
|
|
14
|
+
*/
|
|
15
|
+
declare function inject<Value>(token: Token<Value> | Constructor<Value>, optionsOrContext?: InjectOptions | ClassAccessorDecoratorContext): InjectionDescriptor<Value>;
|
|
16
|
+
/**
|
|
17
|
+
* Same as {@link inject} but marks the dependency as optional — resolves to `undefined` instead
|
|
18
|
+
* of throwing {@link TokenNotBoundError} when no binding exists.
|
|
19
|
+
*/
|
|
20
|
+
declare function optional<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Value>;
|
|
21
|
+
/** Type-guard — returns `true` when `value` is an {@link InjectionDescriptor}. */
|
|
22
|
+
declare function isInjectionDescriptor(value: unknown): value is InjectionDescriptor;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { InjectOptions, inject, isInjectionDescriptor, optional };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { InternalError } from "../errors.mjs";
|
|
2
|
+
import { CODEFAST_DI_ACCESSOR_INJECTIONS } from "../metadata/metadata-keys.mjs";
|
|
3
|
+
//#region src/decorators/inject.ts
|
|
4
|
+
/** Validates and normalises the `tag` option from {@link InjectOptions}; throws {@link InternalError} on bad input. */
|
|
5
|
+
function normalizeTag(tag) {
|
|
6
|
+
if (tag === void 0) return;
|
|
7
|
+
if (!Array.isArray(tag) || tag.length !== 2) throw new InternalError(`@inject tag must be a tuple [tagKey, value] with length 2; received ${String(tag)}`);
|
|
8
|
+
const [tagName, value] = tag;
|
|
9
|
+
if (typeof tagName !== "string") throw new InternalError(`@inject tag key must be a string; received ${typeof tagName}`);
|
|
10
|
+
return [tagName, value];
|
|
11
|
+
}
|
|
12
|
+
/** Builds an {@link InjectionDescriptor} from a token, optional flag, and raw inject options. */
|
|
13
|
+
function toDescriptor(token, optional, options) {
|
|
14
|
+
const normalizedTag = normalizeTag(options?.tag);
|
|
15
|
+
if (options?.name !== void 0) return {
|
|
16
|
+
token,
|
|
17
|
+
optional,
|
|
18
|
+
name: options.name
|
|
19
|
+
};
|
|
20
|
+
if (normalizedTag !== void 0) return {
|
|
21
|
+
token,
|
|
22
|
+
optional,
|
|
23
|
+
tag: normalizedTag
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
token,
|
|
27
|
+
optional
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Type guard — returns `true` when `value` is a TC39 `ClassAccessorDecoratorContext` (accessor field). */
|
|
31
|
+
function isAccessorDecoratorContext(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && "kind" in value && value.kind === "accessor";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates an {@link InjectionDescriptor} for use in an `@injectable(deps)` array, or as an
|
|
36
|
+
* `accessor` field decorator for post-construction property injection.
|
|
37
|
+
*
|
|
38
|
+
* As a deps-array entry: `@injectable([inject(Logger, { name: 'file' })])`
|
|
39
|
+
* As an accessor decorator: `@inject(Logger) accessor logger!: LoggerService`
|
|
40
|
+
*/
|
|
41
|
+
function inject(token, optionsOrContext) {
|
|
42
|
+
if (isAccessorDecoratorContext(optionsOrContext)) {
|
|
43
|
+
const ctx = optionsOrContext;
|
|
44
|
+
const metaRecord = ctx.metadata;
|
|
45
|
+
const key = CODEFAST_DI_ACCESSOR_INJECTIONS;
|
|
46
|
+
if (!Array.isArray(metaRecord[key])) metaRecord[key] = [];
|
|
47
|
+
metaRecord[key].push({
|
|
48
|
+
name: String(ctx.name),
|
|
49
|
+
token,
|
|
50
|
+
optional: false
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
return toDescriptor(token, false, optionsOrContext);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Same as {@link inject} but marks the dependency as optional — resolves to `undefined` instead
|
|
58
|
+
* of throwing {@link TokenNotBoundError} when no binding exists.
|
|
59
|
+
*/
|
|
60
|
+
function optional(token, options) {
|
|
61
|
+
return toDescriptor(token, true, options);
|
|
62
|
+
}
|
|
63
|
+
/** Type-guard — returns `true` when `value` is an {@link InjectionDescriptor}. */
|
|
64
|
+
function isInjectionDescriptor(value) {
|
|
65
|
+
if (typeof value !== "object" || value === null) return false;
|
|
66
|
+
return "token" in value && "optional" in value;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { inject, isInjectionDescriptor, optional };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Token } from "../token.mjs";
|
|
2
|
+
import { BindingScope, Constructor } from "../binding.mjs";
|
|
3
|
+
import { InjectionDescriptor } from "../metadata/metadata-types.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/decorators/injectable.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A single entry in the `deps` array passed to `@injectable()`.
|
|
8
|
+
* Can be a plain token/constructor (resolved with no hint) or an {@link InjectionDescriptor}
|
|
9
|
+
* produced by {@link inject} / {@link optional} when name, tag, or optional semantics are needed.
|
|
10
|
+
*/
|
|
11
|
+
type InjectableDependency = Token<unknown> | Constructor<unknown> | InjectionDescriptor<unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* Returns all classes decorated with `@injectable({ autoRegister: true })`.
|
|
14
|
+
* Pass to {@link Container.loadAutoRegistered} or iterate manually to bind them.
|
|
15
|
+
*/
|
|
16
|
+
declare function getAutoRegistered(): ReadonlyArray<{
|
|
17
|
+
implementationClass: Constructor<unknown>;
|
|
18
|
+
scope: BindingScope;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Stage 3 class decorator that writes constructor dependency metadata into `Symbol.metadata`.
|
|
22
|
+
*
|
|
23
|
+
* @param deps - Ordered list of constructor parameters; length must match the class arity.
|
|
24
|
+
* @param autoRegisterOptions - Optional auto-registration flags.
|
|
25
|
+
* @param autoRegisterOptions.autoRegister - When `true`, registers the class in {@link getAutoRegistered} at
|
|
26
|
+
* class-definition time so {@link Container.loadAutoRegistered} can bind it automatically.
|
|
27
|
+
* @param autoRegisterOptions.scope - Scope used when auto-registering; defaults to `"transient"`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* @injectable([Logger, inject(Config, { name: "app" })])
|
|
32
|
+
* class UserService { constructor(log: Logger, cfg: AppConfig) {} }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare function injectable(deps?: readonly InjectableDependency[], autoRegisterOptions?: {
|
|
36
|
+
autoRegister?: boolean;
|
|
37
|
+
scope?: BindingScope;
|
|
38
|
+
}): <Class extends abstract new (...args: never[]) => unknown>(implementationClass: Class, context: ClassDecoratorContext<Class>) => void;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { InjectableDependency, getAutoRegistered, injectable };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { InternalError } from "../errors.mjs";
|
|
2
|
+
import { CODEFAST_DI_CONSTRUCTOR_METADATA } from "../metadata/metadata-keys.mjs";
|
|
3
|
+
import { isInjectionDescriptor } from "./inject.mjs";
|
|
4
|
+
//#region src/decorators/injectable.ts
|
|
5
|
+
const AUTO_REGISTER_REGISTRY = [];
|
|
6
|
+
/**
|
|
7
|
+
* Returns all classes decorated with `@injectable({ autoRegister: true })`.
|
|
8
|
+
* Pass to {@link Container.loadAutoRegistered} or iterate manually to bind them.
|
|
9
|
+
*/
|
|
10
|
+
function getAutoRegistered() {
|
|
11
|
+
return AUTO_REGISTER_REGISTRY;
|
|
12
|
+
}
|
|
13
|
+
/** Converts a single `@injectable` deps-array entry into a {@link ParamMetadata} record. */
|
|
14
|
+
function toParamMetadata(dependency, index) {
|
|
15
|
+
if (isInjectionDescriptor(dependency)) return {
|
|
16
|
+
index,
|
|
17
|
+
token: dependency.token,
|
|
18
|
+
optional: dependency.optional,
|
|
19
|
+
name: dependency.name,
|
|
20
|
+
tag: dependency.tag
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
index,
|
|
24
|
+
token: dependency,
|
|
25
|
+
optional: false
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stage 3 class decorator that writes constructor dependency metadata into `Symbol.metadata`.
|
|
30
|
+
*
|
|
31
|
+
* @param deps - Ordered list of constructor parameters; length must match the class arity.
|
|
32
|
+
* @param autoRegisterOptions - Optional auto-registration flags.
|
|
33
|
+
* @param autoRegisterOptions.autoRegister - When `true`, registers the class in {@link getAutoRegistered} at
|
|
34
|
+
* class-definition time so {@link Container.loadAutoRegistered} can bind it automatically.
|
|
35
|
+
* @param autoRegisterOptions.scope - Scope used when auto-registering; defaults to `"transient"`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* @injectable([Logger, inject(Config, { name: "app" })])
|
|
40
|
+
* class UserService { constructor(log: Logger, cfg: AppConfig) {} }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function injectable(deps = [], autoRegisterOptions) {
|
|
44
|
+
return (implementationClass, context) => {
|
|
45
|
+
const declaredArity = implementationClass.length;
|
|
46
|
+
if (declaredArity !== deps.length) throw new InternalError(`Class "${String(context.name ?? implementationClass.name)}" declares ${String(declaredArity)} constructor parameters but @injectable(...) received ${String(deps.length)} dependency descriptors.`);
|
|
47
|
+
const payload = { params: deps.map((dependency, index) => toParamMetadata(dependency, index)) };
|
|
48
|
+
const metadataRecord = context.metadata;
|
|
49
|
+
metadataRecord[CODEFAST_DI_CONSTRUCTOR_METADATA] = payload;
|
|
50
|
+
if (autoRegisterOptions?.autoRegister === true) {
|
|
51
|
+
const scope = autoRegisterOptions.scope ?? "transient";
|
|
52
|
+
context.addInitializer(function() {
|
|
53
|
+
AUTO_REGISTER_REGISTRY.push({
|
|
54
|
+
implementationClass: this,
|
|
55
|
+
scope
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { getAutoRegistered, injectable };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/decorators/lifecycle-decorators.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Stage 3 method decorator: marks a method to be called after the class is instantiated by the container.
|
|
4
|
+
* Order: construct → `@postConstruct()` → `.onActivation()` → cache.
|
|
5
|
+
*/
|
|
6
|
+
declare function postConstruct(): (target: () => unknown, context: ClassMethodDecoratorContext) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Stage 3 method decorator: marks a method to be called before the instance is destroyed by the container.
|
|
9
|
+
* Order: `.onDeactivation()` → `@preDestroy()`.
|
|
10
|
+
*/
|
|
11
|
+
declare function preDestroy(): (target: () => unknown, context: ClassMethodDecoratorContext) => void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { postConstruct, preDestroy };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CODEFAST_DI_LIFECYCLE_METADATA } from "../metadata/metadata-keys.mjs";
|
|
2
|
+
//#region src/decorators/lifecycle-decorators.ts
|
|
3
|
+
/**
|
|
4
|
+
* Stage 3 method decorator: marks a method to be called after the class is instantiated by the container.
|
|
5
|
+
* Order: construct → `@postConstruct()` → `.onActivation()` → cache.
|
|
6
|
+
*/
|
|
7
|
+
function postConstruct() {
|
|
8
|
+
return (_target, context) => {
|
|
9
|
+
const metaRecord = context.metadata;
|
|
10
|
+
const existing = metaRecord["codefast/di:lifecycle-metadata:v1"] ?? {};
|
|
11
|
+
if (existing.postConstruct !== void 0) throw new Error(`@postConstruct() is already defined as "${existing.postConstruct}" on this class.`);
|
|
12
|
+
metaRecord[CODEFAST_DI_LIFECYCLE_METADATA] = {
|
|
13
|
+
...existing,
|
|
14
|
+
postConstruct: String(context.name)
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Stage 3 method decorator: marks a method to be called before the instance is destroyed by the container.
|
|
20
|
+
* Order: `.onDeactivation()` → `@preDestroy()`.
|
|
21
|
+
*/
|
|
22
|
+
function preDestroy() {
|
|
23
|
+
return (_target, context) => {
|
|
24
|
+
const metaRecord = context.metadata;
|
|
25
|
+
const existing = metaRecord["codefast/di:lifecycle-metadata:v1"] ?? {};
|
|
26
|
+
if (existing.preDestroy !== void 0) throw new Error(`@preDestroy() is already defined as "${existing.preDestroy}" on this class.`);
|
|
27
|
+
metaRecord[CODEFAST_DI_LIFECYCLE_METADATA] = {
|
|
28
|
+
...existing,
|
|
29
|
+
preDestroy: String(context.name)
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { postConstruct, preDestroy };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { RegistryKey } from "./registry.mjs";
|
|
2
|
+
import { Binding, BindingIdentifier, ResolveHint } from "./binding.mjs";
|
|
3
|
+
import { MetadataReader } from "./metadata/metadata-types.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/dependency-graph.d.ts
|
|
6
|
+
/** A directed edge in the static dependency graph produced by {@link collectStaticDependencyEdges}. */
|
|
7
|
+
type StaticDependencyEdge = {
|
|
8
|
+
readonly fromBindingId: BindingIdentifier;
|
|
9
|
+
readonly toBindingId: BindingIdentifier;
|
|
10
|
+
readonly resolutionPath: readonly string[];
|
|
11
|
+
readonly edgeKind: "sync" | "async"; /** True when the resolved target binding carries a {@link BindingBuilder.when} predicate (runtime may skip this edge). */
|
|
12
|
+
readonly toBindingConditional: boolean; /** Constructor inject hint for this edge (named / tagged), when known statically. */
|
|
13
|
+
readonly injectHintLabel?: string; /** True when the consumer binding is an alias (rebind to another token). */
|
|
14
|
+
readonly isAliasEdge: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** A single resolved dependency entry produced by {@link listResolvedDependencies}. */
|
|
17
|
+
type ResolvedDependency = {
|
|
18
|
+
readonly binding: Binding<unknown>;
|
|
19
|
+
readonly path: readonly string[];
|
|
20
|
+
readonly injectHintLabel?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Converts a {@link ResolveHint} to a human-readable edge label for graph output (`name: x` / `tag: k=v`). */
|
|
23
|
+
declare function injectHintLabelFromResolveHint(hint: ResolveHint | undefined): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Lists direct static dependencies (constructor metadata, `toResolved` tokens, alias targets).
|
|
26
|
+
* Factories (`toDynamic` / `toAsyncDynamic`) have no enumerable dependency keys.
|
|
27
|
+
*/
|
|
28
|
+
declare function listResolvedDependencies(consumer: Binding<unknown>, lookup: (key: RegistryKey) => readonly Binding<unknown>[] | undefined, reader: MetadataReader | undefined, pathPrefix: readonly string[]): readonly ResolvedDependency[];
|
|
29
|
+
/**
|
|
30
|
+
* Flattens the direct dependencies of `consumer` into typed graph edges.
|
|
31
|
+
* Used by {@link ContainerInspector} to build DOT and JSON outputs.
|
|
32
|
+
*/
|
|
33
|
+
declare function collectStaticDependencyEdges(consumer: Binding<unknown>, lookup: (key: RegistryKey) => readonly Binding<unknown>[] | undefined, reader: MetadataReader | undefined, pathPrefix: readonly string[]): readonly StaticDependencyEdge[];
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ResolvedDependency, StaticDependencyEdge, collectStaticDependencyEdges, injectHintLabelFromResolveHint, listResolvedDependencies };
|