@di-framework/di-framework 0.0.0-prerelease-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/LICENSE +21 -0
- package/README.md +505 -0
- package/dist/container.d.ts +136 -0
- package/dist/container.js +408 -0
- package/dist/decorators.d.ts +93 -0
- package/dist/decorators.js +128 -0
- package/dist/types.d.ts +242 -0
- package/dist/types.js +140 -0
- package/package.json +50 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Injection Container
|
|
3
|
+
*
|
|
4
|
+
* Manages service registration, dependency resolution, and instance lifecycle.
|
|
5
|
+
* Supports singleton pattern and automatic dependency injection via decorators.
|
|
6
|
+
* Works with SWC and TypeScript's native decorator support.
|
|
7
|
+
*/
|
|
8
|
+
const INJECTABLE_METADATA_KEY = "di:injectable";
|
|
9
|
+
const INJECT_METADATA_KEY = "di:inject";
|
|
10
|
+
const DESIGN_PARAM_TYPES_KEY = "design:paramtypes";
|
|
11
|
+
export const TELEMETRY_METADATA_KEY = "di:telemetry";
|
|
12
|
+
export const TELEMETRY_LISTENER_METADATA_KEY = "di:telemetry-listener";
|
|
13
|
+
/**
|
|
14
|
+
* Simple metadata storage that doesn't require reflect-metadata
|
|
15
|
+
* Works with SWC's native decorator support
|
|
16
|
+
*/
|
|
17
|
+
const metadataStore = new Map();
|
|
18
|
+
function defineMetadata(key, value, target) {
|
|
19
|
+
if (!metadataStore.has(target)) {
|
|
20
|
+
metadataStore.set(target, new Map());
|
|
21
|
+
}
|
|
22
|
+
metadataStore.get(target).set(key, value);
|
|
23
|
+
}
|
|
24
|
+
function getMetadata(key, target) {
|
|
25
|
+
return metadataStore.get(target)?.get(key);
|
|
26
|
+
}
|
|
27
|
+
function hasMetadata(key, target) {
|
|
28
|
+
return metadataStore.has(target) && metadataStore.get(target).has(key);
|
|
29
|
+
}
|
|
30
|
+
function getOwnMetadata(key, target) {
|
|
31
|
+
return getMetadata(key, target);
|
|
32
|
+
}
|
|
33
|
+
export class Container {
|
|
34
|
+
services = new Map();
|
|
35
|
+
resolutionStack = new Set();
|
|
36
|
+
listeners = new Map();
|
|
37
|
+
/**
|
|
38
|
+
* Register a service class as injectable
|
|
39
|
+
*/
|
|
40
|
+
register(serviceClass, options = { singleton: true }) {
|
|
41
|
+
const name = serviceClass.name;
|
|
42
|
+
this.services.set(name, {
|
|
43
|
+
type: serviceClass,
|
|
44
|
+
singleton: options.singleton ?? true,
|
|
45
|
+
});
|
|
46
|
+
this.services.set(serviceClass, {
|
|
47
|
+
type: serviceClass,
|
|
48
|
+
singleton: options.singleton ?? true,
|
|
49
|
+
});
|
|
50
|
+
this.emit("registered", {
|
|
51
|
+
key: serviceClass,
|
|
52
|
+
singleton: options.singleton ?? true,
|
|
53
|
+
kind: "class",
|
|
54
|
+
});
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Register a service using a factory function
|
|
59
|
+
*/
|
|
60
|
+
registerFactory(name, factory, options = { singleton: true }) {
|
|
61
|
+
this.services.set(name, {
|
|
62
|
+
type: factory,
|
|
63
|
+
singleton: options.singleton ?? true,
|
|
64
|
+
});
|
|
65
|
+
this.emit("registered", {
|
|
66
|
+
key: name,
|
|
67
|
+
singleton: options.singleton ?? true,
|
|
68
|
+
kind: "factory",
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get or create a service instance
|
|
74
|
+
*/
|
|
75
|
+
resolve(serviceClass) {
|
|
76
|
+
const key = typeof serviceClass === "string" ? serviceClass : serviceClass;
|
|
77
|
+
const keyStr = typeof serviceClass === "string" ? serviceClass : serviceClass.name;
|
|
78
|
+
// Check for circular dependencies
|
|
79
|
+
if (this.resolutionStack.has(key)) {
|
|
80
|
+
throw new Error(`Circular dependency detected while resolving ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(" -> ")} -> ${keyStr}`);
|
|
81
|
+
}
|
|
82
|
+
const definition = this.services.get(key);
|
|
83
|
+
if (!definition) {
|
|
84
|
+
throw new Error(`Service '${keyStr}' is not registered in the DI container`);
|
|
85
|
+
}
|
|
86
|
+
const wasCached = definition.singleton && !!definition.instance;
|
|
87
|
+
// Return cached singleton
|
|
88
|
+
if (definition.singleton && definition.instance) {
|
|
89
|
+
this.emit("resolved", {
|
|
90
|
+
key,
|
|
91
|
+
instance: definition.instance,
|
|
92
|
+
singleton: true,
|
|
93
|
+
fromCache: true,
|
|
94
|
+
});
|
|
95
|
+
return definition.instance;
|
|
96
|
+
}
|
|
97
|
+
// Resolve dependencies
|
|
98
|
+
this.resolutionStack.add(key);
|
|
99
|
+
try {
|
|
100
|
+
const instance = this.instantiate(definition.type);
|
|
101
|
+
// Cache singleton
|
|
102
|
+
if (definition.singleton) {
|
|
103
|
+
definition.instance = instance;
|
|
104
|
+
}
|
|
105
|
+
this.emit("resolved", {
|
|
106
|
+
key,
|
|
107
|
+
instance,
|
|
108
|
+
singleton: definition.singleton,
|
|
109
|
+
fromCache: wasCached,
|
|
110
|
+
});
|
|
111
|
+
return instance;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
this.resolutionStack.delete(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Construct a new instance without registering it in the container.
|
|
119
|
+
* Supports constructor overrides for primitives/config (constructor pattern).
|
|
120
|
+
* Always returns a fresh instance (no caching).
|
|
121
|
+
*/
|
|
122
|
+
construct(serviceClass, overrides = {}) {
|
|
123
|
+
const keyStr = serviceClass.name;
|
|
124
|
+
if (this.resolutionStack.has(serviceClass)) {
|
|
125
|
+
throw new Error(`Circular dependency detected while constructing ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(" -> ")} -> ${keyStr}`);
|
|
126
|
+
}
|
|
127
|
+
this.resolutionStack.add(serviceClass);
|
|
128
|
+
try {
|
|
129
|
+
const instance = this.instantiate(serviceClass, overrides);
|
|
130
|
+
this.emit("constructed", { key: serviceClass, instance, overrides });
|
|
131
|
+
return instance;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
this.resolutionStack.delete(serviceClass);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check if a service is registered
|
|
139
|
+
*/
|
|
140
|
+
has(serviceClass) {
|
|
141
|
+
return this.services.has(serviceClass);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Clear all registered services
|
|
145
|
+
*/
|
|
146
|
+
clear() {
|
|
147
|
+
const count = this.services.size;
|
|
148
|
+
this.services.clear();
|
|
149
|
+
this.emit("cleared", { count });
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get all registered service names
|
|
153
|
+
*/
|
|
154
|
+
getServiceNames() {
|
|
155
|
+
const names = new Set();
|
|
156
|
+
this.services.forEach((_, key) => {
|
|
157
|
+
if (typeof key === "string") {
|
|
158
|
+
names.add(key);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return Array.from(names);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Fork the container (prototype pattern): clone registrations into a new container.
|
|
165
|
+
* Optionally carry over existing singleton instances.
|
|
166
|
+
*/
|
|
167
|
+
fork(options = {}) {
|
|
168
|
+
const clone = new Container();
|
|
169
|
+
this.services.forEach((def, key) => {
|
|
170
|
+
clone.services.set(key, {
|
|
171
|
+
...def,
|
|
172
|
+
instance: options.carrySingletons ? def.instance : undefined,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
return clone;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Subscribe to container lifecycle events (observer pattern).
|
|
179
|
+
* Returns an unsubscribe function.
|
|
180
|
+
*/
|
|
181
|
+
on(event, listener) {
|
|
182
|
+
if (!this.listeners.has(event)) {
|
|
183
|
+
this.listeners.set(event, new Set());
|
|
184
|
+
}
|
|
185
|
+
this.listeners.get(event).add(listener);
|
|
186
|
+
return () => this.off(event, listener);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Remove a previously registered listener
|
|
190
|
+
*/
|
|
191
|
+
off(event, listener) {
|
|
192
|
+
this.listeners.get(event)?.delete(listener);
|
|
193
|
+
}
|
|
194
|
+
emit(event, payload) {
|
|
195
|
+
const listeners = this.listeners.get(event);
|
|
196
|
+
if (!listeners || listeners.size === 0)
|
|
197
|
+
return;
|
|
198
|
+
listeners.forEach((listener) => {
|
|
199
|
+
try {
|
|
200
|
+
listener(payload);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error(`[Container] listener for '${event}' threw`, err);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Private method to instantiate a service
|
|
209
|
+
*/
|
|
210
|
+
instantiate(type, overrides = {}) {
|
|
211
|
+
if (typeof type !== "function") {
|
|
212
|
+
throw new Error("Service type must be a constructor or factory function");
|
|
213
|
+
}
|
|
214
|
+
// If it's a factory function (not a class), just call it
|
|
215
|
+
if (!this.isClass(type)) {
|
|
216
|
+
return type();
|
|
217
|
+
}
|
|
218
|
+
// Get constructor parameter types from metadata
|
|
219
|
+
const paramTypes = getMetadata(DESIGN_PARAM_TYPES_KEY, type) || [];
|
|
220
|
+
const paramNames = this.getConstructorParamNames(type);
|
|
221
|
+
// Resolve dependencies
|
|
222
|
+
const dependencies = [];
|
|
223
|
+
const injectMetadata = getOwnMetadata(INJECT_METADATA_KEY, type) || {};
|
|
224
|
+
const paramCount = Math.max(paramTypes.length, paramNames.length);
|
|
225
|
+
for (let i = 0; i < paramCount; i++) {
|
|
226
|
+
if (Object.prototype.hasOwnProperty.call(overrides, i)) {
|
|
227
|
+
dependencies.push(overrides[i]);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const paramType = paramTypes[i];
|
|
231
|
+
const paramName = paramNames[i];
|
|
232
|
+
// Prefer explicit @Component() decorator when present
|
|
233
|
+
const paramInjectTarget = injectMetadata[`param_${i}`];
|
|
234
|
+
if (paramInjectTarget) {
|
|
235
|
+
// Use explicit injection target (can be class or string token)
|
|
236
|
+
dependencies.push(this.resolve(paramInjectTarget));
|
|
237
|
+
}
|
|
238
|
+
else if (paramType && paramType !== Object) {
|
|
239
|
+
// Try to resolve by type when metadata is available
|
|
240
|
+
if (this.has(paramType)) {
|
|
241
|
+
dependencies.push(this.resolve(paramType));
|
|
242
|
+
}
|
|
243
|
+
else if (this.has(paramType.name)) {
|
|
244
|
+
dependencies.push(this.resolve(paramType.name));
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
throw new Error(`Cannot resolve dependency of type ${paramType.name} for parameter '${paramName}' in ${type.name}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// No information available for this parameter; leave undefined (constructor may provide default)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Create instance
|
|
255
|
+
const instance = new type(...dependencies);
|
|
256
|
+
// Apply Telemetry and TelemetryListener
|
|
257
|
+
this.applyTelemetry(instance, type);
|
|
258
|
+
// Call @Component() decorators on properties
|
|
259
|
+
// Check both the instance and the constructor prototype for metadata
|
|
260
|
+
const injectProperties = getMetadata(INJECT_METADATA_KEY, type) || {};
|
|
261
|
+
const protoInjectProperties = getMetadata(INJECT_METADATA_KEY, type.prototype) ||
|
|
262
|
+
{};
|
|
263
|
+
const allInjectProperties = {
|
|
264
|
+
...injectProperties,
|
|
265
|
+
...protoInjectProperties,
|
|
266
|
+
};
|
|
267
|
+
Object.entries(allInjectProperties).forEach(([propName, targetType]) => {
|
|
268
|
+
if (!propName.startsWith("param_") && targetType) {
|
|
269
|
+
try {
|
|
270
|
+
instance[propName] = this.resolve(targetType);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.warn(`Failed to inject property '${propName}' on ${type.name}:`, error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return instance;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Apply telemetry tracking and listeners to an instance
|
|
281
|
+
*/
|
|
282
|
+
applyTelemetry(instance, constructor) {
|
|
283
|
+
const className = constructor.name;
|
|
284
|
+
// Handle @TelemetryListener
|
|
285
|
+
const listenerMethods = getMetadata(TELEMETRY_LISTENER_METADATA_KEY, constructor.prototype) || [];
|
|
286
|
+
listenerMethods.forEach((methodName) => {
|
|
287
|
+
const method = instance[methodName];
|
|
288
|
+
if (typeof method === "function") {
|
|
289
|
+
this.on("telemetry", (payload) => {
|
|
290
|
+
try {
|
|
291
|
+
method.call(instance, payload);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
console.error(`[Container] TelemetryListener '${className}.${methodName}' threw`, err);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
// Handle @Telemetry
|
|
300
|
+
const telemetryMethods = getMetadata(TELEMETRY_METADATA_KEY, constructor.prototype) || {};
|
|
301
|
+
Object.entries(telemetryMethods).forEach(([methodName, options]) => {
|
|
302
|
+
const originalMethod = instance[methodName];
|
|
303
|
+
if (typeof originalMethod === "function") {
|
|
304
|
+
const self = this;
|
|
305
|
+
instance[methodName] = function (...args) {
|
|
306
|
+
const startTime = Date.now();
|
|
307
|
+
const emit = (result, error) => {
|
|
308
|
+
const payload = {
|
|
309
|
+
className,
|
|
310
|
+
methodName,
|
|
311
|
+
args,
|
|
312
|
+
startTime,
|
|
313
|
+
endTime: Date.now(),
|
|
314
|
+
result,
|
|
315
|
+
error,
|
|
316
|
+
};
|
|
317
|
+
if (options.logging) {
|
|
318
|
+
const duration = payload.endTime - payload.startTime;
|
|
319
|
+
const status = error
|
|
320
|
+
? `ERROR: ${error.message || error}`
|
|
321
|
+
: "SUCCESS";
|
|
322
|
+
console.log(`[Telemetry] ${className}.${methodName} - ${status} (${duration}ms)`);
|
|
323
|
+
}
|
|
324
|
+
self.emit("telemetry", payload);
|
|
325
|
+
};
|
|
326
|
+
try {
|
|
327
|
+
const result = originalMethod.apply(this, args);
|
|
328
|
+
if (result instanceof Promise) {
|
|
329
|
+
return result
|
|
330
|
+
.then((val) => {
|
|
331
|
+
emit(val);
|
|
332
|
+
return val;
|
|
333
|
+
})
|
|
334
|
+
.catch((err) => {
|
|
335
|
+
emit(undefined, err);
|
|
336
|
+
throw err;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
emit(result);
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
emit(undefined, err);
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Check if a function is a class constructor
|
|
352
|
+
*/
|
|
353
|
+
isClass(func) {
|
|
354
|
+
return (typeof func === "function" &&
|
|
355
|
+
func.prototype &&
|
|
356
|
+
func.prototype.constructor === func);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Extract parameter names from constructor
|
|
360
|
+
*/
|
|
361
|
+
getConstructorParamNames(target) {
|
|
362
|
+
const funcStr = target.toString();
|
|
363
|
+
const match = funcStr.match(/constructor\s*\(([^)]*)\)/);
|
|
364
|
+
if (!match || !match[1])
|
|
365
|
+
return [];
|
|
366
|
+
const paramsStr = match[1];
|
|
367
|
+
return paramsStr
|
|
368
|
+
.split(",")
|
|
369
|
+
.map((param) => {
|
|
370
|
+
const trimmed = param.trim();
|
|
371
|
+
const withoutDefault = trimmed.split("=")[0] || "";
|
|
372
|
+
const withoutType = withoutDefault.split(":")[0] || "";
|
|
373
|
+
return withoutType.trim();
|
|
374
|
+
})
|
|
375
|
+
.filter((param) => param);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Extract parameter types from TypeScript compiled code
|
|
379
|
+
* Looks for type annotations in the compiled constructor signature
|
|
380
|
+
*/
|
|
381
|
+
extractParamTypesFromSource(target) {
|
|
382
|
+
const funcStr = target.toString();
|
|
383
|
+
// Try to extract types from decorated constructor
|
|
384
|
+
// In compiled TypeScript with emitDecoratorMetadata, types appear in decorator calls
|
|
385
|
+
const decoratorMatch = funcStr.match(/__decorate\(\[\s*(?:\w+\s*\([^)]*\),?\s*)*__param\((\d+),\s*(\w+)\([^)]*\)\)/g);
|
|
386
|
+
if (decoratorMatch) {
|
|
387
|
+
// Found decorator-based metadata
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
// Return empty array - will fall back to type annotations or @Component decorators
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Global DI container instance
|
|
396
|
+
*/
|
|
397
|
+
export const container = new Container();
|
|
398
|
+
/**
|
|
399
|
+
* Get the global DI container
|
|
400
|
+
*/
|
|
401
|
+
export function useContainer() {
|
|
402
|
+
return container;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Export metadata functions for use in decorators
|
|
406
|
+
* These provide a simple, reflect-metadata-free way to store and access metadata
|
|
407
|
+
*/
|
|
408
|
+
export { defineMetadata, getMetadata, hasMetadata, getOwnMetadata };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Injection Decorators
|
|
3
|
+
*
|
|
4
|
+
* @Container - Marks a class as injectable
|
|
5
|
+
* @Component - Marks dependencies for injection (constructor parameters or properties)
|
|
6
|
+
*
|
|
7
|
+
* Works with SWC and TypeScript's native decorator support.
|
|
8
|
+
* No external dependencies required (no reflect-metadata needed).
|
|
9
|
+
*/
|
|
10
|
+
import { Container as DIContainer } from "./container";
|
|
11
|
+
/**
|
|
12
|
+
* Options for the @Telemetry decorator
|
|
13
|
+
*/
|
|
14
|
+
export interface TelemetryOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Whether to log the telemetry event to the console.
|
|
17
|
+
* Defaults to false.
|
|
18
|
+
*/
|
|
19
|
+
logging?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Marks a method for telemetry tracking.
|
|
23
|
+
* When called, it will emit a 'telemetry' event on the container.
|
|
24
|
+
* Compatible with async and sync methods.
|
|
25
|
+
*
|
|
26
|
+
* @param options Configuration options for telemetry
|
|
27
|
+
*/
|
|
28
|
+
export declare function Telemetry(options?: TelemetryOptions): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Marks a method as a listener for telemetry events.
|
|
31
|
+
* The method will be automatically registered to the container's 'telemetry' event.
|
|
32
|
+
*/
|
|
33
|
+
export declare function TelemetryListener(): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Marks a class as injectable and registers it with the DI container
|
|
36
|
+
*
|
|
37
|
+
* @param options Configuration options for the injectable service
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* @Container()
|
|
41
|
+
* class UserService {
|
|
42
|
+
* getUser(id: string) { ... }
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* @example With options
|
|
46
|
+
* @Container({ singleton: false })
|
|
47
|
+
* class RequestScopedService {
|
|
48
|
+
* // New instance created for each resolution
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
export declare function Container(options?: {
|
|
52
|
+
singleton?: boolean;
|
|
53
|
+
container?: DIContainer;
|
|
54
|
+
}): <T extends {
|
|
55
|
+
new (...args: any[]): {};
|
|
56
|
+
}>(constructor: T) => T;
|
|
57
|
+
/**
|
|
58
|
+
* Marks a constructor parameter or property for dependency injection
|
|
59
|
+
*
|
|
60
|
+
* Can be used on:
|
|
61
|
+
* - Constructor parameters
|
|
62
|
+
* - Class properties
|
|
63
|
+
*
|
|
64
|
+
* @param target The class to inject dependencies into. Can be a class constructor or a string identifier.
|
|
65
|
+
*
|
|
66
|
+
* @example Constructor parameter injection
|
|
67
|
+
* @Container()
|
|
68
|
+
* class UserController {
|
|
69
|
+
* constructor(@Component(UserService) userService: UserService) {}
|
|
70
|
+
* }
|
|
71
|
+
*
|
|
72
|
+
* @example Property injection
|
|
73
|
+
* @Container()
|
|
74
|
+
* class UserService {
|
|
75
|
+
* @Component(DatabaseConnection)
|
|
76
|
+
* private db: DatabaseConnection;
|
|
77
|
+
* }
|
|
78
|
+
*
|
|
79
|
+
* @example With string identifier
|
|
80
|
+
* @Container()
|
|
81
|
+
* class PaymentService {
|
|
82
|
+
* constructor(@Component('apiKey') apiKey: string) {}
|
|
83
|
+
* }
|
|
84
|
+
*/
|
|
85
|
+
export declare function Component(target: any): (targetClass: Object | any, propertyKey?: string | symbol, parameterIndex?: number) => void;
|
|
86
|
+
/**
|
|
87
|
+
* Check if a class is marked as injectable
|
|
88
|
+
*/
|
|
89
|
+
export declare function isInjectable(target: any): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Get the container instance used by decorators
|
|
92
|
+
*/
|
|
93
|
+
export declare function getInjectionContainer(): DIContainer;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Injection Decorators
|
|
3
|
+
*
|
|
4
|
+
* @Container - Marks a class as injectable
|
|
5
|
+
* @Component - Marks dependencies for injection (constructor parameters or properties)
|
|
6
|
+
*
|
|
7
|
+
* Works with SWC and TypeScript's native decorator support.
|
|
8
|
+
* No external dependencies required (no reflect-metadata needed).
|
|
9
|
+
*/
|
|
10
|
+
import { useContainer, Container as DIContainer, defineMetadata, getOwnMetadata, getMetadata, TELEMETRY_METADATA_KEY, TELEMETRY_LISTENER_METADATA_KEY, } from "./container";
|
|
11
|
+
const INJECTABLE_METADATA_KEY = "di:injectable";
|
|
12
|
+
const INJECT_METADATA_KEY = "di:inject";
|
|
13
|
+
/**
|
|
14
|
+
* Marks a method for telemetry tracking.
|
|
15
|
+
* When called, it will emit a 'telemetry' event on the container.
|
|
16
|
+
* Compatible with async and sync methods.
|
|
17
|
+
*
|
|
18
|
+
* @param options Configuration options for telemetry
|
|
19
|
+
*/
|
|
20
|
+
export function Telemetry(options = {}) {
|
|
21
|
+
return function (target, propertyKey, descriptor) {
|
|
22
|
+
const methods = getOwnMetadata(TELEMETRY_METADATA_KEY, target) || {};
|
|
23
|
+
methods[propertyKey] = options;
|
|
24
|
+
defineMetadata(TELEMETRY_METADATA_KEY, methods, target);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Marks a method as a listener for telemetry events.
|
|
29
|
+
* The method will be automatically registered to the container's 'telemetry' event.
|
|
30
|
+
*/
|
|
31
|
+
export function TelemetryListener() {
|
|
32
|
+
return function (target, propertyKey, descriptor) {
|
|
33
|
+
const listeners = getOwnMetadata(TELEMETRY_LISTENER_METADATA_KEY, target) || [];
|
|
34
|
+
listeners.push(propertyKey);
|
|
35
|
+
defineMetadata(TELEMETRY_LISTENER_METADATA_KEY, listeners, target);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Marks a class as injectable and registers it with the DI container
|
|
40
|
+
*
|
|
41
|
+
* @param options Configuration options for the injectable service
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* @Container()
|
|
45
|
+
* class UserService {
|
|
46
|
+
* getUser(id: string) { ... }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* @example With options
|
|
50
|
+
* @Container({ singleton: false })
|
|
51
|
+
* class RequestScopedService {
|
|
52
|
+
* // New instance created for each resolution
|
|
53
|
+
* }
|
|
54
|
+
*/
|
|
55
|
+
export function Container(options = {}) {
|
|
56
|
+
return function (constructor) {
|
|
57
|
+
const container = options.container ?? useContainer();
|
|
58
|
+
const singleton = options.singleton ?? true;
|
|
59
|
+
// Mark as injectable using our metadata store
|
|
60
|
+
defineMetadata(INJECTABLE_METADATA_KEY, true, constructor);
|
|
61
|
+
// Register with container
|
|
62
|
+
container.register(constructor, { singleton });
|
|
63
|
+
return constructor;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Marks a constructor parameter or property for dependency injection
|
|
68
|
+
*
|
|
69
|
+
* Can be used on:
|
|
70
|
+
* - Constructor parameters
|
|
71
|
+
* - Class properties
|
|
72
|
+
*
|
|
73
|
+
* @param target The class to inject dependencies into. Can be a class constructor or a string identifier.
|
|
74
|
+
*
|
|
75
|
+
* @example Constructor parameter injection
|
|
76
|
+
* @Container()
|
|
77
|
+
* class UserController {
|
|
78
|
+
* constructor(@Component(UserService) userService: UserService) {}
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* @example Property injection
|
|
82
|
+
* @Container()
|
|
83
|
+
* class UserService {
|
|
84
|
+
* @Component(DatabaseConnection)
|
|
85
|
+
* private db: DatabaseConnection;
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* @example With string identifier
|
|
89
|
+
* @Container()
|
|
90
|
+
* class PaymentService {
|
|
91
|
+
* constructor(@Component('apiKey') apiKey: string) {}
|
|
92
|
+
* }
|
|
93
|
+
*/
|
|
94
|
+
export function Component(target) {
|
|
95
|
+
return function (targetClass, propertyKey, parameterIndex) {
|
|
96
|
+
// Property injection
|
|
97
|
+
if (propertyKey && parameterIndex === undefined) {
|
|
98
|
+
// Store on both the class and its prototype to ensure it's accessible
|
|
99
|
+
const metadata = getOwnMetadata(INJECT_METADATA_KEY, targetClass) || {};
|
|
100
|
+
metadata[propertyKey] = target;
|
|
101
|
+
defineMetadata(INJECT_METADATA_KEY, metadata, targetClass);
|
|
102
|
+
// Also store on the constructor if we have it
|
|
103
|
+
if (targetClass.constructor && targetClass.constructor !== Object) {
|
|
104
|
+
const constructorMetadata = getOwnMetadata(INJECT_METADATA_KEY, targetClass.constructor) || {};
|
|
105
|
+
constructorMetadata[propertyKey] = target;
|
|
106
|
+
defineMetadata(INJECT_METADATA_KEY, constructorMetadata, targetClass.constructor);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Constructor parameter injection
|
|
110
|
+
else if (parameterIndex !== undefined) {
|
|
111
|
+
const metadata = getOwnMetadata(INJECT_METADATA_KEY, targetClass) || {};
|
|
112
|
+
metadata[`param_${parameterIndex}`] = target;
|
|
113
|
+
defineMetadata(INJECT_METADATA_KEY, metadata, targetClass);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if a class is marked as injectable
|
|
119
|
+
*/
|
|
120
|
+
export function isInjectable(target) {
|
|
121
|
+
return getMetadata(INJECTABLE_METADATA_KEY, target) === true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the container instance used by decorators
|
|
125
|
+
*/
|
|
126
|
+
export function getInjectionContainer() {
|
|
127
|
+
return useContainer();
|
|
128
|
+
}
|