vector-framework 0.9.9 → 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 +76 -98
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli.js +134 -69
  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 -21
  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 +40 -20
  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 +14 -9
  44. package/src/cache/manager.ts +23 -14
  45. package/src/cli/index.ts +83 -117
  46. package/src/core/config-loader.ts +19 -27
  47. package/src/core/router.ts +52 -18
  48. package/src/core/server.ts +43 -30
  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
package/src/cli/index.ts CHANGED
@@ -1,57 +1,53 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { watch } from "node:fs";
4
- import { parseArgs } from "node:util";
5
- import { getVectorInstance } from "../core/vector";
6
- import { ConfigLoader } from "../core/config-loader";
3
+ import { watch } from 'node:fs';
4
+ import { parseArgs } from 'node:util';
5
+ import { getVectorInstance } from '../core/vector';
6
+ import { ConfigLoader } from '../core/config-loader';
7
7
 
8
8
  // Compatibility layer for both Node and Bun
9
- const args =
10
- typeof Bun !== "undefined" ? Bun.argv.slice(2) : process.argv.slice(2);
9
+ const args = typeof Bun !== 'undefined' ? Bun.argv.slice(2) : process.argv.slice(2);
11
10
 
12
11
  const { values, positionals } = parseArgs({
13
12
  args,
14
13
  options: {
15
14
  port: {
16
- type: "string",
17
- short: "p",
18
- default: "3000",
15
+ type: 'string',
16
+ short: 'p',
17
+ default: '3000',
19
18
  },
20
19
  host: {
21
- type: "string",
22
- short: "h",
23
- default: "localhost",
20
+ type: 'string',
21
+ short: 'h',
22
+ default: 'localhost',
24
23
  },
25
24
  routes: {
26
- type: "string",
27
- short: "r",
28
- default: "./routes",
25
+ type: 'string',
26
+ short: 'r',
27
+ default: './routes',
29
28
  },
30
29
  watch: {
31
- type: "boolean",
32
- short: "w",
30
+ type: 'boolean',
31
+ short: 'w',
33
32
  default: true,
34
33
  },
35
34
  cors: {
36
- type: "boolean",
35
+ type: 'boolean',
37
36
  default: true,
38
37
  },
39
38
  config: {
40
- type: "string",
41
- short: "c",
39
+ type: 'string',
40
+ short: 'c',
42
41
  },
43
42
  },
44
43
  strict: true,
45
44
  allowPositionals: true,
46
45
  });
47
46
 
48
- const command = positionals[0] || "dev";
47
+ const command = positionals[0] || 'dev';
49
48
 
50
49
  async function runDev() {
51
- const isDev = command === "dev";
52
- console.log(
53
- `\n→ Starting Vector ${isDev ? "development" : "production"} server\n`
54
- );
50
+ const isDev = command === 'dev';
55
51
 
56
52
  let server: any = null;
57
53
  let vector: any = null;
@@ -60,7 +56,7 @@ async function runDev() {
60
56
  // Create a timeout promise that rejects after 10 seconds
61
57
  const timeoutPromise = new Promise<never>((_, reject) => {
62
58
  setTimeout(() => {
63
- reject(new Error("Server startup timed out (10s)"));
59
+ reject(new Error('Server startup timed out (10s)'));
64
60
  }, 10000);
65
61
  });
66
62
 
@@ -69,7 +65,6 @@ async function runDev() {
69
65
  // Load configuration using ConfigLoader
70
66
  const configLoader = new ConfigLoader(values.config as string | undefined);
71
67
  const config = await configLoader.load();
72
- const configSource = configLoader.getConfigSource();
73
68
 
74
69
  // Merge CLI options with loaded config
75
70
  // Only use CLI values if config doesn't have them
@@ -83,11 +78,11 @@ async function runDev() {
83
78
  // Only apply default CORS if config.cors is undefined (not set)
84
79
  if (config.cors === undefined && values.cors) {
85
80
  config.cors = {
86
- origin: "*",
81
+ origin: '*',
87
82
  credentials: true,
88
- allowHeaders: "Content-Type, Authorization",
89
- allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
90
- exposeHeaders: "Authorization",
83
+ allowHeaders: 'Content-Type, Authorization',
84
+ allowMethods: 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
85
+ exposeHeaders: 'Authorization',
91
86
  maxAge: 86400,
92
87
  };
93
88
  }
@@ -112,32 +107,13 @@ async function runDev() {
112
107
 
113
108
  // Verify the server is actually running
114
109
  if (!server || !server.port) {
115
- throw new Error("Server started but is not responding correctly");
110
+ throw new Error('Server started but is not responding correctly');
116
111
  }
117
112
 
118
- const gray = "\x1b[90m";
119
- const reset = "\x1b[0m";
120
- const cyan = "\x1b[36m";
121
- const green = "\x1b[32m";
113
+ const cyan = '\x1b[36m';
114
+ const reset = '\x1b[0m';
122
115
 
123
- console.log(
124
- ` ${gray}Config${reset} ${
125
- configSource === "user" ? "User config loaded" : "Using defaults"
126
- }`
127
- );
128
- console.log(` ${gray}Routes${reset} ${config.routesDir}`);
129
- if (isDev && values.watch) {
130
- console.log(` ${gray}Watching${reset} All project files`);
131
- }
132
- console.log(
133
- ` ${gray}CORS${reset} ${config.cors ? "Enabled" : "Disabled"}`
134
- );
135
- console.log(
136
- ` ${gray}Mode${reset} ${config.development ? "Development" : "Production"}\n`
137
- );
138
- console.log(
139
- ` ${green}Ready${reset} → ${cyan}http://${config.hostname}:${config.port}${reset}\n`
140
- );
116
+ console.log(`\nListening on ${cyan}http://${config.hostname}:${config.port}${reset}\n`);
141
117
 
142
118
  return { server, vector, config };
143
119
  })();
@@ -165,17 +141,16 @@ async function runDev() {
165
141
  const now = Date.now();
166
142
  if (isReloading || now - lastReloadTime < 1000) return;
167
143
 
144
+ const segments = filename ? filename.split(/[/\\]/) : [];
145
+ const excluded = segments.some((s) =>
146
+ ['node_modules', '.git', '.vector', 'dist'].includes(s)
147
+ );
168
148
  if (
169
149
  filename &&
170
- (filename.endsWith(".ts") ||
171
- filename.endsWith(".js") ||
172
- filename.endsWith(".json")) &&
173
- !filename.includes("node_modules") &&
174
- !filename.includes(".git") &&
175
- !filename.includes(".vector") && // Ignore generated files
176
- !filename.includes("dist") && // Ignore dist folder
177
- !filename.includes("bun.lockb") && // Ignore lock files
178
- !filename.endsWith(".generated.ts") // Ignore generated files
150
+ (filename.endsWith('.ts') || filename.endsWith('.js') || filename.endsWith('.json')) &&
151
+ !excluded &&
152
+ !filename.includes('bun.lockb') && // Ignore lock files
153
+ !filename.endsWith('.generated.ts') // Ignore generated files
179
154
  ) {
180
155
  // Track changed files
181
156
  changedFiles.add(filename);
@@ -202,24 +177,13 @@ async function runDev() {
202
177
  // Small delay to ensure file system operations complete
203
178
  await new Promise((resolve) => setTimeout(resolve, 100));
204
179
 
205
- // Clear module cache to ensure fresh imports
206
- // Note: Bun uses ESM and doesn't have require.cache
207
- // The Loader API will handle module reloading automatically
208
- if (typeof require !== 'undefined' && require.cache) {
209
- for (const key in require.cache) {
210
- if (!key.includes("node_modules")) {
211
- delete require.cache[key];
212
- }
213
- }
214
- }
215
-
216
180
  // Restart the server
217
181
  try {
218
182
  const result = await startServer();
219
183
  server = result.server;
220
184
  vector = result.vector;
221
185
  } catch (error: any) {
222
- console.error("\n[Reload Error]", error.message || error);
186
+ console.error('\n[Reload Error]', error.message || error);
223
187
  // Don't exit the process on reload failures, just continue watching
224
188
  } finally {
225
189
  // Reset flag immediately after reload completes
@@ -230,88 +194,90 @@ async function runDev() {
230
194
  }
231
195
  });
232
196
  } catch {
233
- console.warn(" ⚠️ File watching not available");
197
+ const yellow = '\x1b[33m';
198
+ const reset = '\x1b[0m';
199
+ console.warn(`${yellow}Warning: File watching not available${reset}`);
234
200
  }
235
201
  }
236
202
  } catch (error: any) {
237
- const red = "\x1b[31m";
238
- const reset = "\x1b[0m";
239
-
240
- console.error(`\n${red}[ERROR] Failed to start server${reset}\n`);
203
+ const red = '\x1b[31m';
204
+ const reset = '\x1b[0m';
241
205
 
242
- // Always show the error message and stack trace
243
- if (error.message) {
244
- console.error(`Message: ${error.message}`);
245
- }
206
+ console.error(`\n${red}Error: ${error.message || error}${reset}\n`);
246
207
 
247
- if (error.stack) {
248
- console.error(`\nStack trace:`);
208
+ if (error.stack && process.env.NODE_ENV === 'development') {
249
209
  console.error(error.stack);
250
- } else if (!error.message) {
251
- // If no message or stack, show the raw error
252
- console.error(`Raw error:`, error);
253
210
  }
254
211
 
255
- // Ensure we exit with error code
256
212
  process.exit(1);
257
213
  }
258
214
  }
259
215
 
260
216
  async function runBuild() {
261
- console.log("\n→ Building Vector application\n");
262
-
263
217
  try {
264
- const { RouteScanner } = await import("../dev/route-scanner");
265
- const { RouteGenerator } = await import("../dev/route-generator");
218
+ const { RouteScanner } = await import('../dev/route-scanner');
219
+ const { RouteGenerator } = await import('../dev/route-generator');
266
220
 
221
+ // Step 1: Scan and generate routes
267
222
  const scanner = new RouteScanner(values.routes as string);
268
223
  const generator = new RouteGenerator();
269
224
 
270
225
  const routes = await scanner.scan();
271
226
  await generator.generate(routes);
272
227
 
273
- console.log(` Generated ${routes.length} routes`);
274
-
275
- // Use spawn based on runtime
276
- if (typeof Bun !== "undefined") {
228
+ // Step 2: Build the application with Bun
229
+ if (typeof Bun !== 'undefined') {
230
+ // Build the CLI as an executable
277
231
  const buildProcess = Bun.spawn([
278
- "bun",
279
- "build",
280
- "src/index.ts",
281
- "--outdir",
282
- "dist",
283
- "--minify",
232
+ 'bun',
233
+ 'build',
234
+ 'src/cli/index.ts',
235
+ '--target',
236
+ 'bun',
237
+ '--outfile',
238
+ 'dist/server.js',
239
+ '--minify',
284
240
  ]);
285
- await buildProcess.exited;
241
+
242
+ const exitCode = await buildProcess.exited;
243
+ if (exitCode !== 0) {
244
+ throw new Error(`Build failed with exit code ${exitCode}`);
245
+ }
286
246
  } else {
287
247
  // For Node.js, use child_process
288
- const { spawnSync } = await import("child_process");
289
- spawnSync(
290
- "bun",
291
- ["build", "src/index.ts", "--outdir", "dist", "--minify"],
248
+ const { spawnSync } = await import('child_process');
249
+ const result = spawnSync(
250
+ 'bun',
251
+ ['build', 'src/cli/index.ts', '--target', 'bun', '--outfile', 'dist/server.js', '--minify'],
292
252
  {
293
- stdio: "inherit",
253
+ stdio: 'inherit',
294
254
  shell: true,
295
255
  }
296
256
  );
257
+
258
+ if (result.status !== 0) {
259
+ throw new Error(`Build failed with exit code ${result.status}`);
260
+ }
297
261
  }
298
262
 
299
- console.log("\n ✓ Build complete\n");
300
- } catch (error) {
301
- console.error("[ERROR] Build failed:", error);
263
+ console.log('\nBuild complete: dist/server.js\n');
264
+ } catch (error: any) {
265
+ const red = '\x1b[31m';
266
+ const reset = '\x1b[0m';
267
+ console.error(`\n${red}Error: ${error.message || error}${reset}\n`);
302
268
  process.exit(1);
303
269
  }
304
270
  }
305
271
 
306
272
  switch (command) {
307
- case "dev":
273
+ case 'dev':
308
274
  await runDev();
309
275
  break;
310
- case "build":
276
+ case 'build':
311
277
  await runBuild();
312
278
  break;
313
- case "start":
314
- process.env.NODE_ENV = "production";
279
+ case 'start':
280
+ process.env.NODE_ENV = 'production';
315
281
  await runDev();
316
282
  break;
317
283
  default:
@@ -1,6 +1,6 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve, isAbsolute } from "node:path";
3
- import { toFileUrl } from "../utils/path";
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve, isAbsolute } from 'node:path';
3
+ import { toFileUrl } from '../utils/path';
4
4
  import type {
5
5
  CacheHandler,
6
6
  CorsOptions,
@@ -9,48 +9,40 @@ import type {
9
9
  VectorConfig,
10
10
  VectorConfigSchema,
11
11
  VectorTypes,
12
- } from "../types";
12
+ } from '../types';
13
13
 
14
14
  export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
15
15
  private configPath: string;
16
16
  private config: VectorConfigSchema<TTypes> | null = null;
17
- private configSource: "user" | "default" = "default";
17
+ private configSource: 'user' | 'default' = 'default';
18
18
 
19
19
  constructor(configPath?: string) {
20
20
  // Use provided config path or default to vector.config.ts
21
- const path = configPath || "vector.config.ts";
21
+ const path = configPath || 'vector.config.ts';
22
22
 
23
23
  // Handle absolute vs relative paths
24
- this.configPath = isAbsolute(path)
25
- ? path
26
- : resolve(process.cwd(), path);
24
+ this.configPath = isAbsolute(path) ? path : resolve(process.cwd(), path);
27
25
  }
28
26
 
29
27
  async load(): Promise<VectorConfig<TTypes>> {
30
28
  // Check if config file exists before attempting to load
31
29
  if (existsSync(this.configPath)) {
32
30
  try {
33
- console.log(`→ Loading config from: ${this.configPath}`);
34
-
35
31
  // Use explicit file:// URL to ensure correct resolution
36
32
  const userConfigPath = toFileUrl(this.configPath);
37
33
  const userConfig = await import(userConfigPath);
38
34
  this.config = userConfig.default || userConfig;
39
- this.configSource = "user";
40
-
41
- console.log(" ✓ User config loaded successfully");
42
- } catch (error) {
35
+ this.configSource = 'user';
36
+ } catch (error: any) {
37
+ const msg = error instanceof Error ? error.message : String(error);
38
+ console.error(`[Vector] Failed to load config from ${this.configPath}: ${msg}`);
43
39
  console.error(
44
- ` ✗ Failed to load config from ${this.configPath}:`,
45
- error
40
+ '[Vector] Server is using default configuration. Fix your config file and restart.'
46
41
  );
47
- console.log(" → Using default configuration");
48
42
  this.config = {};
49
43
  }
50
44
  } else {
51
45
  // Config file doesn't exist, use defaults
52
- console.log(` → No config file found at: ${this.configPath}`);
53
- console.log(" → Using default configuration");
54
46
  this.config = {};
55
47
  }
56
48
 
@@ -58,7 +50,7 @@ export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
58
50
  return await this.buildLegacyConfig();
59
51
  }
60
52
 
61
- getConfigSource(): "user" | "default" {
53
+ getConfigSource(): 'user' | 'default' {
62
54
  return this.configSource;
63
55
  }
64
56
 
@@ -71,7 +63,7 @@ export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
71
63
  config.hostname = this.config.hostname;
72
64
  config.reusePort = this.config.reusePort;
73
65
  config.development = this.config.development;
74
- config.routesDir = this.config.routesDir || "./routes";
66
+ config.routesDir = this.config.routesDir || './routes';
75
67
  config.idleTimeout = this.config.idleTimeout;
76
68
  }
77
69
 
@@ -80,14 +72,14 @@ export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
80
72
 
81
73
  // CORS configuration
82
74
  if (this.config?.cors) {
83
- if (typeof this.config.cors === "boolean") {
75
+ if (typeof this.config.cors === 'boolean') {
84
76
  config.cors = this.config.cors
85
77
  ? {
86
- origin: "*",
78
+ origin: '*',
87
79
  credentials: true,
88
- allowHeaders: "Content-Type, Authorization",
89
- allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
90
- exposeHeaders: "Authorization",
80
+ allowHeaders: 'Content-Type, Authorization',
81
+ allowMethods: 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
82
+ exposeHeaders: 'Authorization',
91
83
  maxAge: 86400,
92
84
  }
93
85
  : undefined;
@@ -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
  }