@noxfly/noxus 2.4.0 → 2.5.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/dist/child.js +115 -10
- package/dist/child.mjs +115 -10
- package/dist/main.d.mts +72 -21
- package/dist/main.d.ts +72 -21
- package/dist/main.js +160 -381
- package/dist/main.mjs +161 -379
- package/dist/renderer.d.mts +112 -1
- package/dist/renderer.d.ts +112 -1
- package/dist/renderer.js +40 -2
- package/dist/renderer.mjs +39 -2
- package/dist/{index-BxWQVi6C.d.ts → request-CdpZ9qZL.d.ts} +1 -87
- package/dist/{index-DQBQQfMw.d.mts → request-Dx_5Prte.d.mts} +1 -87
- package/package.json +1 -1
- package/src/DI/injector-explorer.ts +49 -4
- package/src/app.ts +37 -0
- package/src/bootstrap.ts +18 -3
- package/src/index.ts +1 -0
- package/src/main.ts +0 -3
- package/src/router.ts +99 -6
package/src/app.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { app, BrowserWindow, ipcMain, MessageChannelMain } from "electron/main";
|
|
|
8
8
|
import { Injectable } from "src/decorators/injectable.decorator";
|
|
9
9
|
import { IMiddleware } from "src/decorators/middleware.decorator";
|
|
10
10
|
import { inject } from "src/DI/app-injector";
|
|
11
|
+
import { InjectorExplorer } from "src/DI/injector-explorer";
|
|
11
12
|
import { IRequest, IResponse, Request } from "src/request";
|
|
12
13
|
import { NoxSocket } from "src/socket";
|
|
13
14
|
import { Router } from "src/router";
|
|
@@ -173,6 +174,42 @@ export class NoxApp {
|
|
|
173
174
|
this.mainWindow = window;
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Registers a lazy-loaded route. The module behind this path prefix
|
|
179
|
+
* will only be dynamically imported when the first IPC request
|
|
180
|
+
* targets this prefix — like Angular's loadChildren.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* noxApp.lazy("auth", () => import("./modules/auth/auth.module.js"));
|
|
185
|
+
* noxApp.lazy("printing", () => import("./modules/printing/printing.module.js"));
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* @param pathPrefix - The route prefix (e.g. "auth", "cash-register").
|
|
189
|
+
* @param loadModule - A function returning a dynamic import promise.
|
|
190
|
+
* @returns NoxApp instance for method chaining.
|
|
191
|
+
*/
|
|
192
|
+
public lazy(pathPrefix: string, loadModule: () => Promise<unknown>): NoxApp {
|
|
193
|
+
this.router.registerLazyRoute(pathPrefix, loadModule);
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Eagerly loads one or more modules with a two-phase DI guarantee.
|
|
199
|
+
* Use this when a service needed at startup lives inside a module
|
|
200
|
+
* (e.g. the Application service depends on LoaderService).
|
|
201
|
+
*
|
|
202
|
+
* All dynamic imports run in parallel; bindings are registered first,
|
|
203
|
+
* then singletons are resolved — safe regardless of import ordering.
|
|
204
|
+
*
|
|
205
|
+
* @param importFns - Functions returning dynamic import promises.
|
|
206
|
+
*/
|
|
207
|
+
public async loadModules(importFns: Array<() => Promise<unknown>>): Promise<void> {
|
|
208
|
+
InjectorExplorer.beginAccumulate();
|
|
209
|
+
await Promise.all(importFns.map(fn => fn()));
|
|
210
|
+
InjectorExplorer.flushAccumulated();
|
|
211
|
+
}
|
|
212
|
+
|
|
176
213
|
/**
|
|
177
214
|
* Configures the NoxApp instance with the provided application class.
|
|
178
215
|
* This method allows you to set the application class that will handle lifecycle events.
|
package/src/bootstrap.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @author NoxFly
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { app, BrowserWindow } from "electron/main";
|
|
7
|
+
import { app, BrowserWindow, screen } from "electron/main";
|
|
8
8
|
import { NoxApp } from "src/app";
|
|
9
9
|
import { getModuleMetadata } from "src/decorators/module.decorator";
|
|
10
10
|
import { inject } from "src/DI/app-injector";
|
|
@@ -38,8 +38,8 @@ export interface BootstrapOptions {
|
|
|
38
38
|
* @return A promise that resolves to the NoxApp instance.
|
|
39
39
|
* @throws Error if the root module is not decorated with @Module, or if the electron process could not start.
|
|
40
40
|
*/
|
|
41
|
-
export async function bootstrapApplication(rootModule
|
|
42
|
-
if(!getModuleMetadata(rootModule)) {
|
|
41
|
+
export async function bootstrapApplication(rootModule?: Type<any> | null, options?: BootstrapOptions): Promise<NoxApp> {
|
|
42
|
+
if(rootModule && !getModuleMetadata(rootModule)) {
|
|
43
43
|
throw new Error(`Root module must be decorated with @Module`);
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -51,6 +51,21 @@ export async function bootstrapApplication(rootModule: Type<any>, options?: Boot
|
|
|
51
51
|
|
|
52
52
|
if(options?.window) {
|
|
53
53
|
mainWindow = new BrowserWindow(options.window);
|
|
54
|
+
|
|
55
|
+
mainWindow.once("ready-to-show", () => {
|
|
56
|
+
mainWindow?.show();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
60
|
+
const { width, height } = primaryDisplay.workAreaSize;
|
|
61
|
+
|
|
62
|
+
if(options.window.minWidth && options.window.minHeight) {
|
|
63
|
+
mainWindow.setSize(
|
|
64
|
+
Math.min(width, options.window.minWidth),
|
|
65
|
+
Math.min(height, options.window.minHeight),
|
|
66
|
+
true,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
// Process all deferred injectable registrations (two-phase: bindings then resolution)
|
package/src/index.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -22,12 +22,9 @@ export * from './decorators/injectable.decorator';
|
|
|
22
22
|
export * from './decorators/inject.decorator';
|
|
23
23
|
export * from './decorators/method.decorator';
|
|
24
24
|
export * from './decorators/module.decorator';
|
|
25
|
-
export * from './preload-bridge';
|
|
26
25
|
export * from './utils/logger';
|
|
27
26
|
export * from './utils/types';
|
|
28
27
|
export * from './utils/forward-ref';
|
|
29
28
|
export * from './request';
|
|
30
|
-
export * from './renderer-events';
|
|
31
|
-
export * from './renderer-client';
|
|
32
29
|
export * from './socket';
|
|
33
30
|
|
package/src/router.ts
CHANGED
|
@@ -12,10 +12,28 @@ import { AtomicHttpMethod, getRouteMetadata } from 'src/decorators/method.decora
|
|
|
12
12
|
import { getMiddlewaresForController, getMiddlewaresForControllerAction, IMiddleware, NextFunction } from 'src/decorators/middleware.decorator';
|
|
13
13
|
import { BadRequestException, MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
|
|
14
14
|
import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from 'src/request';
|
|
15
|
+
import { InjectorExplorer } from 'src/DI/injector-explorer';
|
|
15
16
|
import { Logger } from 'src/utils/logger';
|
|
16
17
|
import { RadixTree } from 'src/utils/radix-tree';
|
|
17
18
|
import { Type } from 'src/utils/types';
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A lazy route entry maps a path prefix to a dynamic import function.
|
|
22
|
+
* The module is loaded on the first request matching the prefix.
|
|
23
|
+
*/
|
|
24
|
+
export interface ILazyRoute {
|
|
25
|
+
/** Path prefix (e.g. "auth", "printing"). Matched against the first segment(s) of the request path. */
|
|
26
|
+
path: string;
|
|
27
|
+
/** Dynamic import function returning the module file. */
|
|
28
|
+
loadModule: () => Promise<unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface LazyRouteEntry {
|
|
32
|
+
loadModule: () => Promise<unknown>;
|
|
33
|
+
loading: Promise<void> | null;
|
|
34
|
+
loaded: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
const ATOMIC_HTTP_METHODS: ReadonlySet<AtomicHttpMethod> = new Set<AtomicHttpMethod>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
20
38
|
|
|
21
39
|
function isAtomicHttpMethod(method: unknown): method is AtomicHttpMethod {
|
|
@@ -51,6 +69,7 @@ export type ControllerAction = (request: Request, response: IResponse) => any;
|
|
|
51
69
|
export class Router {
|
|
52
70
|
private readonly routes = new RadixTree<IRouteDefinition>();
|
|
53
71
|
private readonly rootMiddlewares: Type<IMiddleware>[] = [];
|
|
72
|
+
private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
|
|
54
73
|
|
|
55
74
|
/**
|
|
56
75
|
* Registers a controller class with the router.
|
|
@@ -109,6 +128,21 @@ export class Router {
|
|
|
109
128
|
return this;
|
|
110
129
|
}
|
|
111
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Registers a lazy route. The module behind this route prefix will only
|
|
133
|
+
* be imported (and its controllers/services registered in DI) the first
|
|
134
|
+
* time a request targets this prefix.
|
|
135
|
+
*
|
|
136
|
+
* @param pathPrefix - Route prefix (e.g. "auth"). Matched against the first segment of the request path.
|
|
137
|
+
* @param loadModule - A function that returns a dynamic import promise.
|
|
138
|
+
*/
|
|
139
|
+
public registerLazyRoute(pathPrefix: string, loadModule: () => Promise<unknown>): Router {
|
|
140
|
+
const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
|
|
141
|
+
this.lazyRoutes.set(normalized, { loadModule, loading: null, loaded: false });
|
|
142
|
+
Logger.log(`Registered lazy route prefix {${normalized}}`);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
112
146
|
/**
|
|
113
147
|
* Defines a middleware for the root of the application.
|
|
114
148
|
* This method allows you to register a middleware that will be applied to all requests
|
|
@@ -148,7 +182,7 @@ export class Router {
|
|
|
148
182
|
let isCritical: boolean = false;
|
|
149
183
|
|
|
150
184
|
try {
|
|
151
|
-
const routeDef = this.findRoute(request);
|
|
185
|
+
const routeDef = await this.findRoute(request);
|
|
152
186
|
await this.resolveController(request, response, routeDef);
|
|
153
187
|
|
|
154
188
|
if(response.status > 400) {
|
|
@@ -352,20 +386,79 @@ export class Router {
|
|
|
352
386
|
* @param request - The Request object containing the method and path to search for.
|
|
353
387
|
* @returns The IRouteDefinition for the matched route.
|
|
354
388
|
*/
|
|
355
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Attempts to find a route definition for the given request.
|
|
391
|
+
* Returns undefined instead of throwing when the route is not found,
|
|
392
|
+
* so the caller can try lazy-loading first.
|
|
393
|
+
*/
|
|
394
|
+
private tryFindRoute(request: Request): IRouteDefinition | undefined {
|
|
356
395
|
const matchedRoutes = this.routes.search(request.path);
|
|
357
396
|
|
|
358
397
|
if(matchedRoutes?.node === undefined || matchedRoutes.node.children.length === 0) {
|
|
359
|
-
|
|
398
|
+
return undefined;
|
|
360
399
|
}
|
|
361
400
|
|
|
362
401
|
const routeDef = matchedRoutes.node.findExactChild(request.method);
|
|
402
|
+
return routeDef?.value;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Finds the route definition for a given request.
|
|
407
|
+
* If no eagerly-registered route matches, attempts to load a lazy module
|
|
408
|
+
* whose prefix matches the request path, then retries.
|
|
409
|
+
*/
|
|
410
|
+
private async findRoute(request: Request): Promise<IRouteDefinition> {
|
|
411
|
+
// Fast path: route already registered
|
|
412
|
+
const direct = this.tryFindRoute(request);
|
|
413
|
+
if(direct) return direct;
|
|
363
414
|
|
|
364
|
-
|
|
365
|
-
|
|
415
|
+
// Try lazy route loading
|
|
416
|
+
await this.tryLoadLazyRoute(request.path);
|
|
417
|
+
|
|
418
|
+
// Retry after lazy load
|
|
419
|
+
const afterLazy = this.tryFindRoute(request);
|
|
420
|
+
if(afterLazy) return afterLazy;
|
|
421
|
+
|
|
422
|
+
throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Given a request path, checks whether a lazy route prefix matches
|
|
427
|
+
* and triggers the dynamic import if it hasn't been loaded yet.
|
|
428
|
+
*/
|
|
429
|
+
private async tryLoadLazyRoute(requestPath: string): Promise<void> {
|
|
430
|
+
const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
|
|
431
|
+
|
|
432
|
+
// Check exact first segment, then try multi-segment prefixes
|
|
433
|
+
for(const [prefix, entry] of this.lazyRoutes) {
|
|
434
|
+
if(entry.loaded) continue;
|
|
435
|
+
|
|
436
|
+
const normalizedPath = requestPath.replace(/^\/+/, '');
|
|
437
|
+
if(normalizedPath === prefix || normalizedPath.startsWith(prefix + '/') || firstSegment === prefix) {
|
|
438
|
+
if(!entry.loading) {
|
|
439
|
+
entry.loading = this.loadLazyModule(prefix, entry);
|
|
440
|
+
}
|
|
441
|
+
await entry.loading;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
366
444
|
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Dynamically imports a lazy module and registers its decorated classes
|
|
449
|
+
* (controllers, services) in the DI container using the two-phase strategy.
|
|
450
|
+
*/
|
|
451
|
+
private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
|
|
452
|
+
const t0 = performance.now();
|
|
453
|
+
|
|
454
|
+
InjectorExplorer.beginAccumulate();
|
|
455
|
+
await entry.loadModule();
|
|
456
|
+
InjectorExplorer.flushAccumulated();
|
|
457
|
+
|
|
458
|
+
entry.loaded = true;
|
|
367
459
|
|
|
368
|
-
|
|
460
|
+
const t1 = performance.now();
|
|
461
|
+
Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(t1 - t0)}ms`);
|
|
369
462
|
}
|
|
370
463
|
|
|
371
464
|
/**
|