viojs-core 1.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/README.md +15 -0
- package/dist/core/Application.d.ts +16 -0
- package/dist/core/Application.js +273 -0
- package/dist/core/Container.d.ts +6 -0
- package/dist/core/Container.js +37 -0
- package/dist/core/Context.d.ts +27 -0
- package/dist/core/Context.js +113 -0
- package/dist/core/Router.d.ts +13 -0
- package/dist/core/Router.js +235 -0
- package/dist/core/WsContext.d.ts +32 -0
- package/dist/core/WsContext.js +85 -0
- package/dist/decorators/index.d.ts +41 -0
- package/dist/decorators/index.js +127 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +23 -0
- package/dist/middlewares/cookie.d.ts +16 -0
- package/dist/middlewares/cookie.js +47 -0
- package/dist/validation/index.d.ts +20 -0
- package/dist/validation/index.js +115 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# viojs-core
|
|
2
|
+
|
|
3
|
+
High-performance Node.js framework core built on uWebSockets.js.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- uWebSockets.js integration
|
|
7
|
+
- Koa-style middleware
|
|
8
|
+
- TypeScript Decorators
|
|
9
|
+
- IoC / Dependency Injection
|
|
10
|
+
- WebSocket Hub
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
```bash
|
|
14
|
+
npm install viojs-core
|
|
15
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { us_listen_socket } from 'uWebSockets.js';
|
|
2
|
+
import { Middleware } from './Context';
|
|
3
|
+
export declare class Application {
|
|
4
|
+
private app;
|
|
5
|
+
private middlewares;
|
|
6
|
+
private router;
|
|
7
|
+
constructor();
|
|
8
|
+
use(middleware: Middleware): this;
|
|
9
|
+
publish(topic: string, message: string | ArrayBuffer, isBinary?: boolean, compress?: boolean): void;
|
|
10
|
+
registerWsRoute(pattern: string, behavior: any): void;
|
|
11
|
+
registerRoutes(controllers: any[]): void;
|
|
12
|
+
listen(port: number, cb?: (token: us_listen_socket) => void): Promise<void>;
|
|
13
|
+
private writeHeaders;
|
|
14
|
+
private sendResponse;
|
|
15
|
+
private compose;
|
|
16
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Application = void 0;
|
|
37
|
+
const uWebSockets_js_1 = require("uWebSockets.js");
|
|
38
|
+
const Context_1 = require("./Context");
|
|
39
|
+
const Router_1 = require("./Router");
|
|
40
|
+
const Container_1 = require("./Container");
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
class Application {
|
|
44
|
+
app;
|
|
45
|
+
middlewares = [];
|
|
46
|
+
router;
|
|
47
|
+
constructor() {
|
|
48
|
+
this.app = (0, uWebSockets_js_1.App)();
|
|
49
|
+
this.router = new Router_1.Router(this);
|
|
50
|
+
// Bind the current Application instance to the DI container for global retrieval
|
|
51
|
+
Container_1.container.instances.set(Application, this);
|
|
52
|
+
}
|
|
53
|
+
use(middleware) {
|
|
54
|
+
this.middlewares.push(middleware);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
// Global fast broadcast
|
|
58
|
+
publish(topic, message, isBinary = false, compress = false) {
|
|
59
|
+
this.app.publish(topic, message, isBinary, compress);
|
|
60
|
+
}
|
|
61
|
+
// Register a WebSocket namespace
|
|
62
|
+
registerWsRoute(pattern, behavior) {
|
|
63
|
+
// pattern must be uWS format, e.g. /* or /room/*
|
|
64
|
+
this.app.ws(pattern, behavior);
|
|
65
|
+
}
|
|
66
|
+
// Register router routes
|
|
67
|
+
registerRoutes(controllers) {
|
|
68
|
+
console.error("Application.registerRoutes called with", controllers.length, "controllers");
|
|
69
|
+
this.router.register(controllers);
|
|
70
|
+
this.use(this.router.middleware());
|
|
71
|
+
}
|
|
72
|
+
async listen(port, cb) {
|
|
73
|
+
this.app.any('/*', async (res, req) => {
|
|
74
|
+
const isUpgrade = req.getHeader('upgrade') === 'websocket';
|
|
75
|
+
if (isUpgrade) {
|
|
76
|
+
res.setYield(true);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.error("DEBUG ENTERED HANDLER");
|
|
80
|
+
const url = req.getUrl();
|
|
81
|
+
console.error("URL:", url);
|
|
82
|
+
let contextAborted = false;
|
|
83
|
+
try {
|
|
84
|
+
// Create context
|
|
85
|
+
const ctx = new Context_1.BaseContext(res, req);
|
|
86
|
+
console.log(`Incoming request: ${ctx.method} ${ctx.url}`);
|
|
87
|
+
// Keep track of abort safely beyond context setup since we are going async heavily
|
|
88
|
+
res.onAborted(() => {
|
|
89
|
+
ctx.aborted = true;
|
|
90
|
+
contextAborted = true;
|
|
91
|
+
});
|
|
92
|
+
// Execute middleware chain
|
|
93
|
+
try {
|
|
94
|
+
if (contextAborted)
|
|
95
|
+
return;
|
|
96
|
+
const composed = this.compose(this.middlewares);
|
|
97
|
+
await composed(ctx);
|
|
98
|
+
if (!contextAborted && !ctx.responded) {
|
|
99
|
+
this.sendResponse(ctx, contextAborted);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error('Middleware error:', err);
|
|
104
|
+
if (!contextAborted) {
|
|
105
|
+
res.writeStatus('500 Internal Server Error').end('Internal Server Error');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
console.error('Context creation error:', err);
|
|
111
|
+
if (!contextAborted) {
|
|
112
|
+
res.writeStatus('500 Internal Server Error').end('Context Error');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
this.app.listen(port, (token) => {
|
|
117
|
+
if (cb)
|
|
118
|
+
cb(token);
|
|
119
|
+
else {
|
|
120
|
+
if (token) {
|
|
121
|
+
console.log('Listening to port ' + port);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log('Failed to listen to port ' + port);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Extracted headers writer
|
|
130
|
+
writeHeaders(ctx) {
|
|
131
|
+
const { res, status, responseHeaders } = ctx;
|
|
132
|
+
const statusMap = {
|
|
133
|
+
200: '200 OK',
|
|
134
|
+
201: '201 Created',
|
|
135
|
+
204: '204 No Content',
|
|
136
|
+
400: '400 Bad Request',
|
|
137
|
+
401: '401 Unauthorized',
|
|
138
|
+
403: '403 Forbidden',
|
|
139
|
+
404: '404 Not Found',
|
|
140
|
+
500: '500 Internal Server Error'
|
|
141
|
+
};
|
|
142
|
+
const statusText = statusMap[status] || `${status} Status`;
|
|
143
|
+
res.writeStatus(statusText);
|
|
144
|
+
for (const [key, value] of Object.entries(responseHeaders)) {
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
value.forEach(v => res.writeHeader(key, v));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
res.writeHeader(key, value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
sendResponse(ctx, contextAborted) {
|
|
154
|
+
if (contextAborted)
|
|
155
|
+
return;
|
|
156
|
+
const { res, body, _filePath } = ctx;
|
|
157
|
+
// --- BRANCH A: FILE STREAMING ---
|
|
158
|
+
if (_filePath) {
|
|
159
|
+
fs.stat(_filePath, (err, stats) => {
|
|
160
|
+
if (err || !stats.isFile() || contextAborted || ctx.aborted) {
|
|
161
|
+
if (!contextAborted && !ctx.aborted) {
|
|
162
|
+
ctx.status = 404;
|
|
163
|
+
res.cork(() => {
|
|
164
|
+
this.writeHeaders(ctx);
|
|
165
|
+
res.end('File Not Found');
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const totalSize = stats.size;
|
|
171
|
+
// Basic Mime type guess if not set
|
|
172
|
+
if (!ctx.responseHeaders['Content-Type']) {
|
|
173
|
+
const ext = path.extname(_filePath).toLowerCase();
|
|
174
|
+
const mimeTypes = {
|
|
175
|
+
'.json': 'application/json',
|
|
176
|
+
'.png': 'image/png',
|
|
177
|
+
'.atlas': 'text/plain',
|
|
178
|
+
'.txt': 'text/plain',
|
|
179
|
+
'.jpg': 'image/jpeg',
|
|
180
|
+
'.jpeg': 'image/jpeg'
|
|
181
|
+
};
|
|
182
|
+
ctx.responseHeaders['Content-Type'] = mimeTypes[ext] || 'application/octet-stream';
|
|
183
|
+
}
|
|
184
|
+
res.cork(() => {
|
|
185
|
+
this.writeHeaders(ctx);
|
|
186
|
+
});
|
|
187
|
+
let readStream = fs.createReadStream(_filePath);
|
|
188
|
+
readStream.on('data', (chunk) => {
|
|
189
|
+
if (typeof chunk === 'string') {
|
|
190
|
+
chunk = Buffer.from(chunk);
|
|
191
|
+
}
|
|
192
|
+
// Try to send chunk via uWebSockets
|
|
193
|
+
const chunkArrayBuffer = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
|
|
194
|
+
let ok = false;
|
|
195
|
+
let done = false;
|
|
196
|
+
res.cork(() => {
|
|
197
|
+
const result = res.tryEnd(chunkArrayBuffer, totalSize);
|
|
198
|
+
ok = result[0];
|
|
199
|
+
done = result[1];
|
|
200
|
+
});
|
|
201
|
+
if (done) {
|
|
202
|
+
// Current chunk ended successfully and stream completed (if it was the last piece)
|
|
203
|
+
}
|
|
204
|
+
else if (ok) {
|
|
205
|
+
// Chunk successfully buffered to C++ space
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// **Backpressure activated!**
|
|
209
|
+
// C++ buffer is full (Client network is slow) -> Pause reading from Disk!
|
|
210
|
+
readStream.pause();
|
|
211
|
+
// Wait for C++ buffer to drain
|
|
212
|
+
res.onWritable((offset) => {
|
|
213
|
+
// C++ buffer drained, we can resume reading from Disk
|
|
214
|
+
readStream.resume();
|
|
215
|
+
// Important: You must return true explicitly to tell uWebSockets the stream is still alive
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
readStream.on('error', () => {
|
|
221
|
+
if (!contextAborted && !ctx.aborted) {
|
|
222
|
+
res.cork(() => {
|
|
223
|
+
res.end();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
ctx.responded = true;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// --- BRANCH B: NORMAL JSON / TEXT PAYLOAD ---
|
|
232
|
+
res.cork(() => {
|
|
233
|
+
this.writeHeaders(ctx);
|
|
234
|
+
if (typeof body === 'object') {
|
|
235
|
+
res.writeHeader('Content-Type', 'application/json');
|
|
236
|
+
res.end(JSON.stringify(body));
|
|
237
|
+
}
|
|
238
|
+
else if (body) {
|
|
239
|
+
res.end(String(body));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Default 404 behavior if no body is set
|
|
243
|
+
res.writeStatus('404 Not Found');
|
|
244
|
+
res.writeHeader('Content-Type', 'application/json');
|
|
245
|
+
res.end(JSON.stringify({ error: "Not Found", url: ctx.url, method: ctx.method }));
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
ctx.responded = true;
|
|
249
|
+
}
|
|
250
|
+
compose(middlewares) {
|
|
251
|
+
return function (ctx, next) {
|
|
252
|
+
let index = -1;
|
|
253
|
+
return dispatch(0);
|
|
254
|
+
function dispatch(i) {
|
|
255
|
+
if (i <= index)
|
|
256
|
+
return Promise.reject(new Error('next() called multiple times'));
|
|
257
|
+
index = i;
|
|
258
|
+
let fn = middlewares[i];
|
|
259
|
+
if (i === middlewares.length)
|
|
260
|
+
fn = next; // End of chain
|
|
261
|
+
if (!fn)
|
|
262
|
+
return Promise.resolve();
|
|
263
|
+
try {
|
|
264
|
+
return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
return Promise.reject(err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
exports.Application = Application;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.container = exports.Container = void 0;
|
|
4
|
+
require("reflect-metadata");
|
|
5
|
+
const decorators_1 = require("../decorators");
|
|
6
|
+
class Container {
|
|
7
|
+
instances = new Map();
|
|
8
|
+
resolve(target) {
|
|
9
|
+
// Find if target has been marked as injectable (explicitly or via Controller)
|
|
10
|
+
const isInjectable = Reflect.getMetadata(decorators_1.INJECTABLE_METADATA, target) || target.name !== 'Object';
|
|
11
|
+
if (!isInjectable) {
|
|
12
|
+
throw new Error(`Cannot resolve ${target.name || target}. Make sure it has @Injectable() or @Controller() decorator.`);
|
|
13
|
+
}
|
|
14
|
+
// Return singleton if already instantiated
|
|
15
|
+
if (this.instances.has(target)) {
|
|
16
|
+
return this.instances.get(target);
|
|
17
|
+
}
|
|
18
|
+
// Check constructor dependencies
|
|
19
|
+
const tokens = Reflect.getMetadata('design:paramtypes', target) || [];
|
|
20
|
+
let injections = [];
|
|
21
|
+
if (tokens.length > 0) {
|
|
22
|
+
injections = tokens.map((token) => {
|
|
23
|
+
// Circular dependency or undefined typescript output guard
|
|
24
|
+
if (!token || token === target) {
|
|
25
|
+
throw new Error(`Failed to resolve dependencies for ${target.name}. Circular dependency detected or bad type.`);
|
|
26
|
+
}
|
|
27
|
+
return this.resolve(token);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Instantiate
|
|
31
|
+
const instance = new target(...injections);
|
|
32
|
+
this.instances.set(target, instance);
|
|
33
|
+
return instance;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.Container = Container;
|
|
37
|
+
exports.container = new Container();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type Next = () => Promise<void>;
|
|
2
|
+
export type Middleware<TContext extends BaseContext = BaseContext> = (ctx: TContext, next: Next) => Promise<void>;
|
|
3
|
+
import { HttpRequest, HttpResponse } from 'uWebSockets.js';
|
|
4
|
+
export declare class BaseContext {
|
|
5
|
+
req: HttpRequest;
|
|
6
|
+
res: HttpResponse;
|
|
7
|
+
aborted: boolean;
|
|
8
|
+
responded: boolean;
|
|
9
|
+
method: string;
|
|
10
|
+
url: string;
|
|
11
|
+
query: string;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
params: Record<string, string>;
|
|
14
|
+
body: any;
|
|
15
|
+
status: number;
|
|
16
|
+
responseHeaders: Record<string, string | string[]>;
|
|
17
|
+
_filePath?: string;
|
|
18
|
+
private bodyPromise;
|
|
19
|
+
constructor(res: HttpResponse, req: HttpRequest);
|
|
20
|
+
json(): Promise<any>;
|
|
21
|
+
text(): Promise<string>;
|
|
22
|
+
private _parsedQuery?;
|
|
23
|
+
get queries(): Record<string, string>;
|
|
24
|
+
throw(status: number, message?: string): never;
|
|
25
|
+
redirect(url: string, status?: number): void;
|
|
26
|
+
sendFile(path: string): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseContext = void 0;
|
|
4
|
+
class BaseContext {
|
|
5
|
+
req;
|
|
6
|
+
res;
|
|
7
|
+
aborted = false;
|
|
8
|
+
responded = false; // Tracks if response has been sent to uWebSockets
|
|
9
|
+
// Request properties
|
|
10
|
+
method;
|
|
11
|
+
url;
|
|
12
|
+
query;
|
|
13
|
+
headers;
|
|
14
|
+
params;
|
|
15
|
+
// Response properties
|
|
16
|
+
body;
|
|
17
|
+
status = 200;
|
|
18
|
+
responseHeaders = {};
|
|
19
|
+
// File streaming properties
|
|
20
|
+
_filePath;
|
|
21
|
+
bodyPromise;
|
|
22
|
+
constructor(res, req) {
|
|
23
|
+
this.res = res;
|
|
24
|
+
this.req = req;
|
|
25
|
+
this.method = req.getMethod().toUpperCase();
|
|
26
|
+
this.url = req.getUrl();
|
|
27
|
+
// query params
|
|
28
|
+
this.query = req.getQuery();
|
|
29
|
+
this.headers = {};
|
|
30
|
+
req.forEach((k, v) => {
|
|
31
|
+
this.headers[k] = v;
|
|
32
|
+
});
|
|
33
|
+
this.params = {};
|
|
34
|
+
// Important: Handle abort
|
|
35
|
+
res.onAborted(() => {
|
|
36
|
+
this.aborted = true;
|
|
37
|
+
});
|
|
38
|
+
// Check if we expect a body
|
|
39
|
+
const contentLength = this.headers['content-length'];
|
|
40
|
+
const transferEncoding = this.headers['transfer-encoding'];
|
|
41
|
+
const hasBody = (contentLength && parseInt(contentLength) > 0) || (transferEncoding && transferEncoding.includes('chunked'));
|
|
42
|
+
if (hasBody) {
|
|
43
|
+
this.bodyPromise = new Promise((resolve, reject) => {
|
|
44
|
+
let buffer;
|
|
45
|
+
res.onData((chunk, isLast) => {
|
|
46
|
+
const chunkBuffer = Buffer.from(chunk);
|
|
47
|
+
if (isLast) {
|
|
48
|
+
if (buffer) {
|
|
49
|
+
resolve(Buffer.concat([buffer, chunkBuffer]));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
resolve(chunkBuffer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
if (buffer) {
|
|
57
|
+
buffer = Buffer.concat([buffer, chunkBuffer]);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
buffer = Buffer.concat([chunkBuffer]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.bodyPromise = Promise.resolve(Buffer.alloc(0));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Helper to read body (async because we might need to buffer)
|
|
71
|
+
async json() {
|
|
72
|
+
const buffer = await this.bodyPromise;
|
|
73
|
+
if (!buffer || buffer.length === 0)
|
|
74
|
+
return {};
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(buffer.toString());
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async text() {
|
|
83
|
+
const buffer = await this.bodyPromise;
|
|
84
|
+
return buffer ? buffer.toString() : '';
|
|
85
|
+
}
|
|
86
|
+
// Lazy initialization for query parameters
|
|
87
|
+
_parsedQuery;
|
|
88
|
+
get queries() {
|
|
89
|
+
if (!this._parsedQuery) {
|
|
90
|
+
this._parsedQuery = {};
|
|
91
|
+
if (this.query) {
|
|
92
|
+
const searchParams = new URLSearchParams(this.query);
|
|
93
|
+
for (const [key, value] of searchParams.entries()) {
|
|
94
|
+
this._parsedQuery[key] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return this._parsedQuery;
|
|
99
|
+
}
|
|
100
|
+
throw(status, message) {
|
|
101
|
+
this.status = status;
|
|
102
|
+
throw new Error(message || `Error ${status}`);
|
|
103
|
+
}
|
|
104
|
+
redirect(url, status = 302) {
|
|
105
|
+
this.status = status;
|
|
106
|
+
this.responseHeaders['Location'] = url;
|
|
107
|
+
this.body = '';
|
|
108
|
+
}
|
|
109
|
+
sendFile(path) {
|
|
110
|
+
this._filePath = path;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.BaseContext = BaseContext;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Middleware } from './Context';
|
|
2
|
+
import { Application } from './Application';
|
|
3
|
+
export declare class Router {
|
|
4
|
+
private app;
|
|
5
|
+
private routes;
|
|
6
|
+
constructor(app: Application);
|
|
7
|
+
register(controllers: any[]): void;
|
|
8
|
+
private registerWs;
|
|
9
|
+
private compose;
|
|
10
|
+
private pathToRegexp;
|
|
11
|
+
middleware(): Middleware;
|
|
12
|
+
private findRoute;
|
|
13
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Router = void 0;
|
|
4
|
+
const decorators_1 = require("../decorators");
|
|
5
|
+
const WsContext_1 = require("./WsContext");
|
|
6
|
+
const validation_1 = require("../validation");
|
|
7
|
+
const Container_1 = require("./Container");
|
|
8
|
+
class Router {
|
|
9
|
+
app;
|
|
10
|
+
routes = [];
|
|
11
|
+
constructor(app) {
|
|
12
|
+
this.app = app;
|
|
13
|
+
}
|
|
14
|
+
register(controllers) {
|
|
15
|
+
console.log("HELLO FROM ROUTER REGISTER");
|
|
16
|
+
controllers.forEach(controllerClass => {
|
|
17
|
+
// Instantiate securely with DI Container!
|
|
18
|
+
const instance = Container_1.container.resolve(controllerClass);
|
|
19
|
+
// Check if this is a WebSocket Controller
|
|
20
|
+
const wsPrefix = Reflect.getMetadata(decorators_1.WS_CONTROLLER_METADATA, controllerClass);
|
|
21
|
+
if (wsPrefix !== undefined) {
|
|
22
|
+
this.registerWs(wsPrefix, controllerClass, instance);
|
|
23
|
+
return; // Skip normal HTTP routing
|
|
24
|
+
}
|
|
25
|
+
const prefix = Reflect.getMetadata(decorators_1.CONTROLLER_METADATA, controllerClass);
|
|
26
|
+
const routes = Reflect.getMetadata(decorators_1.ROUTE_METADATA, controllerClass);
|
|
27
|
+
const controllerMiddlewares = Reflect.getMetadata(decorators_1.MIDDLEWARE_METADATA, controllerClass) || [];
|
|
28
|
+
if (!routes)
|
|
29
|
+
return;
|
|
30
|
+
routes.forEach(route => {
|
|
31
|
+
const fullPath = (prefix + route.path).replace(/\/+/g, '/');
|
|
32
|
+
// Convert path to regex /users/:id -> /^\/users\/([^/]+)$/
|
|
33
|
+
const { re, keys } = this.pathToRegexp(fullPath);
|
|
34
|
+
const routeMiddlewares = Reflect.getMetadata(decorators_1.MIDDLEWARE_METADATA, controllerClass.prototype, route.methodName) || [];
|
|
35
|
+
const paramsMetadata = Reflect.getOwnMetadata(decorators_1.PARAM_METADATA, controllerClass.prototype, route.methodName) || [];
|
|
36
|
+
// Compose: Controller Middlewares -> Route Middlewares -> Handler
|
|
37
|
+
const allMiddlewares = [...controllerMiddlewares, ...routeMiddlewares];
|
|
38
|
+
console.log(`Registering route: ${route.method.toUpperCase()} ${fullPath} -> regex: ${re}`);
|
|
39
|
+
this.routes.push({
|
|
40
|
+
method: route.method.toUpperCase(),
|
|
41
|
+
path: re,
|
|
42
|
+
paramNames: keys,
|
|
43
|
+
handler: async (ctx) => {
|
|
44
|
+
const handler = instance[route.methodName].bind(instance);
|
|
45
|
+
console.log("Handler loaded for", route.methodName);
|
|
46
|
+
// Create a mini-chain for this route
|
|
47
|
+
const chain = [...allMiddlewares, async (ctx, next) => {
|
|
48
|
+
let args = [ctx]; // Default backwards-compatible fallback array
|
|
49
|
+
// If parameter decorators are used, we switch into reflection injection mode
|
|
50
|
+
if (paramsMetadata.length > 0) {
|
|
51
|
+
const maxIndex = Math.max(...paramsMetadata.map(p => p.index));
|
|
52
|
+
args = new Array(maxIndex + 1).fill(undefined);
|
|
53
|
+
const hasBody = paramsMetadata.some(p => p.type === 'body');
|
|
54
|
+
let bodyData = null;
|
|
55
|
+
if (hasBody) {
|
|
56
|
+
bodyData = await ctx.json();
|
|
57
|
+
}
|
|
58
|
+
for (const meta of paramsMetadata) {
|
|
59
|
+
let value;
|
|
60
|
+
switch (meta.type) {
|
|
61
|
+
case 'ctx':
|
|
62
|
+
value = ctx;
|
|
63
|
+
break;
|
|
64
|
+
case 'param':
|
|
65
|
+
value = meta.name ? ctx.params[meta.name] : ctx.params;
|
|
66
|
+
break;
|
|
67
|
+
case 'query':
|
|
68
|
+
value = meta.name ? ctx.queries[meta.name] : ctx.queries;
|
|
69
|
+
break;
|
|
70
|
+
case 'body':
|
|
71
|
+
value = meta.name ? (bodyData ? bodyData[meta.name] : undefined) : bodyData;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
// Simple auto-casting for strongly typed metadata via typescript design:paramtypes
|
|
75
|
+
if (value !== undefined && meta.designType) {
|
|
76
|
+
if (meta.designType === Number) {
|
|
77
|
+
const parsed = Number(value);
|
|
78
|
+
if (isNaN(parsed))
|
|
79
|
+
throw new validation_1.ValidationError([`Parameter '${meta.name}' must be a number`]);
|
|
80
|
+
value = parsed;
|
|
81
|
+
}
|
|
82
|
+
else if (meta.designType === Boolean) {
|
|
83
|
+
value = value === 'true' || value === '1' || value === true;
|
|
84
|
+
}
|
|
85
|
+
else if (meta.designType === String) {
|
|
86
|
+
value = String(value);
|
|
87
|
+
}
|
|
88
|
+
else if (meta.designType.name !== 'Object' && meta.designType.name !== 'Array') {
|
|
89
|
+
// It's a potential DTO class! Execute native Validation layer
|
|
90
|
+
value = (0, validation_1.validateDTO)(meta.designType, value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (value === undefined && meta.designType && meta.designType.name !== 'Object' && meta.designType.name !== 'String' && meta.designType.name !== 'Number' && meta.designType.name !== 'Boolean') {
|
|
94
|
+
// Body was undefined/null, let validation engine check if @IsRequired complains!
|
|
95
|
+
value = (0, validation_1.validateDTO)(meta.designType, {});
|
|
96
|
+
}
|
|
97
|
+
args[meta.index] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Execute handler with injected args (passing undefined will trigger ES6 default arguments)
|
|
101
|
+
const result = await handler(...args);
|
|
102
|
+
console.log("Handler returned:", result);
|
|
103
|
+
if (result !== undefined) {
|
|
104
|
+
ctx.body = result;
|
|
105
|
+
}
|
|
106
|
+
}];
|
|
107
|
+
const composed = this.compose(chain);
|
|
108
|
+
await composed(ctx);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
registerWs(prefix, controllerClass, instance) {
|
|
115
|
+
const fullPath = prefix.replace(/\/+/g, '/');
|
|
116
|
+
const openMethod = Reflect.getMetadata(decorators_1.WS_ON_OPEN_METADATA, controllerClass);
|
|
117
|
+
const messageMethod = Reflect.getMetadata(decorators_1.WS_ON_MESSAGE_METADATA, controllerClass);
|
|
118
|
+
const closeMethod = Reflect.getMetadata(decorators_1.WS_ON_CLOSE_METADATA, controllerClass);
|
|
119
|
+
const drainMethod = Reflect.getMetadata(decorators_1.WS_ON_DRAIN_METADATA, controllerClass);
|
|
120
|
+
console.log(`Registering WS route: ${fullPath}`);
|
|
121
|
+
this.app.registerWsRoute(fullPath, {
|
|
122
|
+
idleTimeout: 32,
|
|
123
|
+
maxBackpressure: 1024 * 1024 * 10,
|
|
124
|
+
maxPayloadLength: 1024 * 1024 * 16,
|
|
125
|
+
upgrade: (res, req, context) => {
|
|
126
|
+
// Must read req here safely because req is invalidated outside
|
|
127
|
+
const url = req.getUrl();
|
|
128
|
+
const queryStr = req.getQuery();
|
|
129
|
+
// Extremely simple query parser
|
|
130
|
+
const query = {};
|
|
131
|
+
if (queryStr) {
|
|
132
|
+
queryStr.split('&').forEach((pair) => {
|
|
133
|
+
const [k, v] = pair.split('=');
|
|
134
|
+
query[k] = decodeURIComponent(v || '');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
res.upgrade({ url, query }, req.getHeader('sec-websocket-key'), req.getHeader('sec-websocket-protocol'), req.getHeader('sec-websocket-extensions'), context);
|
|
138
|
+
},
|
|
139
|
+
open: (ws) => {
|
|
140
|
+
const ctx = new WsContext_1.WsContext(ws);
|
|
141
|
+
ws.customCtx = ctx;
|
|
142
|
+
if (openMethod) {
|
|
143
|
+
instance[openMethod](ctx);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
message: (ws, message, isBinary) => {
|
|
147
|
+
if (messageMethod) {
|
|
148
|
+
let data = message;
|
|
149
|
+
if (!isBinary && message instanceof ArrayBuffer) {
|
|
150
|
+
data = Buffer.from(message).toString('utf8');
|
|
151
|
+
}
|
|
152
|
+
instance[messageMethod](ws.customCtx, data, isBinary);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
close: (ws, code, message) => {
|
|
156
|
+
if (closeMethod) {
|
|
157
|
+
instance[closeMethod](ws.customCtx, code, message);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
drain: (ws) => {
|
|
161
|
+
if (drainMethod) {
|
|
162
|
+
instance[drainMethod](ws.customCtx);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Helper to compose middlewares (similar to Application.compose but specific for this scope if needed)
|
|
168
|
+
// Actually we can reuse or duplicate it. Let's duplicate for simplicity to keep Router standalone.
|
|
169
|
+
compose(middlewares) {
|
|
170
|
+
return function (ctx, next) {
|
|
171
|
+
let index = -1;
|
|
172
|
+
return dispatch(0);
|
|
173
|
+
function dispatch(i) {
|
|
174
|
+
if (i <= index)
|
|
175
|
+
return Promise.reject(new Error('next() called multiple times'));
|
|
176
|
+
index = i;
|
|
177
|
+
let fn = middlewares[i];
|
|
178
|
+
if (i === middlewares.length)
|
|
179
|
+
fn = next;
|
|
180
|
+
if (!fn)
|
|
181
|
+
return Promise.resolve();
|
|
182
|
+
try {
|
|
183
|
+
return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return Promise.reject(err);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
pathToRegexp(path) {
|
|
192
|
+
const keys = [];
|
|
193
|
+
// Escape special chars except :
|
|
194
|
+
let pattern = path.replace(/([.+*?^$(){}|[\]\\])/g, '\\$1');
|
|
195
|
+
// Replace :param with capture group
|
|
196
|
+
pattern = pattern.replace(/:(\w+)/g, (_, key) => {
|
|
197
|
+
keys.push(key);
|
|
198
|
+
return '([^/]+)';
|
|
199
|
+
});
|
|
200
|
+
// Add start/end anchors
|
|
201
|
+
return { re: new RegExp(`^${pattern}$`), keys };
|
|
202
|
+
}
|
|
203
|
+
middleware() {
|
|
204
|
+
return async (ctx, next) => {
|
|
205
|
+
// Find matching route
|
|
206
|
+
const matched = this.findRoute(ctx.method, ctx.url);
|
|
207
|
+
if (matched) {
|
|
208
|
+
console.log(`Route matched: ${ctx.method} ${ctx.url}`);
|
|
209
|
+
ctx.params = matched.params;
|
|
210
|
+
await matched.handler(ctx);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log(`No route matched for: ${ctx.method} ${ctx.url}`);
|
|
214
|
+
await next();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
findRoute(method, url) {
|
|
219
|
+
for (const route of this.routes) {
|
|
220
|
+
// console.log(`Checking route: ${route.method} ${route.path} vs ${method} ${url}`);
|
|
221
|
+
if (route.method !== method && route.method !== 'ANY')
|
|
222
|
+
continue;
|
|
223
|
+
const match = route.path.exec(url); // url in uWS does not include query string
|
|
224
|
+
if (match) {
|
|
225
|
+
const params = {};
|
|
226
|
+
route.paramNames.forEach((name, index) => {
|
|
227
|
+
params[name] = decodeURIComponent(match[index + 1] || '');
|
|
228
|
+
});
|
|
229
|
+
return { handler: route.handler, params };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
exports.Router = Router;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { WebSocket } from 'uWebSockets.js';
|
|
2
|
+
export declare class WsContext {
|
|
3
|
+
private readonly ws;
|
|
4
|
+
data: Record<string, any>;
|
|
5
|
+
query: Record<string, string>;
|
|
6
|
+
url: string;
|
|
7
|
+
constructor(ws: WebSocket<any>);
|
|
8
|
+
/**
|
|
9
|
+
* Extremely fast corked C++ network send
|
|
10
|
+
*/
|
|
11
|
+
send(message: string | ArrayBuffer, isBinary?: boolean): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe to a High-Performance C++ Pub/Sub topic
|
|
14
|
+
*/
|
|
15
|
+
subscribe(topic: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Unsubscribe from a topic
|
|
18
|
+
*/
|
|
19
|
+
unsubscribe(topic: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Broadcast to all sockets subscribed to this topic globally.
|
|
22
|
+
*/
|
|
23
|
+
publish(topic: string, message: string | ArrayBuffer, isBinary?: boolean): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Close the connection securely
|
|
26
|
+
*/
|
|
27
|
+
close(code?: number, shortMessage?: string | ArrayBuffer): void;
|
|
28
|
+
/**
|
|
29
|
+
* Extract Player IP
|
|
30
|
+
*/
|
|
31
|
+
getRemoteAddressAsText(): string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WsContext = void 0;
|
|
4
|
+
class WsContext {
|
|
5
|
+
ws;
|
|
6
|
+
data = {};
|
|
7
|
+
query = {};
|
|
8
|
+
url = '';
|
|
9
|
+
constructor(ws) {
|
|
10
|
+
this.ws = ws;
|
|
11
|
+
// Retrieve custom data injected during Upgrade
|
|
12
|
+
const userData = ws.getUserData();
|
|
13
|
+
if (userData) {
|
|
14
|
+
this.url = userData.url || '';
|
|
15
|
+
this.query = userData.query || {};
|
|
16
|
+
// Copy everything else so businesses can use it
|
|
17
|
+
Object.assign(this.data, userData);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extremely fast corked C++ network send
|
|
22
|
+
*/
|
|
23
|
+
send(message, isBinary = false) {
|
|
24
|
+
let result = false;
|
|
25
|
+
// Wrapped in cork to prevent socket chunk fragmentation warning during concurrent IO
|
|
26
|
+
this.ws.cork(() => {
|
|
27
|
+
// Returns: 0 = Backpressure | 1 = Success | 2 = Dropped Max Connections
|
|
28
|
+
result = this.ws.send(message, isBinary, false) === 1;
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Subscribe to a High-Performance C++ Pub/Sub topic
|
|
34
|
+
*/
|
|
35
|
+
subscribe(topic) {
|
|
36
|
+
return this.ws.subscribe(topic);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Unsubscribe from a topic
|
|
40
|
+
*/
|
|
41
|
+
unsubscribe(topic) {
|
|
42
|
+
return this.ws.unsubscribe(topic);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Broadcast to all sockets subscribed to this topic globally.
|
|
46
|
+
*/
|
|
47
|
+
publish(topic, message, isBinary = false) {
|
|
48
|
+
let result = false;
|
|
49
|
+
try {
|
|
50
|
+
// C++ underlying publish. If the socket is detached during @OnClose this will throw.
|
|
51
|
+
// Safely swallow here so Node doesn't crash from wild user attempts.
|
|
52
|
+
this.ws.cork(() => {
|
|
53
|
+
result = this.ws.publish(topic, message, isBinary);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.warn('[WsContext] Local socket publish failed (likely closed). Use Application.publish for global broadcasts.');
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Close the connection securely
|
|
63
|
+
*/
|
|
64
|
+
close(code, shortMessage) {
|
|
65
|
+
if (code) {
|
|
66
|
+
this.ws.end(code, shortMessage);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.ws.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract Player IP
|
|
74
|
+
*/
|
|
75
|
+
getRemoteAddressAsText() {
|
|
76
|
+
try {
|
|
77
|
+
const buffer = this.ws.getRemoteAddressAsText();
|
|
78
|
+
return Buffer.from(buffer).toString('utf-8');
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return 'Unknown';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.WsContext = WsContext;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
export declare const CONTROLLER_METADATA = "CONTROLLER_METADATA";
|
|
3
|
+
export declare const ROUTE_METADATA = "ROUTE_METADATA";
|
|
4
|
+
export declare const INJECTABLE_METADATA = "INJECTABLE_METADATA";
|
|
5
|
+
export declare function Injectable(): ClassDecorator;
|
|
6
|
+
export declare function Controller(prefix?: string): ClassDecorator;
|
|
7
|
+
export interface RouteDefinition {
|
|
8
|
+
path: string;
|
|
9
|
+
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options';
|
|
10
|
+
methodName: string | symbol;
|
|
11
|
+
}
|
|
12
|
+
export declare const MIDDLEWARE_METADATA = "MIDDLEWARE_METADATA";
|
|
13
|
+
export declare function Use(...middlewares: Function[]): ClassDecorator & MethodDecorator;
|
|
14
|
+
export declare const Get: (path?: string) => MethodDecorator;
|
|
15
|
+
export declare const Post: (path?: string) => MethodDecorator;
|
|
16
|
+
export declare const Put: (path?: string) => MethodDecorator;
|
|
17
|
+
export declare const Delete: (path?: string) => MethodDecorator;
|
|
18
|
+
export declare const Patch: (path?: string) => MethodDecorator;
|
|
19
|
+
export declare const Head: (path?: string) => MethodDecorator;
|
|
20
|
+
export declare const Options: (path?: string) => MethodDecorator;
|
|
21
|
+
export declare const PARAM_METADATA = "PARAM_METADATA";
|
|
22
|
+
export interface ParamDefinition {
|
|
23
|
+
index: number;
|
|
24
|
+
type: 'param' | 'query' | 'body' | 'ctx';
|
|
25
|
+
name?: string;
|
|
26
|
+
designType?: any;
|
|
27
|
+
}
|
|
28
|
+
export declare const Param: (name?: string) => ParameterDecorator;
|
|
29
|
+
export declare const Query: (name?: string) => ParameterDecorator;
|
|
30
|
+
export declare const Body: (name?: string) => ParameterDecorator;
|
|
31
|
+
export declare const Ctx: () => ParameterDecorator;
|
|
32
|
+
export declare const WS_CONTROLLER_METADATA = "WS_CONTROLLER_METADATA";
|
|
33
|
+
export declare const WS_ON_OPEN_METADATA = "WS_ON_OPEN_METADATA";
|
|
34
|
+
export declare const WS_ON_MESSAGE_METADATA = "WS_ON_MESSAGE_METADATA";
|
|
35
|
+
export declare const WS_ON_CLOSE_METADATA = "WS_ON_CLOSE_METADATA";
|
|
36
|
+
export declare const WS_ON_DRAIN_METADATA = "WS_ON_DRAIN_METADATA";
|
|
37
|
+
export declare function WebSocketController(path?: string): ClassDecorator;
|
|
38
|
+
export declare const OnOpen: () => MethodDecorator;
|
|
39
|
+
export declare const OnMessage: () => MethodDecorator;
|
|
40
|
+
export declare const OnClose: () => MethodDecorator;
|
|
41
|
+
export declare const OnDrain: () => MethodDecorator;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OnDrain = exports.OnClose = exports.OnMessage = exports.OnOpen = exports.WS_ON_DRAIN_METADATA = exports.WS_ON_CLOSE_METADATA = exports.WS_ON_MESSAGE_METADATA = exports.WS_ON_OPEN_METADATA = exports.WS_CONTROLLER_METADATA = exports.Ctx = exports.Body = exports.Query = exports.Param = exports.PARAM_METADATA = exports.Options = exports.Head = exports.Patch = exports.Delete = exports.Put = exports.Post = exports.Get = exports.MIDDLEWARE_METADATA = exports.INJECTABLE_METADATA = exports.ROUTE_METADATA = exports.CONTROLLER_METADATA = void 0;
|
|
4
|
+
exports.Injectable = Injectable;
|
|
5
|
+
exports.Controller = Controller;
|
|
6
|
+
exports.Use = Use;
|
|
7
|
+
exports.WebSocketController = WebSocketController;
|
|
8
|
+
require("reflect-metadata");
|
|
9
|
+
exports.CONTROLLER_METADATA = 'CONTROLLER_METADATA';
|
|
10
|
+
exports.ROUTE_METADATA = 'ROUTE_METADATA';
|
|
11
|
+
exports.INJECTABLE_METADATA = 'INJECTABLE_METADATA';
|
|
12
|
+
function Injectable() {
|
|
13
|
+
return (target) => {
|
|
14
|
+
Reflect.defineMetadata(exports.INJECTABLE_METADATA, true, target);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function Controller(prefix = '') {
|
|
18
|
+
return (target) => {
|
|
19
|
+
Reflect.defineMetadata(exports.CONTROLLER_METADATA, prefix, target);
|
|
20
|
+
// Controllers are automatically considered injectable
|
|
21
|
+
Reflect.defineMetadata(exports.INJECTABLE_METADATA, true, target);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function createRouteDecorator(method) {
|
|
25
|
+
return (path = '/') => {
|
|
26
|
+
return (target, propertyKey, descriptor) => {
|
|
27
|
+
console.log(`@${method} called for ${String(propertyKey)}`);
|
|
28
|
+
if (!Reflect.hasMetadata(exports.ROUTE_METADATA, target.constructor)) {
|
|
29
|
+
Reflect.defineMetadata(exports.ROUTE_METADATA, [], target.constructor);
|
|
30
|
+
}
|
|
31
|
+
const routes = Reflect.getMetadata(exports.ROUTE_METADATA, target.constructor);
|
|
32
|
+
routes.push({
|
|
33
|
+
method: method,
|
|
34
|
+
path,
|
|
35
|
+
methodName: propertyKey,
|
|
36
|
+
});
|
|
37
|
+
Reflect.defineMetadata(exports.ROUTE_METADATA, routes, target.constructor);
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
exports.MIDDLEWARE_METADATA = 'MIDDLEWARE_METADATA';
|
|
42
|
+
function Use(...middlewares) {
|
|
43
|
+
return (target, propertyKey, descriptor) => {
|
|
44
|
+
try {
|
|
45
|
+
console.log(`@Use called for ${String(propertyKey || target.name)}`);
|
|
46
|
+
if (propertyKey) {
|
|
47
|
+
// Method decorator
|
|
48
|
+
// target is prototype for instance members
|
|
49
|
+
if (!Reflect.hasMetadata(exports.MIDDLEWARE_METADATA, target, propertyKey)) {
|
|
50
|
+
Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [], target, propertyKey);
|
|
51
|
+
}
|
|
52
|
+
const existing = Reflect.getMetadata(exports.MIDDLEWARE_METADATA, target, propertyKey);
|
|
53
|
+
Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [...existing, ...middlewares], target, propertyKey);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Class decorator
|
|
57
|
+
if (!Reflect.hasMetadata(exports.MIDDLEWARE_METADATA, target)) {
|
|
58
|
+
Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [], target);
|
|
59
|
+
}
|
|
60
|
+
const existing = Reflect.getMetadata(exports.MIDDLEWARE_METADATA, target);
|
|
61
|
+
Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [...existing, ...middlewares], target);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.error(`Error in @Use decorator:`, e);
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
exports.Get = createRouteDecorator('get');
|
|
71
|
+
exports.Post = createRouteDecorator('post');
|
|
72
|
+
exports.Put = createRouteDecorator('put');
|
|
73
|
+
exports.Delete = createRouteDecorator('delete');
|
|
74
|
+
exports.Patch = createRouteDecorator('patch');
|
|
75
|
+
exports.Head = createRouteDecorator('head');
|
|
76
|
+
exports.Options = createRouteDecorator('options');
|
|
77
|
+
exports.PARAM_METADATA = 'PARAM_METADATA';
|
|
78
|
+
function createParamDecorator(type) {
|
|
79
|
+
return (name) => {
|
|
80
|
+
return (target, propertyKey, parameterIndex) => {
|
|
81
|
+
if (!propertyKey)
|
|
82
|
+
return;
|
|
83
|
+
const params = Reflect.getOwnMetadata(exports.PARAM_METADATA, target, propertyKey) || [];
|
|
84
|
+
// Extract the strong type info. "design:paramtypes" is generated by TS when emitDecoratorMetadata is true.
|
|
85
|
+
const designTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey);
|
|
86
|
+
const designType = designTypes ? designTypes[parameterIndex] : undefined;
|
|
87
|
+
params.push({
|
|
88
|
+
index: parameterIndex,
|
|
89
|
+
type,
|
|
90
|
+
name,
|
|
91
|
+
designType
|
|
92
|
+
});
|
|
93
|
+
Reflect.defineMetadata(exports.PARAM_METADATA, params, target, propertyKey);
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
exports.Param = createParamDecorator('param');
|
|
98
|
+
exports.Query = createParamDecorator('query');
|
|
99
|
+
exports.Body = createParamDecorator('body');
|
|
100
|
+
const Ctx = () => createParamDecorator('ctx')();
|
|
101
|
+
exports.Ctx = Ctx;
|
|
102
|
+
// --- WebSocket Decorators ---
|
|
103
|
+
exports.WS_CONTROLLER_METADATA = 'WS_CONTROLLER_METADATA';
|
|
104
|
+
exports.WS_ON_OPEN_METADATA = 'WS_ON_OPEN_METADATA';
|
|
105
|
+
exports.WS_ON_MESSAGE_METADATA = 'WS_ON_MESSAGE_METADATA';
|
|
106
|
+
exports.WS_ON_CLOSE_METADATA = 'WS_ON_CLOSE_METADATA';
|
|
107
|
+
exports.WS_ON_DRAIN_METADATA = 'WS_ON_DRAIN_METADATA';
|
|
108
|
+
function WebSocketController(path = '/*') {
|
|
109
|
+
return (target) => {
|
|
110
|
+
Reflect.defineMetadata(exports.WS_CONTROLLER_METADATA, path, target);
|
|
111
|
+
// WS Controllers are also injectable
|
|
112
|
+
Reflect.defineMetadata(exports.INJECTABLE_METADATA, true, target);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function createWsMethodDecorator(metadataKey) {
|
|
116
|
+
return (target, propertyKey) => {
|
|
117
|
+
Reflect.defineMetadata(metadataKey, propertyKey, target.constructor);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const OnOpen = () => createWsMethodDecorator(exports.WS_ON_OPEN_METADATA);
|
|
121
|
+
exports.OnOpen = OnOpen;
|
|
122
|
+
const OnMessage = () => createWsMethodDecorator(exports.WS_ON_MESSAGE_METADATA);
|
|
123
|
+
exports.OnMessage = OnMessage;
|
|
124
|
+
const OnClose = () => createWsMethodDecorator(exports.WS_ON_CLOSE_METADATA);
|
|
125
|
+
exports.OnClose = OnClose;
|
|
126
|
+
const OnDrain = () => createWsMethodDecorator(exports.WS_ON_DRAIN_METADATA);
|
|
127
|
+
exports.OnDrain = OnDrain;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './core/Context';
|
|
2
|
+
export * from './core/WsContext';
|
|
3
|
+
export * from './core/Router';
|
|
4
|
+
export * from './core/Application';
|
|
5
|
+
export * from './core/Container';
|
|
6
|
+
export * from './decorators';
|
|
7
|
+
export * from './validation';
|
|
8
|
+
export type { Middleware, Next } from './core/Context';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./core/Context"), exports);
|
|
18
|
+
__exportStar(require("./core/WsContext"), exports);
|
|
19
|
+
__exportStar(require("./core/Router"), exports);
|
|
20
|
+
__exportStar(require("./core/Application"), exports);
|
|
21
|
+
__exportStar(require("./core/Container"), exports);
|
|
22
|
+
__exportStar(require("./decorators"), exports);
|
|
23
|
+
__exportStar(require("./validation"), exports);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseContext, Next } from '../core/Context';
|
|
2
|
+
export interface CookieOptions {
|
|
3
|
+
maxAge?: number;
|
|
4
|
+
path?: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
secure?: boolean;
|
|
7
|
+
httpOnly?: boolean;
|
|
8
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
9
|
+
}
|
|
10
|
+
declare module '../core/Context' {
|
|
11
|
+
interface BaseContext {
|
|
12
|
+
getCookie(name: string): string | undefined;
|
|
13
|
+
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export declare const cookieMiddleware: () => (ctx: BaseContext, next: Next) => Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cookieMiddleware = void 0;
|
|
4
|
+
const cookieMiddleware = () => {
|
|
5
|
+
return async (ctx, next) => {
|
|
6
|
+
let parsedCookies;
|
|
7
|
+
ctx.getCookie = (name) => {
|
|
8
|
+
if (!parsedCookies) {
|
|
9
|
+
parsedCookies = {};
|
|
10
|
+
const cookieHeader = ctx.headers['cookie'];
|
|
11
|
+
if (cookieHeader) {
|
|
12
|
+
cookieHeader.split(';').forEach(c => {
|
|
13
|
+
const [k, v] = c.split('=');
|
|
14
|
+
if (k && v) {
|
|
15
|
+
parsedCookies[k.trim()] = decodeURIComponent(v.trim());
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return parsedCookies[name];
|
|
21
|
+
};
|
|
22
|
+
ctx.setCookie = (name, value, options = {}) => {
|
|
23
|
+
let cookieStr = `${name}=${encodeURIComponent(value)}`;
|
|
24
|
+
if (options.maxAge !== undefined)
|
|
25
|
+
cookieStr += `; Max-Age=${options.maxAge}`;
|
|
26
|
+
if (options.path)
|
|
27
|
+
cookieStr += `; Path=${options.path}`;
|
|
28
|
+
if (options.domain)
|
|
29
|
+
cookieStr += `; Domain=${options.domain}`;
|
|
30
|
+
if (options.secure)
|
|
31
|
+
cookieStr += `; Secure`;
|
|
32
|
+
if (options.httpOnly)
|
|
33
|
+
cookieStr += `; HttpOnly`;
|
|
34
|
+
if (options.sameSite)
|
|
35
|
+
cookieStr += `; SameSite=${options.sameSite}`;
|
|
36
|
+
if (!ctx.responseHeaders['Set-Cookie']) {
|
|
37
|
+
ctx.responseHeaders['Set-Cookie'] = [];
|
|
38
|
+
}
|
|
39
|
+
else if (typeof ctx.responseHeaders['Set-Cookie'] === 'string') {
|
|
40
|
+
ctx.responseHeaders['Set-Cookie'] = [ctx.responseHeaders['Set-Cookie']];
|
|
41
|
+
}
|
|
42
|
+
ctx.responseHeaders['Set-Cookie'].push(cookieStr);
|
|
43
|
+
};
|
|
44
|
+
await next();
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
exports.cookieMiddleware = cookieMiddleware;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
export declare const VALIDATION_METADATA_KEY = "VIO:VALIDATION";
|
|
3
|
+
export interface ValidationRule {
|
|
4
|
+
property: string;
|
|
5
|
+
type: string;
|
|
6
|
+
value?: any;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class ValidationError extends Error {
|
|
10
|
+
errors: string[];
|
|
11
|
+
constructor(errors: string[]);
|
|
12
|
+
}
|
|
13
|
+
export declare function IsRequired(message?: string): PropertyDecorator;
|
|
14
|
+
export declare function IsString(message?: string): PropertyDecorator;
|
|
15
|
+
export declare function IsNumber(message?: string): PropertyDecorator;
|
|
16
|
+
export declare function IsInt(message?: string): PropertyDecorator;
|
|
17
|
+
export declare function IsBoolean(message?: string): PropertyDecorator;
|
|
18
|
+
export declare function Min(val: number, message?: string): PropertyDecorator;
|
|
19
|
+
export declare function Max(val: number, message?: string): PropertyDecorator;
|
|
20
|
+
export declare function validateDTO(dtoClass: any, plainObject: any): any;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ValidationError = exports.VALIDATION_METADATA_KEY = void 0;
|
|
4
|
+
exports.IsRequired = IsRequired;
|
|
5
|
+
exports.IsString = IsString;
|
|
6
|
+
exports.IsNumber = IsNumber;
|
|
7
|
+
exports.IsInt = IsInt;
|
|
8
|
+
exports.IsBoolean = IsBoolean;
|
|
9
|
+
exports.Min = Min;
|
|
10
|
+
exports.Max = Max;
|
|
11
|
+
exports.validateDTO = validateDTO;
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
exports.VALIDATION_METADATA_KEY = 'VIO:VALIDATION';
|
|
14
|
+
function addValidationRule(target, propertyKey, rule) {
|
|
15
|
+
const rules = Reflect.getMetadata(exports.VALIDATION_METADATA_KEY, target.constructor) || [];
|
|
16
|
+
rules.push({ property: propertyKey, ...rule });
|
|
17
|
+
Reflect.defineMetadata(exports.VALIDATION_METADATA_KEY, rules, target.constructor);
|
|
18
|
+
}
|
|
19
|
+
// Validation Error Exception class
|
|
20
|
+
class ValidationError extends Error {
|
|
21
|
+
errors;
|
|
22
|
+
constructor(errors) {
|
|
23
|
+
super(errors.join(' | ')); // Joined by pipe for error message output
|
|
24
|
+
this.errors = errors;
|
|
25
|
+
this.name = 'ValidationError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.ValidationError = ValidationError;
|
|
29
|
+
// Built-in Validation Property Decorators
|
|
30
|
+
function IsRequired(message) {
|
|
31
|
+
return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'required', message });
|
|
32
|
+
}
|
|
33
|
+
function IsString(message) {
|
|
34
|
+
return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'string', message });
|
|
35
|
+
}
|
|
36
|
+
function IsNumber(message) {
|
|
37
|
+
return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'number', message });
|
|
38
|
+
}
|
|
39
|
+
function IsInt(message) {
|
|
40
|
+
return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'int', message });
|
|
41
|
+
}
|
|
42
|
+
function IsBoolean(message) {
|
|
43
|
+
return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'boolean', message });
|
|
44
|
+
}
|
|
45
|
+
function Min(val, message) {
|
|
46
|
+
return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'min', value: val, message });
|
|
47
|
+
}
|
|
48
|
+
function Max(val, message) {
|
|
49
|
+
return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'max', value: val, message });
|
|
50
|
+
}
|
|
51
|
+
// High performance native DTO Validator Engine
|
|
52
|
+
function validateDTO(dtoClass, plainObject) {
|
|
53
|
+
const rules = Reflect.getMetadata(exports.VALIDATION_METADATA_KEY, dtoClass) || [];
|
|
54
|
+
// No decorators, immediately return transparent wrapper
|
|
55
|
+
if (rules.length === 0) {
|
|
56
|
+
if (plainObject && typeof plainObject === 'object') {
|
|
57
|
+
const temp = new dtoClass();
|
|
58
|
+
Object.assign(temp, plainObject);
|
|
59
|
+
return temp;
|
|
60
|
+
}
|
|
61
|
+
return plainObject;
|
|
62
|
+
}
|
|
63
|
+
const errors = [];
|
|
64
|
+
const instance = new dtoClass();
|
|
65
|
+
// Copy arbitrary fields ignoring safety for now to support nested objects
|
|
66
|
+
if (plainObject && typeof plainObject === 'object') {
|
|
67
|
+
Object.assign(instance, plainObject);
|
|
68
|
+
}
|
|
69
|
+
for (const rule of rules) {
|
|
70
|
+
const val = instance[rule.property];
|
|
71
|
+
const isMissing = (val === undefined || val === null || val === '');
|
|
72
|
+
if (rule.type === 'required' && isMissing) {
|
|
73
|
+
errors.push(rule.message || `Parameter [${rule.property}] is required.`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!isMissing) {
|
|
77
|
+
if (rule.type === 'string' && typeof val !== 'string') {
|
|
78
|
+
errors.push(rule.message || `Parameter [${rule.property}] must be a valid string.`);
|
|
79
|
+
}
|
|
80
|
+
else if (rule.type === 'number') {
|
|
81
|
+
const num = Number(val);
|
|
82
|
+
if (isNaN(num))
|
|
83
|
+
errors.push(rule.message || `Parameter [${rule.property}] must be a number.`);
|
|
84
|
+
else
|
|
85
|
+
instance[rule.property] = num;
|
|
86
|
+
}
|
|
87
|
+
else if (rule.type === 'int') {
|
|
88
|
+
const num = Number(val);
|
|
89
|
+
if (isNaN(num) || !Number.isInteger(num))
|
|
90
|
+
errors.push(rule.message || `Parameter [${rule.property}] must be an integer.`);
|
|
91
|
+
else
|
|
92
|
+
instance[rule.property] = num;
|
|
93
|
+
}
|
|
94
|
+
else if (rule.type === 'boolean') {
|
|
95
|
+
if (val !== 'true' && val !== 'false' && val !== true && val !== false && val !== 1 && val !== 0 && val !== '1' && val !== '0') {
|
|
96
|
+
errors.push(rule.message || `Parameter [${rule.property}] must be a boolean.`);
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
instance[rule.property] = (val === 'true' || val === '1' || val === true || val === 1);
|
|
100
|
+
}
|
|
101
|
+
else if (rule.type === 'min') {
|
|
102
|
+
if (Number(val) < rule.value)
|
|
103
|
+
errors.push(rule.message || `Parameter [${rule.property}] must be >= ${rule.value}.`);
|
|
104
|
+
}
|
|
105
|
+
else if (rule.type === 'max') {
|
|
106
|
+
if (Number(val) > rule.value)
|
|
107
|
+
errors.push(rule.message || `Parameter [${rule.property}] must be <= ${rule.value}.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (errors.length > 0) {
|
|
112
|
+
throw new ValidationError(errors);
|
|
113
|
+
}
|
|
114
|
+
return instance; // Cleanly parsed and casted Data Transfer Object (DTO)
|
|
115
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "viojs-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core functionality for vio framework",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"package.json",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"mime-types": "^3.0.2",
|
|
18
|
+
"reflect-metadata": "^0.2.1",
|
|
19
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.58.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@swc/core": "^1.15.11",
|
|
23
|
+
"@types/mime-types": "^3.0.1",
|
|
24
|
+
"@types/node": "^20.11.19",
|
|
25
|
+
"tsup": "^8.5.1",
|
|
26
|
+
"typescript": "^5.3.3"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"reflect-metadata": "^0.2.1"
|
|
30
|
+
}
|
|
31
|
+
}
|