@noxfly/noxus 3.0.0-dev.9 → 3.0.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/LICENSE +20 -20
- package/README.md +686 -686
- package/dist/child.d.mts +1 -1
- package/dist/child.d.ts +1 -1
- package/dist/child.js +39 -39
- package/dist/child.js.map +1 -1
- package/dist/child.mjs +39 -39
- package/dist/child.mjs.map +1 -1
- package/dist/main.d.mts +17 -7
- package/dist/main.d.ts +17 -7
- package/dist/main.js +175 -100
- package/dist/main.js.map +1 -1
- package/dist/main.mjs +176 -100
- package/dist/main.mjs.map +1 -1
- package/dist/preload.js.map +1 -1
- package/dist/preload.mjs.map +1 -1
- package/dist/renderer.js +40 -24
- package/dist/renderer.js.map +1 -1
- package/dist/renderer.mjs +40 -24
- package/dist/renderer.mjs.map +1 -1
- package/package.json +99 -94
- package/dist/app-injector-Bz3Upc0y.d.mts +0 -125
- package/dist/app-injector-Bz3Upc0y.d.ts +0 -125
- package/dist/request-BlTtiHbi.d.ts +0 -112
- package/dist/request-qJ9EiDZc.d.mts +0 -112
package/README.md
CHANGED
|
@@ -1,686 +1,686 @@
|
|
|
1
|
-
# @noxfly/noxus
|
|
2
|
-
|
|
3
|
-
Lightweight IPC framework for Electron, inspired by NestJS and Angular. It simulates HTTP-like requests between the renderer and the main process via `MessageChannel` / `MessagePort`, with routing, DI, guards, middlewares, and lazy-loading.
|
|
4
|
-
|
|
5
|
-
No dependency on `reflect-metadata` or `emitDecoratorMetadata`.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Installation
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install @noxfly/noxus
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
In your `tsconfig.json`:
|
|
16
|
-
```json
|
|
17
|
-
{
|
|
18
|
-
"compilerOptions": {
|
|
19
|
-
"experimentalDecorators": true,
|
|
20
|
-
"emitDecoratorMetadata": false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## Concepts
|
|
28
|
-
|
|
29
|
-
| Concept | Role |
|
|
30
|
-
| ------------------------ | ----------------------------------------------------- |
|
|
31
|
-
| **Controller** | Handles a set of IPC routes under a path prefix |
|
|
32
|
-
| **Injectable** | Service injectable into other services or controllers |
|
|
33
|
-
| **Guard** | Protects a route or controller (authorization) |
|
|
34
|
-
| **Middleware** | Runs before guards and the handler |
|
|
35
|
-
| **Token** | Explicit identifier for non-class dependencies |
|
|
36
|
-
| **WindowManager** | Singleton service for managing Electron windows |
|
|
37
|
-
| **bootstrapApplication** | Single entry point of the application |
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Quick start
|
|
42
|
-
|
|
43
|
-
### 1. Create a service
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
// services/user.service.ts
|
|
47
|
-
import { Injectable } from '@noxfly/noxus/main';
|
|
48
|
-
|
|
49
|
-
@Injectable({ lifetime: 'singleton' })
|
|
50
|
-
export class UserService {
|
|
51
|
-
private users = [{ id: 1, name: 'Alice' }];
|
|
52
|
-
|
|
53
|
-
findAll() {
|
|
54
|
-
return this.users;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
findById(id: number) {
|
|
58
|
-
return this.users.find(u => u.id === id);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 2. Create a controller
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
// controllers/user.controller.ts
|
|
67
|
-
import { Controller, Get, Post, Request } from '@noxfly/noxus/main';
|
|
68
|
-
import { UserService } from '../services/user.service';
|
|
69
|
-
|
|
70
|
-
@Controller({ path: 'users', deps: [UserService] })
|
|
71
|
-
export class UserController {
|
|
72
|
-
constructor(private svc: UserService) {}
|
|
73
|
-
|
|
74
|
-
@Get('list')
|
|
75
|
-
list(req: Request) {
|
|
76
|
-
return this.svc.findAll();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
@Get(':id')
|
|
80
|
-
getOne(req: Request) {
|
|
81
|
-
const id = parseInt(req.params['id']!);
|
|
82
|
-
return this.svc.findById(id);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@Post('create')
|
|
86
|
-
create(req: Request) {
|
|
87
|
-
return { created: true, body: req.body };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### 3. Create the application service
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
// app.service.ts
|
|
96
|
-
import { IApp, Injectable, WindowManager } from '@noxfly/noxus/main';
|
|
97
|
-
import path from 'path';
|
|
98
|
-
|
|
99
|
-
@Injectable({ lifetime: 'singleton', deps: [WindowManager] })
|
|
100
|
-
export class AppService implements IApp {
|
|
101
|
-
constructor(private wm: WindowManager) {}
|
|
102
|
-
|
|
103
|
-
async onReady() {
|
|
104
|
-
const win = await this.wm.createSplash({
|
|
105
|
-
webPreferences: {
|
|
106
|
-
preload: path.join(__dirname, 'preload.js'),
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
win.loadFile('index.html');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async onActivated() {
|
|
114
|
-
if (this.wm.count === 0) await this.onReady();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async dispose() {
|
|
118
|
-
// cleanup on app close
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### 4. Define routes
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
// routes.ts
|
|
127
|
-
import { defineRoutes } from '@noxfly/noxus/main';
|
|
128
|
-
|
|
129
|
-
export const routes = defineRoutes([
|
|
130
|
-
{ path: 'users', load: () => import('./controllers/user.controller.js') },
|
|
131
|
-
{ path: 'orders', load: () => import('./controllers/order.controller.js') },
|
|
132
|
-
]);
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### 5. Bootstrap the application
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
// main.ts
|
|
139
|
-
import { bootstrapApplication } from '@noxfly/noxus/main';
|
|
140
|
-
import { AppService } from './app.service';
|
|
141
|
-
import { routes } from './routes';
|
|
142
|
-
|
|
143
|
-
const noxApp = await bootstrapApplication({ routes });
|
|
144
|
-
|
|
145
|
-
noxApp
|
|
146
|
-
.configure(AppService)
|
|
147
|
-
.start();
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
## Dependency Injection
|
|
153
|
-
|
|
154
|
-
### `@Injectable`
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
@Injectable({ lifetime: 'singleton', deps: [RepoA, RepoB] })
|
|
158
|
-
class MyService {
|
|
159
|
-
constructor(private a: RepoA, private b: RepoB) {}
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
| Option | Type | Default | Description |
|
|
164
|
-
| ---------- | --------------------------------------- | --------- | ---------------------------------- |
|
|
165
|
-
| `lifetime` | `'singleton' \| 'scope' \| 'transient'` | `'scope'` | Instance lifetime |
|
|
166
|
-
| `deps` | `TokenKey[]` | `[]` | Constructor dependencies, in order |
|
|
167
|
-
|
|
168
|
-
**Lifetimes:**
|
|
169
|
-
- `singleton` — one instance for the entire lifetime of the app
|
|
170
|
-
- `scope` — one instance per IPC request
|
|
171
|
-
- `transient` — a new instance on every resolution
|
|
172
|
-
|
|
173
|
-
### `token()` — Non-class dependencies
|
|
174
|
-
|
|
175
|
-
To inject values that are not classes (strings, interfaces, config objects):
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
// tokens.ts
|
|
179
|
-
import { token } from '@noxfly/noxus/main';
|
|
180
|
-
|
|
181
|
-
export const DB_URL = token<string>('DB_URL');
|
|
182
|
-
export const APP_CONFIG = token<AppConfig>('APP_CONFIG');
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
// Declaring the dependency
|
|
187
|
-
@Injectable({ deps: [DB_URL, APP_CONFIG] })
|
|
188
|
-
class DbService {
|
|
189
|
-
constructor(private url: string, private config: AppConfig) {}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Providing the value in bootstrapApplication
|
|
193
|
-
bootstrapApplication({
|
|
194
|
-
singletons: [
|
|
195
|
-
{ token: DB_URL, useValue: process.env.DATABASE_URL! },
|
|
196
|
-
{ token: APP_CONFIG, useValue: { debug: true } },
|
|
197
|
-
],
|
|
198
|
-
});
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### `inject()` — Manual resolution
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { inject } from '@noxfly/noxus/main';
|
|
205
|
-
|
|
206
|
-
const userService = inject(UserService);
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
When you decide to inject manually with `inject()`, you may not duplicate the dependency in the `deps` array of `@Injectable`.
|
|
210
|
-
The resolve dependency in deps will be ignored in the constructor and will be re-solved at runtime by `inject()`.
|
|
211
|
-
|
|
212
|
-
Useful outside a constructor — in callbacks, factories, etc.
|
|
213
|
-
|
|
214
|
-
### `forwardRef()` — Circular dependencies
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
import { forwardRef } from '@noxfly/noxus/main';
|
|
218
|
-
|
|
219
|
-
@Injectable({ deps: [forwardRef(() => ServiceB)] })
|
|
220
|
-
class ServiceA {
|
|
221
|
-
constructor(private b: ServiceB) {}
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## Routing
|
|
228
|
-
|
|
229
|
-
### Available HTTP methods
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
import { Get, Post, Put, Patch, Delete } from '@noxfly/noxus/main';
|
|
233
|
-
|
|
234
|
-
@Controller({ path: 'products', deps: [ProductService] })
|
|
235
|
-
class ProductController {
|
|
236
|
-
constructor(private svc: ProductService) {}
|
|
237
|
-
|
|
238
|
-
@Get('list') list(req: Request) { ... }
|
|
239
|
-
@Post('create') create(req: Request) { ... }
|
|
240
|
-
@Put(':id') replace(req: Request) { ... }
|
|
241
|
-
@Patch(':id') update(req: Request) { ... }
|
|
242
|
-
@Delete(':id') remove(req: Request) { ... }
|
|
243
|
-
}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Route parameters
|
|
247
|
-
|
|
248
|
-
```ts
|
|
249
|
-
@Get('category/:categoryId/product/:productId')
|
|
250
|
-
getProduct(req: Request) {
|
|
251
|
-
const { categoryId, productId } = req.params;
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Query parameters
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
// Renderer side:
|
|
259
|
-
await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin', page: '1' } });
|
|
260
|
-
|
|
261
|
-
// Controller side:
|
|
262
|
-
@Get('list')
|
|
263
|
-
list(req: Request) {
|
|
264
|
-
const role = req.query['role']; // 'admin'
|
|
265
|
-
const page = req.query['page']; // '1'
|
|
266
|
-
return this.svc.findAll({ role, page: parseInt(page!) });
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### Request body
|
|
271
|
-
|
|
272
|
-
```ts
|
|
273
|
-
@Post('create')
|
|
274
|
-
create(req: Request) {
|
|
275
|
-
const { name, price } = req.body as { name: string; price: number };
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Response
|
|
280
|
-
|
|
281
|
-
The value returned by the handler is automatically placed in `response.body`. To control the status code:
|
|
282
|
-
|
|
283
|
-
```ts
|
|
284
|
-
@Post('create')
|
|
285
|
-
create(req: Request, res: IResponse) {
|
|
286
|
-
res.status = 201;
|
|
287
|
-
return { id: 42 };
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
## Route definitions (`defineRoutes`)
|
|
294
|
-
|
|
295
|
-
`defineRoutes` is the single source of truth for your routing table. It validates prefixes, detects duplicates and overlapping paths, and supports nested routes:
|
|
296
|
-
|
|
297
|
-
```ts
|
|
298
|
-
import { defineRoutes } from '@noxfly/noxus/main';
|
|
299
|
-
|
|
300
|
-
export const routes = defineRoutes([
|
|
301
|
-
{ path: 'users', load: () => import('./modules/users/users.controller.js'), guards: [authGuard] },
|
|
302
|
-
{ path: 'orders', load: () => import('./modules/orders/orders.controller.js') },
|
|
303
|
-
]);
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Nested routes
|
|
307
|
-
|
|
308
|
-
Parent routes can omit `load` and only serve as a shared prefix with inherited guards/middlewares:
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
export const routes = defineRoutes([
|
|
312
|
-
{
|
|
313
|
-
path: 'admin',
|
|
314
|
-
guards: [authGuard, adminGuard],
|
|
315
|
-
children: [
|
|
316
|
-
{ path: 'users', load: () => import('./admin/users.controller.js') },
|
|
317
|
-
{ path: 'products', load: () => import('./admin/products.controller.js') },
|
|
318
|
-
],
|
|
319
|
-
},
|
|
320
|
-
]);
|
|
321
|
-
// Produces flat routes: admin/users and admin/products, both inheriting authGuard + adminGuard.
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Lazy loading
|
|
325
|
-
|
|
326
|
-
This is the core mechanism for keeping startup fast. A lazy controller is never imported until an IPC request targets its prefix.
|
|
327
|
-
|
|
328
|
-
Routes declared via `defineRoutes` are lazy by default. You can also register lazy routes manually:
|
|
329
|
-
|
|
330
|
-
```ts
|
|
331
|
-
noxApp
|
|
332
|
-
.lazy('users', () => import('./modules/users/users.controller.js'))
|
|
333
|
-
.lazy('orders', () => import('./modules/orders/orders.controller.js'))
|
|
334
|
-
.lazy('printing', () => import('./modules/printing/printing.controller.js'))
|
|
335
|
-
.start();
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
> **Important:** the `import()` argument must not statically reference heavy modules. If `users.controller.ts` imports `applicationinsights` at the top of the file, the library will be loaded on the first `users/*` request — not at startup.
|
|
339
|
-
|
|
340
|
-
### Eager loading (`eagerLoad`)
|
|
341
|
-
|
|
342
|
-
For modules whose services are needed before `onReady()`:
|
|
343
|
-
|
|
344
|
-
```ts
|
|
345
|
-
bootstrapApplication({
|
|
346
|
-
eagerLoad: [
|
|
347
|
-
() => import('./modules/auth/auth.controller.js'),
|
|
348
|
-
],
|
|
349
|
-
});
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Manual loading from NoxApp
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
await noxApp.load([
|
|
356
|
-
() => import('./modules/reporting/reporting.controller.js'),
|
|
357
|
-
]);
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
## Guards
|
|
363
|
-
|
|
364
|
-
A guard is a plain function that decides whether a request can reach its handler.
|
|
365
|
-
|
|
366
|
-
```ts
|
|
367
|
-
// guards/auth.guard.ts
|
|
368
|
-
import { Guard } from '@noxfly/noxus/main';
|
|
369
|
-
|
|
370
|
-
export const authGuard: Guard = async (req) => {
|
|
371
|
-
return req.body?.token === 'secret'; // your auth logic
|
|
372
|
-
};
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
**On an entire controller:**
|
|
376
|
-
```ts
|
|
377
|
-
@Controller({ path: 'admin', deps: [AdminService], guards: [authGuard] })
|
|
378
|
-
class AdminController { ... }
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
**On a specific route:**
|
|
382
|
-
```ts
|
|
383
|
-
@Delete(':id', { guards: [authGuard, adminGuard] })
|
|
384
|
-
remove(req: Request) { ... }
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
Controller guards and route guards are **cumulative** — both run, in the given order.
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
|
-
## Middlewares
|
|
392
|
-
|
|
393
|
-
A middleware is a plain function that runs before guards.
|
|
394
|
-
|
|
395
|
-
```ts
|
|
396
|
-
// middlewares/log.middleware.ts
|
|
397
|
-
import { Middleware } from '@noxfly/noxus/main';
|
|
398
|
-
|
|
399
|
-
export const logMiddleware: Middleware = async (req, res, next) => {
|
|
400
|
-
console.log(`→ ${req.method} ${req.path}`);
|
|
401
|
-
await next();
|
|
402
|
-
console.log(`← ${res.status}`);
|
|
403
|
-
};
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
**Global (all routes):**
|
|
407
|
-
```ts
|
|
408
|
-
noxApp.use(logMiddleware);
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
**On a controller:**
|
|
412
|
-
```ts
|
|
413
|
-
@Controller({ path: 'users', deps: [...], middlewares: [logMiddleware] })
|
|
414
|
-
class UserController { ... }
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
**On a route:**
|
|
418
|
-
```ts
|
|
419
|
-
@Post('upload', { middlewares: [fileSizeMiddleware] })
|
|
420
|
-
upload(req: Request) { ... }
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
**Execution order:** global middlewares → controller middlewares → route middlewares → guards → handler.
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
## WindowManager
|
|
428
|
-
|
|
429
|
-
Injectable singleton service for managing `BrowserWindow` instances.
|
|
430
|
-
|
|
431
|
-
```ts
|
|
432
|
-
@Injectable({ lifetime: 'singleton', deps: [WindowManager] })
|
|
433
|
-
class AppService implements IApp {
|
|
434
|
-
constructor(private wm: WindowManager) {}
|
|
435
|
-
|
|
436
|
-
async onReady() {
|
|
437
|
-
// Creates a 600×600 window, animates it to full screen,
|
|
438
|
-
// then resolves the promise once the animation is complete.
|
|
439
|
-
// loadFile() is therefore always called at the correct size — no viewbox freeze.
|
|
440
|
-
const win = await this.wm.createSplash({
|
|
441
|
-
webPreferences: { preload: path.join(__dirname, 'preload.js') },
|
|
442
|
-
});
|
|
443
|
-
win.loadFile('index.html');
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
### Full API
|
|
449
|
-
|
|
450
|
-
```ts
|
|
451
|
-
// Creation
|
|
452
|
-
const win = await wm.createSplash(options); // animated main window
|
|
453
|
-
const win2 = await wm.create(config, isMain?); // custom window
|
|
454
|
-
|
|
455
|
-
// Access
|
|
456
|
-
wm.getMain() // main window
|
|
457
|
-
wm.getById(id) // by Electron id
|
|
458
|
-
wm.getAll() // all open windows
|
|
459
|
-
wm.count // number of open windows
|
|
460
|
-
|
|
461
|
-
// Actions
|
|
462
|
-
wm.close(id) // close a window
|
|
463
|
-
wm.closeAll() // close all windows
|
|
464
|
-
|
|
465
|
-
// Messaging
|
|
466
|
-
wm.send(id, 'channel', ...args) // send a message to one window
|
|
467
|
-
wm.broadcast('channel', ...args) // send to all windows
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
### `createSplash` vs `create`
|
|
471
|
-
|
|
472
|
-
| | `createSplash` | `create` |
|
|
473
|
-
| ------------ | ---------------------- | ----------------------------------- |
|
|
474
|
-
| Initial size | 600×600 centered | Whatever you define |
|
|
475
|
-
| Animation | Expands to work area | Optional (`expandToWorkArea: true`) |
|
|
476
|
-
| `show` | `true` immediately | `false` until `ready-to-show` |
|
|
477
|
-
| Use case | Main window at startup | Secondary windows |
|
|
478
|
-
|
|
479
|
-
---
|
|
480
|
-
|
|
481
|
-
## External singletons
|
|
482
|
-
|
|
483
|
-
To inject values built outside the DI container (DB connection, third-party SDK):
|
|
484
|
-
|
|
485
|
-
```ts
|
|
486
|
-
// main.ts
|
|
487
|
-
import { MikroORM } from '@mikro-orm/core';
|
|
488
|
-
import { bootstrapApplication } from '@noxfly/noxus/main';
|
|
489
|
-
|
|
490
|
-
const orm = await MikroORM.init(ormConfig);
|
|
491
|
-
|
|
492
|
-
const noxApp = await bootstrapApplication({
|
|
493
|
-
singletons: [
|
|
494
|
-
{ token: MikroORM, useValue: orm },
|
|
495
|
-
],
|
|
496
|
-
});
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
These values are then available via injection in any service:
|
|
500
|
-
|
|
501
|
-
```ts
|
|
502
|
-
@Injectable({ lifetime: 'singleton', deps: [MikroORM] })
|
|
503
|
-
class UserRepository {
|
|
504
|
-
constructor(private orm: MikroORM) {}
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
---
|
|
509
|
-
|
|
510
|
-
## Renderer side
|
|
511
|
-
|
|
512
|
-
### Preload
|
|
513
|
-
|
|
514
|
-
```ts
|
|
515
|
-
// preload.ts
|
|
516
|
-
import { exposeNoxusBridge } from '@noxfly/noxus/preload';
|
|
517
|
-
|
|
518
|
-
exposeNoxusBridge(); // exposes window.__noxus__ to the renderer
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
### IPC client
|
|
522
|
-
|
|
523
|
-
```ts
|
|
524
|
-
// In the renderer (Angular, React, Vue, Vanilla...)
|
|
525
|
-
import { NoxRendererClient } from '@noxfly/noxus';
|
|
526
|
-
|
|
527
|
-
const client = new NoxRendererClient({
|
|
528
|
-
requestTimeout: 10_000, // 10s (default). Set to 0 to disable.
|
|
529
|
-
});
|
|
530
|
-
await client.setup(); // requests the MessagePort from main
|
|
531
|
-
|
|
532
|
-
// Requests
|
|
533
|
-
const users = await client.request<User[]>({ method: 'GET', path: 'users/list' });
|
|
534
|
-
const user = await client.request<User> ({ method: 'GET', path: 'users/42' });
|
|
535
|
-
await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin' } });
|
|
536
|
-
await client.request({ method: 'POST', path: 'users/create', body: { name: 'Bob' } });
|
|
537
|
-
await client.request({ method: 'PUT', path: 'users/42', body: { name: 'Bob Updated' } });
|
|
538
|
-
await client.request({ method: 'DELETE', path: 'users/42' });
|
|
539
|
-
|
|
540
|
-
// Per-request timeout override (takes precedence over the global requestTimeout)
|
|
541
|
-
const report = await client.request<Report>(
|
|
542
|
-
{ method: 'GET', path: 'reports/heavy' },
|
|
543
|
-
{ timeout: 60_000 }, // 60s for this specific request
|
|
544
|
-
);
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
### Push events (main → renderer)
|
|
548
|
-
|
|
549
|
-
On the main side, via `NoxSocket`:
|
|
550
|
-
|
|
551
|
-
```ts
|
|
552
|
-
@Injectable({ lifetime: 'singleton', deps: [NoxSocket] })
|
|
553
|
-
class NotificationService {
|
|
554
|
-
constructor(private socket: NoxSocket) {}
|
|
555
|
-
|
|
556
|
-
notifyAll(message: string) {
|
|
557
|
-
this.socket.emit('notification', { message });
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
notifyOne(senderId: number, message: string) {
|
|
561
|
-
this.socket.emitToRenderer(senderId, 'notification', { message });
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
On the renderer side:
|
|
567
|
-
|
|
568
|
-
```ts
|
|
569
|
-
const sub = client.events.subscribe<INotification>('notification', (payload) => {
|
|
570
|
-
console.log(payload.message);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// Unsubscribe when done
|
|
574
|
-
sub.unsubscribe();
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
---
|
|
578
|
-
|
|
579
|
-
## Batch requests
|
|
580
|
-
|
|
581
|
-
Multiple IPC requests in a single round-trip:
|
|
582
|
-
|
|
583
|
-
```ts
|
|
584
|
-
const results = await client.batch([
|
|
585
|
-
{ method: 'GET', path: 'users/list', query: { role: 'admin' } },
|
|
586
|
-
{ method: 'GET', path: 'products/list' },
|
|
587
|
-
{ method: 'POST', path: 'orders/create', body: { ... } },
|
|
588
|
-
]);
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
593
|
-
## Exceptions
|
|
594
|
-
|
|
595
|
-
Noxus provides an HTTP exception hierarchy to throw from handlers:
|
|
596
|
-
|
|
597
|
-
```ts
|
|
598
|
-
import {
|
|
599
|
-
BadRequestException, // 400
|
|
600
|
-
UnauthorizedException, // 401
|
|
601
|
-
ForbiddenException, // 403
|
|
602
|
-
NotFoundException, // 404
|
|
603
|
-
ConflictException, // 409
|
|
604
|
-
InternalServerException, // 500
|
|
605
|
-
// ... and all other 4xx/5xx
|
|
606
|
-
} from '@noxfly/noxus/main';
|
|
607
|
-
|
|
608
|
-
@Get(':id')
|
|
609
|
-
getOne(req: Request) {
|
|
610
|
-
const user = this.svc.findById(parseInt(req.params['id']!));
|
|
611
|
-
if (!user) throw new NotFoundException(`User not found`);
|
|
612
|
-
return user;
|
|
613
|
-
}
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
The exception is automatically caught by the router and translated into a response with the correct HTTP status.
|
|
617
|
-
|
|
618
|
-
---
|
|
619
|
-
|
|
620
|
-
## Recommended project structure
|
|
621
|
-
|
|
622
|
-
```
|
|
623
|
-
src/
|
|
624
|
-
├── main.ts ← bootstrapApplication + lazy routes
|
|
625
|
-
├── app.service.ts ← implements IApp
|
|
626
|
-
├── modules/
|
|
627
|
-
│ ├── users/
|
|
628
|
-
│ │ ├── user.controller.ts
|
|
629
|
-
│ │ ├── user.service.ts
|
|
630
|
-
│ │ └── user.repository.ts
|
|
631
|
-
│ ├── orders/
|
|
632
|
-
│ │ ├── order.controller.ts
|
|
633
|
-
│ │ └── order.service.ts
|
|
634
|
-
│ └── printing/
|
|
635
|
-
│ ├── printing.controller.ts
|
|
636
|
-
│ └── printing.service.ts
|
|
637
|
-
├── guards/
|
|
638
|
-
│ └── auth.guard.ts
|
|
639
|
-
├── middlewares/
|
|
640
|
-
│ └── log.middleware.ts
|
|
641
|
-
└── tokens.ts ← shared named tokens
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
Each `module/` folder is **self-contained** — the controller imports its own services directly, with no central declaration. `main.ts` only knows the lazy loading paths.
|
|
645
|
-
|
|
646
|
-
---
|
|
647
|
-
|
|
648
|
-
## Log level
|
|
649
|
-
|
|
650
|
-
By default, all framework logs are enabled (`debug` level). Control verbosity via `bootstrapApplication`:
|
|
651
|
-
|
|
652
|
-
```ts
|
|
653
|
-
const noxApp = await bootstrapApplication({
|
|
654
|
-
logLevel: 'none', // 'debug' | 'info' | 'none'
|
|
655
|
-
});
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
You can also pass an array of specific levels:
|
|
659
|
-
|
|
660
|
-
```ts
|
|
661
|
-
bootstrapApplication({ logLevel: ['warn', 'error', 'critical'] });
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
Or change it at runtime:
|
|
665
|
-
|
|
666
|
-
```ts
|
|
667
|
-
import { Logger } from '@noxfly/noxus/main';
|
|
668
|
-
|
|
669
|
-
Logger.setLogLevel('info');
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
---
|
|
673
|
-
|
|
674
|
-
## Testing
|
|
675
|
-
|
|
676
|
-
To reset the DI container between tests (avoids leaking singletons across test suites):
|
|
677
|
-
|
|
678
|
-
```ts
|
|
679
|
-
import { resetRootInjector } from '@noxfly/noxus/main';
|
|
680
|
-
|
|
681
|
-
afterEach(() => {
|
|
682
|
-
resetRootInjector();
|
|
683
|
-
});
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
This clears all bindings, singletons, and scoped instances from the root injector.
|
|
1
|
+
# @noxfly/noxus
|
|
2
|
+
|
|
3
|
+
Lightweight IPC framework for Electron, inspired by NestJS and Angular. It simulates HTTP-like requests between the renderer and the main process via `MessageChannel` / `MessagePort`, with routing, DI, guards, middlewares, and lazy-loading.
|
|
4
|
+
|
|
5
|
+
No dependency on `reflect-metadata` or `emitDecoratorMetadata`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @noxfly/noxus
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
In your `tsconfig.json`:
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"compilerOptions": {
|
|
19
|
+
"experimentalDecorators": true,
|
|
20
|
+
"emitDecoratorMetadata": false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Concepts
|
|
28
|
+
|
|
29
|
+
| Concept | Role |
|
|
30
|
+
| ------------------------ | ----------------------------------------------------- |
|
|
31
|
+
| **Controller** | Handles a set of IPC routes under a path prefix |
|
|
32
|
+
| **Injectable** | Service injectable into other services or controllers |
|
|
33
|
+
| **Guard** | Protects a route or controller (authorization) |
|
|
34
|
+
| **Middleware** | Runs before guards and the handler |
|
|
35
|
+
| **Token** | Explicit identifier for non-class dependencies |
|
|
36
|
+
| **WindowManager** | Singleton service for managing Electron windows |
|
|
37
|
+
| **bootstrapApplication** | Single entry point of the application |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
### 1. Create a service
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// services/user.service.ts
|
|
47
|
+
import { Injectable } from '@noxfly/noxus/main';
|
|
48
|
+
|
|
49
|
+
@Injectable({ lifetime: 'singleton' })
|
|
50
|
+
export class UserService {
|
|
51
|
+
private users = [{ id: 1, name: 'Alice' }];
|
|
52
|
+
|
|
53
|
+
findAll() {
|
|
54
|
+
return this.users;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
findById(id: number) {
|
|
58
|
+
return this.users.find(u => u.id === id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Create a controller
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// controllers/user.controller.ts
|
|
67
|
+
import { Controller, Get, Post, Request } from '@noxfly/noxus/main';
|
|
68
|
+
import { UserService } from '../services/user.service';
|
|
69
|
+
|
|
70
|
+
@Controller({ path: 'users', deps: [UserService] })
|
|
71
|
+
export class UserController {
|
|
72
|
+
constructor(private svc: UserService) {}
|
|
73
|
+
|
|
74
|
+
@Get('list')
|
|
75
|
+
list(req: Request) {
|
|
76
|
+
return this.svc.findAll();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Get(':id')
|
|
80
|
+
getOne(req: Request) {
|
|
81
|
+
const id = parseInt(req.params['id']!);
|
|
82
|
+
return this.svc.findById(id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Post('create')
|
|
86
|
+
create(req: Request) {
|
|
87
|
+
return { created: true, body: req.body };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Create the application service
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// app.service.ts
|
|
96
|
+
import { IApp, Injectable, WindowManager } from '@noxfly/noxus/main';
|
|
97
|
+
import path from 'path';
|
|
98
|
+
|
|
99
|
+
@Injectable({ lifetime: 'singleton', deps: [WindowManager] })
|
|
100
|
+
export class AppService implements IApp {
|
|
101
|
+
constructor(private wm: WindowManager) {}
|
|
102
|
+
|
|
103
|
+
async onReady() {
|
|
104
|
+
const win = await this.wm.createSplash({
|
|
105
|
+
webPreferences: {
|
|
106
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
win.loadFile('index.html');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async onActivated() {
|
|
114
|
+
if (this.wm.count === 0) await this.onReady();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async dispose() {
|
|
118
|
+
// cleanup on app close
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Define routes
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// routes.ts
|
|
127
|
+
import { defineRoutes } from '@noxfly/noxus/main';
|
|
128
|
+
|
|
129
|
+
export const routes = defineRoutes([
|
|
130
|
+
{ path: 'users', load: () => import('./controllers/user.controller.js') },
|
|
131
|
+
{ path: 'orders', load: () => import('./controllers/order.controller.js') },
|
|
132
|
+
]);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 5. Bootstrap the application
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// main.ts
|
|
139
|
+
import { bootstrapApplication } from '@noxfly/noxus/main';
|
|
140
|
+
import { AppService } from './app.service';
|
|
141
|
+
import { routes } from './routes';
|
|
142
|
+
|
|
143
|
+
const noxApp = await bootstrapApplication({ routes });
|
|
144
|
+
|
|
145
|
+
noxApp
|
|
146
|
+
.configure(AppService)
|
|
147
|
+
.start();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Dependency Injection
|
|
153
|
+
|
|
154
|
+
### `@Injectable`
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
@Injectable({ lifetime: 'singleton', deps: [RepoA, RepoB] })
|
|
158
|
+
class MyService {
|
|
159
|
+
constructor(private a: RepoA, private b: RepoB) {}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Option | Type | Default | Description |
|
|
164
|
+
| ---------- | --------------------------------------- | --------- | ---------------------------------- |
|
|
165
|
+
| `lifetime` | `'singleton' \| 'scope' \| 'transient'` | `'scope'` | Instance lifetime |
|
|
166
|
+
| `deps` | `TokenKey[]` | `[]` | Constructor dependencies, in order |
|
|
167
|
+
|
|
168
|
+
**Lifetimes:**
|
|
169
|
+
- `singleton` — one instance for the entire lifetime of the app
|
|
170
|
+
- `scope` — one instance per IPC request
|
|
171
|
+
- `transient` — a new instance on every resolution
|
|
172
|
+
|
|
173
|
+
### `token()` — Non-class dependencies
|
|
174
|
+
|
|
175
|
+
To inject values that are not classes (strings, interfaces, config objects):
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// tokens.ts
|
|
179
|
+
import { token } from '@noxfly/noxus/main';
|
|
180
|
+
|
|
181
|
+
export const DB_URL = token<string>('DB_URL');
|
|
182
|
+
export const APP_CONFIG = token<AppConfig>('APP_CONFIG');
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Declaring the dependency
|
|
187
|
+
@Injectable({ deps: [DB_URL, APP_CONFIG] })
|
|
188
|
+
class DbService {
|
|
189
|
+
constructor(private url: string, private config: AppConfig) {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Providing the value in bootstrapApplication
|
|
193
|
+
bootstrapApplication({
|
|
194
|
+
singletons: [
|
|
195
|
+
{ token: DB_URL, useValue: process.env.DATABASE_URL! },
|
|
196
|
+
{ token: APP_CONFIG, useValue: { debug: true } },
|
|
197
|
+
],
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `inject()` — Manual resolution
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { inject } from '@noxfly/noxus/main';
|
|
205
|
+
|
|
206
|
+
const userService = inject(UserService);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
When you decide to inject manually with `inject()`, you may not duplicate the dependency in the `deps` array of `@Injectable`.
|
|
210
|
+
The resolve dependency in deps will be ignored in the constructor and will be re-solved at runtime by `inject()`.
|
|
211
|
+
|
|
212
|
+
Useful outside a constructor — in callbacks, factories, etc.
|
|
213
|
+
|
|
214
|
+
### `forwardRef()` — Circular dependencies
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { forwardRef } from '@noxfly/noxus/main';
|
|
218
|
+
|
|
219
|
+
@Injectable({ deps: [forwardRef(() => ServiceB)] })
|
|
220
|
+
class ServiceA {
|
|
221
|
+
constructor(private b: ServiceB) {}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Routing
|
|
228
|
+
|
|
229
|
+
### Available HTTP methods
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { Get, Post, Put, Patch, Delete } from '@noxfly/noxus/main';
|
|
233
|
+
|
|
234
|
+
@Controller({ path: 'products', deps: [ProductService] })
|
|
235
|
+
class ProductController {
|
|
236
|
+
constructor(private svc: ProductService) {}
|
|
237
|
+
|
|
238
|
+
@Get('list') list(req: Request) { ... }
|
|
239
|
+
@Post('create') create(req: Request) { ... }
|
|
240
|
+
@Put(':id') replace(req: Request) { ... }
|
|
241
|
+
@Patch(':id') update(req: Request) { ... }
|
|
242
|
+
@Delete(':id') remove(req: Request) { ... }
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Route parameters
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
@Get('category/:categoryId/product/:productId')
|
|
250
|
+
getProduct(req: Request) {
|
|
251
|
+
const { categoryId, productId } = req.params;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Query parameters
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
// Renderer side:
|
|
259
|
+
await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin', page: '1' } });
|
|
260
|
+
|
|
261
|
+
// Controller side:
|
|
262
|
+
@Get('list')
|
|
263
|
+
list(req: Request) {
|
|
264
|
+
const role = req.query['role']; // 'admin'
|
|
265
|
+
const page = req.query['page']; // '1'
|
|
266
|
+
return this.svc.findAll({ role, page: parseInt(page!) });
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Request body
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
@Post('create')
|
|
274
|
+
create(req: Request) {
|
|
275
|
+
const { name, price } = req.body as { name: string; price: number };
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Response
|
|
280
|
+
|
|
281
|
+
The value returned by the handler is automatically placed in `response.body`. To control the status code:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
@Post('create')
|
|
285
|
+
create(req: Request, res: IResponse) {
|
|
286
|
+
res.status = 201;
|
|
287
|
+
return { id: 42 };
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Route definitions (`defineRoutes`)
|
|
294
|
+
|
|
295
|
+
`defineRoutes` is the single source of truth for your routing table. It validates prefixes, detects duplicates and overlapping paths, and supports nested routes:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { defineRoutes } from '@noxfly/noxus/main';
|
|
299
|
+
|
|
300
|
+
export const routes = defineRoutes([
|
|
301
|
+
{ path: 'users', load: () => import('./modules/users/users.controller.js'), guards: [authGuard] },
|
|
302
|
+
{ path: 'orders', load: () => import('./modules/orders/orders.controller.js') },
|
|
303
|
+
]);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Nested routes
|
|
307
|
+
|
|
308
|
+
Parent routes can omit `load` and only serve as a shared prefix with inherited guards/middlewares:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
export const routes = defineRoutes([
|
|
312
|
+
{
|
|
313
|
+
path: 'admin',
|
|
314
|
+
guards: [authGuard, adminGuard],
|
|
315
|
+
children: [
|
|
316
|
+
{ path: 'users', load: () => import('./admin/users.controller.js') },
|
|
317
|
+
{ path: 'products', load: () => import('./admin/products.controller.js') },
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
]);
|
|
321
|
+
// Produces flat routes: admin/users and admin/products, both inheriting authGuard + adminGuard.
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Lazy loading
|
|
325
|
+
|
|
326
|
+
This is the core mechanism for keeping startup fast. A lazy controller is never imported until an IPC request targets its prefix.
|
|
327
|
+
|
|
328
|
+
Routes declared via `defineRoutes` are lazy by default. You can also register lazy routes manually:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
noxApp
|
|
332
|
+
.lazy('users', () => import('./modules/users/users.controller.js'))
|
|
333
|
+
.lazy('orders', () => import('./modules/orders/orders.controller.js'))
|
|
334
|
+
.lazy('printing', () => import('./modules/printing/printing.controller.js'))
|
|
335
|
+
.start();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
> **Important:** the `import()` argument must not statically reference heavy modules. If `users.controller.ts` imports `applicationinsights` at the top of the file, the library will be loaded on the first `users/*` request — not at startup.
|
|
339
|
+
|
|
340
|
+
### Eager loading (`eagerLoad`)
|
|
341
|
+
|
|
342
|
+
For modules whose services are needed before `onReady()`:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
bootstrapApplication({
|
|
346
|
+
eagerLoad: [
|
|
347
|
+
() => import('./modules/auth/auth.controller.js'),
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Manual loading from NoxApp
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
await noxApp.load([
|
|
356
|
+
() => import('./modules/reporting/reporting.controller.js'),
|
|
357
|
+
]);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Guards
|
|
363
|
+
|
|
364
|
+
A guard is a plain function that decides whether a request can reach its handler.
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
// guards/auth.guard.ts
|
|
368
|
+
import { Guard } from '@noxfly/noxus/main';
|
|
369
|
+
|
|
370
|
+
export const authGuard: Guard = async (req) => {
|
|
371
|
+
return req.body?.token === 'secret'; // your auth logic
|
|
372
|
+
};
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**On an entire controller:**
|
|
376
|
+
```ts
|
|
377
|
+
@Controller({ path: 'admin', deps: [AdminService], guards: [authGuard] })
|
|
378
|
+
class AdminController { ... }
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**On a specific route:**
|
|
382
|
+
```ts
|
|
383
|
+
@Delete(':id', { guards: [authGuard, adminGuard] })
|
|
384
|
+
remove(req: Request) { ... }
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Controller guards and route guards are **cumulative** — both run, in the given order.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Middlewares
|
|
392
|
+
|
|
393
|
+
A middleware is a plain function that runs before guards.
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
// middlewares/log.middleware.ts
|
|
397
|
+
import { Middleware } from '@noxfly/noxus/main';
|
|
398
|
+
|
|
399
|
+
export const logMiddleware: Middleware = async (req, res, next) => {
|
|
400
|
+
console.log(`→ ${req.method} ${req.path}`);
|
|
401
|
+
await next();
|
|
402
|
+
console.log(`← ${res.status}`);
|
|
403
|
+
};
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Global (all routes):**
|
|
407
|
+
```ts
|
|
408
|
+
noxApp.use(logMiddleware);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**On a controller:**
|
|
412
|
+
```ts
|
|
413
|
+
@Controller({ path: 'users', deps: [...], middlewares: [logMiddleware] })
|
|
414
|
+
class UserController { ... }
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**On a route:**
|
|
418
|
+
```ts
|
|
419
|
+
@Post('upload', { middlewares: [fileSizeMiddleware] })
|
|
420
|
+
upload(req: Request) { ... }
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Execution order:** global middlewares → controller middlewares → route middlewares → guards → handler.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## WindowManager
|
|
428
|
+
|
|
429
|
+
Injectable singleton service for managing `BrowserWindow` instances.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
@Injectable({ lifetime: 'singleton', deps: [WindowManager] })
|
|
433
|
+
class AppService implements IApp {
|
|
434
|
+
constructor(private wm: WindowManager) {}
|
|
435
|
+
|
|
436
|
+
async onReady() {
|
|
437
|
+
// Creates a 600×600 window, animates it to full screen,
|
|
438
|
+
// then resolves the promise once the animation is complete.
|
|
439
|
+
// loadFile() is therefore always called at the correct size — no viewbox freeze.
|
|
440
|
+
const win = await this.wm.createSplash({
|
|
441
|
+
webPreferences: { preload: path.join(__dirname, 'preload.js') },
|
|
442
|
+
});
|
|
443
|
+
win.loadFile('index.html');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Full API
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
// Creation
|
|
452
|
+
const win = await wm.createSplash(options); // animated main window
|
|
453
|
+
const win2 = await wm.create(config, isMain?); // custom window
|
|
454
|
+
|
|
455
|
+
// Access
|
|
456
|
+
wm.getMain() // main window
|
|
457
|
+
wm.getById(id) // by Electron id
|
|
458
|
+
wm.getAll() // all open windows
|
|
459
|
+
wm.count // number of open windows
|
|
460
|
+
|
|
461
|
+
// Actions
|
|
462
|
+
wm.close(id) // close a window
|
|
463
|
+
wm.closeAll() // close all windows
|
|
464
|
+
|
|
465
|
+
// Messaging
|
|
466
|
+
wm.send(id, 'channel', ...args) // send a message to one window
|
|
467
|
+
wm.broadcast('channel', ...args) // send to all windows
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### `createSplash` vs `create`
|
|
471
|
+
|
|
472
|
+
| | `createSplash` | `create` |
|
|
473
|
+
| ------------ | ---------------------- | ----------------------------------- |
|
|
474
|
+
| Initial size | 600×600 centered | Whatever you define |
|
|
475
|
+
| Animation | Expands to work area | Optional (`expandToWorkArea: true`) |
|
|
476
|
+
| `show` | `true` immediately | `false` until `ready-to-show` |
|
|
477
|
+
| Use case | Main window at startup | Secondary windows |
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## External singletons
|
|
482
|
+
|
|
483
|
+
To inject values built outside the DI container (DB connection, third-party SDK):
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
// main.ts
|
|
487
|
+
import { MikroORM } from '@mikro-orm/core';
|
|
488
|
+
import { bootstrapApplication } from '@noxfly/noxus/main';
|
|
489
|
+
|
|
490
|
+
const orm = await MikroORM.init(ormConfig);
|
|
491
|
+
|
|
492
|
+
const noxApp = await bootstrapApplication({
|
|
493
|
+
singletons: [
|
|
494
|
+
{ token: MikroORM, useValue: orm },
|
|
495
|
+
],
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
These values are then available via injection in any service:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
@Injectable({ lifetime: 'singleton', deps: [MikroORM] })
|
|
503
|
+
class UserRepository {
|
|
504
|
+
constructor(private orm: MikroORM) {}
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## Renderer side
|
|
511
|
+
|
|
512
|
+
### Preload
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
// preload.ts
|
|
516
|
+
import { exposeNoxusBridge } from '@noxfly/noxus/preload';
|
|
517
|
+
|
|
518
|
+
exposeNoxusBridge(); // exposes window.__noxus__ to the renderer
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### IPC client
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
// In the renderer (Angular, React, Vue, Vanilla...)
|
|
525
|
+
import { NoxRendererClient } from '@noxfly/noxus';
|
|
526
|
+
|
|
527
|
+
const client = new NoxRendererClient({
|
|
528
|
+
requestTimeout: 10_000, // 10s (default). Set to 0 to disable.
|
|
529
|
+
});
|
|
530
|
+
await client.setup(); // requests the MessagePort from main
|
|
531
|
+
|
|
532
|
+
// Requests
|
|
533
|
+
const users = await client.request<User[]>({ method: 'GET', path: 'users/list' });
|
|
534
|
+
const user = await client.request<User> ({ method: 'GET', path: 'users/42' });
|
|
535
|
+
await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin' } });
|
|
536
|
+
await client.request({ method: 'POST', path: 'users/create', body: { name: 'Bob' } });
|
|
537
|
+
await client.request({ method: 'PUT', path: 'users/42', body: { name: 'Bob Updated' } });
|
|
538
|
+
await client.request({ method: 'DELETE', path: 'users/42' });
|
|
539
|
+
|
|
540
|
+
// Per-request timeout override (takes precedence over the global requestTimeout)
|
|
541
|
+
const report = await client.request<Report>(
|
|
542
|
+
{ method: 'GET', path: 'reports/heavy' },
|
|
543
|
+
{ timeout: 60_000 }, // 60s for this specific request
|
|
544
|
+
);
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Push events (main → renderer)
|
|
548
|
+
|
|
549
|
+
On the main side, via `NoxSocket`:
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
@Injectable({ lifetime: 'singleton', deps: [NoxSocket] })
|
|
553
|
+
class NotificationService {
|
|
554
|
+
constructor(private socket: NoxSocket) {}
|
|
555
|
+
|
|
556
|
+
notifyAll(message: string) {
|
|
557
|
+
this.socket.emit('notification', { message });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
notifyOne(senderId: number, message: string) {
|
|
561
|
+
this.socket.emitToRenderer(senderId, 'notification', { message });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
On the renderer side:
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
const sub = client.events.subscribe<INotification>('notification', (payload) => {
|
|
570
|
+
console.log(payload.message);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Unsubscribe when done
|
|
574
|
+
sub.unsubscribe();
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Batch requests
|
|
580
|
+
|
|
581
|
+
Multiple IPC requests in a single round-trip:
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
const results = await client.batch([
|
|
585
|
+
{ method: 'GET', path: 'users/list', query: { role: 'admin' } },
|
|
586
|
+
{ method: 'GET', path: 'products/list' },
|
|
587
|
+
{ method: 'POST', path: 'orders/create', body: { ... } },
|
|
588
|
+
]);
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Exceptions
|
|
594
|
+
|
|
595
|
+
Noxus provides an HTTP exception hierarchy to throw from handlers:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
import {
|
|
599
|
+
BadRequestException, // 400
|
|
600
|
+
UnauthorizedException, // 401
|
|
601
|
+
ForbiddenException, // 403
|
|
602
|
+
NotFoundException, // 404
|
|
603
|
+
ConflictException, // 409
|
|
604
|
+
InternalServerException, // 500
|
|
605
|
+
// ... and all other 4xx/5xx
|
|
606
|
+
} from '@noxfly/noxus/main';
|
|
607
|
+
|
|
608
|
+
@Get(':id')
|
|
609
|
+
getOne(req: Request) {
|
|
610
|
+
const user = this.svc.findById(parseInt(req.params['id']!));
|
|
611
|
+
if (!user) throw new NotFoundException(`User not found`);
|
|
612
|
+
return user;
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
The exception is automatically caught by the router and translated into a response with the correct HTTP status.
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Recommended project structure
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
src/
|
|
624
|
+
├── main.ts ← bootstrapApplication + lazy routes
|
|
625
|
+
├── app.service.ts ← implements IApp
|
|
626
|
+
├── modules/
|
|
627
|
+
│ ├── users/
|
|
628
|
+
│ │ ├── user.controller.ts
|
|
629
|
+
│ │ ├── user.service.ts
|
|
630
|
+
│ │ └── user.repository.ts
|
|
631
|
+
│ ├── orders/
|
|
632
|
+
│ │ ├── order.controller.ts
|
|
633
|
+
│ │ └── order.service.ts
|
|
634
|
+
│ └── printing/
|
|
635
|
+
│ ├── printing.controller.ts
|
|
636
|
+
│ └── printing.service.ts
|
|
637
|
+
├── guards/
|
|
638
|
+
│ └── auth.guard.ts
|
|
639
|
+
├── middlewares/
|
|
640
|
+
│ └── log.middleware.ts
|
|
641
|
+
└── tokens.ts ← shared named tokens
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Each `module/` folder is **self-contained** — the controller imports its own services directly, with no central declaration. `main.ts` only knows the lazy loading paths.
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Log level
|
|
649
|
+
|
|
650
|
+
By default, all framework logs are enabled (`debug` level). Control verbosity via `bootstrapApplication`:
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
const noxApp = await bootstrapApplication({
|
|
654
|
+
logLevel: 'none', // 'debug' | 'info' | 'none'
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
You can also pass an array of specific levels:
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
bootstrapApplication({ logLevel: ['warn', 'error', 'critical'] });
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Or change it at runtime:
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
import { Logger } from '@noxfly/noxus/main';
|
|
668
|
+
|
|
669
|
+
Logger.setLogLevel('info');
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Testing
|
|
675
|
+
|
|
676
|
+
To reset the DI container between tests (avoids leaking singletons across test suites):
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
import { resetRootInjector } from '@noxfly/noxus/main';
|
|
680
|
+
|
|
681
|
+
afterEach(() => {
|
|
682
|
+
resetRootInjector();
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
This clears all bindings, singletons, and scoped instances from the root injector.
|