@noxfly/noxus 2.5.0 → 3.0.0-dev.1

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.
Files changed (58) hide show
  1. package/README.md +405 -340
  2. package/dist/app-injector-Bz3Upc0y.d.mts +125 -0
  3. package/dist/app-injector-Bz3Upc0y.d.ts +125 -0
  4. package/dist/child.d.mts +157 -23
  5. package/dist/child.d.ts +157 -23
  6. package/dist/child.js +1111 -1341
  7. package/dist/child.mjs +1086 -1294
  8. package/dist/main.d.mts +720 -284
  9. package/dist/main.d.ts +720 -284
  10. package/dist/main.js +1471 -1650
  11. package/dist/main.mjs +1409 -1559
  12. package/dist/preload.d.mts +28 -0
  13. package/dist/preload.d.ts +28 -0
  14. package/dist/preload.js +95 -0
  15. package/dist/preload.mjs +70 -0
  16. package/dist/renderer.d.mts +159 -22
  17. package/dist/renderer.d.ts +159 -22
  18. package/dist/renderer.js +104 -177
  19. package/dist/renderer.mjs +100 -172
  20. package/dist/request-BlTtiHbi.d.ts +112 -0
  21. package/dist/request-qJ9EiDZc.d.mts +112 -0
  22. package/package.json +24 -19
  23. package/src/DI/app-injector.ts +95 -106
  24. package/src/DI/injector-explorer.ts +93 -119
  25. package/src/DI/token.ts +53 -0
  26. package/src/decorators/controller.decorator.ts +38 -27
  27. package/src/decorators/guards.decorator.ts +5 -64
  28. package/src/decorators/injectable.decorator.ts +68 -15
  29. package/src/decorators/method.decorator.ts +40 -81
  30. package/src/decorators/middleware.decorator.ts +5 -72
  31. package/src/index.ts +4 -5
  32. package/src/internal/app.ts +217 -0
  33. package/src/internal/bootstrap.ts +108 -0
  34. package/src/{preload-bridge.ts → internal/preload-bridge.ts} +1 -1
  35. package/src/{renderer-client.ts → internal/renderer-client.ts} +2 -2
  36. package/src/{renderer-events.ts → internal/renderer-events.ts} +1 -1
  37. package/src/{request.ts → internal/request.ts} +3 -3
  38. package/src/internal/router.ts +353 -0
  39. package/src/internal/routes.ts +78 -0
  40. package/src/{socket.ts → internal/socket.ts} +4 -4
  41. package/src/main.ts +10 -14
  42. package/src/non-electron-process.ts +1 -2
  43. package/src/preload.ts +10 -0
  44. package/src/renderer.ts +13 -0
  45. package/src/window/window-manager.ts +255 -0
  46. package/tsconfig.json +5 -10
  47. package/tsup.config.ts +29 -13
  48. package/dist/app-injector-B3MvgV3k.d.mts +0 -95
  49. package/dist/app-injector-B3MvgV3k.d.ts +0 -95
  50. package/dist/request-CdpZ9qZL.d.ts +0 -167
  51. package/dist/request-Dx_5Prte.d.mts +0 -167
  52. package/src/app.ts +0 -244
  53. package/src/bootstrap.ts +0 -84
  54. package/src/decorators/inject.decorator.ts +0 -24
  55. package/src/decorators/injectable.metadata.ts +0 -15
  56. package/src/decorators/module.decorator.ts +0 -75
  57. package/src/router.ts +0 -594
  58. /package/src/{exceptions.ts → internal/exceptions.ts} +0 -0
package/package.json CHANGED
@@ -1,35 +1,40 @@
1
1
  {
2
2
  "name": "@noxfly/noxus",
3
- "version": "2.5.0",
3
+ "version": "3.0.0-dev.1",
4
4
  "main": "dist/main.js",
5
5
  "module": "dist/main.mjs",
6
6
  "types": "dist/main.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
9
  "browser": {
10
- "types": "./dist/renderer.d.ts",
11
- "import": "./dist/renderer.mjs",
10
+ "types": "./dist/renderer.d.ts",
11
+ "import": "./dist/renderer.mjs",
12
12
  "require": "./dist/renderer.js"
13
13
  },
14
14
  "default": {
15
- "types": "./dist/child.d.ts",
16
- "import": "./dist/child.mjs",
15
+ "types": "./dist/child.d.ts",
16
+ "import": "./dist/child.mjs",
17
17
  "require": "./dist/child.js"
18
18
  }
19
19
  },
20
+ "./main": {
21
+ "types": "./dist/main.d.ts",
22
+ "import": "./dist/main.mjs",
23
+ "require": "./dist/main.js"
24
+ },
20
25
  "./renderer": {
21
- "types": "./dist/renderer.d.ts",
22
- "import": "./dist/renderer.mjs",
26
+ "types": "./dist/renderer.d.ts",
27
+ "import": "./dist/renderer.mjs",
23
28
  "require": "./dist/renderer.js"
24
29
  },
25
- "./main": {
26
- "types": "./dist/main.d.ts",
27
- "import": "./dist/main.mjs",
28
- "require": "./dist/main.js"
30
+ "./preload": {
31
+ "types": "./dist/preload.d.ts",
32
+ "import": "./dist/preload.mjs",
33
+ "require": "./dist/preload.js"
29
34
  },
30
35
  "./child": {
31
- "types": "./dist/child.d.ts",
32
- "import": "./dist/child.mjs",
36
+ "types": "./dist/child.d.ts",
37
+ "import": "./dist/child.mjs",
33
38
  "require": "./dist/child.js"
34
39
  }
35
40
  },
@@ -45,12 +50,15 @@
45
50
  "nodejs",
46
51
  "typescript",
47
52
  "framework",
48
- "web framework",
49
- "electron"
53
+ "electron",
54
+ "ipc",
55
+ "dependency-injection",
56
+ "standalone",
57
+ "lazy-loading"
50
58
  ],
51
59
  "author": "NoxFly",
52
60
  "license": "MIT",
53
- "description": "Simulate lightweight HTTP-like requests between renderer and main process in Electron applications with MessagePort, with structured and modular design.",
61
+ "description": "Lightweight HTTP-like IPC framework for Electron with standalone controllers, explicit DI, and lazy route loading. No reflect-metadata required.",
54
62
  "homepage": "https://github.com/NoxFly/noxus",
55
63
  "repository": {
56
64
  "type": "git",
@@ -73,8 +81,5 @@
73
81
  "tsup": "^8.5.0",
74
82
  "typescript": "^5.8.3",
75
83
  "typescript-eslint": "^8.36.0"
76
- },
77
- "peerDependencies": {
78
- "reflect-metadata": "^0.2.2"
79
84
  }
80
85
  }
@@ -4,159 +4,148 @@
4
4
  * @author NoxFly
5
5
  */
6
6
 
7
- import 'reflect-metadata';
8
- import { INJECT_METADATA_KEY } from 'src/decorators/inject.decorator';
9
- import { InternalServerException } from 'src/exceptions';
10
- import { ForwardReference } from 'src/utils/forward-ref';
11
- import { Type } from 'src/utils/types';
7
+ import { ForwardReference } from '../utils/forward-ref';
8
+ import { Type } from '../utils/types';
9
+ import { Token, TokenKey } from './token';
12
10
 
13
11
  /**
14
- * Represents a lifetime of a binding in the dependency injection system.
15
- * It can be one of the following:
16
- * - 'singleton': The instance is created once and shared across the application.
17
- * - 'scope': The instance is created once per scope (e.g., per request).
18
- * - 'transient': A new instance is created every time it is requested.
12
+ * Lifetime of a binding in the DI container.
13
+ * - singleton: created once, shared for the lifetime of the app.
14
+ * - scope: created once per request scope.
15
+ * - transient: new instance every time it is resolved.
19
16
  */
20
17
  export type Lifetime = 'singleton' | 'scope' | 'transient';
21
18
 
22
19
  /**
23
- * Represents a binding in the dependency injection system.
24
- * It contains the lifetime of the binding, the implementation type, and optionally an instance.
20
+ * Internal representation of a registered binding.
25
21
  */
26
- export interface IBinding {
22
+ export interface IBinding<T = unknown> {
27
23
  lifetime: Lifetime;
28
- implementation: Type<unknown>;
29
- instance?: InstanceType<Type<unknown>>;
24
+ implementation: Type<T>;
25
+ /** Explicit constructor dependencies, declared by the class itself. */
26
+ deps: ReadonlyArray<TokenKey>;
27
+ instance?: T;
28
+ }
29
+
30
+ function keyOf<T>(k: TokenKey<T>): Type<T> | Token<T> {
31
+ return k;
30
32
  }
31
33
 
32
34
  /**
33
- * AppInjector is the root dependency injection container.
34
- * It is used to register and resolve dependencies in the application.
35
- * It supports different lifetimes for dependencies:
36
- * This should not be manually instantiated, outside of the framework.
37
- * Use the `RootInjector` instance instead.
35
+ * AppInjector is the core DI container.
36
+ * It no longer uses reflect-metadata all dependency information
37
+ * comes from explicitly declared `deps` arrays on each binding.
38
38
  */
39
39
  export class AppInjector {
40
- public bindings = new Map<Type<unknown>, IBinding>();
41
- public singletons = new Map<Type<unknown>, InstanceType<Type<unknown>>>();
42
- public scoped = new Map<Type<unknown>, InstanceType<Type<unknown>>>();
40
+ public readonly bindings = new Map<Type<unknown> | Token<unknown>, IBinding<unknown>>();
41
+ public readonly singletons = new Map<Type<unknown> | Token<unknown>, unknown>();
42
+ public readonly scoped = new Map<Type<unknown> | Token<unknown>, unknown>();
43
43
 
44
- constructor(
45
- public readonly name: string | null = null,
46
- ) {}
44
+ constructor(public readonly name: string | null = null) {}
47
45
 
48
46
  /**
49
- * Typically used to create a dependency injection scope
50
- * at the "scope" level (i.e., per-request lifetime).
51
- *
52
- * SHOULD NOT BE USED by anything else than the framework itself.
47
+ * Creates a child scope for per-request lifetime resolution.
53
48
  */
54
49
  public createScope(): AppInjector {
55
50
  const scope = new AppInjector();
56
- scope.bindings = this.bindings; // pass injectable declarations
57
- scope.singletons = this.singletons; // share parent's singletons to avoid recreating them
58
- // do not keep parent's scoped instances
51
+ (scope as any).bindings = this.bindings;
52
+ (scope as any).singletons = this.singletons;
59
53
  return scope;
60
54
  }
61
55
 
62
56
  /**
63
- * Called when resolving a dependency,
64
- * i.e., retrieving the instance of a given class.
57
+ * Registers a binding explicitly.
65
58
  */
66
- public resolve<T>(target: Type<T> | ForwardReference<T>): T {
67
- if (target instanceof ForwardReference) {
68
- return new Proxy({}, {
69
- get: (obj, prop, receiver) => {
70
- const realType = target.forwardRefFn();
71
- const instance = this.resolve(realType) as any;
72
- const value = Reflect.get(instance, prop, receiver);
73
-
74
- return typeof value === 'function' ? value.bind(instance) : value;
75
- },
76
- set: (obj, prop, value, receiver) => {
77
- const realType = target.forwardRefFn();
78
- const instance = this.resolve(realType) as any;
79
- return Reflect.set(instance, prop, value, receiver);
80
- },
81
- getPrototypeOf: () => {
82
- const realType = target.forwardRefFn();
83
- return (realType as any).prototype;
84
- }
85
- }) as T;
59
+ public register<T>(
60
+ key: TokenKey<T>,
61
+ implementation: Type<T>,
62
+ lifetime: Lifetime,
63
+ deps: ReadonlyArray<TokenKey> = [],
64
+ ): void {
65
+ const k = keyOf(key) as TokenKey<unknown>;
66
+ if (!this.bindings.has(k)) {
67
+ this.bindings.set(k, { lifetime, implementation: implementation as Type<unknown>, deps });
86
68
  }
69
+ }
87
70
 
88
- const binding = this.bindings.get(target);
89
-
90
- if(!binding) {
91
- if(target === undefined) {
92
- throw new InternalServerException(
93
- "Failed to resolve a dependency injection : Undefined target type.\n"
94
- + "This might be caused by a circular dependency."
95
- );
96
- }
71
+ /**
72
+ * Resolves a dependency by token or class reference.
73
+ */
74
+ public resolve<T>(target: TokenKey<T> | ForwardReference<T>): T {
75
+ if (target instanceof ForwardReference) {
76
+ return this._resolveForwardRef(target);
77
+ }
97
78
 
98
- const name = target.name || "unknown";
79
+ const k = keyOf(target) as TokenKey<unknown>;
80
+ const binding = this.bindings.get(k);
99
81
 
100
- throw new InternalServerException(
101
- `Failed to resolve a dependency injection : No binding for type ${name}.\n`
102
- + `Did you forget to use @Injectable() decorator ?`
82
+ if (!binding) {
83
+ const name = target instanceof Token ? target.description : (target as Type<unknown>).name ?? 'unknown';
84
+ throw new Error(
85
+ `[Noxus DI] No binding found for "${name}".\n`
86
+ + `Did you forget to declare it in @Injectable({ deps }) or in bootstrapApplication({ singletons })?`,
103
87
  );
104
88
  }
105
89
 
106
- switch(binding.lifetime) {
90
+ switch (binding.lifetime) {
107
91
  case 'transient':
108
- return this.instantiate(binding.implementation) as T;
92
+ return this._instantiate(binding) as T;
109
93
 
110
94
  case 'scope': {
111
- if(this.scoped.has(target)) {
112
- return this.scoped.get(target) as T;
113
- }
114
-
115
- const instance = this.instantiate(binding.implementation);
116
- this.scoped.set(target, instance);
117
-
118
- return instance as T;
95
+ if (this.scoped.has(k)) return this.scoped.get(k) as T;
96
+ const inst = this._instantiate(binding);
97
+ this.scoped.set(k, inst);
98
+ return inst as T;
119
99
  }
120
100
 
121
101
  case 'singleton': {
122
- if(binding.instance === undefined && this.name === 'root') {
123
- binding.instance = this.instantiate(binding.implementation);
124
- this.singletons.set(target, binding.instance);
102
+ if (this.singletons.has(k)) return this.singletons.get(k) as T;
103
+ const inst = this._instantiate(binding);
104
+ this.singletons.set(k, inst);
105
+ if (binding.instance === undefined) {
106
+ (binding as IBinding<unknown>).instance = inst as unknown;
125
107
  }
126
-
127
- return binding.instance as T;
108
+ return inst as T;
128
109
  }
129
110
  }
130
111
  }
131
112
 
132
- /**
133
- * Instantiates a class, resolving its dependencies.
134
- */
135
- private instantiate<T extends Type<unknown>>(target: T): InstanceType<T> {
136
- const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
137
- const injectParams = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
138
-
139
- const params = paramTypes.map((paramType: any, index: number) => {
140
- const overrideToken = injectParams[index];
141
- const actualToken = overrideToken !== undefined ? overrideToken : paramType;
142
-
143
- return this.resolve(actualToken);
144
- });
113
+ // -------------------------------------------------------------------------
114
+
115
+ private _resolveForwardRef<T>(ref: ForwardReference<T>): T {
116
+ return new Proxy({} as object, {
117
+ get: (_obj, prop, receiver) => {
118
+ const realType = ref.forwardRefFn();
119
+ const instance = this.resolve(realType) as Record<string | symbol, unknown>;
120
+ const value = Reflect.get(instance, prop, receiver);
121
+ return typeof value === 'function' ? (value as Function).bind(instance) : value;
122
+ },
123
+ set: (_obj, prop, value, receiver) => {
124
+ const realType = ref.forwardRefFn();
125
+ const instance = this.resolve(realType) as object;
126
+ return Reflect.set(instance, prop, value, receiver);
127
+ },
128
+ getPrototypeOf: () => {
129
+ const realType = ref.forwardRefFn();
130
+ return (realType as unknown as { prototype: object }).prototype;
131
+ },
132
+ }) as T;
133
+ }
145
134
 
146
- return new target(...params) as InstanceType<T>;
135
+ private _instantiate<T>(binding: IBinding<T>): T {
136
+ const resolvedDeps = binding.deps.map((dep) => this.resolve(dep));
137
+ return new binding.implementation(...resolvedDeps) as T;
147
138
  }
148
139
  }
149
140
 
150
141
  /**
151
- * Injects a type from the dependency injection system.
152
- * This function is used to retrieve an instance of a type that has been registered in the dependency injection system.
153
- * It is typically used in the constructor of a class to inject dependencies.
154
- * @param t - The type to inject.
155
- * @returns An instance of the type.
156
- * @throws If the type is not registered in the dependency injection system.
142
+ * The global root injector. All singletons live here.
157
143
  */
158
- export function inject<T>(t: Type<T> | ForwardReference<T>): T {
144
+ export const RootInjector = new AppInjector('root');
145
+
146
+ /**
147
+ * Convenience function: resolve a token from the root injector.
148
+ */
149
+ export function inject<T>(t: TokenKey<T> | ForwardReference<T>): T {
159
150
  return RootInjector.resolve(t);
160
151
  }
161
-
162
- export const RootInjector = new AppInjector('root');
@@ -4,166 +4,140 @@
4
4
  * @author NoxFly
5
5
  */
6
6
 
7
- import { getControllerMetadata } from "src/decorators/controller.decorator";
8
- import { getInjectableMetadata } from "src/decorators/injectable.metadata";
9
- import { getRouteMetadata } from "src/decorators/method.decorator";
10
- import { getModuleMetadata } from "src/decorators/module.decorator";
11
- import { Lifetime, RootInjector } from "src/DI/app-injector";
12
- import { Router } from "src/router";
13
- import { Logger } from "src/utils/logger";
14
- import { Type } from "src/utils/types";
15
-
16
- interface PendingRegistration {
17
- target: Type<unknown>;
7
+ import { Lifetime, RootInjector } from './app-injector';
8
+ import { TokenKey } from './token';
9
+ import { Type } from '../utils/types';
10
+ import { Logger } from '../utils/logger';
11
+ import { Guard, Middleware } from "src/main";
12
+
13
+ export interface PendingRegistration {
14
+ key: TokenKey;
15
+ implementation: Type<unknown>;
18
16
  lifetime: Lifetime;
17
+ deps: ReadonlyArray<TokenKey>;
18
+ isController: boolean;
19
+ pathPrefix?: string;
19
20
  }
20
21
 
21
22
  /**
22
- * InjectorExplorer is a utility class that explores the dependency injection system at the startup.
23
- * It collects decorated classes during the import phase and defers their actual registration
24
- * and resolution to when {@link processPending} is called by bootstrapApplication.
23
+ * InjectorExplorer accumulates registrations emitted by decorators
24
+ * at import time, then flushes them in two phases (bind resolve)
25
+ * once bootstrapApplication triggers processing.
26
+ *
27
+ * Because deps are now explicit arrays (no reflect-metadata), this class
28
+ * no longer needs to introspect constructor parameter types.
25
29
  */
26
30
  export class InjectorExplorer {
27
31
  private static readonly pending: PendingRegistration[] = [];
28
32
  private static processed = false;
29
33
  private static accumulating = false;
30
34
 
31
- /**
32
- * Enqueues a class for deferred registration.
33
- * Called by the @Injectable decorator at import time.
34
- *
35
- * If {@link processPending} has already been called (i.e. after bootstrap)
36
- * and accumulation mode is not active, the class is registered immediately
37
- * so that late dynamic imports (e.g. middlewares loaded after bootstrap)
38
- * work correctly.
39
- *
40
- * When accumulation mode is active (between {@link beginAccumulate} and
41
- * {@link flushAccumulated}), classes are queued instead — preserving the
42
- * two-phase binding/resolution guarantee for lazy-loaded modules.
43
- */
44
- public static enqueue(target: Type<unknown>, lifetime: Lifetime): void {
45
- if(InjectorExplorer.processed && !InjectorExplorer.accumulating) {
46
- InjectorExplorer.registerImmediate(target, lifetime);
35
+ // -------------------------------------------------------------------------
36
+ // Public API
37
+ // -------------------------------------------------------------------------
38
+
39
+ public static enqueue(reg: PendingRegistration): void {
40
+ if (InjectorExplorer.processed && !InjectorExplorer.accumulating) {
41
+ InjectorExplorer._registerImmediate(reg);
47
42
  return;
48
43
  }
49
-
50
- InjectorExplorer.pending.push({ target, lifetime });
44
+ InjectorExplorer.pending.push(reg);
51
45
  }
52
46
 
53
47
  /**
54
- * Enters accumulation mode. While active, all decorated classes discovered
55
- * via dynamic imports are queued in {@link pending} rather than registered
56
- * immediately. Call {@link flushAccumulated} to process them with the
57
- * full two-phase (bind-then-resolve) guarantee.
48
+ * Two-phase flush of all pending registrations collected at startup.
49
+ * Called by bootstrapApplication after app.whenReady().
58
50
  */
51
+ public static processPending(singletonOverrides?: Map<TokenKey, unknown>): void {
52
+ const queue = [...InjectorExplorer.pending];
53
+ InjectorExplorer.pending.length = 0;
54
+
55
+ InjectorExplorer._phaseOne(queue);
56
+ InjectorExplorer._phaseTwo(queue, singletonOverrides);
57
+
58
+ InjectorExplorer.processed = true;
59
+ }
60
+
61
+ /** Enters accumulation mode for lazy-loaded batches. */
59
62
  public static beginAccumulate(): void {
60
63
  InjectorExplorer.accumulating = true;
61
64
  }
62
65
 
63
66
  /**
64
- * Exits accumulation mode and processes every class queued since
65
- * {@link beginAccumulate} was called. Uses the same two-phase strategy
66
- * as {@link processPending} (register all bindings first, then resolve
67
- * singletons / controllers) so import ordering within a lazy batch
68
- * does not cause resolution failures.
67
+ * Exits accumulation mode and flushes queued registrations
68
+ * with the same two-phase guarantee as processPending.
69
69
  */
70
- public static flushAccumulated(): void {
70
+ public static flushAccumulated(
71
+ routeGuards: Guard[] = [],
72
+ routeMiddlewares: Middleware[] = [],
73
+ pathPrefix = '',
74
+ ): void {
71
75
  InjectorExplorer.accumulating = false;
72
-
73
76
  const queue = [...InjectorExplorer.pending];
74
77
  InjectorExplorer.pending.length = 0;
78
+ InjectorExplorer._phaseOne(queue);
75
79
 
76
- // Phase 1: register all bindings without instantiation
77
- for(const { target, lifetime } of queue) {
78
- if(!RootInjector.bindings.has(target)) {
79
- RootInjector.bindings.set(target, {
80
- implementation: target,
81
- lifetime
82
- });
83
- }
80
+ // Stamp the path prefix on controller registrations
81
+ for (const reg of queue) {
82
+ if (reg.isController) reg.pathPrefix = pathPrefix;
84
83
  }
85
84
 
86
- // Phase 2: resolve singletons, register controllers, log modules
87
- for(const { target, lifetime } of queue) {
88
- InjectorExplorer.processRegistration(target, lifetime);
89
- }
85
+ InjectorExplorer._phaseTwo(queue, undefined, routeGuards, routeMiddlewares);
90
86
  }
91
87
 
92
- /**
93
- * Processes all pending registrations in two phases:
94
- * 1. Register all bindings (no instantiation) so every dependency is known.
95
- * 2. Resolve singletons, register controllers and log module readiness.
96
- *
97
- * This two-phase approach makes the system resilient to import ordering:
98
- * all bindings exist before any singleton is instantiated.
99
- */
100
- public static processPending(): void {
101
- const queue = InjectorExplorer.pending;
102
-
103
- // Phase 1: register all bindings without instantiation
104
- for(const { target, lifetime } of queue) {
105
- if(!RootInjector.bindings.has(target)) {
106
- RootInjector.bindings.set(target, {
107
- implementation: target,
108
- lifetime
109
- });
110
- }
111
- }
88
+ // -------------------------------------------------------------------------
89
+ // Private helpers
90
+ // -------------------------------------------------------------------------
112
91
 
113
- // Phase 2: resolve singletons, register controllers, log modules
114
- for(const { target, lifetime } of queue) {
115
- InjectorExplorer.processRegistration(target, lifetime);
92
+ /** Phase 1: register all bindings without instantiating anything. */
93
+ private static _phaseOne(queue: PendingRegistration[]): void {
94
+ for (const reg of queue) {
95
+ RootInjector.register(reg.key, reg.implementation, reg.lifetime, reg.deps);
116
96
  }
117
-
118
- queue.length = 0;
119
- InjectorExplorer.processed = true;
120
97
  }
121
98
 
122
- /**
123
- * Registers a single class immediately (post-bootstrap path).
124
- * Used for classes discovered via late dynamic imports.
125
- */
126
- private static registerImmediate(target: Type<unknown>, lifetime: Lifetime): void {
127
- if(RootInjector.bindings.has(target)) {
128
- return;
129
- }
130
-
131
- RootInjector.bindings.set(target, {
132
- implementation: target,
133
- lifetime
134
- });
135
-
136
- InjectorExplorer.processRegistration(target, lifetime);
137
- }
99
+ /** Phase 2: resolve singletons and register controllers in the router. */
100
+ private static _phaseTwo(
101
+ queue: PendingRegistration[],
102
+ overrides?: Map<TokenKey, unknown>,
103
+ routeGuards: Guard[] = [],
104
+ routeMiddlewares: Middleware[] = [],
105
+ ): void {
106
+ for (const reg of queue) {
107
+ // Apply value overrides (e.g. singleton instances provided via bootstrapApplication config)
108
+ if (overrides?.has(reg.key)) {
109
+ const override = overrides.get(reg.key);
110
+ RootInjector.singletons.set(reg.key as any, override);
111
+ Logger.log(`Registered ${reg.implementation.name} as singleton (overridden)`);
112
+ continue;
113
+ }
138
114
 
139
- /**
140
- * Performs phase-2 work for a single registration: resolve singletons,
141
- * register controllers, and log module readiness.
142
- */
143
- private static processRegistration(target: Type<unknown>, lifetime: Lifetime): void {
144
- if(lifetime === 'singleton') {
145
- RootInjector.resolve(target);
146
- }
115
+ if (reg.lifetime === 'singleton') {
116
+ RootInjector.resolve(reg.key);
117
+ }
147
118
 
148
- if(getModuleMetadata(target)) {
149
- Logger.log(`${target.name} dependencies initialized`);
150
- return;
119
+ if (reg.isController) {
120
+ // Lazily import Router to avoid circular dependency at module load time
121
+ const { Router } = require('../internal/router') as { Router: { prototype: { registerController(t: Type<unknown>): void } } };
122
+ const router = RootInjector.resolve(Router as any) as { registerController(t: Type<unknown>, pathPrefix: string, routeGuards: Guard[], routeMiddlewares: Middleware[]): void };
123
+ router.registerController(reg.implementation, reg.pathPrefix ?? '', routeGuards, routeMiddlewares);
124
+ } else if (reg.lifetime !== 'singleton') {
125
+ Logger.log(`Registered ${reg.implementation.name} as ${reg.lifetime}`);
126
+ }
151
127
  }
128
+ }
152
129
 
153
- const controllerMeta = getControllerMetadata(target);
154
-
155
- if(controllerMeta) {
156
- const router = RootInjector.resolve(Router);
157
- router?.registerController(target);
158
- return;
159
- }
130
+ private static _registerImmediate(reg: PendingRegistration): void {
131
+ RootInjector.register(reg.key, reg.implementation, reg.lifetime, reg.deps);
160
132
 
161
- if(getRouteMetadata(target).length > 0) {
162
- return;
133
+ if (reg.lifetime === 'singleton') {
134
+ RootInjector.resolve(reg.key);
163
135
  }
164
136
 
165
- if(getInjectableMetadata(target)) {
166
- Logger.log(`Registered ${target.name} as ${lifetime}`);
137
+ if (reg.isController) {
138
+ const { Router } = require('../internal/router') as { Router: { prototype: { registerController(t: Type<unknown>): void } } };
139
+ const router = RootInjector.resolve(Router as any) as { registerController(t: Type<unknown>): void };
140
+ router.registerController(reg.implementation);
167
141
  }
168
142
  }
169
143
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @copyright 2025 NoxFly
3
+ * @license MIT
4
+ * @author NoxFly
5
+ */
6
+
7
+ import { Type } from '../utils/types';
8
+
9
+ /**
10
+ * A DI token uniquely identifies a dependency.
11
+ * It can wrap a class (Type<T>) or be a named symbol token.
12
+ *
13
+ * Using tokens instead of reflect-metadata means dependencies are
14
+ * declared explicitly — no magic type inference, no emitDecoratorMetadata.
15
+ *
16
+ * @example
17
+ * // Class token (most common)
18
+ * const MY_SERVICE = token(MyService);
19
+ *
20
+ * // Named symbol token (for interfaces or non-class values)
21
+ * const DB_URL = token<string>('DB_URL');
22
+ */
23
+ export class Token<T> {
24
+ public readonly description: string;
25
+
26
+ constructor(
27
+ public readonly target: Type<T> | string,
28
+ ) {
29
+ this.description = typeof target === 'string' ? target : target.name;
30
+ }
31
+
32
+ public toString(): string {
33
+ return `Token(${this.description})`;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Creates a DI token for a class type or a named value.
39
+ *
40
+ * @example
41
+ * export const MY_SERVICE = token(MyService);
42
+ * export const DB_URL = token<string>('DB_URL');
43
+ */
44
+ export function token<T>(target: Type<T> | string): Token<T> {
45
+ return new Token<T>(target);
46
+ }
47
+
48
+ /**
49
+ * The key used to look up a class token in the registry.
50
+ * For class tokens, the key is the class constructor itself.
51
+ * For named tokens, the key is the Token instance.
52
+ */
53
+ export type TokenKey<T = unknown> = Type<T> | Token<T>;