vector-framework 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +508 -0
- package/dist/auth/protected.d.ts +9 -0
- package/dist/auth/protected.d.ts.map +1 -0
- package/dist/auth/protected.js +26 -0
- package/dist/auth/protected.js.map +1 -0
- package/dist/cache/manager.d.ts +21 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +92 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +142 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/constants/index.d.ts +84 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +88 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/core/router.d.ts +26 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +208 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/server.d.ts +18 -0
- package/dist/core/server.d.ts.map +1 -0
- package/dist/core/server.js +89 -0
- package/dist/core/server.js.map +1 -0
- package/dist/core/vector.d.ts +43 -0
- package/dist/core/vector.d.ts.map +1 -0
- package/dist/core/vector.js +179 -0
- package/dist/core/vector.js.map +1 -0
- package/dist/dev/route-generator.d.ts +8 -0
- package/dist/dev/route-generator.d.ts.map +1 -0
- package/dist/dev/route-generator.js +77 -0
- package/dist/dev/route-generator.js.map +1 -0
- package/dist/dev/route-scanner.d.ts +9 -0
- package/dist/dev/route-scanner.d.ts.map +1 -0
- package/dist/dev/route-scanner.js +85 -0
- package/dist/dev/route-scanner.js.map +1 -0
- package/dist/errors/index.d.ts +24 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +73 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http.d.ts +73 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +143 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +21 -0
- package/dist/middleware/manager.d.ts +11 -0
- package/dist/middleware/manager.d.ts.map +1 -0
- package/dist/middleware/manager.js +35 -0
- package/dist/middleware/manager.js.map +1 -0
- package/dist/types/index.d.ts +85 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +68 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +48 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +110 -0
- package/src/auth/protected.ts +41 -0
- package/src/cache/manager.ts +133 -0
- package/src/cli/index.ts +157 -0
- package/src/constants/index.ts +93 -0
- package/src/core/router.ts +258 -0
- package/src/core/server.ts +107 -0
- package/src/core/vector.ts +228 -0
- package/src/dev/route-generator.ts +93 -0
- package/src/dev/route-scanner.ts +97 -0
- package/src/errors/index.ts +91 -0
- package/src/http.ts +331 -0
- package/src/index.ts +19 -0
- package/src/middleware/manager.ts +53 -0
- package/src/types/index.ts +126 -0
- package/src/utils/logger.ts +87 -0
- package/src/utils/validation.ts +58 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { RouteEntry } from 'itty-router';
|
|
2
|
+
import type { AuthManager } from '../auth/protected';
|
|
3
|
+
import type { CacheManager } from '../cache/manager';
|
|
4
|
+
import { APIError, createResponse } from '../http';
|
|
5
|
+
import type { MiddlewareManager } from '../middleware/manager';
|
|
6
|
+
import type {
|
|
7
|
+
DefaultVectorTypes,
|
|
8
|
+
RouteHandler,
|
|
9
|
+
RouteOptions,
|
|
10
|
+
VectorRequest,
|
|
11
|
+
VectorTypes,
|
|
12
|
+
} from '../types';
|
|
13
|
+
|
|
14
|
+
export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
15
|
+
private middlewareManager: MiddlewareManager<TTypes>;
|
|
16
|
+
private authManager: AuthManager<TTypes>;
|
|
17
|
+
private cacheManager: CacheManager<TTypes>;
|
|
18
|
+
private routes: RouteEntry[] = [];
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
middlewareManager: MiddlewareManager<TTypes>,
|
|
22
|
+
authManager: AuthManager<TTypes>,
|
|
23
|
+
cacheManager: CacheManager<TTypes>
|
|
24
|
+
) {
|
|
25
|
+
this.middlewareManager = middlewareManager;
|
|
26
|
+
this.authManager = authManager;
|
|
27
|
+
this.cacheManager = cacheManager;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private getRouteSpecificity(path: string): number {
|
|
31
|
+
const STATIC_SEGMENT_WEIGHT = 1000;
|
|
32
|
+
const PARAM_SEGMENT_WEIGHT = 10;
|
|
33
|
+
const WILDCARD_WEIGHT = 1;
|
|
34
|
+
const EXACT_MATCH_BONUS = 10000;
|
|
35
|
+
|
|
36
|
+
let score = 0;
|
|
37
|
+
const segments = path.split('/').filter(Boolean);
|
|
38
|
+
|
|
39
|
+
for (const segment of segments) {
|
|
40
|
+
if (this.isStaticSegment(segment)) {
|
|
41
|
+
score += STATIC_SEGMENT_WEIGHT;
|
|
42
|
+
} else if (this.isParamSegment(segment)) {
|
|
43
|
+
score += PARAM_SEGMENT_WEIGHT;
|
|
44
|
+
} else if (this.isWildcardSegment(segment)) {
|
|
45
|
+
score += WILDCARD_WEIGHT;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
score += path.length;
|
|
50
|
+
|
|
51
|
+
if (this.isExactPath(path)) {
|
|
52
|
+
score += EXACT_MATCH_BONUS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return score;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private isStaticSegment(segment: string): boolean {
|
|
59
|
+
return !segment.startsWith(':') && !segment.includes('*');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private isParamSegment(segment: string): boolean {
|
|
63
|
+
return segment.startsWith(':');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private isWildcardSegment(segment: string): boolean {
|
|
67
|
+
return segment.includes('*');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private isExactPath(path: string): boolean {
|
|
71
|
+
return !path.includes(':') && !path.includes('*');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sortRoutes(): void {
|
|
75
|
+
this.routes.sort((a, b) => {
|
|
76
|
+
const pathA = this.extractPath(a);
|
|
77
|
+
const pathB = this.extractPath(b);
|
|
78
|
+
|
|
79
|
+
const scoreA = this.getRouteSpecificity(pathA);
|
|
80
|
+
const scoreB = this.getRouteSpecificity(pathB);
|
|
81
|
+
|
|
82
|
+
return scoreB - scoreA;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private extractPath(route: RouteEntry): string {
|
|
87
|
+
const PATH_INDEX = 3;
|
|
88
|
+
return route[PATH_INDEX] || '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
route(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
|
|
92
|
+
const wrappedHandler = this.wrapHandler(options, handler);
|
|
93
|
+
const routeEntry: RouteEntry = [
|
|
94
|
+
options.method.toUpperCase(),
|
|
95
|
+
this.createRouteRegex(options.path),
|
|
96
|
+
[wrappedHandler],
|
|
97
|
+
options.path,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
this.routes.push(routeEntry);
|
|
101
|
+
this.sortRoutes(); // Sort routes after adding
|
|
102
|
+
return routeEntry;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private createRouteRegex(path: string): RegExp {
|
|
106
|
+
return RegExp(
|
|
107
|
+
`^${path
|
|
108
|
+
.replace(/\/+(\/|$)/g, '$1')
|
|
109
|
+
.replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))')
|
|
110
|
+
.replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))')
|
|
111
|
+
.replace(/\./g, '\\.')
|
|
112
|
+
.replace(/(\/?)\*/g, '($1.*)?')}/*$`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private wrapHandler(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>) {
|
|
117
|
+
return async (request: any) => {
|
|
118
|
+
// Ensure request has required properties
|
|
119
|
+
const vectorRequest = request as VectorRequest<TTypes>;
|
|
120
|
+
|
|
121
|
+
// Initialize context if not present
|
|
122
|
+
if (!vectorRequest.context) {
|
|
123
|
+
vectorRequest.context = {} as any;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse query parameters from URL (handles duplicate params as arrays)
|
|
127
|
+
if (!vectorRequest.query && vectorRequest.url) {
|
|
128
|
+
const url = new URL(vectorRequest.url);
|
|
129
|
+
const query: Record<string, string | string[]> = {};
|
|
130
|
+
for (let [k, v] of url.searchParams) {
|
|
131
|
+
query[k] = query[k] ? ([] as string[]).concat(query[k], v) : v;
|
|
132
|
+
}
|
|
133
|
+
vectorRequest.query = query;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add metadata to request if provided
|
|
137
|
+
if (options.metadata) {
|
|
138
|
+
vectorRequest.metadata = options.metadata;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
request = vectorRequest;
|
|
142
|
+
try {
|
|
143
|
+
if (!options.expose) {
|
|
144
|
+
return APIError.forbidden('Forbidden');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const beforeResult = await this.middlewareManager.executeBefore(request);
|
|
148
|
+
if (beforeResult instanceof Response) {
|
|
149
|
+
return beforeResult;
|
|
150
|
+
}
|
|
151
|
+
request = beforeResult as any;
|
|
152
|
+
|
|
153
|
+
if (options.auth) {
|
|
154
|
+
try {
|
|
155
|
+
await this.authManager.authenticate(request);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return APIError.unauthorized(
|
|
158
|
+
error instanceof Error ? error.message : 'Authentication failed',
|
|
159
|
+
options.responseContentType
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
|
|
165
|
+
try {
|
|
166
|
+
const contentType = request.headers.get('content-type');
|
|
167
|
+
if (contentType?.includes('application/json')) {
|
|
168
|
+
request.content = await request.json();
|
|
169
|
+
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
170
|
+
request.content = Object.fromEntries(await request.formData());
|
|
171
|
+
} else if (contentType?.includes('multipart/form-data')) {
|
|
172
|
+
request.content = await request.formData();
|
|
173
|
+
} else {
|
|
174
|
+
request.content = await request.text();
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
request.content = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let result;
|
|
182
|
+
const cacheOptions = options.cache;
|
|
183
|
+
|
|
184
|
+
if (cacheOptions && typeof cacheOptions === 'number' && cacheOptions > 0) {
|
|
185
|
+
const cacheKey = this.cacheManager.generateKey(request as any, {
|
|
186
|
+
authUser: request.authUser,
|
|
187
|
+
});
|
|
188
|
+
result = await this.cacheManager.get(cacheKey, () => handler(request), cacheOptions);
|
|
189
|
+
} else if (cacheOptions && typeof cacheOptions === 'object' && cacheOptions.ttl) {
|
|
190
|
+
const cacheKey =
|
|
191
|
+
cacheOptions.key ||
|
|
192
|
+
this.cacheManager.generateKey(request as any, {
|
|
193
|
+
authUser: request.authUser,
|
|
194
|
+
});
|
|
195
|
+
result = await this.cacheManager.get(cacheKey, () => handler(request), cacheOptions.ttl);
|
|
196
|
+
} else {
|
|
197
|
+
result = await handler(request);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let response: Response;
|
|
201
|
+
if (options.rawResponse || result instanceof Response) {
|
|
202
|
+
response = result instanceof Response ? result : new Response(result);
|
|
203
|
+
} else {
|
|
204
|
+
response = createResponse(200, result, options.responseContentType);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
response = await this.middlewareManager.executeFinally(response, request);
|
|
208
|
+
|
|
209
|
+
return response;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof Response) {
|
|
212
|
+
return error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.error('Route handler error:', error);
|
|
216
|
+
return APIError.internalServerError(
|
|
217
|
+
error instanceof Error ? error.message : String(error),
|
|
218
|
+
options.responseContentType
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
addRoute(routeEntry: RouteEntry) {
|
|
225
|
+
this.routes.push(routeEntry);
|
|
226
|
+
this.sortRoutes(); // Sort routes after adding a new one
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getRoutes(): RouteEntry[] {
|
|
230
|
+
return this.routes;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async handle(request: Request): Promise<Response> {
|
|
234
|
+
const url = new URL(request.url);
|
|
235
|
+
const pathname = url.pathname;
|
|
236
|
+
|
|
237
|
+
for (const [method, regex, handlers] of this.routes) {
|
|
238
|
+
if (request.method === 'OPTIONS' || request.method === method) {
|
|
239
|
+
const match = pathname.match(regex);
|
|
240
|
+
if (match) {
|
|
241
|
+
const req = request as any as VectorRequest<TTypes>;
|
|
242
|
+
// Initialize context for new request
|
|
243
|
+
if (!req.context) {
|
|
244
|
+
req.context = {} as any;
|
|
245
|
+
}
|
|
246
|
+
req.params = match.groups || {};
|
|
247
|
+
|
|
248
|
+
for (const handler of handlers) {
|
|
249
|
+
const response = await handler(req as any);
|
|
250
|
+
if (response) return response;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return APIError.notFound('Route not found');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Server } from 'bun';
|
|
2
|
+
import { cors } from 'itty-router';
|
|
3
|
+
import type { CorsOptions, DefaultVectorTypes, VectorConfig, VectorTypes } from '../types';
|
|
4
|
+
import type { VectorRouter } from './router';
|
|
5
|
+
|
|
6
|
+
export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
7
|
+
private server: Server | null = null;
|
|
8
|
+
private router: VectorRouter<TTypes>;
|
|
9
|
+
private config: VectorConfig<TTypes>;
|
|
10
|
+
private corsHandler: any;
|
|
11
|
+
|
|
12
|
+
constructor(router: VectorRouter<TTypes>, config: VectorConfig<TTypes>) {
|
|
13
|
+
this.router = router;
|
|
14
|
+
this.config = config;
|
|
15
|
+
|
|
16
|
+
if (config.cors) {
|
|
17
|
+
const { preflight, corsify } = cors(this.normalizeCorsOptions(config.cors));
|
|
18
|
+
this.corsHandler = { preflight, corsify };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private normalizeCorsOptions(options: CorsOptions): any {
|
|
23
|
+
return {
|
|
24
|
+
origin: options.origin || '*',
|
|
25
|
+
credentials: options.credentials !== false,
|
|
26
|
+
allowHeaders: Array.isArray(options.allowHeaders)
|
|
27
|
+
? options.allowHeaders.join(', ')
|
|
28
|
+
: options.allowHeaders || 'Content-Type, Authorization',
|
|
29
|
+
allowMethods: Array.isArray(options.allowMethods)
|
|
30
|
+
? options.allowMethods.join(', ')
|
|
31
|
+
: options.allowMethods || 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
32
|
+
exposeHeaders: Array.isArray(options.exposeHeaders)
|
|
33
|
+
? options.exposeHeaders.join(', ')
|
|
34
|
+
: options.exposeHeaders || 'Authorization',
|
|
35
|
+
maxAge: options.maxAge || 86400,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async start(): Promise<Server> {
|
|
40
|
+
const port = this.config.port || 3000;
|
|
41
|
+
const hostname = this.config.hostname || 'localhost';
|
|
42
|
+
|
|
43
|
+
const fetch = async (request: Request): Promise<Response> => {
|
|
44
|
+
try {
|
|
45
|
+
// Handle CORS preflight
|
|
46
|
+
if (this.corsHandler && request.method === 'OPTIONS') {
|
|
47
|
+
return this.corsHandler.preflight(request);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Try to handle the request with our router
|
|
51
|
+
let response = await this.router.handle(request);
|
|
52
|
+
|
|
53
|
+
// Apply CORS headers if configured
|
|
54
|
+
if (this.corsHandler) {
|
|
55
|
+
response = this.corsHandler.corsify(response, request);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Server error:', error);
|
|
61
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.server = Bun.serve({
|
|
66
|
+
port,
|
|
67
|
+
hostname,
|
|
68
|
+
reusePort: this.config.reusePort !== false,
|
|
69
|
+
fetch,
|
|
70
|
+
error: (error) => {
|
|
71
|
+
console.error('[ERROR] Server error:', error);
|
|
72
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Server logs are handled by CLI, keep this minimal
|
|
77
|
+
console.log(`→ Vector server running at http://${hostname}:${port}`);
|
|
78
|
+
|
|
79
|
+
return this.server;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stop() {
|
|
83
|
+
if (this.server) {
|
|
84
|
+
this.server.stop();
|
|
85
|
+
this.server = null;
|
|
86
|
+
console.log('Server stopped');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getServer(): Server | null {
|
|
91
|
+
return this.server;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getPort(): number {
|
|
95
|
+
return this.server?.port || this.config.port || 3000;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getHostname(): string {
|
|
99
|
+
return this.server?.hostname || this.config.hostname || 'localhost';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getUrl(): string {
|
|
103
|
+
const port = this.getPort();
|
|
104
|
+
const hostname = this.getHostname();
|
|
105
|
+
return `http://${hostname}:${port}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { Server } from 'bun';
|
|
2
|
+
import type { RouteEntry } from 'itty-router';
|
|
3
|
+
import { AuthManager } from '../auth/protected';
|
|
4
|
+
import { CacheManager } from '../cache/manager';
|
|
5
|
+
import { RouteGenerator } from '../dev/route-generator';
|
|
6
|
+
import { RouteScanner } from '../dev/route-scanner';
|
|
7
|
+
import { MiddlewareManager } from '../middleware/manager';
|
|
8
|
+
import type {
|
|
9
|
+
AfterMiddlewareHandler,
|
|
10
|
+
BeforeMiddlewareHandler,
|
|
11
|
+
CacheHandler,
|
|
12
|
+
DefaultVectorTypes,
|
|
13
|
+
ProtectedHandler,
|
|
14
|
+
RouteHandler,
|
|
15
|
+
RouteOptions,
|
|
16
|
+
VectorConfig,
|
|
17
|
+
VectorTypes,
|
|
18
|
+
} from '../types';
|
|
19
|
+
import { VectorRouter } from './router';
|
|
20
|
+
import { VectorServer } from './server';
|
|
21
|
+
|
|
22
|
+
export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
23
|
+
private static instance: Vector<any>;
|
|
24
|
+
private router: VectorRouter<TTypes>;
|
|
25
|
+
private server: VectorServer<TTypes> | null = null;
|
|
26
|
+
private middlewareManager: MiddlewareManager<TTypes>;
|
|
27
|
+
private authManager: AuthManager<TTypes>;
|
|
28
|
+
private cacheManager: CacheManager<TTypes>;
|
|
29
|
+
private config: VectorConfig<TTypes> = {};
|
|
30
|
+
private routeScanner: RouteScanner | null = null;
|
|
31
|
+
private routeGenerator: RouteGenerator | null = null;
|
|
32
|
+
private _protectedHandler: ProtectedHandler<TTypes> | null = null;
|
|
33
|
+
private _cacheHandler: CacheHandler | null = null;
|
|
34
|
+
|
|
35
|
+
private constructor() {
|
|
36
|
+
this.middlewareManager = new MiddlewareManager<TTypes>();
|
|
37
|
+
this.authManager = new AuthManager<TTypes>();
|
|
38
|
+
this.cacheManager = new CacheManager<TTypes>();
|
|
39
|
+
this.router = new VectorRouter<TTypes>(
|
|
40
|
+
this.middlewareManager,
|
|
41
|
+
this.authManager,
|
|
42
|
+
this.cacheManager
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static getInstance<T extends VectorTypes = DefaultVectorTypes>(): Vector<T> {
|
|
47
|
+
if (!Vector.instance) {
|
|
48
|
+
Vector.instance = new Vector<T>();
|
|
49
|
+
}
|
|
50
|
+
return Vector.instance as Vector<T>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set protected(handler: ProtectedHandler<TTypes>) {
|
|
54
|
+
this._protectedHandler = handler;
|
|
55
|
+
this.authManager.setProtectedHandler(handler);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get protected(): ProtectedHandler<TTypes> | null {
|
|
59
|
+
return this._protectedHandler;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
set cache(handler: CacheHandler) {
|
|
63
|
+
this._cacheHandler = handler;
|
|
64
|
+
this.cacheManager.setCacheHandler(handler);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get cache(): CacheHandler | null {
|
|
68
|
+
return this._cacheHandler;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
route(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
|
|
72
|
+
return this.router.route(options, handler);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
use(...middleware: BeforeMiddlewareHandler<TTypes>[]): this {
|
|
76
|
+
this.middlewareManager.addBefore(...middleware);
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
before(...middleware: BeforeMiddlewareHandler<TTypes>[]): this {
|
|
81
|
+
this.middlewareManager.addBefore(...middleware);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
finally(...middleware: AfterMiddlewareHandler<TTypes>[]): this {
|
|
86
|
+
this.middlewareManager.addFinally(...middleware);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async serve(config?: VectorConfig<TTypes>): Promise<Server> {
|
|
91
|
+
this.config = { ...this.config, ...config };
|
|
92
|
+
|
|
93
|
+
if (config?.before) {
|
|
94
|
+
this.middlewareManager.addBefore(...config.before);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config?.finally) {
|
|
98
|
+
this.middlewareManager.addFinally(...config.finally);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this.config.autoDiscover !== false) {
|
|
102
|
+
await this.discoverRoutes();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.server = new VectorServer<TTypes>(this.router, this.config);
|
|
106
|
+
const bunServer = await this.server.start();
|
|
107
|
+
|
|
108
|
+
if (this.config.development && this.routeScanner) {
|
|
109
|
+
this.routeScanner.enableWatch(async () => {
|
|
110
|
+
await this.discoverRoutes();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return bunServer;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async discoverRoutes() {
|
|
118
|
+
const routesDir = this.config.routesDir || './routes';
|
|
119
|
+
|
|
120
|
+
if (!this.routeScanner) {
|
|
121
|
+
this.routeScanner = new RouteScanner(routesDir);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!this.routeGenerator) {
|
|
125
|
+
this.routeGenerator = new RouteGenerator();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const routes = await this.routeScanner.scan();
|
|
130
|
+
|
|
131
|
+
if (routes.length > 0) {
|
|
132
|
+
if (this.config.development) {
|
|
133
|
+
await this.routeGenerator.generate(routes);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const route of routes) {
|
|
137
|
+
try {
|
|
138
|
+
// Convert Windows paths to URLs for import
|
|
139
|
+
const importPath =
|
|
140
|
+
process.platform === 'win32'
|
|
141
|
+
? `file:///${route.path.replace(/\\/g, '/')}`
|
|
142
|
+
: route.path;
|
|
143
|
+
|
|
144
|
+
const module = await import(importPath);
|
|
145
|
+
const exported = route.name === 'default' ? module.default : module[route.name];
|
|
146
|
+
|
|
147
|
+
if (exported) {
|
|
148
|
+
if (this.isRouteEntry(exported)) {
|
|
149
|
+
this.router.addRoute(exported as RouteEntry);
|
|
150
|
+
this.logRouteLoaded(exported as RouteEntry);
|
|
151
|
+
} else if (typeof exported === 'function') {
|
|
152
|
+
this.router.route(route.options as any, exported);
|
|
153
|
+
this.logRouteLoaded(route.options);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(`Failed to load route ${route.name} from ${route.path}:`, error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Ensure routes are properly sorted after loading all
|
|
162
|
+
this.router.sortRoutes();
|
|
163
|
+
console.log(`✅ Loaded ${routes.length} routes from ${routesDir}`);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if ((error as any).code !== 'ENOENT') {
|
|
167
|
+
console.error('Failed to discover routes:', error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async loadRoute(routeModule: any) {
|
|
173
|
+
if (typeof routeModule === 'function') {
|
|
174
|
+
const routeEntry = routeModule();
|
|
175
|
+
if (Array.isArray(routeEntry)) {
|
|
176
|
+
this.router.addRoute(routeEntry as RouteEntry);
|
|
177
|
+
}
|
|
178
|
+
} else if (routeModule && typeof routeModule === 'object') {
|
|
179
|
+
for (const [, value] of Object.entries(routeModule)) {
|
|
180
|
+
if (typeof value === 'function') {
|
|
181
|
+
const routeEntry = (value as any)();
|
|
182
|
+
if (Array.isArray(routeEntry)) {
|
|
183
|
+
this.router.addRoute(routeEntry as RouteEntry);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private isRouteEntry(value: any): boolean {
|
|
191
|
+
return Array.isArray(value) && value.length >= 3;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private logRouteLoaded(route: RouteEntry | RouteOptions): void {
|
|
195
|
+
if (Array.isArray(route)) {
|
|
196
|
+
console.log(` ✓ Loaded route: ${route[0]} ${route[3] || route[1]}`);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(` ✓ Loaded route: ${route.method} ${route.path}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
stop(): void {
|
|
203
|
+
if (this.server) {
|
|
204
|
+
this.server.stop();
|
|
205
|
+
this.server = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getServer(): VectorServer<TTypes> | null {
|
|
210
|
+
return this.server;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getRouter(): VectorRouter<TTypes> {
|
|
214
|
+
return this.router;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getCacheManager(): CacheManager<TTypes> {
|
|
218
|
+
return this.cacheManager;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getAuthManager(): AuthManager<TTypes> {
|
|
222
|
+
return this.authManager;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const vector = Vector.getInstance();
|
|
227
|
+
|
|
228
|
+
export default vector;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, relative } from 'node:path';
|
|
3
|
+
import type { GeneratedRoute } from '../types';
|
|
4
|
+
|
|
5
|
+
export class RouteGenerator {
|
|
6
|
+
private outputPath: string;
|
|
7
|
+
|
|
8
|
+
constructor(outputPath = './.vector/routes.generated.ts') {
|
|
9
|
+
this.outputPath = outputPath;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async generate(routes: GeneratedRoute[]): Promise<void> {
|
|
13
|
+
const outputDir = dirname(this.outputPath);
|
|
14
|
+
await mkdir(outputDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
const imports: string[] = [];
|
|
17
|
+
const groupedByFile = new Map<string, GeneratedRoute[]>();
|
|
18
|
+
|
|
19
|
+
for (const route of routes) {
|
|
20
|
+
if (!groupedByFile.has(route.path)) {
|
|
21
|
+
groupedByFile.set(route.path, []);
|
|
22
|
+
}
|
|
23
|
+
groupedByFile.get(route.path)!.push(route);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let importIndex = 0;
|
|
27
|
+
const routeEntries: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [filePath, fileRoutes] of groupedByFile) {
|
|
30
|
+
const relativePath = relative(dirname(this.outputPath), filePath)
|
|
31
|
+
.replace(/\\/g, '/')
|
|
32
|
+
.replace(/\.(ts|js)$/, '');
|
|
33
|
+
|
|
34
|
+
const importName = `route_${importIndex++}`;
|
|
35
|
+
const namedImports = fileRoutes.filter((r) => r.name !== 'default').map((r) => r.name);
|
|
36
|
+
|
|
37
|
+
if (fileRoutes.some((r) => r.name === 'default')) {
|
|
38
|
+
if (namedImports.length > 0) {
|
|
39
|
+
imports.push(
|
|
40
|
+
`import ${importName}, { ${namedImports.join(', ')} } from '${relativePath}';`
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
imports.push(`import ${importName} from '${relativePath}';`);
|
|
44
|
+
}
|
|
45
|
+
} else if (namedImports.length > 0) {
|
|
46
|
+
imports.push(`import { ${namedImports.join(', ')} } from '${relativePath}';`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const route of fileRoutes) {
|
|
50
|
+
const routeVar = route.name === 'default' ? importName : route.name;
|
|
51
|
+
routeEntries.push(` ${routeVar},`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const content = `// This file is auto-generated. Do not edit manually.
|
|
56
|
+
// Generated at: ${new Date().toISOString()}
|
|
57
|
+
|
|
58
|
+
${imports.join('\n')}
|
|
59
|
+
|
|
60
|
+
export const routes = [
|
|
61
|
+
${routeEntries.join('\n')}
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export default routes;
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
await writeFile(this.outputPath, content, 'utf-8');
|
|
68
|
+
console.log(`Generated routes file: ${this.outputPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async generateDynamic(routes: GeneratedRoute[]): Promise<string> {
|
|
72
|
+
const routeEntries: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const route of routes) {
|
|
75
|
+
const routeObj = JSON.stringify({
|
|
76
|
+
method: route.method,
|
|
77
|
+
path: route.options.path,
|
|
78
|
+
options: route.options,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
routeEntries.push(` await import('${route.path}').then(m => ({
|
|
82
|
+
...${routeObj},
|
|
83
|
+
handler: m.${route.name === 'default' ? 'default' : route.name}
|
|
84
|
+
}))`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return `export const loadRoutes = async () => {
|
|
88
|
+
return Promise.all([
|
|
89
|
+
${routeEntries.join(',\n')}
|
|
90
|
+
]);
|
|
91
|
+
};`;
|
|
92
|
+
}
|
|
93
|
+
}
|