vector-framework 1.1.1 → 1.2.0

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 (98) hide show
  1. package/README.md +87 -635
  2. package/dist/auth/protected.d.ts.map +1 -1
  3. package/dist/auth/protected.js.map +1 -1
  4. package/dist/cache/manager.d.ts.map +1 -1
  5. package/dist/cache/manager.js +2 -7
  6. package/dist/cache/manager.js.map +1 -1
  7. package/dist/cli/index.js +17 -62
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/option-resolution.d.ts +4 -0
  10. package/dist/cli/option-resolution.d.ts.map +1 -0
  11. package/dist/cli/option-resolution.js +28 -0
  12. package/dist/cli/option-resolution.js.map +1 -0
  13. package/dist/cli.js +2721 -617
  14. package/dist/constants/index.d.ts +3 -0
  15. package/dist/constants/index.d.ts.map +1 -1
  16. package/dist/constants/index.js +6 -0
  17. package/dist/constants/index.js.map +1 -1
  18. package/dist/core/config-loader.d.ts.map +1 -1
  19. package/dist/core/config-loader.js +2 -0
  20. package/dist/core/config-loader.js.map +1 -1
  21. package/dist/core/router.d.ts +41 -17
  22. package/dist/core/router.d.ts.map +1 -1
  23. package/dist/core/router.js +432 -153
  24. package/dist/core/router.js.map +1 -1
  25. package/dist/core/server.d.ts +14 -1
  26. package/dist/core/server.d.ts.map +1 -1
  27. package/dist/core/server.js +250 -30
  28. package/dist/core/server.js.map +1 -1
  29. package/dist/core/vector.d.ts +4 -3
  30. package/dist/core/vector.d.ts.map +1 -1
  31. package/dist/core/vector.js +21 -12
  32. package/dist/core/vector.js.map +1 -1
  33. package/dist/dev/route-generator.d.ts.map +1 -1
  34. package/dist/dev/route-generator.js.map +1 -1
  35. package/dist/dev/route-scanner.d.ts.map +1 -1
  36. package/dist/dev/route-scanner.js +1 -5
  37. package/dist/dev/route-scanner.js.map +1 -1
  38. package/dist/http.d.ts +14 -14
  39. package/dist/http.d.ts.map +1 -1
  40. package/dist/http.js +34 -41
  41. package/dist/http.js.map +1 -1
  42. package/dist/index.js +1314 -8
  43. package/dist/index.mjs +1314 -8
  44. package/dist/middleware/manager.d.ts.map +1 -1
  45. package/dist/middleware/manager.js +4 -0
  46. package/dist/middleware/manager.js.map +1 -1
  47. package/dist/openapi/docs-ui.d.ts +2 -0
  48. package/dist/openapi/docs-ui.d.ts.map +1 -0
  49. package/dist/openapi/docs-ui.js +1313 -0
  50. package/dist/openapi/docs-ui.js.map +1 -0
  51. package/dist/openapi/generator.d.ts +12 -0
  52. package/dist/openapi/generator.d.ts.map +1 -0
  53. package/dist/openapi/generator.js +273 -0
  54. package/dist/openapi/generator.js.map +1 -0
  55. package/dist/types/index.d.ts +70 -11
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/standard-schema.d.ts +118 -0
  58. package/dist/types/standard-schema.d.ts.map +1 -0
  59. package/dist/types/standard-schema.js +2 -0
  60. package/dist/types/standard-schema.js.map +1 -0
  61. package/dist/utils/cors.d.ts +13 -0
  62. package/dist/utils/cors.d.ts.map +1 -0
  63. package/dist/utils/cors.js +89 -0
  64. package/dist/utils/cors.js.map +1 -0
  65. package/dist/utils/path.d.ts +6 -0
  66. package/dist/utils/path.d.ts.map +1 -1
  67. package/dist/utils/path.js +5 -0
  68. package/dist/utils/path.js.map +1 -1
  69. package/dist/utils/schema-validation.d.ts +31 -0
  70. package/dist/utils/schema-validation.d.ts.map +1 -0
  71. package/dist/utils/schema-validation.js +77 -0
  72. package/dist/utils/schema-validation.js.map +1 -0
  73. package/dist/utils/validation.d.ts.map +1 -1
  74. package/dist/utils/validation.js +1 -0
  75. package/dist/utils/validation.js.map +1 -1
  76. package/package.json +13 -12
  77. package/src/auth/protected.ts +3 -13
  78. package/src/cache/manager.ts +4 -18
  79. package/src/cli/index.ts +19 -75
  80. package/src/cli/option-resolution.ts +40 -0
  81. package/src/constants/index.ts +7 -0
  82. package/src/core/config-loader.ts +3 -3
  83. package/src/core/router.ts +502 -156
  84. package/src/core/server.ts +327 -32
  85. package/src/core/vector.ts +49 -29
  86. package/src/dev/route-generator.ts +1 -3
  87. package/src/dev/route-scanner.ts +2 -9
  88. package/src/http.ts +85 -125
  89. package/src/middleware/manager.ts +4 -0
  90. package/src/openapi/assets/tailwindcdn.js +83 -0
  91. package/src/openapi/docs-ui.ts +1317 -0
  92. package/src/openapi/generator.ts +359 -0
  93. package/src/types/index.ts +104 -17
  94. package/src/types/standard-schema.ts +147 -0
  95. package/src/utils/cors.ts +101 -0
  96. package/src/utils/path.ts +6 -0
  97. package/src/utils/schema-validation.ts +123 -0
  98. package/src/utils/validation.ts +1 -0
@@ -1,28 +1,107 @@
1
1
  import type { Server } from 'bun';
2
- import { cors } from 'itty-router';
3
- import type { CorsOptions, DefaultVectorTypes, VectorConfig, VectorTypes } from '../types';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { STATIC_RESPONSES } from '../constants';
5
+ import { cors } from '../utils/cors';
6
+ import { renderOpenAPIDocsHtml } from '../openapi/docs-ui';
7
+ import { generateOpenAPIDocument } from '../openapi/generator';
8
+ import type { CorsOptions, DefaultVectorTypes, OpenAPIOptions, VectorConfig, VectorTypes } from '../types';
4
9
  import type { VectorRouter } from './router';
5
10
 
11
+ interface NormalizedOpenAPIConfig {
12
+ enabled: boolean;
13
+ path: string;
14
+ target: string;
15
+ docs: {
16
+ enabled: boolean;
17
+ path: string;
18
+ };
19
+ info?: {
20
+ title?: string;
21
+ version?: string;
22
+ description?: string;
23
+ };
24
+ }
25
+
26
+ const OPENAPI_TAILWIND_ASSET_PATH = '/_vector/openapi/tailwindcdn.js';
27
+ const OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES = [
28
+ // Source execution (src/core/server.ts -> src/openapi/assets/tailwindcdn.js)
29
+ '../openapi/assets/tailwindcdn.js',
30
+ // Bundled dist entrypoints (dist/index.mjs|dist/cli.js -> src/openapi/assets/tailwindcdn.js)
31
+ '../src/openapi/assets/tailwindcdn.js',
32
+ // Unbundled dist/core/server.js execution (dist/core -> src/openapi/assets/tailwindcdn.js)
33
+ '../../src/openapi/assets/tailwindcdn.js',
34
+ ] as const;
35
+ const OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES = [
36
+ 'src/openapi/assets/tailwindcdn.js',
37
+ 'openapi/assets/tailwindcdn.js',
38
+ 'dist/openapi/assets/tailwindcdn.js',
39
+ ] as const;
40
+ const OPENAPI_TAILWIND_ASSET_INLINE_FALLBACK = '/* OpenAPI docs runtime asset missing: tailwind disabled */';
41
+
42
+ function resolveOpenAPITailwindAssetFile(): ReturnType<typeof Bun.file> | null {
43
+ for (const relativePath of OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES) {
44
+ try {
45
+ const fileUrl = new URL(relativePath, import.meta.url);
46
+ if (existsSync(fileUrl)) {
47
+ return Bun.file(fileUrl);
48
+ }
49
+ } catch {
50
+ // Ignore resolution failures and try the next candidate.
51
+ }
52
+ }
53
+
54
+ const cwd = process.cwd();
55
+ for (const relativePath of OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES) {
56
+ const absolutePath = join(cwd, relativePath);
57
+ if (existsSync(absolutePath)) {
58
+ return Bun.file(absolutePath);
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ const OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPITailwindAssetFile();
66
+ const DOCS_HTML_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
67
+ const DOCS_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
68
+
69
+ interface OpenAPIDocsHtmlCacheEntry {
70
+ html: string;
71
+ gzip: Uint8Array;
72
+ etag: string;
73
+ }
74
+
6
75
  export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
7
76
  private server: Server | null = null;
8
77
  private router: VectorRouter<TTypes>;
9
78
  private config: VectorConfig<TTypes>;
10
- private corsHandler: any;
11
- private corsHeaders: Record<string, string> | null = null;
79
+ private openapiConfig: NormalizedOpenAPIConfig;
80
+ private openapiDocCache: Record<string, unknown> | null = null;
81
+ private openapiDocsHtmlCache: OpenAPIDocsHtmlCacheEntry | null = null;
82
+ private openapiWarningsLogged = false;
83
+ private openapiTailwindMissingLogged = false;
84
+ private corsHandler: {
85
+ preflight: (request: Request) => Response;
86
+ corsify: (response: Response, request: Request) => Response;
87
+ } | null = null;
88
+ private corsHeadersEntries: [string, string][] | null = null;
12
89
 
13
90
  constructor(router: VectorRouter<TTypes>, config: VectorConfig<TTypes>) {
14
91
  this.router = router;
15
92
  this.config = config;
93
+ this.openapiConfig = this.normalizeOpenAPIConfig(config.openapi, config.development);
16
94
 
17
95
  if (config.cors) {
18
96
  const opts = this.normalizeCorsOptions(config.cors);
19
97
  const { preflight, corsify } = cors(opts);
20
98
  this.corsHandler = { preflight, corsify };
21
99
 
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 = {
100
+ // Pre-build static CORS headers when origin does not require per-request reflection.
101
+ const canUseStaticCorsHeaders = typeof opts.origin === 'string' && (opts.origin !== '*' || !opts.credentials);
102
+
103
+ if (canUseStaticCorsHeaders) {
104
+ const corsHeaders: Record<string, string> = {
26
105
  'access-control-allow-origin': opts.origin,
27
106
  'access-control-allow-methods': opts.allowMethods,
28
107
  'access-control-allow-headers': opts.allowHeaders,
@@ -30,10 +109,214 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
30
109
  'access-control-max-age': String(opts.maxAge),
31
110
  };
32
111
  if (opts.credentials) {
33
- this.corsHeaders['access-control-allow-credentials'] = 'true';
112
+ corsHeaders['access-control-allow-credentials'] = 'true';
113
+ }
114
+ this.corsHeadersEntries = Object.entries(corsHeaders);
115
+ }
116
+
117
+ // Pass CORS behavior to router so matched routes also receive CORS headers.
118
+ this.router.setCorsHeaders(this.corsHeadersEntries);
119
+ this.router.setCorsHandler(this.corsHeadersEntries ? null : this.corsHandler.corsify);
120
+ }
121
+ }
122
+
123
+ private normalizeOpenAPIConfig(
124
+ openapi: OpenAPIOptions | boolean | undefined,
125
+ development: boolean | undefined
126
+ ): NormalizedOpenAPIConfig {
127
+ const isDev = development !== false && process.env.NODE_ENV !== 'production';
128
+ const defaultEnabled = isDev;
129
+
130
+ if (openapi === false) {
131
+ return {
132
+ enabled: false,
133
+ path: '/openapi.json',
134
+ target: 'openapi-3.0',
135
+ docs: { enabled: false, path: '/docs' },
136
+ };
137
+ }
138
+
139
+ if (openapi === true) {
140
+ return {
141
+ enabled: true,
142
+ path: '/openapi.json',
143
+ target: 'openapi-3.0',
144
+ docs: { enabled: false, path: '/docs' },
145
+ };
146
+ }
147
+
148
+ const openapiObject = openapi || {};
149
+ const docsValue = openapiObject.docs;
150
+ const docs =
151
+ typeof docsValue === 'boolean'
152
+ ? { enabled: docsValue, path: '/docs' }
153
+ : {
154
+ enabled: docsValue?.enabled === true,
155
+ path: docsValue?.path || '/docs',
156
+ };
157
+
158
+ return {
159
+ enabled: openapiObject.enabled ?? defaultEnabled,
160
+ path: openapiObject.path || '/openapi.json',
161
+ target: openapiObject.target || 'openapi-3.0',
162
+ docs,
163
+ info: openapiObject.info,
164
+ };
165
+ }
166
+
167
+ private isDocsReservedPath(path: string): boolean {
168
+ return (
169
+ path === this.openapiConfig.path || (this.openapiConfig.docs.enabled && path === this.openapiConfig.docs.path)
170
+ );
171
+ }
172
+
173
+ private getOpenAPIDocument(): Record<string, unknown> {
174
+ if (this.openapiDocCache) {
175
+ return this.openapiDocCache;
176
+ }
177
+
178
+ const routes = this.router.getRouteDefinitions().filter((route) => !this.isDocsReservedPath(route.path));
179
+
180
+ const result = generateOpenAPIDocument(routes as any, {
181
+ target: this.openapiConfig.target,
182
+ info: this.openapiConfig.info,
183
+ });
184
+
185
+ if (!this.openapiWarningsLogged && result.warnings.length > 0) {
186
+ for (const warning of result.warnings) {
187
+ console.warn(warning);
188
+ }
189
+ this.openapiWarningsLogged = true;
190
+ }
191
+
192
+ this.openapiDocCache = result.document;
193
+ return this.openapiDocCache;
194
+ }
195
+
196
+ private getOpenAPIDocsHtmlCacheEntry(): OpenAPIDocsHtmlCacheEntry {
197
+ if (this.openapiDocsHtmlCache) {
198
+ return this.openapiDocsHtmlCache;
199
+ }
200
+
201
+ const html = renderOpenAPIDocsHtml(this.getOpenAPIDocument(), this.openapiConfig.path, OPENAPI_TAILWIND_ASSET_PATH);
202
+ const gzip = Bun.gzipSync(html);
203
+ const etag = `"${Bun.hash(html).toString(16)}"`;
204
+
205
+ this.openapiDocsHtmlCache = { html, gzip, etag };
206
+ return this.openapiDocsHtmlCache;
207
+ }
208
+
209
+ private requestAcceptsGzip(request: Request): boolean {
210
+ const acceptEncoding = request.headers.get('accept-encoding');
211
+ return Boolean(acceptEncoding && /\bgzip\b/i.test(acceptEncoding));
212
+ }
213
+
214
+ private validateReservedOpenAPIPaths(): void {
215
+ if (!this.openapiConfig.enabled) {
216
+ return;
217
+ }
218
+
219
+ const reserved = new Set<string>([this.openapiConfig.path]);
220
+ if (this.openapiConfig.docs.enabled) {
221
+ reserved.add(this.openapiConfig.docs.path);
222
+ reserved.add(OPENAPI_TAILWIND_ASSET_PATH);
223
+ }
224
+
225
+ const methodConflicts = this.router
226
+ .getRouteDefinitions()
227
+ .filter((route) => reserved.has(route.path))
228
+ .map((route) => `${route.method} ${route.path}`);
229
+
230
+ const staticConflicts = Object.entries(this.router.getRouteTable())
231
+ .filter(([path, value]) => reserved.has(path) && value instanceof Response)
232
+ .map(([path]) => `STATIC ${path}`);
233
+
234
+ const conflicts = [...methodConflicts, ...staticConflicts];
235
+
236
+ if (conflicts.length > 0) {
237
+ throw new Error(
238
+ `OpenAPI reserved path conflict: ${conflicts.join(
239
+ ', '
240
+ )}. Change your route path(s) or reconfigure openapi.path/docs.path.`
241
+ );
242
+ }
243
+ }
244
+
245
+ private tryHandleOpenAPIRequest(request: Request): Response | null {
246
+ if (!this.openapiConfig.enabled || request.method !== 'GET') {
247
+ return null;
248
+ }
249
+
250
+ const pathname = new URL(request.url).pathname;
251
+ if (pathname === this.openapiConfig.path) {
252
+ return Response.json(this.getOpenAPIDocument());
253
+ }
254
+
255
+ if (this.openapiConfig.docs.enabled && pathname === this.openapiConfig.docs.path) {
256
+ const { html, gzip, etag } = this.getOpenAPIDocsHtmlCacheEntry();
257
+ if (request.headers.get('if-none-match') === etag) {
258
+ return new Response(null, {
259
+ status: 304,
260
+ headers: {
261
+ etag,
262
+ 'cache-control': DOCS_HTML_CACHE_CONTROL,
263
+ vary: 'accept-encoding',
264
+ },
265
+ });
266
+ }
267
+
268
+ if (this.requestAcceptsGzip(request)) {
269
+ return new Response(gzip, {
270
+ status: 200,
271
+ headers: {
272
+ 'content-type': 'text/html; charset=utf-8',
273
+ 'content-encoding': 'gzip',
274
+ etag,
275
+ 'cache-control': DOCS_HTML_CACHE_CONTROL,
276
+ vary: 'accept-encoding',
277
+ },
278
+ });
279
+ }
280
+
281
+ return new Response(html, {
282
+ status: 200,
283
+ headers: {
284
+ 'content-type': 'text/html; charset=utf-8',
285
+ etag,
286
+ 'cache-control': DOCS_HTML_CACHE_CONTROL,
287
+ vary: 'accept-encoding',
288
+ },
289
+ });
290
+ }
291
+
292
+ if (this.openapiConfig.docs.enabled && pathname === OPENAPI_TAILWIND_ASSET_PATH) {
293
+ if (!OPENAPI_TAILWIND_ASSET_FILE) {
294
+ if (!this.openapiTailwindMissingLogged) {
295
+ this.openapiTailwindMissingLogged = true;
296
+ console.warn(
297
+ '[OpenAPI] Missing docs runtime asset "tailwindcdn.js". Serving inline fallback script instead.'
298
+ );
34
299
  }
300
+
301
+ return new Response(OPENAPI_TAILWIND_ASSET_INLINE_FALLBACK, {
302
+ status: 200,
303
+ headers: {
304
+ 'content-type': 'application/javascript; charset=utf-8',
305
+ 'cache-control': DOCS_ASSET_CACHE_CONTROL,
306
+ },
307
+ });
35
308
  }
309
+
310
+ return new Response(OPENAPI_TAILWIND_ASSET_FILE, {
311
+ status: 200,
312
+ headers: {
313
+ 'content-type': 'application/javascript; charset=utf-8',
314
+ 'cache-control': DOCS_ASSET_CACHE_CONTROL,
315
+ },
316
+ });
36
317
  }
318
+
319
+ return null;
37
320
  }
38
321
 
39
322
  private normalizeCorsOptions(options: CorsOptions): any {
@@ -53,33 +336,45 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
53
336
  };
54
337
  }
55
338
 
339
+ private applyCors(response: Response, request?: Request): Response {
340
+ if (this.corsHeadersEntries) {
341
+ for (const [k, v] of this.corsHeadersEntries) {
342
+ response.headers.set(k, v);
343
+ }
344
+ return response;
345
+ }
346
+
347
+ if (this.corsHandler && request) {
348
+ return this.corsHandler.corsify(response, request);
349
+ }
350
+
351
+ return response;
352
+ }
353
+
56
354
  async start(): Promise<Server> {
57
- const port = this.config.port || 3000;
355
+ const port = this.config.port ?? 3000;
58
356
  const hostname = this.config.hostname || 'localhost';
59
357
 
60
- const fetch = async (request: Request): Promise<Response> => {
358
+ this.validateReservedOpenAPIPaths();
359
+
360
+ const fallbackFetch = async (request: Request): Promise<Response> => {
61
361
  try {
62
- // Handle CORS preflight
362
+ // Handle CORS preflight for any path
63
363
  if (this.corsHandler && request.method === 'OPTIONS') {
64
364
  return this.corsHandler.preflight(request);
65
365
  }
66
366
 
67
- // Try to handle the request with our router
68
- let response = await this.router.handle(request);
69
-
70
- // Apply CORS headers if configured
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) {
76
- response = this.corsHandler.corsify(response, request);
367
+ // Handle built-in docs endpoints for requests that fell through the Bun route table.
368
+ const openapiResponse = this.tryHandleOpenAPIRequest(request);
369
+ if (openapiResponse) {
370
+ return this.applyCors(openapiResponse, request);
77
371
  }
78
372
 
79
- return response;
373
+ // No route matched — return 404
374
+ return this.applyCors(STATIC_RESPONSES.NOT_FOUND.clone() as unknown as Response, request);
80
375
  } catch (error) {
81
376
  console.error('Server error:', error);
82
- return new Response('Internal Server Error', { status: 500 });
377
+ return this.applyCors(new Response('Internal Server Error', { status: 500 }), request);
83
378
  }
84
379
  };
85
380
 
@@ -88,24 +383,21 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
88
383
  port,
89
384
  hostname,
90
385
  reusePort: this.config.reusePort !== false,
91
- fetch,
386
+ routes: this.router.getRouteTable(),
387
+ fetch: fallbackFetch,
92
388
  idleTimeout: this.config.idleTimeout || 60,
93
- error: (error) => {
389
+ error: (error, request?: Request) => {
94
390
  console.error('[ERROR] Server error:', error);
95
- return new Response('Internal Server Error', { status: 500 });
391
+ return this.applyCors(new Response('Internal Server Error', { status: 500 }), request);
96
392
  },
97
393
  });
98
394
 
99
- // Validate that the server actually started
100
395
  if (!this.server || !this.server.port) {
101
396
  throw new Error(`Failed to start server on ${hostname}:${port} - server object is invalid`);
102
397
  }
103
398
 
104
- // Server logs are handled by CLI
105
-
106
399
  return this.server;
107
400
  } catch (error: any) {
108
- // Enhance error message with context for common issues
109
401
  if (error.code === 'EADDRINUSE' || error.message?.includes('address already in use')) {
110
402
  error.message = `Port ${port} is already in use`;
111
403
  error.port = port;
@@ -125,6 +417,9 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
125
417
  if (this.server) {
126
418
  this.server.stop();
127
419
  this.server = null;
420
+ this.openapiDocCache = null;
421
+ this.openapiDocsHtmlCache = null;
422
+ this.openapiWarningsLogged = false;
128
423
  console.log('Server stopped');
129
424
  }
130
425
  }
@@ -134,7 +429,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
134
429
  }
135
430
 
136
431
  getPort(): number {
137
- return this.server?.port || this.config.port || 3000;
432
+ return this.server?.port ?? this.config.port ?? 3000;
138
433
  }
139
434
 
140
435
  getHostname(): string {
@@ -1,5 +1,4 @@
1
1
  import type { Server } from 'bun';
2
- import type { RouteEntry } from 'itty-router';
3
2
  import { AuthManager } from '../auth/protected';
4
3
  import { CacheManager } from '../cache/manager';
5
4
  import { RouteGenerator } from '../dev/route-generator';
@@ -9,15 +8,24 @@ import { toFileUrl } from '../utils/path';
9
8
  import type {
10
9
  CacheHandler,
11
10
  DefaultVectorTypes,
11
+ InferRouteInputFromSchemaDefinition,
12
+ LegacyRouteEntry,
12
13
  ProtectedHandler,
13
14
  RouteHandler,
14
15
  RouteOptions,
16
+ RouteSchemaDefinition,
15
17
  VectorConfig,
16
18
  VectorTypes,
17
19
  } from '../types';
18
20
  import { VectorRouter } from './router';
19
21
  import { VectorServer } from './server';
20
22
 
23
+ interface LoadedRouteDefinition<TTypes extends VectorTypes = DefaultVectorTypes> {
24
+ entry: { method: string; path: string };
25
+ options: RouteOptions<TTypes>;
26
+ handler: RouteHandler<TTypes>;
27
+ }
28
+
21
29
  // Internal-only class - not exposed to users
22
30
  export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
23
31
  private static instance: Vector<any>;
@@ -36,11 +44,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
36
44
  this.middlewareManager = new MiddlewareManager<TTypes>();
37
45
  this.authManager = new AuthManager<TTypes>();
38
46
  this.cacheManager = new CacheManager<TTypes>();
39
- this.router = new VectorRouter<TTypes>(
40
- this.middlewareManager,
41
- this.authManager,
42
- this.cacheManager
43
- );
47
+ this.router = new VectorRouter<TTypes>(this.middlewareManager, this.authManager, this.cacheManager);
44
48
  }
45
49
 
46
50
  // Internal use only - not exposed to users
@@ -72,13 +76,20 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
72
76
  }
73
77
 
74
78
  // Internal method to add route
75
- addRoute(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
76
- return this.router.route(options, handler);
79
+ addRoute<TSchemaDef extends RouteSchemaDefinition | undefined>(
80
+ options: Omit<RouteOptions<TTypes>, 'schema'> & { schema?: TSchemaDef },
81
+ handler: RouteHandler<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>>
82
+ ): void;
83
+ addRoute(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): void {
84
+ this.router.route(options, handler);
77
85
  }
78
86
 
79
87
  // Internal method to start server - only called by CLI
80
88
  async startServer(config?: VectorConfig<TTypes>): Promise<Server> {
81
89
  this.config = { ...this.config, ...config };
90
+ const routeDefaults = { ...this.config.defaults?.route };
91
+ this.router.setRouteBooleanDefaults(routeDefaults);
92
+ this.router.setDevelopmentMode(this.config.development);
82
93
 
83
94
  // Clear previous middleware to avoid accumulation across multiple starts
84
95
  this.middlewareManager.clear();
@@ -136,25 +147,21 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
136
147
  if (exported) {
137
148
  if (this.isRouteDefinition(exported)) {
138
149
  // Use router.route() to ensure middleware is applied
139
- const routeDef = exported as any;
140
- this.router.route(routeDef.options, routeDef.handler);
141
- this.logRouteLoaded(routeDef.options);
150
+ this.router.route(exported.options, exported.handler);
151
+ this.logRouteLoaded(exported.options);
142
152
  } else if (this.isRouteEntry(exported)) {
143
153
  // Legacy support for direct RouteEntry (won't have middleware)
144
- this.router.addRoute(exported as RouteEntry);
145
- this.logRouteLoaded(exported as RouteEntry);
154
+ this.router.addRoute(exported);
155
+ this.logRouteLoaded(exported);
146
156
  } else if (typeof exported === 'function') {
147
- this.router.route(route.options as any, exported);
148
- this.logRouteLoaded(route.options);
157
+ this.router.route(route.options as RouteOptions<TTypes>, exported as RouteHandler<TTypes>);
158
+ this.logRouteLoaded(route.options as RouteOptions<TTypes>);
149
159
  }
150
160
  }
151
161
  } catch (error) {
152
162
  console.error(`Failed to load route ${route.name} from ${route.path}:`, error);
153
163
  }
154
164
  }
155
-
156
- // Ensure routes are properly sorted after loading all
157
- this.router.sortRoutes();
158
165
  }
159
166
  } catch (error) {
160
167
  if ((error as any).code !== 'ENOENT' && (error as any).code !== 'ENOTDIR') {
@@ -166,36 +173,49 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
166
173
  async loadRoute(routeModule: any) {
167
174
  if (typeof routeModule === 'function') {
168
175
  const routeEntry = routeModule();
169
- if (Array.isArray(routeEntry)) {
170
- this.router.addRoute(routeEntry as RouteEntry);
176
+ if (this.isRouteEntry(routeEntry)) {
177
+ this.router.addRoute(routeEntry);
171
178
  }
172
179
  } else if (routeModule && typeof routeModule === 'object') {
173
180
  for (const [, value] of Object.entries(routeModule)) {
174
181
  if (typeof value === 'function') {
175
- const routeEntry = (value as any)();
176
- if (Array.isArray(routeEntry)) {
177
- this.router.addRoute(routeEntry as RouteEntry);
182
+ const routeEntry = value();
183
+ if (this.isRouteEntry(routeEntry)) {
184
+ this.router.addRoute(routeEntry);
178
185
  }
179
186
  }
180
187
  }
181
188
  }
182
189
  }
183
190
 
184
- private isRouteEntry(value: any): boolean {
185
- return Array.isArray(value) && value.length >= 3;
191
+ private isRouteEntry(value: unknown): value is LegacyRouteEntry {
192
+ if (!Array.isArray(value) || value.length < 3) {
193
+ return false;
194
+ }
195
+
196
+ const [method, matcher, handlers, path] = value;
197
+ return (
198
+ typeof method === 'string' &&
199
+ matcher instanceof RegExp &&
200
+ Array.isArray(handlers) &&
201
+ handlers.length > 0 &&
202
+ handlers.every((handler) => typeof handler === 'function') &&
203
+ (path === undefined || typeof path === 'string')
204
+ );
186
205
  }
187
206
 
188
- private isRouteDefinition(value: any): boolean {
207
+ private isRouteDefinition(value: unknown): value is LoadedRouteDefinition<TTypes> {
189
208
  return (
190
- value &&
209
+ value !== null &&
191
210
  typeof value === 'object' &&
192
211
  'entry' in value &&
193
212
  'options' in value &&
194
- 'handler' in value
213
+ 'handler' in value &&
214
+ typeof (value as LoadedRouteDefinition<TTypes>).handler === 'function'
195
215
  );
196
216
  }
197
217
 
198
- private logRouteLoaded(_: RouteEntry | RouteOptions): void {
218
+ private logRouteLoaded(_: RouteOptions<TTypes> | LegacyRouteEntry): void {
199
219
  // Silent - no logging
200
220
  }
201
221
 
@@ -36,9 +36,7 @@ export class RouteGenerator {
36
36
 
37
37
  if (fileRoutes.some((r) => r.name === 'default')) {
38
38
  if (namedImports.length > 0) {
39
- imports.push(
40
- `import ${importName}, { ${namedImports.join(', ')} } from '${relativePath}';`
41
- );
39
+ imports.push(`import ${importName}, { ${namedImports.join(', ')} } from '${relativePath}';`);
42
40
  } else {
43
41
  imports.push(`import ${importName} from '${relativePath}';`);
44
42
  }
@@ -95,8 +95,7 @@ export class RouteScanner {
95
95
 
96
96
  try {
97
97
  // Convert Windows paths to URLs for import
98
- const importPath =
99
- process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
98
+ const importPath = process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
100
99
 
101
100
  const module = await import(importPath);
102
101
 
@@ -117,13 +116,7 @@ export class RouteScanner {
117
116
  if (name === 'default') continue;
118
117
 
119
118
  // Check for new RouteDefinition format
120
- if (
121
- value &&
122
- typeof value === 'object' &&
123
- 'entry' in value &&
124
- 'options' in value &&
125
- 'handler' in value
126
- ) {
119
+ if (value && typeof value === 'object' && 'entry' in value && 'options' in value && 'handler' in value) {
127
120
  const routeDef = value as any;
128
121
  routes.push({
129
122
  name,