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.
- 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 +76 -98
- package/dist/cli/index.js.map +1 -1
- package/dist/cli.js +134 -69
- 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 -21
- 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 +40 -20
- 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 +14 -9
- package/src/cache/manager.ts +23 -14
- package/src/cli/index.ts +83 -117
- package/src/core/config-loader.ts +19 -27
- package/src/core/router.ts +52 -18
- package/src/core/server.ts +43 -30
- 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/cli/index.ts
CHANGED
|
@@ -1,57 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { watch } from
|
|
4
|
-
import { parseArgs } from
|
|
5
|
-
import { getVectorInstance } from
|
|
6
|
-
import { ConfigLoader } from
|
|
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:
|
|
17
|
-
short:
|
|
18
|
-
default:
|
|
15
|
+
type: 'string',
|
|
16
|
+
short: 'p',
|
|
17
|
+
default: '3000',
|
|
19
18
|
},
|
|
20
19
|
host: {
|
|
21
|
-
type:
|
|
22
|
-
short:
|
|
23
|
-
default:
|
|
20
|
+
type: 'string',
|
|
21
|
+
short: 'h',
|
|
22
|
+
default: 'localhost',
|
|
24
23
|
},
|
|
25
24
|
routes: {
|
|
26
|
-
type:
|
|
27
|
-
short:
|
|
28
|
-
default:
|
|
25
|
+
type: 'string',
|
|
26
|
+
short: 'r',
|
|
27
|
+
default: './routes',
|
|
29
28
|
},
|
|
30
29
|
watch: {
|
|
31
|
-
type:
|
|
32
|
-
short:
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
short: 'w',
|
|
33
32
|
default: true,
|
|
34
33
|
},
|
|
35
34
|
cors: {
|
|
36
|
-
type:
|
|
35
|
+
type: 'boolean',
|
|
37
36
|
default: true,
|
|
38
37
|
},
|
|
39
38
|
config: {
|
|
40
|
-
type:
|
|
41
|
-
short:
|
|
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] ||
|
|
47
|
+
const command = positionals[0] || 'dev';
|
|
49
48
|
|
|
50
49
|
async function runDev() {
|
|
51
|
-
const isDev = command ===
|
|
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(
|
|
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:
|
|
89
|
-
allowMethods:
|
|
90
|
-
exposeHeaders:
|
|
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(
|
|
110
|
+
throw new Error('Server started but is not responding correctly');
|
|
116
111
|
}
|
|
117
112
|
|
|
118
|
-
const
|
|
119
|
-
const reset =
|
|
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(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
!filename.
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
238
|
-
const reset =
|
|
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
|
-
|
|
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(
|
|
265
|
-
const { RouteGenerator } = await import(
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
232
|
+
'bun',
|
|
233
|
+
'build',
|
|
234
|
+
'src/cli/index.ts',
|
|
235
|
+
'--target',
|
|
236
|
+
'bun',
|
|
237
|
+
'--outfile',
|
|
238
|
+
'dist/server.js',
|
|
239
|
+
'--minify',
|
|
284
240
|
]);
|
|
285
|
-
|
|
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(
|
|
289
|
-
spawnSync(
|
|
290
|
-
|
|
291
|
-
[
|
|
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:
|
|
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(
|
|
300
|
-
} catch (error) {
|
|
301
|
-
|
|
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
|
|
273
|
+
case 'dev':
|
|
308
274
|
await runDev();
|
|
309
275
|
break;
|
|
310
|
-
case
|
|
276
|
+
case 'build':
|
|
311
277
|
await runBuild();
|
|
312
278
|
break;
|
|
313
|
-
case
|
|
314
|
-
process.env.NODE_ENV =
|
|
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
|
|
2
|
-
import { resolve, isAbsolute } from
|
|
3
|
-
import { toFileUrl } from
|
|
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
|
|
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:
|
|
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 ||
|
|
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 =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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():
|
|
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 ||
|
|
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 ===
|
|
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:
|
|
89
|
-
allowMethods:
|
|
90
|
-
exposeHeaders:
|
|
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;
|
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
|
}
|