vector-framework 1.0.0 → 1.1.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/README.md +6 -5
- package/dist/cache/manager.d.ts +5 -2
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +21 -7
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/index.js +62 -83
- package/dist/cli/index.js.map +1 -1
- package/dist/cli.js +108 -37
- package/dist/core/config-loader.d.ts +2 -2
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +16 -18
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +2 -0
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +52 -16
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +4 -3
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +39 -18
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +7 -7
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +20 -21
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-scanner.d.ts +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +40 -42
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +70 -63
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/dist/middleware/manager.d.ts +1 -1
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js.map +1 -1
- package/dist/utils/path.d.ts +1 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +9 -3
- package/dist/utils/path.js.map +1 -1
- package/package.json +12 -8
- package/src/cache/manager.ts +23 -14
- package/src/cli/index.ts +66 -89
- package/src/core/config-loader.ts +18 -20
- package/src/core/router.ts +52 -18
- package/src/core/server.ts +42 -28
- package/src/core/vector.ts +25 -35
- package/src/dev/route-scanner.ts +41 -47
- package/src/http.ts +82 -112
- package/src/index.ts +3 -3
- package/src/middleware/manager.ts +4 -11
- package/src/utils/path.ts +13 -4
package/src/core/router.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { RouteEntry } from 'itty-router';
|
|
2
|
-
import { withCookies } from 'itty-router';
|
|
3
2
|
import type { AuthManager } from '../auth/protected';
|
|
4
3
|
import type { CacheManager } from '../cache/manager';
|
|
5
4
|
import { APIError, createResponse } from '../http';
|
|
@@ -11,12 +10,14 @@ import type {
|
|
|
11
10
|
VectorRequest,
|
|
12
11
|
VectorTypes,
|
|
13
12
|
} from '../types';
|
|
13
|
+
import { buildRouteRegex } from '../utils/path';
|
|
14
14
|
|
|
15
15
|
export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
16
16
|
private middlewareManager: MiddlewareManager<TTypes>;
|
|
17
17
|
private authManager: AuthManager<TTypes>;
|
|
18
18
|
private cacheManager: CacheManager<TTypes>;
|
|
19
19
|
private routes: RouteEntry[] = [];
|
|
20
|
+
private specificityCache: Map<string, number> = new Map();
|
|
20
21
|
|
|
21
22
|
constructor(
|
|
22
23
|
middlewareManager: MiddlewareManager<TTypes>,
|
|
@@ -29,6 +30,9 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
private getRouteSpecificity(path: string): number {
|
|
33
|
+
const cached = this.specificityCache.get(path);
|
|
34
|
+
if (cached !== undefined) return cached;
|
|
35
|
+
|
|
32
36
|
const STATIC_SEGMENT_WEIGHT = 1000;
|
|
33
37
|
const PARAM_SEGMENT_WEIGHT = 10;
|
|
34
38
|
const WILDCARD_WEIGHT = 1;
|
|
@@ -53,6 +57,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
53
57
|
score += EXACT_MATCH_BONUS;
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
this.specificityCache.set(path, score);
|
|
56
61
|
return score;
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -104,14 +109,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
private createRouteRegex(path: string): RegExp {
|
|
107
|
-
return
|
|
108
|
-
`^${path
|
|
109
|
-
.replace(/\/+(\/|$)/g, '$1')
|
|
110
|
-
.replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))')
|
|
111
|
-
.replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))')
|
|
112
|
-
.replace(/\./g, '\\.')
|
|
113
|
-
.replace(/(\/?)\*/g, '($1.*)?')}/*$`
|
|
114
|
-
);
|
|
112
|
+
return buildRouteRegex(path);
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
private prepareRequest(
|
|
@@ -140,7 +138,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
140
138
|
|
|
141
139
|
// Parse query parameters from URL if not already parsed
|
|
142
140
|
if (!request.query && request.url) {
|
|
143
|
-
const url = new URL(request.url);
|
|
141
|
+
const url = (request as any)._parsedUrl ?? new URL(request.url);
|
|
144
142
|
const query: Record<string, string | string[]> = {};
|
|
145
143
|
for (const [key, value] of url.searchParams) {
|
|
146
144
|
if (key in query) {
|
|
@@ -156,9 +154,31 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
156
154
|
request.query = query;
|
|
157
155
|
}
|
|
158
156
|
|
|
159
|
-
//
|
|
160
|
-
if (!request
|
|
161
|
-
|
|
157
|
+
// Lazy cookie parsing — only parse the Cookie header when first accessed
|
|
158
|
+
if (!Object.getOwnPropertyDescriptor(request, 'cookies')) {
|
|
159
|
+
Object.defineProperty(request, 'cookies', {
|
|
160
|
+
get() {
|
|
161
|
+
const cookieHeader = this.headers.get('cookie') ?? '';
|
|
162
|
+
const cookies: Record<string, string> = {};
|
|
163
|
+
if (cookieHeader) {
|
|
164
|
+
for (const pair of cookieHeader.split(';')) {
|
|
165
|
+
const idx = pair.indexOf('=');
|
|
166
|
+
if (idx > 0) {
|
|
167
|
+
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
Object.defineProperty(this, 'cookies', {
|
|
172
|
+
value: cookies,
|
|
173
|
+
writable: true,
|
|
174
|
+
configurable: true,
|
|
175
|
+
enumerable: true,
|
|
176
|
+
});
|
|
177
|
+
return cookies;
|
|
178
|
+
},
|
|
179
|
+
configurable: true,
|
|
180
|
+
enumerable: true,
|
|
181
|
+
});
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
184
|
|
|
@@ -169,7 +189,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
169
189
|
|
|
170
190
|
// Prepare the request with common logic
|
|
171
191
|
this.prepareRequest(vectorRequest, {
|
|
172
|
-
metadata: options.metadata
|
|
192
|
+
metadata: options.metadata,
|
|
173
193
|
});
|
|
174
194
|
|
|
175
195
|
request = vectorRequest;
|
|
@@ -225,7 +245,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
225
245
|
_isResponse: true,
|
|
226
246
|
body: await res.text(),
|
|
227
247
|
status: res.status,
|
|
228
|
-
headers: Object.fromEntries(res.headers.entries())
|
|
248
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
229
249
|
};
|
|
230
250
|
}
|
|
231
251
|
return res;
|
|
@@ -251,7 +271,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
251
271
|
if (result && typeof result === 'object' && result._isResponse === true) {
|
|
252
272
|
result = new Response(result.body, {
|
|
253
273
|
status: result.status,
|
|
254
|
-
headers: result.headers
|
|
274
|
+
headers: result.headers,
|
|
255
275
|
});
|
|
256
276
|
}
|
|
257
277
|
|
|
@@ -284,12 +304,25 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
284
304
|
this.sortRoutes(); // Sort routes after adding a new one
|
|
285
305
|
}
|
|
286
306
|
|
|
307
|
+
bulkAddRoutes(entries: RouteEntry[]): void {
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
this.routes.push(entry);
|
|
310
|
+
}
|
|
311
|
+
this.sortRoutes(); // Sort once after all routes are added — O(n log n) instead of O(n²)
|
|
312
|
+
}
|
|
313
|
+
|
|
287
314
|
getRoutes(): RouteEntry[] {
|
|
288
315
|
return this.routes;
|
|
289
316
|
}
|
|
290
317
|
|
|
291
318
|
async handle(request: Request): Promise<Response> {
|
|
292
|
-
|
|
319
|
+
let url: URL;
|
|
320
|
+
try {
|
|
321
|
+
url = new URL(request.url);
|
|
322
|
+
} catch {
|
|
323
|
+
return APIError.badRequest('Malformed request URL');
|
|
324
|
+
}
|
|
325
|
+
(request as any)._parsedUrl = url;
|
|
293
326
|
const pathname = url.pathname;
|
|
294
327
|
|
|
295
328
|
for (const [method, regex, handlers, path] of this.routes) {
|
|
@@ -301,7 +334,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
301
334
|
// Prepare the request with common logic
|
|
302
335
|
this.prepareRequest(req, {
|
|
303
336
|
params: match.groups || {},
|
|
304
|
-
route: path || pathname
|
|
337
|
+
route: path || pathname,
|
|
305
338
|
});
|
|
306
339
|
|
|
307
340
|
for (const handler of handlers) {
|
|
@@ -317,5 +350,6 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
317
350
|
|
|
318
351
|
clearRoutes(): void {
|
|
319
352
|
this.routes = [];
|
|
353
|
+
this.specificityCache.clear();
|
|
320
354
|
}
|
|
321
355
|
}
|
package/src/core/server.ts
CHANGED
|
@@ -1,56 +1,66 @@
|
|
|
1
|
-
import type { Server } from
|
|
2
|
-
import { cors } from
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
DefaultVectorTypes,
|
|
6
|
-
VectorConfig,
|
|
7
|
-
VectorTypes,
|
|
8
|
-
} from "../types";
|
|
9
|
-
import type { VectorRouter } from "./router";
|
|
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';
|
|
10
5
|
|
|
11
6
|
export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
12
7
|
private server: Server | null = null;
|
|
13
8
|
private router: VectorRouter<TTypes>;
|
|
14
9
|
private config: VectorConfig<TTypes>;
|
|
15
10
|
private corsHandler: any;
|
|
11
|
+
private corsHeaders: Record<string, string> | null = null;
|
|
16
12
|
|
|
17
13
|
constructor(router: VectorRouter<TTypes>, config: VectorConfig<TTypes>) {
|
|
18
14
|
this.router = router;
|
|
19
15
|
this.config = config;
|
|
20
16
|
|
|
21
17
|
if (config.cors) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
);
|
|
18
|
+
const opts = this.normalizeCorsOptions(config.cors);
|
|
19
|
+
const { preflight, corsify } = cors(opts);
|
|
25
20
|
this.corsHandler = { preflight, corsify };
|
|
21
|
+
|
|
22
|
+
// Pre-build static CORS headers when origin is a fixed string.
|
|
23
|
+
// Avoids cloning the Response on every request via corsify().
|
|
24
|
+
if (typeof opts.origin === 'string') {
|
|
25
|
+
this.corsHeaders = {
|
|
26
|
+
'access-control-allow-origin': opts.origin,
|
|
27
|
+
'access-control-allow-methods': opts.allowMethods,
|
|
28
|
+
'access-control-allow-headers': opts.allowHeaders,
|
|
29
|
+
'access-control-expose-headers': opts.exposeHeaders,
|
|
30
|
+
'access-control-max-age': String(opts.maxAge),
|
|
31
|
+
};
|
|
32
|
+
if (opts.credentials) {
|
|
33
|
+
this.corsHeaders['access-control-allow-credentials'] = 'true';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
private normalizeCorsOptions(options: CorsOptions): any {
|
|
30
40
|
return {
|
|
31
|
-
origin: options.origin ||
|
|
41
|
+
origin: options.origin || '*',
|
|
32
42
|
credentials: options.credentials !== false,
|
|
33
43
|
allowHeaders: Array.isArray(options.allowHeaders)
|
|
34
|
-
? options.allowHeaders.join(
|
|
35
|
-
: options.allowHeaders ||
|
|
44
|
+
? options.allowHeaders.join(', ')
|
|
45
|
+
: options.allowHeaders || 'Content-Type, Authorization',
|
|
36
46
|
allowMethods: Array.isArray(options.allowMethods)
|
|
37
|
-
? options.allowMethods.join(
|
|
38
|
-
: options.allowMethods ||
|
|
47
|
+
? options.allowMethods.join(', ')
|
|
48
|
+
: options.allowMethods || 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
39
49
|
exposeHeaders: Array.isArray(options.exposeHeaders)
|
|
40
|
-
? options.exposeHeaders.join(
|
|
41
|
-
: options.exposeHeaders ||
|
|
50
|
+
? options.exposeHeaders.join(', ')
|
|
51
|
+
: options.exposeHeaders || 'Authorization',
|
|
42
52
|
maxAge: options.maxAge || 86400,
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
async start(): Promise<Server> {
|
|
47
57
|
const port = this.config.port || 3000;
|
|
48
|
-
const hostname = this.config.hostname ||
|
|
58
|
+
const hostname = this.config.hostname || 'localhost';
|
|
49
59
|
|
|
50
60
|
const fetch = async (request: Request): Promise<Response> => {
|
|
51
61
|
try {
|
|
52
62
|
// Handle CORS preflight
|
|
53
|
-
if (this.corsHandler && request.method ===
|
|
63
|
+
if (this.corsHandler && request.method === 'OPTIONS') {
|
|
54
64
|
return this.corsHandler.preflight(request);
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -58,14 +68,18 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
58
68
|
let response = await this.router.handle(request);
|
|
59
69
|
|
|
60
70
|
// Apply CORS headers if configured
|
|
61
|
-
if (this.
|
|
71
|
+
if (this.corsHeaders) {
|
|
72
|
+
for (const [k, v] of Object.entries(this.corsHeaders)) {
|
|
73
|
+
response.headers.set(k, v);
|
|
74
|
+
}
|
|
75
|
+
} else if (this.corsHandler) {
|
|
62
76
|
response = this.corsHandler.corsify(response, request);
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
return response;
|
|
66
80
|
} catch (error) {
|
|
67
|
-
console.error(
|
|
68
|
-
return new Response(
|
|
81
|
+
console.error('Server error:', error);
|
|
82
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
69
83
|
}
|
|
70
84
|
};
|
|
71
85
|
|
|
@@ -77,8 +91,8 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
77
91
|
fetch,
|
|
78
92
|
idleTimeout: this.config.idleTimeout || 60,
|
|
79
93
|
error: (error) => {
|
|
80
|
-
console.error(
|
|
81
|
-
return new Response(
|
|
94
|
+
console.error('[ERROR] Server error:', error);
|
|
95
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
82
96
|
},
|
|
83
97
|
});
|
|
84
98
|
|
|
@@ -111,7 +125,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
111
125
|
if (this.server) {
|
|
112
126
|
this.server.stop();
|
|
113
127
|
this.server = null;
|
|
114
|
-
console.log(
|
|
128
|
+
console.log('Server stopped');
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
|
|
@@ -124,7 +138,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
getHostname(): string {
|
|
127
|
-
return this.server?.hostname || this.config.hostname ||
|
|
141
|
+
return this.server?.hostname || this.config.hostname || 'localhost';
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
getUrl(): string {
|
package/src/core/vector.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Server } from
|
|
2
|
-
import type { RouteEntry } from
|
|
3
|
-
import { AuthManager } from
|
|
4
|
-
import { CacheManager } from
|
|
5
|
-
import { RouteGenerator } from
|
|
6
|
-
import { RouteScanner } from
|
|
7
|
-
import { MiddlewareManager } from
|
|
8
|
-
import { toFileUrl } from
|
|
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 { toFileUrl } from '../utils/path';
|
|
9
9
|
import type {
|
|
10
10
|
CacheHandler,
|
|
11
11
|
DefaultVectorTypes,
|
|
@@ -14,9 +14,9 @@ import type {
|
|
|
14
14
|
RouteOptions,
|
|
15
15
|
VectorConfig,
|
|
16
16
|
VectorTypes,
|
|
17
|
-
} from
|
|
18
|
-
import { VectorRouter } from
|
|
19
|
-
import { VectorServer } from
|
|
17
|
+
} from '../types';
|
|
18
|
+
import { VectorRouter } from './router';
|
|
19
|
+
import { VectorServer } from './server';
|
|
20
20
|
|
|
21
21
|
// Internal-only class - not exposed to users
|
|
22
22
|
export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
@@ -72,10 +72,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// Internal method to add route
|
|
75
|
-
addRoute(
|
|
76
|
-
options: RouteOptions<TTypes>,
|
|
77
|
-
handler: RouteHandler<TTypes>
|
|
78
|
-
): RouteEntry {
|
|
75
|
+
addRoute(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
|
|
79
76
|
return this.router.route(options, handler);
|
|
80
77
|
}
|
|
81
78
|
|
|
@@ -110,7 +107,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
110
107
|
}
|
|
111
108
|
|
|
112
109
|
private async discoverRoutes() {
|
|
113
|
-
const routesDir = this.config.routesDir ||
|
|
110
|
+
const routesDir = this.config.routesDir || './routes';
|
|
114
111
|
const excludePatterns = this.config.routeExcludePatterns;
|
|
115
112
|
|
|
116
113
|
// Always create a new RouteScanner with the current config's routesDir
|
|
@@ -134,8 +131,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
134
131
|
const importPath = toFileUrl(route.path);
|
|
135
132
|
|
|
136
133
|
const module = await import(importPath);
|
|
137
|
-
const exported =
|
|
138
|
-
route.name === "default" ? module.default : module[route.name];
|
|
134
|
+
const exported = route.name === 'default' ? module.default : module[route.name];
|
|
139
135
|
|
|
140
136
|
if (exported) {
|
|
141
137
|
if (this.isRouteDefinition(exported)) {
|
|
@@ -147,16 +143,13 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
147
143
|
// Legacy support for direct RouteEntry (won't have middleware)
|
|
148
144
|
this.router.addRoute(exported as RouteEntry);
|
|
149
145
|
this.logRouteLoaded(exported as RouteEntry);
|
|
150
|
-
} else if (typeof exported ===
|
|
146
|
+
} else if (typeof exported === 'function') {
|
|
151
147
|
this.router.route(route.options as any, exported);
|
|
152
148
|
this.logRouteLoaded(route.options);
|
|
153
149
|
}
|
|
154
150
|
}
|
|
155
151
|
} catch (error) {
|
|
156
|
-
console.error(
|
|
157
|
-
`Failed to load route ${route.name} from ${route.path}:`,
|
|
158
|
-
error
|
|
159
|
-
);
|
|
152
|
+
console.error(`Failed to load route ${route.name} from ${route.path}:`, error);
|
|
160
153
|
}
|
|
161
154
|
}
|
|
162
155
|
|
|
@@ -164,24 +157,21 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
164
157
|
this.router.sortRoutes();
|
|
165
158
|
}
|
|
166
159
|
} catch (error) {
|
|
167
|
-
if (
|
|
168
|
-
(
|
|
169
|
-
(error as any).code !== "ENOTDIR"
|
|
170
|
-
) {
|
|
171
|
-
console.error("Failed to discover routes:", error);
|
|
160
|
+
if ((error as any).code !== 'ENOENT' && (error as any).code !== 'ENOTDIR') {
|
|
161
|
+
console.error('Failed to discover routes:', error);
|
|
172
162
|
}
|
|
173
163
|
}
|
|
174
164
|
}
|
|
175
165
|
|
|
176
166
|
async loadRoute(routeModule: any) {
|
|
177
|
-
if (typeof routeModule ===
|
|
167
|
+
if (typeof routeModule === 'function') {
|
|
178
168
|
const routeEntry = routeModule();
|
|
179
169
|
if (Array.isArray(routeEntry)) {
|
|
180
170
|
this.router.addRoute(routeEntry as RouteEntry);
|
|
181
171
|
}
|
|
182
|
-
} else if (routeModule && typeof routeModule ===
|
|
172
|
+
} else if (routeModule && typeof routeModule === 'object') {
|
|
183
173
|
for (const [, value] of Object.entries(routeModule)) {
|
|
184
|
-
if (typeof value ===
|
|
174
|
+
if (typeof value === 'function') {
|
|
185
175
|
const routeEntry = (value as any)();
|
|
186
176
|
if (Array.isArray(routeEntry)) {
|
|
187
177
|
this.router.addRoute(routeEntry as RouteEntry);
|
|
@@ -198,10 +188,10 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
198
188
|
private isRouteDefinition(value: any): boolean {
|
|
199
189
|
return (
|
|
200
190
|
value &&
|
|
201
|
-
typeof value ===
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
typeof value === 'object' &&
|
|
192
|
+
'entry' in value &&
|
|
193
|
+
'options' in value &&
|
|
194
|
+
'handler' in value
|
|
205
195
|
);
|
|
206
196
|
}
|
|
207
197
|
|
package/src/dev/route-scanner.ts
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
|
-
import { existsSync, promises as fs } from
|
|
2
|
-
import { join, relative, resolve, sep } from
|
|
3
|
-
import type { GeneratedRoute } from
|
|
1
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import type { GeneratedRoute } from '../types';
|
|
4
4
|
|
|
5
5
|
export class RouteScanner {
|
|
6
6
|
private routesDir: string;
|
|
7
7
|
private excludePatterns: string[];
|
|
8
8
|
private static readonly DEFAULT_EXCLUDE_PATTERNS = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
'*.test.ts',
|
|
10
|
+
'*.test.js',
|
|
11
|
+
'*.test.tsx',
|
|
12
|
+
'*.test.jsx',
|
|
13
|
+
'*.spec.ts',
|
|
14
|
+
'*.spec.js',
|
|
15
|
+
'*.spec.tsx',
|
|
16
|
+
'*.spec.jsx',
|
|
17
|
+
'*.tests.ts',
|
|
18
|
+
'*.tests.js',
|
|
19
|
+
'**/__tests__/**',
|
|
20
|
+
'*.interface.ts',
|
|
21
|
+
'*.type.ts',
|
|
22
|
+
'*.d.ts',
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
constructor(routesDir =
|
|
25
|
+
constructor(routesDir = './routes', excludePatterns?: string[]) {
|
|
26
26
|
// Always resolve from the current working directory (user's project)
|
|
27
27
|
this.routesDir = resolve(process.cwd(), routesDir);
|
|
28
|
-
this.excludePatterns =
|
|
29
|
-
excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
|
|
28
|
+
this.excludePatterns = excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
async scan(): Promise<GeneratedRoute[]> {
|
|
@@ -40,7 +39,7 @@ export class RouteScanner {
|
|
|
40
39
|
try {
|
|
41
40
|
await this.scanDirectory(this.routesDir, routes);
|
|
42
41
|
} catch (error) {
|
|
43
|
-
if ((error as any).code ===
|
|
42
|
+
if ((error as any).code === 'ENOENT') {
|
|
44
43
|
console.warn(` ✗ Routes directory not accessible: ${this.routesDir}`);
|
|
45
44
|
return [];
|
|
46
45
|
}
|
|
@@ -56,15 +55,16 @@ export class RouteScanner {
|
|
|
56
55
|
for (const pattern of this.excludePatterns) {
|
|
57
56
|
// Convert glob pattern to regex
|
|
58
57
|
const regexPattern = pattern
|
|
59
|
-
.replace(/\./g,
|
|
60
|
-
.replace(
|
|
61
|
-
.replace(
|
|
62
|
-
.replace(
|
|
58
|
+
.replace(/\./g, '\\.') // Escape dots
|
|
59
|
+
.replace(/\*\*/g, '__GLOBSTAR__') // protect ** before * replacement
|
|
60
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
61
|
+
.replace(/__GLOBSTAR__/g, '.*') // ** matches anything including /
|
|
62
|
+
.replace(/\?/g, '.'); // ? matches single character
|
|
63
63
|
|
|
64
64
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
65
65
|
|
|
66
66
|
// Check both the full relative path and just the filename
|
|
67
|
-
const filename = relativePath.split(sep).pop() ||
|
|
67
|
+
const filename = relativePath.split(sep).pop() || '';
|
|
68
68
|
if (regex.test(relativePath) || regex.test(filename)) {
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
@@ -73,11 +73,7 @@ export class RouteScanner {
|
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
private async scanDirectory(
|
|
77
|
-
dir: string,
|
|
78
|
-
routes: GeneratedRoute[],
|
|
79
|
-
basePath = ""
|
|
80
|
-
): Promise<void> {
|
|
76
|
+
private async scanDirectory(dir: string, routes: GeneratedRoute[], basePath = ''): Promise<void> {
|
|
81
77
|
const entries = await fs.readdir(dir);
|
|
82
78
|
|
|
83
79
|
for (const entry of entries) {
|
|
@@ -87,32 +83,30 @@ export class RouteScanner {
|
|
|
87
83
|
if (stats.isDirectory()) {
|
|
88
84
|
const newBasePath = basePath ? `${basePath}/${entry}` : entry;
|
|
89
85
|
await this.scanDirectory(fullPath, routes, newBasePath);
|
|
90
|
-
} else if (entry.endsWith(
|
|
86
|
+
} else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
|
|
91
87
|
// Skip excluded files (test files, etc.)
|
|
92
88
|
if (this.isExcluded(fullPath)) {
|
|
93
89
|
continue;
|
|
94
90
|
}
|
|
95
91
|
const routePath = relative(this.routesDir, fullPath)
|
|
96
|
-
.replace(/\.(ts|js)$/,
|
|
92
|
+
.replace(/\.(ts|js)$/, '')
|
|
97
93
|
.split(sep)
|
|
98
|
-
.join(
|
|
94
|
+
.join('/');
|
|
99
95
|
|
|
100
96
|
try {
|
|
101
97
|
// Convert Windows paths to URLs for import
|
|
102
98
|
const importPath =
|
|
103
|
-
process.platform ===
|
|
104
|
-
? `file:///${fullPath.replace(/\\/g, "/")}`
|
|
105
|
-
: fullPath;
|
|
99
|
+
process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
|
|
106
100
|
|
|
107
101
|
const module = await import(importPath);
|
|
108
102
|
|
|
109
|
-
if (module.default && typeof module.default ===
|
|
103
|
+
if (module.default && typeof module.default === 'function') {
|
|
110
104
|
routes.push({
|
|
111
|
-
name:
|
|
105
|
+
name: 'default',
|
|
112
106
|
path: fullPath,
|
|
113
|
-
method:
|
|
107
|
+
method: 'GET',
|
|
114
108
|
options: {
|
|
115
|
-
method:
|
|
109
|
+
method: 'GET',
|
|
116
110
|
path: `/${routePath}`,
|
|
117
111
|
expose: true,
|
|
118
112
|
},
|
|
@@ -120,15 +114,15 @@ export class RouteScanner {
|
|
|
120
114
|
}
|
|
121
115
|
|
|
122
116
|
for (const [name, value] of Object.entries(module)) {
|
|
123
|
-
if (name ===
|
|
117
|
+
if (name === 'default') continue;
|
|
124
118
|
|
|
125
119
|
// Check for new RouteDefinition format
|
|
126
120
|
if (
|
|
127
121
|
value &&
|
|
128
|
-
typeof value ===
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
typeof value === 'object' &&
|
|
123
|
+
'entry' in value &&
|
|
124
|
+
'options' in value &&
|
|
125
|
+
'handler' in value
|
|
132
126
|
) {
|
|
133
127
|
const routeDef = value as any;
|
|
134
128
|
routes.push({
|
|
@@ -161,7 +155,7 @@ export class RouteScanner {
|
|
|
161
155
|
}
|
|
162
156
|
|
|
163
157
|
enableWatch(callback: () => void) {
|
|
164
|
-
if (typeof Bun !==
|
|
158
|
+
if (typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'development') {
|
|
165
159
|
console.log(`Watching for route changes in ${this.routesDir}`);
|
|
166
160
|
|
|
167
161
|
setInterval(async () => {
|