@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
@@ -0,0 +1,353 @@
1
+ /**
2
+ * @copyright 2025 NoxFly
3
+ * @license MIT
4
+ * @author NoxFly
5
+ */
6
+
7
+ import { getControllerMetadata } from '../decorators/controller.decorator';
8
+ import { Guard } from '../decorators/guards.decorator';
9
+ import { Injectable } from '../decorators/injectable.decorator';
10
+ import { getRouteMetadata, isAtomicHttpMethod } from '../decorators/method.decorator';
11
+ import { Middleware, NextFunction } from '../decorators/middleware.decorator';
12
+ import { InjectorExplorer } from '../DI/injector-explorer';
13
+ import { Logger } from '../utils/logger';
14
+ import { RadixTree } from '../utils/radix-tree';
15
+ import { Type } from '../utils/types';
16
+ import {
17
+ BadRequestException,
18
+ NotFoundException,
19
+ ResponseException,
20
+ UnauthorizedException
21
+ } from './exceptions';
22
+ import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from './request';
23
+
24
+ export interface ILazyRoute {
25
+ load: () => Promise<unknown>;
26
+ guards: Guard[];
27
+ middlewares: Middleware[];
28
+ loading: Promise<void> | null;
29
+ loaded: boolean;
30
+ }
31
+
32
+ interface LazyRouteEntry {
33
+ load: (() => Promise<unknown>) | null;
34
+ guards: Guard[];
35
+ middlewares: Middleware[];
36
+ loading: Promise<void> | null;
37
+ loaded: boolean;
38
+ }
39
+
40
+ export interface IRouteDefinition {
41
+ method: string;
42
+ path: string;
43
+ controller: Type<unknown>;
44
+ handler: string;
45
+ guards: Guard[];
46
+ middlewares: Middleware[];
47
+ }
48
+
49
+ export type ControllerAction = (request: Request, response: IResponse) => unknown;
50
+
51
+ @Injectable({ lifetime: 'singleton' })
52
+ export class Router {
53
+ private readonly routes = new RadixTree<IRouteDefinition>();
54
+ private readonly rootMiddlewares: Middleware[] = [];
55
+ private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
56
+
57
+ // -------------------------------------------------------------------------
58
+ // Registration
59
+ // -------------------------------------------------------------------------
60
+
61
+ public registerController(
62
+ controllerClass: Type<unknown>,
63
+ pathPrefix: string,
64
+ routeGuards: Guard[] = [],
65
+ routeMiddlewares: Middleware[] = [],
66
+ ): this {
67
+ const meta = getControllerMetadata(controllerClass);
68
+
69
+ if (!meta) {
70
+ throw new Error(`[Noxus] Missing @Controller decorator on ${controllerClass.name}`);
71
+ }
72
+
73
+ const routeMeta = getRouteMetadata(controllerClass);
74
+
75
+ for (const def of routeMeta) {
76
+ const fullPath = `${pathPrefix}/${def.path}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
77
+
78
+ // Route-level guards/middlewares from defineRoutes() + action-level ones
79
+ const guards = [...new Set([...routeGuards, ...def.guards])];
80
+ const middlewares = [...new Set([...routeMiddlewares, ...def.middlewares])];
81
+
82
+ const routeDef: IRouteDefinition = {
83
+ method: def.method,
84
+ path: fullPath,
85
+ controller: controllerClass,
86
+ handler: def.handler,
87
+ guards,
88
+ middlewares,
89
+ };
90
+
91
+ this.routes.insert(fullPath + '/' + def.method, routeDef);
92
+
93
+ const guardInfo = guards.length ? `<${guards.map(g => g.name).join('|')}>` : '';
94
+ Logger.log(`Mapped {${def.method} /${fullPath}}${guardInfo} route`);
95
+ }
96
+
97
+ const ctrlGuardInfo = routeGuards.length
98
+ ? `<${routeGuards.map(g => g.name).join('|')}>`
99
+ : '';
100
+ Logger.log(`Mapped ${controllerClass.name}${ctrlGuardInfo} controller's routes`);
101
+
102
+ return this;
103
+ }
104
+
105
+ public registerLazyRoute(
106
+ pathPrefix: string,
107
+ load: () => Promise<unknown>,
108
+ guards: Guard[] = [],
109
+ middlewares: Middleware[] = [],
110
+ ): this {
111
+ const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
112
+ this.lazyRoutes.set(normalized, { load, guards, middlewares, loading: null, loaded: false });
113
+ Logger.log(`Registered lazy route prefix {${normalized}}`);
114
+ return this;
115
+ }
116
+
117
+ public defineRootMiddleware(middleware: Middleware): this {
118
+ this.rootMiddlewares.push(middleware);
119
+ return this;
120
+ }
121
+
122
+ // -------------------------------------------------------------------------
123
+ // Request handling
124
+ // -------------------------------------------------------------------------
125
+
126
+ public async handle(request: Request): Promise<IResponse> {
127
+ return request.method === 'BATCH'
128
+ ? this.handleBatch(request)
129
+ : this.handleAtomic(request);
130
+ }
131
+
132
+ private async handleAtomic(request: Request): Promise<IResponse> {
133
+ Logger.comment(`> ${request.method} /${request.path}`);
134
+ const t0 = performance.now();
135
+
136
+ const response: IResponse = { requestId: request.id, status: 200, body: null };
137
+ let isCritical = false;
138
+
139
+ try {
140
+ const routeDef = await this.findRoute(request);
141
+ await this.resolveController(request, response, routeDef);
142
+
143
+ if (response.status >= 400) throw new ResponseException(response.status, response.error);
144
+ }
145
+ catch (error) {
146
+ this.fillErrorResponse(response, error, (c) => { isCritical = c; });
147
+ }
148
+ finally {
149
+ this.logResponse(request, response, performance.now() - t0, isCritical);
150
+ return response;
151
+ }
152
+ }
153
+
154
+ private async handleBatch(request: Request): Promise<IResponse> {
155
+ Logger.comment(`> ${request.method} /${request.path}`);
156
+ const t0 = performance.now();
157
+
158
+ const response: IResponse<IBatchResponsePayload> = {
159
+ requestId: request.id,
160
+ status: 200,
161
+ body: { responses: [] },
162
+ };
163
+ let isCritical = false;
164
+
165
+ try {
166
+ const payload = this.normalizeBatchPayload(request.body);
167
+ response.body!.responses = await Promise.all(
168
+ payload.requests.map((item, i) => {
169
+ const id = item.requestId ?? `${request.id}:${i}`;
170
+ return this.handleAtomic(new Request(request.event, request.senderId, id, item.method, item.path, item.body));
171
+ }),
172
+ );
173
+ }
174
+ catch (error) {
175
+ this.fillErrorResponse(response, error, (c) => { isCritical = c; });
176
+ }
177
+ finally {
178
+ this.logResponse(request, response, performance.now() - t0, isCritical);
179
+ return response;
180
+ }
181
+ }
182
+
183
+ // -------------------------------------------------------------------------
184
+ // Route resolution
185
+ // -------------------------------------------------------------------------
186
+
187
+ private tryFindRoute(request: Request): IRouteDefinition | undefined {
188
+ const matched = this.routes.search(request.path);
189
+ if (!matched?.node || matched.node.children.length === 0) return undefined;
190
+ return matched.node.findExactChild(request.method)?.value;
191
+ }
192
+
193
+ private async findRoute(request: Request): Promise<IRouteDefinition> {
194
+ const direct = this.tryFindRoute(request);
195
+ if (direct) return direct;
196
+
197
+ await this.tryLoadLazyRoute(request.path);
198
+
199
+ const afterLazy = this.tryFindRoute(request);
200
+ if (afterLazy) return afterLazy;
201
+
202
+ throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
203
+ }
204
+
205
+ private async tryLoadLazyRoute(requestPath: string): Promise<void> {
206
+ const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
207
+
208
+ for (const [prefix, entry] of this.lazyRoutes) {
209
+ if (entry.loaded) continue;
210
+ const normalized = requestPath.replace(/^\/+/, '');
211
+ if (normalized === prefix || normalized.startsWith(prefix + '/') || firstSegment === prefix) {
212
+ if (!entry.loading) entry.loading = this.loadLazyModule(prefix, entry);
213
+ await entry.loading;
214
+ return;
215
+ }
216
+ }
217
+ }
218
+
219
+ private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
220
+ const t0 = performance.now();
221
+ InjectorExplorer.beginAccumulate();
222
+
223
+ await entry.load?.();
224
+
225
+ entry.loading = null;
226
+ entry.load = null;
227
+
228
+ InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
229
+
230
+ entry.loaded = true;
231
+
232
+ Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(performance.now() - t0)}ms`);
233
+ }
234
+
235
+ // -------------------------------------------------------------------------
236
+ // Pipeline
237
+ // -------------------------------------------------------------------------
238
+
239
+ private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
240
+ const instance = request.context.resolve(routeDef.controller);
241
+ Object.assign(request.params, this.extractParams(request.path, routeDef.path));
242
+ await this.runPipeline(request, response, routeDef, instance);
243
+ }
244
+
245
+ private async runPipeline(
246
+ request: Request,
247
+ response: IResponse,
248
+ routeDef: IRouteDefinition,
249
+ controllerInstance: unknown,
250
+ ): Promise<void> {
251
+ const middlewares = [...new Set([...this.rootMiddlewares, ...routeDef.middlewares])];
252
+ const mwMax = middlewares.length - 1;
253
+ const guardMax = mwMax + routeDef.guards.length;
254
+ let index = -1;
255
+
256
+ const dispatch = async (i: number): Promise<void> => {
257
+ if (i <= index) throw new Error('next() called multiple times');
258
+ index = i;
259
+
260
+ if (i <= mwMax) {
261
+ await this.runMiddleware(request, response, dispatch.bind(null, i + 1), middlewares[i]!);
262
+ if (response.status >= 400) throw new ResponseException(response.status, response.error);
263
+ return;
264
+ }
265
+
266
+ if (i <= guardMax) {
267
+ await this.runGuard(request, routeDef.guards[i - middlewares.length]!);
268
+ await dispatch(i + 1);
269
+ return;
270
+ }
271
+
272
+ const action = (controllerInstance as Record<string, ControllerAction>)[routeDef.handler]!;
273
+ response.body = await action.call(controllerInstance, request, response);
274
+ if (response.body === undefined) response.body = {};
275
+ };
276
+
277
+ await dispatch(0);
278
+ }
279
+
280
+ private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middleware: Middleware): Promise<void> {
281
+ await middleware(request, response, next);
282
+ }
283
+
284
+ private async runGuard(request: Request, guard: Guard): Promise<void> {
285
+ if (!await guard(request)) {
286
+ throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
287
+ }
288
+ }
289
+
290
+ // -------------------------------------------------------------------------
291
+ // Utilities
292
+ // -------------------------------------------------------------------------
293
+
294
+ private extractParams(actual: string, template: string): Record<string, string> {
295
+ const aParts = actual.split('/');
296
+ const tParts = template.split('/');
297
+ const params: Record<string, string> = {};
298
+ tParts.forEach((part, i) => {
299
+ if (part.startsWith(':')) params[part.slice(1)] = aParts[i] ?? '';
300
+ });
301
+ return params;
302
+ }
303
+
304
+ private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
305
+ if (body === null || typeof body !== 'object') {
306
+ throw new BadRequestException('Batch payload must be an object containing a requests array.');
307
+ }
308
+ const { requests } = body as Partial<IBatchRequestPayload>;
309
+ if (!Array.isArray(requests)) throw new BadRequestException('Batch payload must define a requests array.');
310
+ return { requests: requests.map((e, i) => this.normalizeBatchItem(e, i)) };
311
+ }
312
+
313
+ private normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
314
+ if (entry === null || typeof entry !== 'object') throw new BadRequestException(`Batch request at index ${index} must be an object.`);
315
+ const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
316
+ if (requestId !== undefined && typeof requestId !== 'string') throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
317
+ if (typeof path !== 'string' || !path.length) throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
318
+ if (typeof method !== 'string') throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
319
+ const normalized = method.toUpperCase();
320
+ if (!isAtomicHttpMethod(normalized)) throw new BadRequestException(`Batch request at index ${index} uses unsupported method ${method}.`);
321
+ return { requestId, path, method: normalized, body };
322
+ }
323
+
324
+ private fillErrorResponse(response: IResponse, error: unknown, setCritical: (v: boolean) => void): void {
325
+ response.body = undefined;
326
+ if (error instanceof ResponseException) {
327
+ response.status = error.status;
328
+ response.error = error.message;
329
+ response.stack = error.stack;
330
+ } else if (error instanceof Error) {
331
+ setCritical(true);
332
+ response.status = 500;
333
+ response.error = error.message || 'Internal Server Error';
334
+ response.stack = error.stack;
335
+ } else {
336
+ setCritical(true);
337
+ response.status = 500;
338
+ response.error = 'Unknown error occurred';
339
+ }
340
+ }
341
+
342
+ private logResponse(request: Request, response: IResponse, ms: number, isCritical: boolean): void {
343
+ const msg = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(ms)}ms${Logger.colors.initial}`;
344
+ if (response.status < 400) Logger.log(msg);
345
+ else if (response.status < 500) Logger.warn(msg);
346
+ else isCritical ? Logger.critical(msg) : Logger.error(msg);
347
+
348
+ if (response.error) {
349
+ isCritical ? Logger.critical(response.error) : Logger.error(response.error);
350
+ if (response.stack) Logger.errorStack(response.stack);
351
+ }
352
+ }
353
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @copyright 2025 NoxFly
3
+ * @license MIT
4
+ * @author NoxFly
5
+ */
6
+
7
+ import { Guard } from '../decorators/guards.decorator';
8
+ import { Middleware } from '../decorators/middleware.decorator';
9
+
10
+ /**
11
+ * A single route entry in the application routing table.
12
+ */
13
+ export interface RouteDefinition {
14
+ /**
15
+ * The path prefix for this route (e.g. 'users', 'orders').
16
+ * All actions defined in the controller will be prefixed with this path.
17
+ */
18
+ path: string;
19
+
20
+ /**
21
+ * Dynamic import function returning the controller file.
22
+ * The controller is loaded lazily on the first IPC request targeting this prefix.
23
+ *
24
+ * @example
25
+ * load: () => import('./modules/users/users.controller')
26
+ */
27
+ load: () => Promise<unknown>;
28
+
29
+ /**
30
+ * Guards applied to every action in this controller.
31
+ * Merged with action-level guards.
32
+ */
33
+ guards?: Guard[];
34
+
35
+ /**
36
+ * Middlewares applied to every action in this controller.
37
+ * Merged with action-level middlewares.
38
+ */
39
+ middlewares?: Middleware[];
40
+ }
41
+
42
+ /**
43
+ * Defines the application routing table.
44
+ * Each entry maps a path prefix to a lazily-loaded controller.
45
+ *
46
+ * This is the single source of truth for routing — no path is declared
47
+ * in @Controller(), preventing duplicate route prefixes across controllers.
48
+ *
49
+ * @example
50
+ * export const routes = defineRoutes([
51
+ * {
52
+ * path: 'users',
53
+ * load: () => import('./modules/users/users.controller'),
54
+ * guards: [authGuard],
55
+ * },
56
+ * {
57
+ * path: 'orders',
58
+ * load: () => import('./modules/orders/orders.controller'),
59
+ * guards: [authGuard],
60
+ * middlewares: [logMiddleware],
61
+ * },
62
+ * ]);
63
+ */
64
+ export function defineRoutes(routes: RouteDefinition[]): RouteDefinition[] {
65
+ const paths = routes.map(r => r.path.replace(/^\/+|\/+$/g, ''));
66
+ const duplicates = paths.filter((p, i) => paths.indexOf(p) !== i);
67
+
68
+ if (duplicates.length > 0) {
69
+ throw new Error(
70
+ `[Noxus] Duplicate route prefixes detected: ${[...new Set(duplicates)].map(d => `"${d}"`).join(', ')}`
71
+ );
72
+ }
73
+
74
+ return routes.map(r => ({
75
+ ...r,
76
+ path: r.path.replace(/^\/+|\/+$/g, ''),
77
+ }));
78
+ }
@@ -8,16 +8,16 @@
8
8
  * Centralizes MessagePort storage for renderer communication and handles
9
9
  * push-event delivery back to renderer processes.
10
10
  */
11
- import { Injectable } from 'src/decorators/injectable.decorator';
12
- import { createRendererEventMessage } from 'src/request';
13
- import { Logger } from 'src/utils/logger';
11
+ import { Injectable } from '../decorators/injectable.decorator';
12
+ import { Logger } from '../utils/logger';
13
+ import { createRendererEventMessage } from './request';
14
14
 
15
15
  interface RendererChannels {
16
16
  request: Electron.MessageChannelMain;
17
17
  socket: Electron.MessageChannelMain;
18
18
  }
19
19
 
20
- @Injectable('singleton')
20
+ @Injectable({ lifetime: 'singleton' })
21
21
  export class NoxSocket {
22
22
  private readonly channels = new Map<number, RendererChannels>();
23
23
 
package/src/main.ts CHANGED
@@ -2,29 +2,25 @@
2
2
  * @copyright 2025 NoxFly
3
3
  * @license MIT
4
4
  * @author NoxFly
5
- */
6
-
7
- /**
5
+ *
8
6
  * Entry point for Electron main-process consumers.
9
- * order of exports here matters and can affect module resolution.
10
- * Please be cautious when modifying.
11
7
  */
12
8
 
13
9
  export * from './DI/app-injector';
14
- export * from './router';
15
- export * from './app';
16
- export * from './bootstrap';
17
- export * from './exceptions';
10
+ export * from './DI/token';
11
+ export * from './internal/router';
12
+ export * from './internal/app';
13
+ export * from './internal/bootstrap';
14
+ export * from './internal/exceptions';
18
15
  export * from './decorators/middleware.decorator';
19
16
  export * from './decorators/guards.decorator';
20
17
  export * from './decorators/controller.decorator';
21
18
  export * from './decorators/injectable.decorator';
22
- export * from './decorators/inject.decorator';
23
19
  export * from './decorators/method.decorator';
24
- export * from './decorators/module.decorator';
25
20
  export * from './utils/logger';
26
21
  export * from './utils/types';
27
22
  export * from './utils/forward-ref';
28
- export * from './request';
29
- export * from './socket';
30
-
23
+ export * from './internal/request';
24
+ export * from './internal/socket';
25
+ export * from './window/window-manager';
26
+ export * from './internal/routes';
@@ -15,9 +15,8 @@
15
15
  */
16
16
 
17
17
  export * from './DI/app-injector';
18
- export * from './exceptions';
18
+ export * from './internal/exceptions';
19
19
  export * from './decorators/injectable.decorator';
20
- export * from './decorators/inject.decorator';
21
20
  export * from './utils/logger';
22
21
  export * from './utils/types';
23
22
  export * from './utils/forward-ref';
package/src/preload.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @copyright 2025 NoxFly
3
+ * @license MIT
4
+ * @author NoxFly
5
+ *
6
+ * Entry point for Electron preload scripts.
7
+ * Imports electron/renderer — must NOT be bundled into renderer web code.
8
+ */
9
+
10
+ export * from './internal/preload-bridge';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @copyright 2025 NoxFly
3
+ * @license MIT
4
+ * @author NoxFly
5
+ *
6
+ * Entry point for renderer web consumers (Angular, React, Vue, Vanilla...).
7
+ * No Electron imports — safe to bundle with any web bundler.
8
+ */
9
+
10
+ export * from './internal/renderer-client';
11
+ export * from './internal/renderer-events';
12
+ export * from './internal/request';
13
+ export type { HttpMethod, AtomicHttpMethod } from './decorators/method.decorator';