@martel/calyx 0.1.0 → 1.1.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/.github/workflows/release.yml +6 -2
- package/CHANGELOG.md +13 -0
- package/README.md +130 -0
- package/benchmarks/di-benchmark.ts +15 -15
- package/benchmarks/run-calyx-lifecycle.ts +2 -2
- package/benchmarks/run-calyx.ts +2 -2
- package/bun.lock +1261 -0
- package/docs/controllers.md +2 -2
- package/docs/dependency-injection.md +1 -1
- package/docs/lifecycle.md +2 -2
- package/docs/migration.md +5 -5
- package/package.json +9 -5
- package/src/cli/index.ts +323 -0
- package/src/core/container.ts +252 -27
- package/src/core/decorators.ts +64 -10
- package/src/core/index.ts +1 -0
- package/src/core/metadata.ts +13 -0
- package/src/core/module-ref.ts +3 -1
- package/src/core/reflector.ts +32 -0
- package/src/http/application.ts +323 -154
- package/src/http/decorators.ts +29 -8
- package/src/http/factory.ts +4 -4
- package/src/http/router.ts +12 -0
- package/src/lifecycle/context.ts +2 -2
- package/src/lifecycle/interfaces.ts +20 -0
- package/tests/cli.test.ts +93 -0
- package/tests/di.test.ts +11 -11
- package/tests/dynamic-module.test.ts +2 -2
- package/tests/lifecycle.test.ts +4 -4
- package/tests/phase1.test.ts +143 -0
- package/tests/phase2.test.ts +107 -0
- package/tests/phase3.test.ts +203 -0
- package/tests/phase5.test.ts +73 -0
- package/tests/routing.test.ts +4 -4
package/src/core/container.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
ModuleMetadata,
|
|
7
7
|
Type,
|
|
8
8
|
resolveForwardRef,
|
|
9
|
+
Scope,
|
|
10
|
+
REQUEST,
|
|
9
11
|
} from './metadata.ts';
|
|
10
12
|
import { ModuleRef } from './module-ref.ts';
|
|
11
13
|
|
|
@@ -28,7 +30,7 @@ interface ModuleRecord {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
class ContainerModuleRef extends ModuleRef {
|
|
31
|
-
constructor(private container:
|
|
33
|
+
constructor(private container: CalyxContainer, private moduleClass: any) {
|
|
32
34
|
super();
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -40,12 +42,28 @@ class ContainerModuleRef extends ModuleRef {
|
|
|
40
42
|
return this.container.getGlobalOrAnyInstance(token);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
async resolve<T>(token: InjectionToken, contextId?: any, options?: { strict: boolean }): Promise<T> {
|
|
47
|
+
const requestContext = contextId instanceof Map ? contextId : new Map<any, any>();
|
|
48
|
+
const strict = options?.strict ?? true;
|
|
49
|
+
if (strict) {
|
|
50
|
+
return this.container.resolveTokenInModuleContext(this.moduleClass, token, requestContext);
|
|
51
|
+
} else {
|
|
52
|
+
return this.container.resolveTokenGlobally(token, requestContext);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async create<T>(type: Type<T>): Promise<T> {
|
|
57
|
+
return this.container.instantiateClass(type, this.moduleClass);
|
|
58
|
+
}
|
|
43
59
|
}
|
|
44
60
|
|
|
45
|
-
export class
|
|
61
|
+
export class CalyxContainer {
|
|
46
62
|
private modules = new Map<any, ModuleRecord>();
|
|
47
63
|
private globalModules = new Set<any>();
|
|
48
64
|
private resolvingStack: { moduleClass: any; token: InjectionToken }[] = [];
|
|
65
|
+
private providerScopes = new Map<InjectionToken, Scope>();
|
|
66
|
+
private controllerScopes = new Map<any, Scope>();
|
|
49
67
|
|
|
50
68
|
getModuleRecord(moduleClass: any): ModuleRecord | undefined {
|
|
51
69
|
return this.modules.get(resolveForwardRef(moduleClass));
|
|
@@ -55,6 +73,19 @@ export class calyxContainer {
|
|
|
55
73
|
return this.modules;
|
|
56
74
|
}
|
|
57
75
|
|
|
76
|
+
getProviderAndControllerInstances(): any[] {
|
|
77
|
+
const instances = new Set<any>();
|
|
78
|
+
for (const record of this.modules.values()) {
|
|
79
|
+
for (const [token, instance] of record.instances.entries()) {
|
|
80
|
+
if (token === ModuleRef) continue;
|
|
81
|
+
if (instance && typeof instance === 'object') {
|
|
82
|
+
instances.add(instance);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Array.from(instances);
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
addModule(moduleClass: any) {
|
|
59
90
|
const resolvedClass = resolveForwardRef(moduleClass);
|
|
60
91
|
if (this.modules.has(resolvedClass)) return;
|
|
@@ -146,7 +177,7 @@ export class calyxContainer {
|
|
|
146
177
|
return provider.provide;
|
|
147
178
|
}
|
|
148
179
|
|
|
149
|
-
resolveTokenInModuleContext<T>(moduleClass: any, token: InjectionToken): T {
|
|
180
|
+
resolveTokenInModuleContext<T>(moduleClass: any, token: InjectionToken, requestContext?: Map<any, any>): T {
|
|
150
181
|
// 1. Check circular dependency
|
|
151
182
|
const isResolving = this.resolvingStack.some(
|
|
152
183
|
(item) => item.moduleClass === moduleClass && item.token === token
|
|
@@ -167,6 +198,13 @@ export class calyxContainer {
|
|
|
167
198
|
}
|
|
168
199
|
|
|
169
200
|
// Special resolution cases
|
|
201
|
+
if (token === REQUEST) {
|
|
202
|
+
if (!requestContext || !requestContext.has(REQUEST)) {
|
|
203
|
+
throw new Error(`calyx DI: REQUEST token resolved outside of a request context`);
|
|
204
|
+
}
|
|
205
|
+
return requestContext.get(REQUEST);
|
|
206
|
+
}
|
|
207
|
+
|
|
170
208
|
if (token === ModuleRef) {
|
|
171
209
|
const instance = record.instances.get(ModuleRef);
|
|
172
210
|
if (instance) return instance;
|
|
@@ -178,7 +216,7 @@ export class calyxContainer {
|
|
|
178
216
|
if (token === moduleClass) {
|
|
179
217
|
const instance = record.instances.get(moduleClass);
|
|
180
218
|
if (instance) return instance;
|
|
181
|
-
const instantiatedModule = this.instantiateClass(moduleClass, moduleClass);
|
|
219
|
+
const instantiatedModule = this.instantiateClass(moduleClass, moduleClass, requestContext);
|
|
182
220
|
record.instances.set(moduleClass, instantiatedModule);
|
|
183
221
|
return instantiatedModule;
|
|
184
222
|
}
|
|
@@ -215,26 +253,30 @@ export class calyxContainer {
|
|
|
215
253
|
const { targetModuleClass, provider } = resolution;
|
|
216
254
|
const targetRecord = this.modules.get(targetModuleClass)!;
|
|
217
255
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
256
|
+
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
257
|
+
|
|
258
|
+
if (scope === Scope.REQUEST) {
|
|
259
|
+
if (!requestContext) {
|
|
260
|
+
throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(token.name ?? token)}" without request context.`);
|
|
261
|
+
}
|
|
262
|
+
if (requestContext.has(token)) {
|
|
263
|
+
return requestContext.get(token);
|
|
264
|
+
}
|
|
265
|
+
const instance = this.createInstanceFromProvider(provider, targetModuleClass, requestContext);
|
|
266
|
+
requestContext.set(token, instance);
|
|
267
|
+
return instance;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (scope === Scope.TRANSIENT) {
|
|
271
|
+
return this.createInstanceFromProvider(provider, targetModuleClass, requestContext);
|
|
221
272
|
}
|
|
222
273
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
instance = provider.useValue;
|
|
227
|
-
} else if (typeof provider !== 'function' && 'useClass' in provider) {
|
|
228
|
-
instance = this.instantiateClass(provider.useClass, targetModuleClass);
|
|
229
|
-
} else if (typeof provider !== 'function' && 'useFactory' in provider) {
|
|
230
|
-
const injectTokens = provider.inject || [];
|
|
231
|
-
const args = injectTokens.map((t) => this.resolveTokenInModuleContext(targetModuleClass, resolveForwardRef(t)));
|
|
232
|
-
instance = provider.useFactory(...args);
|
|
233
|
-
} else {
|
|
234
|
-
// Raw class provider
|
|
235
|
-
instance = this.instantiateClass(provider as Type<any>, targetModuleClass);
|
|
274
|
+
// Check if instance already exists in target module (Singleton)
|
|
275
|
+
if (targetRecord.instances.has(token)) {
|
|
276
|
+
return targetRecord.instances.get(token);
|
|
236
277
|
}
|
|
237
278
|
|
|
279
|
+
const instance = this.createInstanceFromProvider(provider, targetModuleClass, requestContext);
|
|
238
280
|
targetRecord.instances.set(token, instance);
|
|
239
281
|
return instance;
|
|
240
282
|
} finally {
|
|
@@ -242,6 +284,20 @@ export class calyxContainer {
|
|
|
242
284
|
}
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
private createInstanceFromProvider(provider: Provider, targetModuleClass: any, requestContext?: Map<any, any>): any {
|
|
288
|
+
if (typeof provider !== 'function' && 'useValue' in provider) {
|
|
289
|
+
return provider.useValue;
|
|
290
|
+
} else if (typeof provider !== 'function' && 'useClass' in provider) {
|
|
291
|
+
return this.instantiateClass(provider.useClass, targetModuleClass, requestContext);
|
|
292
|
+
} else if (typeof provider !== 'function' && 'useFactory' in provider) {
|
|
293
|
+
const injectTokens = provider.inject || [];
|
|
294
|
+
const args = injectTokens.map((t) => this.resolveTokenInModuleContext(targetModuleClass, resolveForwardRef(t), requestContext));
|
|
295
|
+
return provider.useFactory(...args);
|
|
296
|
+
} else {
|
|
297
|
+
return this.instantiateClass(provider as Type<any>, targetModuleClass, requestContext);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
245
301
|
private isTokenExportedByModule(moduleClass: any, token: InjectionToken, visited = new Set<any>()): boolean {
|
|
246
302
|
if (visited.has(moduleClass)) return false;
|
|
247
303
|
visited.add(moduleClass);
|
|
@@ -310,7 +366,7 @@ export class calyxContainer {
|
|
|
310
366
|
return null;
|
|
311
367
|
}
|
|
312
368
|
|
|
313
|
-
|
|
369
|
+
public instantiateClass(Class: Type<any>, moduleClass: any, requestContext?: Map<any, any>): any {
|
|
314
370
|
if (
|
|
315
371
|
!Reflect.hasMetadata(METADATA_KEYS.INJECTABLE, Class) &&
|
|
316
372
|
!Reflect.hasMetadata(METADATA_KEYS.CONTROLLER, Class) &&
|
|
@@ -328,7 +384,7 @@ export class calyxContainer {
|
|
|
328
384
|
const resolvedToken = resolveForwardRef(token);
|
|
329
385
|
|
|
330
386
|
try {
|
|
331
|
-
return this.resolveTokenInModuleContext(moduleClass, resolvedToken);
|
|
387
|
+
return this.resolveTokenInModuleContext(moduleClass, resolvedToken, requestContext);
|
|
332
388
|
} catch (err) {
|
|
333
389
|
if (optionalParams.has(i)) {
|
|
334
390
|
return undefined;
|
|
@@ -337,7 +393,16 @@ export class calyxContainer {
|
|
|
337
393
|
}
|
|
338
394
|
});
|
|
339
395
|
|
|
340
|
-
|
|
396
|
+
const instance = new Class(...args);
|
|
397
|
+
|
|
398
|
+
// Resolve property-level injections
|
|
399
|
+
const propertyInjects: Map<string | symbol, InjectionToken> =
|
|
400
|
+
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
|
|
401
|
+
for (const [propertyKey, token] of propertyInjects.entries()) {
|
|
402
|
+
instance[propertyKey] = this.resolveTokenInModuleContext(moduleClass, token, requestContext);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return instance;
|
|
341
406
|
}
|
|
342
407
|
|
|
343
408
|
getGlobalOrAnyInstance(token: InjectionToken): any {
|
|
@@ -352,17 +417,26 @@ export class calyxContainer {
|
|
|
352
417
|
bootstrap(rootModule: any) {
|
|
353
418
|
this.addModule(rootModule);
|
|
354
419
|
|
|
355
|
-
//
|
|
420
|
+
// Compute scopes of all providers and controllers (including bubble-up)
|
|
421
|
+
this.resolveProviderAndControllerScopes();
|
|
422
|
+
|
|
423
|
+
// Instantiate all singleton providers
|
|
356
424
|
for (const [moduleClass, record] of this.modules.entries()) {
|
|
357
425
|
for (const token of record.providers.keys()) {
|
|
358
|
-
this.
|
|
426
|
+
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
427
|
+
if (scope === Scope.DEFAULT) {
|
|
428
|
+
this.resolveTokenInModuleContext(moduleClass, token);
|
|
429
|
+
}
|
|
359
430
|
}
|
|
360
431
|
}
|
|
361
432
|
|
|
362
|
-
// Instantiate all controllers
|
|
433
|
+
// Instantiate all singleton controllers
|
|
363
434
|
for (const [moduleClass, record] of this.modules.entries()) {
|
|
364
435
|
for (const controllerClass of record.controllers) {
|
|
365
|
-
this.
|
|
436
|
+
const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
|
|
437
|
+
if (scope === Scope.DEFAULT) {
|
|
438
|
+
this.resolveController(moduleClass, controllerClass);
|
|
439
|
+
}
|
|
366
440
|
}
|
|
367
441
|
}
|
|
368
442
|
|
|
@@ -384,4 +458,155 @@ export class calyxContainer {
|
|
|
384
458
|
record.instances.set(controllerClass, instance);
|
|
385
459
|
return instance;
|
|
386
460
|
}
|
|
461
|
+
|
|
462
|
+
resolveTokenGlobally<T>(token: InjectionToken, requestContext?: Map<any, any>): T {
|
|
463
|
+
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
464
|
+
if (scope === Scope.REQUEST && requestContext?.has(token)) {
|
|
465
|
+
return requestContext.get(token);
|
|
466
|
+
}
|
|
467
|
+
for (const [moduleClass, record] of this.modules.entries()) {
|
|
468
|
+
if (record.providers.has(token)) {
|
|
469
|
+
return this.resolveTokenInModuleContext(moduleClass, token, requestContext);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return this.getGlobalOrAnyInstance(token);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
resolveControllerInRequestContext(moduleClass: any, controllerClass: any, requestContext: Map<any, any>): any {
|
|
476
|
+
const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
|
|
477
|
+
if (scope === Scope.REQUEST) {
|
|
478
|
+
if (requestContext.has(controllerClass)) {
|
|
479
|
+
return requestContext.get(controllerClass);
|
|
480
|
+
}
|
|
481
|
+
const instance = this.instantiateClass(controllerClass, moduleClass, requestContext);
|
|
482
|
+
requestContext.set(controllerClass, instance);
|
|
483
|
+
return instance;
|
|
484
|
+
}
|
|
485
|
+
const record = this.modules.get(moduleClass)!;
|
|
486
|
+
return record.instances.get(controllerClass);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
getProviderScope(token: InjectionToken): Scope {
|
|
490
|
+
return this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
getControllerScope(controllerClass: any): Scope {
|
|
494
|
+
return this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private resolveProviderAndControllerScopes() {
|
|
498
|
+
// 1. Resolve direct scopes for providers
|
|
499
|
+
for (const record of this.modules.values()) {
|
|
500
|
+
for (const [token, provider] of record.providers.entries()) {
|
|
501
|
+
let scope = Scope.DEFAULT;
|
|
502
|
+
if (typeof provider === 'function') {
|
|
503
|
+
scope = Reflect.getMetadata(METADATA_KEYS.SCOPE, provider) ?? Scope.DEFAULT;
|
|
504
|
+
} else if (provider && typeof provider === 'object') {
|
|
505
|
+
scope = provider.scope ?? Scope.DEFAULT;
|
|
506
|
+
if (scope === Scope.DEFAULT && 'useClass' in provider) {
|
|
507
|
+
scope = Reflect.getMetadata(METADATA_KEYS.SCOPE, provider.useClass) ?? Scope.DEFAULT;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
this.providerScopes.set(token, scope);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 2. Bubble up Request Scope through provider dependencies
|
|
515
|
+
let changed = true;
|
|
516
|
+
while (changed) {
|
|
517
|
+
changed = false;
|
|
518
|
+
for (const record of this.modules.values()) {
|
|
519
|
+
for (const [token, provider] of record.providers.entries()) {
|
|
520
|
+
const currentScope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
521
|
+
if (currentScope === Scope.REQUEST) continue;
|
|
522
|
+
|
|
523
|
+
let dependsOnRequestScope = false;
|
|
524
|
+
const Class = typeof provider === 'function'
|
|
525
|
+
? provider
|
|
526
|
+
: ('useClass' in provider ? provider.useClass : null);
|
|
527
|
+
|
|
528
|
+
if (Class) {
|
|
529
|
+
const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', Class) || [];
|
|
530
|
+
const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, Class) || new Map();
|
|
531
|
+
for (let i = 0; i < paramTypes.length; i++) {
|
|
532
|
+
const depToken = injectTokens.get(i) ?? paramTypes[i];
|
|
533
|
+
const resolvedDepToken = resolveForwardRef(depToken);
|
|
534
|
+
const depScope = this.providerScopes.get(resolvedDepToken) ?? Scope.DEFAULT;
|
|
535
|
+
if (depScope === Scope.REQUEST) {
|
|
536
|
+
dependsOnRequestScope = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!dependsOnRequestScope) {
|
|
542
|
+
const propertyInjects: Map<string | symbol, InjectionToken> =
|
|
543
|
+
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
|
|
544
|
+
for (const depToken of propertyInjects.values()) {
|
|
545
|
+
const resolvedDepToken = resolveForwardRef(depToken);
|
|
546
|
+
const depScope = this.providerScopes.get(resolvedDepToken) ?? Scope.DEFAULT;
|
|
547
|
+
if (depScope === Scope.REQUEST) {
|
|
548
|
+
dependsOnRequestScope = true;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else if (provider && typeof provider === 'object' && 'useFactory' in provider) {
|
|
554
|
+
const injectTokens = provider.inject || [];
|
|
555
|
+
for (const depToken of injectTokens) {
|
|
556
|
+
const resolvedDepToken = resolveForwardRef(depToken);
|
|
557
|
+
const depScope = this.providerScopes.get(resolvedDepToken) ?? Scope.DEFAULT;
|
|
558
|
+
if (depScope === Scope.REQUEST) {
|
|
559
|
+
dependsOnRequestScope = true;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (dependsOnRequestScope) {
|
|
566
|
+
this.providerScopes.set(token, Scope.REQUEST);
|
|
567
|
+
changed = true;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 3. Resolve scopes for controllers
|
|
574
|
+
for (const record of this.modules.values()) {
|
|
575
|
+
for (const controllerClass of record.controllers) {
|
|
576
|
+
let scope = Reflect.getMetadata(METADATA_KEYS.SCOPE, controllerClass) ?? Scope.DEFAULT;
|
|
577
|
+
if (scope !== Scope.REQUEST) {
|
|
578
|
+
const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', controllerClass) || [];
|
|
579
|
+
const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, controllerClass) || new Map();
|
|
580
|
+
let dependsOnRequestScope = false;
|
|
581
|
+
for (let i = 0; i < paramTypes.length; i++) {
|
|
582
|
+
const depToken = injectTokens.get(i) ?? paramTypes[i];
|
|
583
|
+
const resolvedDepToken = resolveForwardRef(depToken);
|
|
584
|
+
const depScope = this.providerScopes.get(resolvedDepToken) ?? Scope.DEFAULT;
|
|
585
|
+
if (depScope === Scope.REQUEST) {
|
|
586
|
+
dependsOnRequestScope = true;
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (!dependsOnRequestScope) {
|
|
592
|
+
const propertyInjects: Map<string | symbol, InjectionToken> =
|
|
593
|
+
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, controllerClass) || new Map();
|
|
594
|
+
for (const depToken of propertyInjects.values()) {
|
|
595
|
+
const resolvedDepToken = resolveForwardRef(depToken);
|
|
596
|
+
const depScope = this.providerScopes.get(resolvedDepToken) ?? Scope.DEFAULT;
|
|
597
|
+
if (depScope === Scope.REQUEST) {
|
|
598
|
+
dependsOnRequestScope = true;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (dependsOnRequestScope) {
|
|
605
|
+
scope = Scope.REQUEST;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
this.controllerScopes.set(controllerClass, scope);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
387
612
|
}
|
package/src/core/decorators.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import { METADATA_KEYS, ModuleMetadata, InjectionToken, ForwardReference } from './metadata.ts';
|
|
2
|
+
import { METADATA_KEYS, ModuleMetadata, InjectionToken, ForwardReference, Scope } from './metadata.ts';
|
|
3
3
|
|
|
4
4
|
export function Module(metadata: ModuleMetadata): ClassDecorator {
|
|
5
5
|
return (target) => {
|
|
@@ -7,20 +7,44 @@ export function Module(metadata: ModuleMetadata): ClassDecorator {
|
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export interface InjectableOptions {
|
|
11
|
+
scope?: Scope;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Injectable(options?: InjectableOptions): ClassDecorator {
|
|
11
15
|
return (target) => {
|
|
12
16
|
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
17
|
+
if (options?.scope !== undefined) {
|
|
18
|
+
Reflect.defineMetadata(METADATA_KEYS.SCOPE, options.scope, target);
|
|
19
|
+
}
|
|
13
20
|
};
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
export function Inject(token
|
|
17
|
-
return (target, propertyKey, parameterIndex) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Reflect.
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
export function Inject(token?: InjectionToken): ParameterDecorator & PropertyDecorator {
|
|
24
|
+
return (target: any, propertyKey?: string | symbol, parameterIndex?: number) => {
|
|
25
|
+
if (propertyKey !== undefined && parameterIndex === undefined) {
|
|
26
|
+
// Property decorator
|
|
27
|
+
const classTarget = target.constructor;
|
|
28
|
+
const resolvedToken = token ?? Reflect.getMetadata('design:type', target, propertyKey);
|
|
29
|
+
if (!resolvedToken) {
|
|
30
|
+
throw new Error(`Cannot resolve injection token for property "${String(propertyKey)}" of class ${classTarget.name}. Make sure reflect-metadata is imported and emitDecoratorMetadata is enabled, or pass the token explicitly to @Inject().`);
|
|
31
|
+
}
|
|
32
|
+
const existingPropertyInjects: Map<string | symbol, InjectionToken> =
|
|
33
|
+
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, classTarget) || new Map();
|
|
34
|
+
existingPropertyInjects.set(propertyKey, resolvedToken);
|
|
35
|
+
Reflect.defineMetadata(METADATA_KEYS.PROPERTY_INJECTS, existingPropertyInjects, classTarget);
|
|
36
|
+
} else {
|
|
37
|
+
// Parameter decorator
|
|
38
|
+
const classTarget = propertyKey === undefined ? target : target.constructor;
|
|
39
|
+
const resolvedToken = token ?? (Reflect.getMetadata('design:paramtypes', classTarget) || [])[parameterIndex!];
|
|
40
|
+
if (!resolvedToken) {
|
|
41
|
+
throw new Error(`Cannot resolve injection token for constructor parameter at index ${parameterIndex} of class ${classTarget.name}. Make sure reflect-metadata is imported and emitDecoratorMetadata is enabled, or pass the token explicitly to @Inject().`);
|
|
42
|
+
}
|
|
43
|
+
const existingInjectTokens: Map<number, InjectionToken> =
|
|
44
|
+
Reflect.getOwnMetadata(METADATA_KEYS.INJECT_TOKENS, classTarget) || new Map();
|
|
45
|
+
existingInjectTokens.set(parameterIndex!, resolvedToken);
|
|
46
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECT_TOKENS, existingInjectTokens, classTarget);
|
|
47
|
+
}
|
|
24
48
|
};
|
|
25
49
|
}
|
|
26
50
|
|
|
@@ -43,3 +67,33 @@ export function Global(): ClassDecorator {
|
|
|
43
67
|
export function forwardRef(fn: () => any): ForwardReference {
|
|
44
68
|
return { forwardRef: fn };
|
|
45
69
|
}
|
|
70
|
+
|
|
71
|
+
export interface CustomDecorator<TKey = any> {
|
|
72
|
+
(target: object, key?: string | symbol, descriptor?: any): any;
|
|
73
|
+
KEY: TKey;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function SetMetadata<K = any, V = any>(metadataKey: K, metadataValue: V): CustomDecorator<K> {
|
|
77
|
+
const decoratorFn = (target: any, key?: string | symbol, descriptor?: any) => {
|
|
78
|
+
if (descriptor) {
|
|
79
|
+
Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
|
|
80
|
+
return descriptor;
|
|
81
|
+
}
|
|
82
|
+
Reflect.defineMetadata(metadataKey, metadataValue, target);
|
|
83
|
+
return target;
|
|
84
|
+
};
|
|
85
|
+
decoratorFn.KEY = metadataKey;
|
|
86
|
+
return decoratorFn;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function applyDecorators(...decorators: any[]) {
|
|
90
|
+
return (target: any, propertyKey?: string | symbol, descriptor?: any) => {
|
|
91
|
+
for (const decorator of decorators) {
|
|
92
|
+
if (target instanceof Function && !propertyKey) {
|
|
93
|
+
decorator(target);
|
|
94
|
+
} else {
|
|
95
|
+
decorator(target, propertyKey, descriptor);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/core/index.ts
CHANGED
package/src/core/metadata.ts
CHANGED
|
@@ -15,6 +15,8 @@ export const METADATA_KEYS = {
|
|
|
15
15
|
INTERCEPTORS: 'calyx:interceptors',
|
|
16
16
|
PIPES: 'calyx:pipes',
|
|
17
17
|
FILTERS: 'calyx:filters',
|
|
18
|
+
PROPERTY_INJECTS: 'calyx:property_injects',
|
|
19
|
+
SCOPE: 'calyx:scope',
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
export interface Type<T = any> extends Function {
|
|
@@ -26,17 +28,20 @@ export type InjectionToken = string | symbol | Type<any>;
|
|
|
26
28
|
export interface ValueProvider {
|
|
27
29
|
provide: InjectionToken;
|
|
28
30
|
useValue: any;
|
|
31
|
+
scope?: Scope;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
export interface ClassProvider {
|
|
32
35
|
provide: InjectionToken;
|
|
33
36
|
useClass: Type<any>;
|
|
37
|
+
scope?: Scope;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export interface FactoryProvider {
|
|
37
41
|
provide: InjectionToken;
|
|
38
42
|
useFactory: (...args: any[]) => any;
|
|
39
43
|
inject?: InjectionToken[];
|
|
44
|
+
scope?: Scope;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
export type Provider = Type<any> | ValueProvider | ClassProvider | FactoryProvider;
|
|
@@ -59,3 +64,11 @@ export function isForwardReference(obj: any): obj is ForwardReference {
|
|
|
59
64
|
export function resolveForwardRef(typeOrFunc: any): any {
|
|
60
65
|
return isForwardReference(typeOrFunc) ? typeOrFunc.forwardRef() : typeOrFunc;
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
export enum Scope {
|
|
69
|
+
DEFAULT = 0,
|
|
70
|
+
TRANSIENT = 1,
|
|
71
|
+
REQUEST = 2,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const REQUEST = Symbol('REQUEST');
|
package/src/core/module-ref.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { InjectionToken } from './metadata.ts';
|
|
1
|
+
import { InjectionToken, Type } from './metadata.ts';
|
|
2
2
|
|
|
3
3
|
export abstract class ModuleRef {
|
|
4
4
|
abstract get<T>(token: InjectionToken, options?: { strict: boolean }): T;
|
|
5
|
+
abstract resolve<T>(token: InjectionToken, contextId?: any, options?: { strict: boolean }): Promise<T>;
|
|
6
|
+
abstract create<T>(type: Type<T>): Promise<T>;
|
|
5
7
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Injectable } from './decorators.ts';
|
|
2
|
+
import { Type } from './metadata.ts';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class Reflector {
|
|
6
|
+
get<T = any>(metadataKey: any, target: Function | Type<any>): T {
|
|
7
|
+
return Reflect.getMetadata(metadataKey, target);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getAllAndOverride<T = any>(metadataKey: any, targets: (Function | Type<any>)[]): T {
|
|
11
|
+
for (const target of targets) {
|
|
12
|
+
if (!target) continue;
|
|
13
|
+
const val = Reflect.getMetadata(metadataKey, target);
|
|
14
|
+
if (val !== undefined) return val;
|
|
15
|
+
}
|
|
16
|
+
return undefined as any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getAllAndMerge<T = any>(metadataKey: any, targets: (Function | Type<any>)[]): T {
|
|
20
|
+
const result: any[] = [];
|
|
21
|
+
for (const target of targets) {
|
|
22
|
+
if (!target) continue;
|
|
23
|
+
const val = Reflect.getMetadata(metadataKey, target);
|
|
24
|
+
if (Array.isArray(val)) {
|
|
25
|
+
result.push(...val);
|
|
26
|
+
} else if (val !== undefined) {
|
|
27
|
+
result.push(val);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result as any as T;
|
|
31
|
+
}
|
|
32
|
+
}
|