@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,217 @@
1
+ /**
2
+ * @copyright 2025 NoxFly
3
+ * @license MIT
4
+ * @author NoxFly
5
+ */
6
+
7
+ import { app, BrowserWindow, ipcMain, MessageChannelMain } from 'electron/main';
8
+ import { Guard } from "src/main";
9
+ import { Injectable } from '../decorators/injectable.decorator';
10
+ import { Middleware } from '../decorators/middleware.decorator';
11
+ import { inject } from '../DI/app-injector';
12
+ import { InjectorExplorer } from '../DI/injector-explorer';
13
+ import { Logger } from '../utils/logger';
14
+ import { Type } from '../utils/types';
15
+ import { WindowManager } from '../window/window-manager';
16
+ import { IResponse, Request } from './request';
17
+ import { Router } from './router';
18
+ import { NoxSocket } from './socket';
19
+
20
+ /**
21
+ * Your application service should implement IApp.
22
+ * Noxus calls these lifecycle methods at the appropriate time.
23
+ *
24
+ * Unlike v2, IApp no longer receives a BrowserWindow in onReady.
25
+ * Use the injected WindowManager instead — it is more flexible and
26
+ * does not couple the lifecycle to a single pre-created window.
27
+ *
28
+ * @example
29
+ * @Injectable({ lifetime: 'singleton', deps: [WindowManager, MyService] })
30
+ * class AppService implements IApp {
31
+ * constructor(private wm: WindowManager, private svc: MyService) {}
32
+ *
33
+ * async onReady() {
34
+ * const win = await this.wm.createSplash({ webPreferences: { preload: ... } });
35
+ * win.loadFile('index.html');
36
+ * }
37
+ *
38
+ * async onActivated() { ... }
39
+ * async dispose() { ... }
40
+ * }
41
+ */
42
+ export interface IApp {
43
+ dispose(): Promise<void>;
44
+ onReady(): Promise<void>;
45
+ onActivated(): Promise<void>;
46
+ }
47
+
48
+ @Injectable({ lifetime: 'singleton', deps: [Router, NoxSocket, WindowManager] })
49
+ export class NoxApp {
50
+ private appService: IApp | undefined;
51
+
52
+ private readonly router = inject(Router);
53
+ private readonly socket = inject(NoxSocket);
54
+ public readonly windowManager = inject(WindowManager);
55
+
56
+ // -------------------------------------------------------------------------
57
+ // Initialisation
58
+ // -------------------------------------------------------------------------
59
+
60
+ public async init(): Promise<this> {
61
+ ipcMain.on('gimme-my-port', this.giveTheRendererAPort.bind(this));
62
+ app.once('activate', this.onAppActivated.bind(this));
63
+ app.once('window-all-closed', this.onAllWindowsClosed.bind(this));
64
+ console.log('');
65
+ return this;
66
+ }
67
+
68
+ // -------------------------------------------------------------------------
69
+ // Public API
70
+ // -------------------------------------------------------------------------
71
+
72
+ /**
73
+ * Registers a lazy route. The file behind this prefix is dynamically
74
+ * imported on the first IPC request that targets it.
75
+ *
76
+ * The import function should NOT statically reference heavy modules —
77
+ * the whole point is to defer their loading.
78
+ *
79
+ * @example
80
+ * noxApp.lazy('auth', () => import('./modules/auth/auth.controller.js'));
81
+ * noxApp.lazy('reporting', () => import('./modules/reporting/index.js'));
82
+ */
83
+ public lazy(
84
+ pathPrefix: string,
85
+ load: () => Promise<unknown>,
86
+ guards: Guard[] = [],
87
+ middlewares: Middleware[] = [],
88
+ ): this {
89
+ this.router.registerLazyRoute(pathPrefix, load, guards, middlewares);
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * Eagerly loads a set of modules (controllers + services) before start().
95
+ * Use this for modules that provide services needed by your IApp.onReady().
96
+ *
97
+ * All imports run in parallel; DI is flushed with the two-phase guarantee.
98
+ */
99
+ public async load(importFns: Array<() => Promise<unknown>>): Promise<this> {
100
+ InjectorExplorer.beginAccumulate();
101
+ await Promise.all(importFns.map((fn) => fn()));
102
+ InjectorExplorer.flushAccumulated();
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * Registers a global middleware applied to every route.
108
+ */
109
+ public use(middleware: Middleware): this {
110
+ this.router.defineRootMiddleware(middleware);
111
+ return this;
112
+ }
113
+
114
+ /**
115
+ * Sets the application service (implements IApp) that receives lifecycle events.
116
+ * @param appClass - Class decorated with @Injectable that implements IApp.
117
+ */
118
+ public configure(appClass: Type<IApp>): this {
119
+ this.appService = inject(appClass);
120
+ return this;
121
+ }
122
+
123
+ /**
124
+ * Calls IApp.onReady(). Should be called after configure() and any lazy()
125
+ * registrations are set up.
126
+ */
127
+ public start(): this {
128
+ this.appService?.onReady();
129
+ return this;
130
+ }
131
+
132
+ // -------------------------------------------------------------------------
133
+ // IPC
134
+ // -------------------------------------------------------------------------
135
+
136
+ private readonly onRendererMessage = async (event: Electron.MessageEvent): Promise<void> => {
137
+ const { senderId, requestId, path, method, body }: import('./request').IRequest = event.data;
138
+ const channels = this.socket.get(senderId);
139
+
140
+ if (!channels) {
141
+ Logger.error(`No message channel found for sender ID: ${senderId}`);
142
+ return;
143
+ }
144
+
145
+ try {
146
+ const request = new Request(event, senderId, requestId, method, path, body);
147
+ const response = await this.router.handle(request);
148
+ channels.request.port1.postMessage(response);
149
+ }
150
+ catch (err: unknown) {
151
+ const response: IResponse = {
152
+ requestId,
153
+ status: 500,
154
+ body: null,
155
+ error: err instanceof Error ? err.message : 'Internal Server Error',
156
+ };
157
+ channels.request.port1.postMessage(response);
158
+ }
159
+ };
160
+
161
+ private giveTheRendererAPort(event: Electron.IpcMainInvokeEvent): void {
162
+ const senderId = event.sender.id;
163
+
164
+ if (this.socket.get(senderId)) {
165
+ this.shutdownChannel(senderId);
166
+ }
167
+
168
+ const requestChannel = new MessageChannelMain();
169
+ const socketChannel = new MessageChannelMain();
170
+
171
+ requestChannel.port1.on('message', this.onRendererMessage);
172
+ requestChannel.port1.start();
173
+ socketChannel.port1.start();
174
+
175
+ event.sender.once('destroyed', () => this.shutdownChannel(senderId));
176
+
177
+ this.socket.register(senderId, requestChannel, socketChannel);
178
+ event.sender.postMessage('port', { senderId }, [requestChannel.port2, socketChannel.port2]);
179
+ }
180
+
181
+ // -------------------------------------------------------------------------
182
+ // Lifecycle
183
+ // -------------------------------------------------------------------------
184
+
185
+ private onAppActivated(): void {
186
+ if (process.platform === 'darwin' && BrowserWindow.getAllWindows().length === 0) {
187
+ this.appService?.onActivated();
188
+ }
189
+ }
190
+
191
+ private async onAllWindowsClosed(): Promise<void> {
192
+ for (const senderId of this.socket.getSenderIds()) {
193
+ this.shutdownChannel(senderId);
194
+ }
195
+
196
+ Logger.info('All windows closed, shutting down application...');
197
+ await this.appService?.dispose();
198
+
199
+ if (process.platform !== 'darwin') app.quit();
200
+ }
201
+
202
+ private shutdownChannel(channelSenderId: number): void {
203
+ const channels = this.socket.get(channelSenderId);
204
+
205
+ if (!channels) {
206
+ return;
207
+ }
208
+
209
+ channels.request.port1.off('message', this.onRendererMessage);
210
+ channels.request.port1.close();
211
+ channels.request.port2.close();
212
+ channels.socket.port1.close();
213
+ channels.socket.port2.close();
214
+
215
+ this.socket.unregister(channelSenderId);
216
+ }
217
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @copyright 2025 NoxFly
3
+ * @license MIT
4
+ * @author NoxFly
5
+ */
6
+
7
+ import { app } from 'electron/main';
8
+ import { inject, RootInjector } from '../DI/app-injector';
9
+ import { InjectorExplorer } from '../DI/injector-explorer';
10
+ import { TokenKey } from '../DI/token';
11
+ import { NoxApp } from './app';
12
+ import { RouteDefinition } from "./routes";
13
+
14
+ /**
15
+ * A singleton value override: provides an already-constructed instance
16
+ * for a given token, bypassing the DI factory.
17
+ *
18
+ * Useful for injecting external singletons (e.g. a database connection,
19
+ * a logger already configured, a third-party SDK wrapper) that cannot
20
+ * or should not be constructed by the DI container.
21
+ *
22
+ * @example
23
+ * { token: MikroORM, useValue: await MikroORM.init(config) }
24
+ * { token: DB_URL, useValue: process.env.DATABASE_URL }
25
+ */
26
+ export interface SingletonOverride<T = unknown> {
27
+ token: TokenKey<T>;
28
+ useValue: T;
29
+ }
30
+
31
+ /**
32
+ * Configuration object for bootstrapApplication.
33
+ */
34
+ export interface BootstrapConfig {
35
+ /**
36
+ * Application routing table, produced by defineRoutes().
37
+ * All lazy routes are registered before the app starts.
38
+ */
39
+ routes?: RouteDefinition[];
40
+
41
+ /**
42
+ * Pre-built singleton instances to inject into the DI container
43
+ * before the application starts.
44
+ *
45
+ * This replaces the v2 module/provider declaration pattern for
46
+ * external singletons.
47
+ *
48
+ * @example
49
+ * singletons: [
50
+ * { token: MikroORM, useValue: await MikroORM.init(ormConfig) },
51
+ * { token: DB_URL, useValue: process.env.DATABASE_URL! },
52
+ * ]
53
+ */
54
+ singletons?: SingletonOverride[];
55
+
56
+ /**
57
+ * Controllers and services to eagerly load before NoxApp.start() is called.
58
+ * Each entry is a dynamic import function — files are imported in parallel.
59
+ *
60
+ * Use this only for things needed at startup (e.g. if your IApp service
61
+ * depends on a service in an otherwise lazy module).
62
+ *
63
+ * Everything else should be registered via noxApp.lazy().
64
+ *
65
+ * @example
66
+ * eagerLoad: [
67
+ * () => import('./modules/auth/auth.controller.js'),
68
+ * ]
69
+ */
70
+ eagerLoad?: Array<() => Promise<unknown>>;
71
+ }
72
+
73
+ /**
74
+ * Bootstraps the Noxus application.
75
+ */
76
+ export async function bootstrapApplication(config: BootstrapConfig = {}): Promise<NoxApp> {
77
+ await app.whenReady();
78
+
79
+ // Build override map for the DI flush phase
80
+ const overrides = new Map<TokenKey, unknown>();
81
+ for (const { token, useValue } of config.singletons ?? []) {
82
+ overrides.set(token, useValue);
83
+ // Pre-register the binding so the injector knows the token exists
84
+ RootInjector.singletons.set(token as any, useValue);
85
+ }
86
+
87
+ // Flush all classes enqueued by decorators at import time (two-phase)
88
+ InjectorExplorer.processPending(overrides);
89
+
90
+ // Resolve core framework singletons
91
+ const noxApp = inject(NoxApp);
92
+
93
+ // Register routes from the routing table
94
+ if (config.routes?.length) {
95
+ for (const route of config.routes) {
96
+ noxApp.lazy(route.path, route.load, route.guards, route.middlewares);
97
+ }
98
+ }
99
+
100
+ // Eagerly load optional modules
101
+ if (config.eagerLoad?.length) {
102
+ await noxApp.load(config.eagerLoad);
103
+ }
104
+
105
+ await noxApp.init();
106
+
107
+ return noxApp;
108
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { contextBridge, ipcRenderer } from 'electron/renderer';
8
- import type { IPortRequester } from 'src/renderer-client';
8
+ import type { IPortRequester } from './renderer-client';
9
9
 
10
10
  export interface NoxusPreloadAPI extends IPortRequester {}
11
11
 
@@ -4,8 +4,8 @@
4
4
  * @author NoxFly
5
5
  */
6
6
 
7
- import { IBatchRequestItem, IBatchResponsePayload, IRequest, IResponse } from 'src/request';
8
- import { RendererEventRegistry } from 'src/renderer-events';
7
+ import { IBatchRequestItem, IBatchResponsePayload, IRequest, IResponse } from './request';
8
+ import { RendererEventRegistry } from './renderer-events';
9
9
 
10
10
  export interface IPortRequester {
11
11
  requestPort(): void;
@@ -8,7 +8,7 @@
8
8
  * Lightweight event registry to help renderer processes subscribe to
9
9
  * push messages sent by the main process through Noxus.
10
10
  */
11
- import { IRendererEventMessage, isRendererEventMessage } from 'src/request';
11
+ import { IRendererEventMessage, isRendererEventMessage } from './request';
12
12
 
13
13
  export type RendererEventHandler<TPayload = unknown> = (payload: TPayload) => void;
14
14
 
@@ -4,9 +4,9 @@
4
4
  * @author NoxFly
5
5
  */
6
6
 
7
- import 'reflect-metadata';
8
- import { AtomicHttpMethod, HttpMethod } from 'src/decorators/method.decorator';
9
- import { AppInjector, RootInjector } from 'src/DI/app-injector';
7
+
8
+ import { AtomicHttpMethod, HttpMethod } from '../decorators/method.decorator';
9
+ import { AppInjector, RootInjector } from '../DI/app-injector';
10
10
 
11
11
  /**
12
12
  * The Request class represents an HTTP request in the Noxus framework.