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.
Files changed (54) hide show
  1. package/README.md +6 -5
  2. package/dist/cache/manager.d.ts +5 -2
  3. package/dist/cache/manager.d.ts.map +1 -1
  4. package/dist/cache/manager.js +21 -7
  5. package/dist/cache/manager.js.map +1 -1
  6. package/dist/cli/index.js +62 -83
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli.js +108 -37
  9. package/dist/core/config-loader.d.ts +2 -2
  10. package/dist/core/config-loader.d.ts.map +1 -1
  11. package/dist/core/config-loader.js +16 -18
  12. package/dist/core/config-loader.js.map +1 -1
  13. package/dist/core/router.d.ts +2 -0
  14. package/dist/core/router.d.ts.map +1 -1
  15. package/dist/core/router.js +52 -16
  16. package/dist/core/router.js.map +1 -1
  17. package/dist/core/server.d.ts +4 -3
  18. package/dist/core/server.d.ts.map +1 -1
  19. package/dist/core/server.js +39 -18
  20. package/dist/core/server.js.map +1 -1
  21. package/dist/core/vector.d.ts +7 -7
  22. package/dist/core/vector.d.ts.map +1 -1
  23. package/dist/core/vector.js +20 -21
  24. package/dist/core/vector.js.map +1 -1
  25. package/dist/dev/route-scanner.d.ts +1 -1
  26. package/dist/dev/route-scanner.d.ts.map +1 -1
  27. package/dist/dev/route-scanner.js +40 -42
  28. package/dist/dev/route-scanner.js.map +1 -1
  29. package/dist/http.d.ts +2 -2
  30. package/dist/http.d.ts.map +1 -1
  31. package/dist/http.js +70 -63
  32. package/dist/http.js.map +1 -1
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +4 -4
  35. package/dist/index.mjs +4 -4
  36. package/dist/middleware/manager.d.ts +1 -1
  37. package/dist/middleware/manager.d.ts.map +1 -1
  38. package/dist/middleware/manager.js.map +1 -1
  39. package/dist/utils/path.d.ts +1 -0
  40. package/dist/utils/path.d.ts.map +1 -1
  41. package/dist/utils/path.js +9 -3
  42. package/dist/utils/path.js.map +1 -1
  43. package/package.json +12 -8
  44. package/src/cache/manager.ts +23 -14
  45. package/src/cli/index.ts +66 -89
  46. package/src/core/config-loader.ts +18 -20
  47. package/src/core/router.ts +52 -18
  48. package/src/core/server.ts +42 -28
  49. package/src/core/vector.ts +25 -35
  50. package/src/dev/route-scanner.ts +41 -47
  51. package/src/http.ts +82 -112
  52. package/src/index.ts +3 -3
  53. package/src/middleware/manager.ts +4 -11
  54. package/src/utils/path.ts +13 -4
@@ -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 RegExp(
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
- // Parse cookies if not already parsed
160
- if (!request.cookies) {
161
- withCookies(request as any);
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
- const url = new URL(request.url);
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
  }
@@ -1,56 +1,66 @@
1
- import type { Server } from "bun";
2
- import { cors } from "itty-router";
3
- import type {
4
- CorsOptions,
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 { preflight, corsify } = cors(
23
- this.normalizeCorsOptions(config.cors)
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 || "Content-Type, Authorization",
44
+ ? options.allowHeaders.join(', ')
45
+ : options.allowHeaders || 'Content-Type, Authorization',
36
46
  allowMethods: Array.isArray(options.allowMethods)
37
- ? options.allowMethods.join(", ")
38
- : options.allowMethods || "GET, POST, PUT, PATCH, DELETE, OPTIONS",
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 || "Authorization",
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 || "localhost";
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 === "OPTIONS") {
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.corsHandler) {
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("Server error:", error);
68
- return new Response("Internal Server Error", { status: 500 });
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("[ERROR] Server error:", error);
81
- return new Response("Internal Server Error", { status: 500 });
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("Server stopped");
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 || "localhost";
141
+ return this.server?.hostname || this.config.hostname || 'localhost';
128
142
  }
129
143
 
130
144
  getUrl(): string {
@@ -1,11 +1,11 @@
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";
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 "../types";
18
- import { VectorRouter } from "./router";
19
- import { VectorServer } from "./server";
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 || "./routes";
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 === "function") {
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
- (error as any).code !== "ENOENT" &&
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 === "function") {
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 === "object") {
172
+ } else if (routeModule && typeof routeModule === 'object') {
183
173
  for (const [, value] of Object.entries(routeModule)) {
184
- if (typeof value === "function") {
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 === "object" &&
202
- "entry" in value &&
203
- "options" in value &&
204
- "handler" in value
191
+ typeof value === 'object' &&
192
+ 'entry' in value &&
193
+ 'options' in value &&
194
+ 'handler' in value
205
195
  );
206
196
  }
207
197
 
@@ -1,32 +1,31 @@
1
- import { existsSync, promises as fs } from "node:fs";
2
- import { join, relative, resolve, sep } from "node:path";
3
- import type { GeneratedRoute } from "../types";
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
- "*.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",
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 = "./routes", excludePatterns?: string[]) {
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 === "ENOENT") {
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, "\\.") // Escape dots
60
- .replace(/\*/g, "[^/]*") // * matches anything except /
61
- .replace(/\*\*/g, ".*") // ** matches anything including /
62
- .replace(/\?/g, "."); // ? matches single character
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(".ts") || entry.endsWith(".js")) {
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 === "win32"
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 === "function") {
103
+ if (module.default && typeof module.default === 'function') {
110
104
  routes.push({
111
- name: "default",
105
+ name: 'default',
112
106
  path: fullPath,
113
- method: "GET",
107
+ method: 'GET',
114
108
  options: {
115
- method: "GET",
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 === "default") continue;
117
+ if (name === 'default') continue;
124
118
 
125
119
  // Check for new RouteDefinition format
126
120
  if (
127
121
  value &&
128
- typeof value === "object" &&
129
- "entry" in value &&
130
- "options" in value &&
131
- "handler" in value
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 !== "undefined" && Bun.env.NODE_ENV === "development") {
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 () => {