@martel/calyx 0.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.
@@ -0,0 +1,387 @@
1
+ import 'reflect-metadata';
2
+ import {
3
+ METADATA_KEYS,
4
+ Provider,
5
+ InjectionToken,
6
+ ModuleMetadata,
7
+ Type,
8
+ resolveForwardRef,
9
+ } from './metadata.ts';
10
+ import { ModuleRef } from './module-ref.ts';
11
+
12
+ export interface DynamicModule extends ModuleMetadata {
13
+ module: any;
14
+ global?: boolean;
15
+ }
16
+
17
+ export function isDynamicModule(obj: any): obj is DynamicModule {
18
+ return obj && typeof obj === 'object' && 'module' in obj;
19
+ }
20
+
21
+ interface ModuleRecord {
22
+ moduleClass: any;
23
+ imports: Set<any>;
24
+ providers: Map<InjectionToken, Provider>;
25
+ controllers: Set<any>;
26
+ exports: Set<any>;
27
+ instances: Map<InjectionToken, any>;
28
+ }
29
+
30
+ class ContainerModuleRef extends ModuleRef {
31
+ constructor(private container: calyxContainer, private moduleClass: any) {
32
+ super();
33
+ }
34
+
35
+ get<T>(token: InjectionToken, options?: { strict: boolean }): T {
36
+ const strict = options?.strict ?? true;
37
+ if (strict) {
38
+ return this.container.resolveTokenInModuleContext(this.moduleClass, token);
39
+ } else {
40
+ return this.container.getGlobalOrAnyInstance(token);
41
+ }
42
+ }
43
+ }
44
+
45
+ export class calyxContainer {
46
+ private modules = new Map<any, ModuleRecord>();
47
+ private globalModules = new Set<any>();
48
+ private resolvingStack: { moduleClass: any; token: InjectionToken }[] = [];
49
+
50
+ getModuleRecord(moduleClass: any): ModuleRecord | undefined {
51
+ return this.modules.get(resolveForwardRef(moduleClass));
52
+ }
53
+
54
+ getModules(): Map<any, ModuleRecord> {
55
+ return this.modules;
56
+ }
57
+
58
+ addModule(moduleClass: any) {
59
+ const resolvedClass = resolveForwardRef(moduleClass);
60
+ if (this.modules.has(resolvedClass)) return;
61
+
62
+ const metadata: ModuleMetadata = Reflect.getMetadata(METADATA_KEYS.MODULE, resolvedClass);
63
+ if (!metadata) {
64
+ throw new Error(`Class ${resolvedClass.name || resolvedClass} is not a valid Module (missing @Module decorator)`);
65
+ }
66
+
67
+ const isGlobal = Reflect.hasMetadata(METADATA_KEYS.GLOBAL, resolvedClass);
68
+ if (isGlobal) {
69
+ this.globalModules.add(resolvedClass);
70
+ }
71
+
72
+ const record: ModuleRecord = {
73
+ moduleClass: resolvedClass,
74
+ imports: new Set(),
75
+ providers: new Map(),
76
+ controllers: new Set(),
77
+ exports: new Set(),
78
+ instances: new Map(),
79
+ };
80
+ this.modules.set(resolvedClass, record);
81
+
82
+ // Register self and ModuleRef
83
+ record.instances.set(resolvedClass, null); // Placeholder, will instantiate module class later
84
+ record.instances.set(ModuleRef, new ContainerModuleRef(this, resolvedClass));
85
+
86
+ // Process imports
87
+ const imports = metadata.imports || [];
88
+ for (const imp of imports) {
89
+ const resolvedImp = resolveForwardRef(imp);
90
+ const actualModule = isDynamicModule(resolvedImp) ? resolvedImp.module : resolvedImp;
91
+
92
+ this.addModule(actualModule);
93
+ record.imports.add(actualModule);
94
+
95
+ // If it is dynamic module, merge its extra providers/exports/controllers
96
+ if (isDynamicModule(resolvedImp)) {
97
+ const dynamicRecord = this.modules.get(actualModule)!;
98
+ if (resolvedImp.providers) {
99
+ for (const prov of resolvedImp.providers) {
100
+ const token = this.getProviderToken(prov);
101
+ dynamicRecord.providers.set(token, prov);
102
+ }
103
+ }
104
+ if (resolvedImp.controllers) {
105
+ for (const ctrl of resolvedImp.controllers) {
106
+ dynamicRecord.controllers.add(ctrl);
107
+ }
108
+ }
109
+ if (resolvedImp.exports) {
110
+ for (const exp of resolvedImp.exports) {
111
+ const resolvedExp = resolveForwardRef(exp);
112
+ dynamicRecord.exports.add(resolvedExp);
113
+ }
114
+ }
115
+ if (resolvedImp.global) {
116
+ this.globalModules.add(actualModule);
117
+ }
118
+ }
119
+ }
120
+
121
+ // Process providers
122
+ const providers = metadata.providers || [];
123
+ for (const prov of providers) {
124
+ const token = this.getProviderToken(prov);
125
+ record.providers.set(token, prov);
126
+ }
127
+
128
+ // Process controllers
129
+ const controllers = metadata.controllers || [];
130
+ for (const ctrl of controllers) {
131
+ record.controllers.add(ctrl);
132
+ }
133
+
134
+ // Process exports
135
+ const exportsList = metadata.exports || [];
136
+ for (const exp of exportsList) {
137
+ const resolvedExp = resolveForwardRef(exp);
138
+ record.exports.add(resolvedExp);
139
+ }
140
+ }
141
+
142
+ private getProviderToken(provider: Provider): InjectionToken {
143
+ if (typeof provider === 'function') {
144
+ return provider;
145
+ }
146
+ return provider.provide;
147
+ }
148
+
149
+ resolveTokenInModuleContext<T>(moduleClass: any, token: InjectionToken): T {
150
+ // 1. Check circular dependency
151
+ const isResolving = this.resolvingStack.some(
152
+ (item) => item.moduleClass === moduleClass && item.token === token
153
+ );
154
+ if (isResolving) {
155
+ const path = this.resolvingStack
156
+ .map((item) => `${item.moduleClass.name ?? item.moduleClass}::${String(item.token.name ?? item.token)}`)
157
+ .join(' -> ');
158
+ throw new Error(`Circular dependency detected: ${path} -> ${moduleClass.name ?? moduleClass}::${String(token.name ?? token)}`);
159
+ }
160
+
161
+ this.resolvingStack.push({ moduleClass, token });
162
+
163
+ try {
164
+ const record = this.modules.get(moduleClass);
165
+ if (!record) {
166
+ throw new Error(`Module ${moduleClass.name || moduleClass} is not registered in the container`);
167
+ }
168
+
169
+ // Special resolution cases
170
+ if (token === ModuleRef) {
171
+ const instance = record.instances.get(ModuleRef);
172
+ if (instance) return instance;
173
+ const moduleRefInstance = new ContainerModuleRef(this, moduleClass);
174
+ record.instances.set(ModuleRef, moduleRefInstance);
175
+ return moduleRefInstance as any;
176
+ }
177
+
178
+ if (token === moduleClass) {
179
+ const instance = record.instances.get(moduleClass);
180
+ if (instance) return instance;
181
+ const instantiatedModule = this.instantiateClass(moduleClass, moduleClass);
182
+ record.instances.set(moduleClass, instantiatedModule);
183
+ return instantiatedModule;
184
+ }
185
+
186
+ // Find where the token is defined
187
+ const resolution = this.findProviderDefinition(moduleClass, token);
188
+ if (!resolution) {
189
+ // Not found. Check if the token is optional
190
+ const currentResolving = this.resolvingStack[this.resolvingStack.length - 2];
191
+ if (currentResolving) {
192
+ const parentClass = currentResolving.token;
193
+ if (typeof parentClass === 'function') {
194
+ const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, parentClass) || new Set();
195
+ const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, parentClass) || new Map();
196
+ const paramTypes = Reflect.getMetadata('design:paramtypes', parentClass) || [];
197
+
198
+ let isOptional = false;
199
+ for (let i = 0; i < paramTypes.length; i++) {
200
+ const pToken = injectTokens.get(i) ?? paramTypes[i];
201
+ const resolvedPToken = resolveForwardRef(pToken);
202
+ if (resolvedPToken === token && optionalParams.has(i)) {
203
+ isOptional = true;
204
+ break;
205
+ }
206
+ }
207
+ if (isOptional) {
208
+ return undefined as any;
209
+ }
210
+ }
211
+ }
212
+ throw new Error(`calyx DI: Cannot resolve dependency "${String(token.name ?? token)}" in module ${moduleClass.name || moduleClass}`);
213
+ }
214
+
215
+ const { targetModuleClass, provider } = resolution;
216
+ const targetRecord = this.modules.get(targetModuleClass)!;
217
+
218
+ // Check if instance already exists in target module
219
+ if (targetRecord.instances.has(token)) {
220
+ return targetRecord.instances.get(token);
221
+ }
222
+
223
+ // Instantiate based on provider definition
224
+ let instance: any;
225
+ if (typeof provider !== 'function' && 'useValue' in provider) {
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);
236
+ }
237
+
238
+ targetRecord.instances.set(token, instance);
239
+ return instance;
240
+ } finally {
241
+ this.resolvingStack.pop();
242
+ }
243
+ }
244
+
245
+ private isTokenExportedByModule(moduleClass: any, token: InjectionToken, visited = new Set<any>()): boolean {
246
+ if (visited.has(moduleClass)) return false;
247
+ visited.add(moduleClass);
248
+
249
+ const record = this.modules.get(moduleClass);
250
+ if (!record) return false;
251
+
252
+ if (record.exports.has(token)) {
253
+ return true;
254
+ }
255
+
256
+ for (const exportedItem of record.exports) {
257
+ if (this.modules.has(exportedItem)) {
258
+ if (this.isTokenExportedByModule(exportedItem, token, visited)) {
259
+ return true;
260
+ }
261
+ }
262
+ }
263
+
264
+ return false;
265
+ }
266
+
267
+ private findProviderInModuleOrExports(moduleClass: any, token: InjectionToken, visited = new Set<any>()): { targetModuleClass: any; provider: Provider } | null {
268
+ if (visited.has(moduleClass)) return null;
269
+ visited.add(moduleClass);
270
+
271
+ const record = this.modules.get(moduleClass);
272
+ if (!record) return null;
273
+
274
+ if (record.providers.has(token)) {
275
+ return { targetModuleClass: moduleClass, provider: record.providers.get(token)! };
276
+ }
277
+
278
+ for (const importedModule of record.imports) {
279
+ if (this.isTokenExportedByModule(importedModule, token)) {
280
+ const found = this.findProviderInModuleOrExports(importedModule, token, visited);
281
+ if (found) return found;
282
+ }
283
+ }
284
+
285
+ return null;
286
+ }
287
+
288
+ private findProviderDefinition(moduleClass: any, token: InjectionToken): { targetModuleClass: any; provider: Provider } | null {
289
+ const record = this.modules.get(moduleClass);
290
+ if (record && record.providers.has(token)) {
291
+ return { targetModuleClass: moduleClass, provider: record.providers.get(token)! };
292
+ }
293
+
294
+ if (record) {
295
+ for (const importedModule of record.imports) {
296
+ if (this.isTokenExportedByModule(importedModule, token)) {
297
+ const found = this.findProviderInModuleOrExports(importedModule, token);
298
+ if (found) return found;
299
+ }
300
+ }
301
+ }
302
+
303
+ for (const globalModule of this.globalModules) {
304
+ if (globalModule !== moduleClass && this.isTokenExportedByModule(globalModule, token)) {
305
+ const found = this.findProviderInModuleOrExports(globalModule, token);
306
+ if (found) return found;
307
+ }
308
+ }
309
+
310
+ return null;
311
+ }
312
+
313
+ private instantiateClass(Class: Type<any>, moduleClass: any): any {
314
+ if (
315
+ !Reflect.hasMetadata(METADATA_KEYS.INJECTABLE, Class) &&
316
+ !Reflect.hasMetadata(METADATA_KEYS.CONTROLLER, Class) &&
317
+ !Reflect.hasMetadata(METADATA_KEYS.MODULE, Class)
318
+ ) {
319
+ throw new Error(`Class ${Class.name} is missing @Injectable(), @Controller() or @Module() decorator`);
320
+ }
321
+
322
+ const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', Class) || [];
323
+ const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, Class) || new Map();
324
+ const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, Class) || new Set();
325
+
326
+ const args = paramTypes.map((paramType, i) => {
327
+ const token = injectTokens.get(i) ?? paramType;
328
+ const resolvedToken = resolveForwardRef(token);
329
+
330
+ try {
331
+ return this.resolveTokenInModuleContext(moduleClass, resolvedToken);
332
+ } catch (err) {
333
+ if (optionalParams.has(i)) {
334
+ return undefined;
335
+ }
336
+ throw err;
337
+ }
338
+ });
339
+
340
+ return new Class(...args);
341
+ }
342
+
343
+ getGlobalOrAnyInstance(token: InjectionToken): any {
344
+ for (const record of this.modules.values()) {
345
+ if (record.instances.has(token)) {
346
+ return record.instances.get(token);
347
+ }
348
+ }
349
+ throw new Error(`calyx DI: Instance of "${String(token.name ?? token)}" not found in any module context`);
350
+ }
351
+
352
+ bootstrap(rootModule: any) {
353
+ this.addModule(rootModule);
354
+
355
+ // Instantiate all providers
356
+ for (const [moduleClass, record] of this.modules.entries()) {
357
+ for (const token of record.providers.keys()) {
358
+ this.resolveTokenInModuleContext(moduleClass, token);
359
+ }
360
+ }
361
+
362
+ // Instantiate all controllers
363
+ for (const [moduleClass, record] of this.modules.entries()) {
364
+ for (const controllerClass of record.controllers) {
365
+ this.resolveController(moduleClass, controllerClass);
366
+ }
367
+ }
368
+
369
+ // Instantiate module classes themselves
370
+ for (const [moduleClass, record] of this.modules.entries()) {
371
+ if (record.instances.get(moduleClass) === null) {
372
+ const instance = this.instantiateClass(moduleClass, moduleClass);
373
+ record.instances.set(moduleClass, instance);
374
+ }
375
+ }
376
+ }
377
+
378
+ private resolveController(moduleClass: any, controllerClass: any): any {
379
+ const record = this.modules.get(moduleClass)!;
380
+ if (record.instances.has(controllerClass)) {
381
+ return record.instances.get(controllerClass);
382
+ }
383
+ const instance = this.instantiateClass(controllerClass, moduleClass);
384
+ record.instances.set(controllerClass, instance);
385
+ return instance;
386
+ }
387
+ }
@@ -0,0 +1,45 @@
1
+ import 'reflect-metadata';
2
+ import { METADATA_KEYS, ModuleMetadata, InjectionToken, ForwardReference } from './metadata.ts';
3
+
4
+ export function Module(metadata: ModuleMetadata): ClassDecorator {
5
+ return (target) => {
6
+ Reflect.defineMetadata(METADATA_KEYS.MODULE, metadata, target);
7
+ };
8
+ }
9
+
10
+ export function Injectable(): ClassDecorator {
11
+ return (target) => {
12
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
13
+ };
14
+ }
15
+
16
+ export function Inject(token: InjectionToken): ParameterDecorator {
17
+ return (target, propertyKey, parameterIndex) => {
18
+ // For constructor parameters, propertyKey is undefined
19
+ const classTarget = propertyKey === undefined ? target : target.constructor;
20
+ const existingInjectTokens: Map<number, InjectionToken> =
21
+ Reflect.getOwnMetadata(METADATA_KEYS.INJECT_TOKENS, classTarget) || new Map();
22
+ existingInjectTokens.set(parameterIndex, token);
23
+ Reflect.defineMetadata(METADATA_KEYS.INJECT_TOKENS, existingInjectTokens, classTarget);
24
+ };
25
+ }
26
+
27
+ export function Optional(): ParameterDecorator {
28
+ return (target, propertyKey, parameterIndex) => {
29
+ const classTarget = propertyKey === undefined ? target : target.constructor;
30
+ const existingOptionalParams: Set<number> =
31
+ Reflect.getOwnMetadata(METADATA_KEYS.OPTIONAL_PARAMS, classTarget) || new Set();
32
+ existingOptionalParams.add(parameterIndex);
33
+ Reflect.defineMetadata(METADATA_KEYS.OPTIONAL_PARAMS, existingOptionalParams, classTarget);
34
+ };
35
+ }
36
+
37
+ export function Global(): ClassDecorator {
38
+ return (target) => {
39
+ Reflect.defineMetadata(METADATA_KEYS.GLOBAL, true, target);
40
+ };
41
+ }
42
+
43
+ export function forwardRef(fn: () => any): ForwardReference {
44
+ return { forwardRef: fn };
45
+ }
@@ -0,0 +1,4 @@
1
+ export * from './metadata.ts';
2
+ export * from './decorators.ts';
3
+ export * from './module-ref.ts';
4
+ export * from './container.ts';
@@ -0,0 +1,61 @@
1
+ export const METADATA_KEYS = {
2
+ MODULE: 'calyx:module',
3
+ INJECTABLE: 'calyx:injectable',
4
+ INJECT_TOKENS: 'calyx:inject_tokens',
5
+ OPTIONAL_PARAMS: 'calyx:optional_params',
6
+ GLOBAL: 'calyx:global',
7
+ CONTROLLER: 'calyx:controller',
8
+ CATCH: 'calyx:catch',
9
+ HTTP_METHOD: 'calyx:http_method',
10
+ HTTP_PARAMS: 'calyx:http_params',
11
+ HTTP_CODE: 'calyx:http_code',
12
+ HTTP_HEADERS: 'calyx:http_headers',
13
+ REDIRECT: 'calyx:redirect',
14
+ GUARDS: 'calyx:guards',
15
+ INTERCEPTORS: 'calyx:interceptors',
16
+ PIPES: 'calyx:pipes',
17
+ FILTERS: 'calyx:filters',
18
+ };
19
+
20
+ export interface Type<T = any> extends Function {
21
+ new (...args: any[]): T;
22
+ }
23
+
24
+ export type InjectionToken = string | symbol | Type<any>;
25
+
26
+ export interface ValueProvider {
27
+ provide: InjectionToken;
28
+ useValue: any;
29
+ }
30
+
31
+ export interface ClassProvider {
32
+ provide: InjectionToken;
33
+ useClass: Type<any>;
34
+ }
35
+
36
+ export interface FactoryProvider {
37
+ provide: InjectionToken;
38
+ useFactory: (...args: any[]) => any;
39
+ inject?: InjectionToken[];
40
+ }
41
+
42
+ export type Provider = Type<any> | ValueProvider | ClassProvider | FactoryProvider;
43
+
44
+ export interface ModuleMetadata {
45
+ imports?: any[];
46
+ controllers?: any[];
47
+ providers?: Provider[];
48
+ exports?: any[];
49
+ }
50
+
51
+ export interface ForwardReference {
52
+ forwardRef: () => any;
53
+ }
54
+
55
+ export function isForwardReference(obj: any): obj is ForwardReference {
56
+ return obj && typeof obj === 'object' && typeof obj.forwardRef === 'function';
57
+ }
58
+
59
+ export function resolveForwardRef(typeOrFunc: any): any {
60
+ return isForwardReference(typeOrFunc) ? typeOrFunc.forwardRef() : typeOrFunc;
61
+ }
@@ -0,0 +1,5 @@
1
+ import { InjectionToken } from './metadata.ts';
2
+
3
+ export abstract class ModuleRef {
4
+ abstract get<T>(token: InjectionToken, options?: { strict: boolean }): T;
5
+ }