vector-framework 1.0.0 → 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 (104) hide show
  1. package/README.md +87 -634
  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 +5 -2
  5. package/dist/cache/manager.d.ts.map +1 -1
  6. package/dist/cache/manager.js +21 -12
  7. package/dist/cache/manager.js.map +1 -1
  8. package/dist/cli/index.js +60 -126
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/option-resolution.d.ts +4 -0
  11. package/dist/cli/option-resolution.d.ts.map +1 -0
  12. package/dist/cli/option-resolution.js +28 -0
  13. package/dist/cli/option-resolution.js.map +1 -0
  14. package/dist/cli.js +2774 -599
  15. package/dist/constants/index.d.ts +3 -0
  16. package/dist/constants/index.d.ts.map +1 -1
  17. package/dist/constants/index.js +6 -0
  18. package/dist/constants/index.js.map +1 -1
  19. package/dist/core/config-loader.d.ts +2 -2
  20. package/dist/core/config-loader.d.ts.map +1 -1
  21. package/dist/core/config-loader.js +18 -18
  22. package/dist/core/config-loader.js.map +1 -1
  23. package/dist/core/router.d.ts +41 -15
  24. package/dist/core/router.d.ts.map +1 -1
  25. package/dist/core/router.js +465 -150
  26. package/dist/core/router.js.map +1 -1
  27. package/dist/core/server.d.ts +17 -3
  28. package/dist/core/server.d.ts.map +1 -1
  29. package/dist/core/server.js +274 -33
  30. package/dist/core/server.js.map +1 -1
  31. package/dist/core/vector.d.ts +9 -8
  32. package/dist/core/vector.d.ts.map +1 -1
  33. package/dist/core/vector.js +40 -32
  34. package/dist/core/vector.js.map +1 -1
  35. package/dist/dev/route-generator.d.ts.map +1 -1
  36. package/dist/dev/route-generator.js.map +1 -1
  37. package/dist/dev/route-scanner.d.ts +1 -1
  38. package/dist/dev/route-scanner.d.ts.map +1 -1
  39. package/dist/dev/route-scanner.js +37 -43
  40. package/dist/dev/route-scanner.js.map +1 -1
  41. package/dist/http.d.ts +14 -14
  42. package/dist/http.d.ts.map +1 -1
  43. package/dist/http.js +84 -84
  44. package/dist/http.js.map +1 -1
  45. package/dist/index.d.ts +3 -3
  46. package/dist/index.js +1314 -8
  47. package/dist/index.mjs +1314 -8
  48. package/dist/middleware/manager.d.ts +1 -1
  49. package/dist/middleware/manager.d.ts.map +1 -1
  50. package/dist/middleware/manager.js +4 -0
  51. package/dist/middleware/manager.js.map +1 -1
  52. package/dist/openapi/docs-ui.d.ts +2 -0
  53. package/dist/openapi/docs-ui.d.ts.map +1 -0
  54. package/dist/openapi/docs-ui.js +1313 -0
  55. package/dist/openapi/docs-ui.js.map +1 -0
  56. package/dist/openapi/generator.d.ts +12 -0
  57. package/dist/openapi/generator.d.ts.map +1 -0
  58. package/dist/openapi/generator.js +273 -0
  59. package/dist/openapi/generator.js.map +1 -0
  60. package/dist/types/index.d.ts +70 -11
  61. package/dist/types/index.d.ts.map +1 -1
  62. package/dist/types/standard-schema.d.ts +118 -0
  63. package/dist/types/standard-schema.d.ts.map +1 -0
  64. package/dist/types/standard-schema.js +2 -0
  65. package/dist/types/standard-schema.js.map +1 -0
  66. package/dist/utils/cors.d.ts +13 -0
  67. package/dist/utils/cors.d.ts.map +1 -0
  68. package/dist/utils/cors.js +89 -0
  69. package/dist/utils/cors.js.map +1 -0
  70. package/dist/utils/path.d.ts +7 -0
  71. package/dist/utils/path.d.ts.map +1 -1
  72. package/dist/utils/path.js +14 -3
  73. package/dist/utils/path.js.map +1 -1
  74. package/dist/utils/schema-validation.d.ts +31 -0
  75. package/dist/utils/schema-validation.d.ts.map +1 -0
  76. package/dist/utils/schema-validation.js +77 -0
  77. package/dist/utils/schema-validation.js.map +1 -0
  78. package/dist/utils/validation.d.ts.map +1 -1
  79. package/dist/utils/validation.js +1 -0
  80. package/dist/utils/validation.js.map +1 -1
  81. package/package.json +24 -19
  82. package/src/auth/protected.ts +3 -13
  83. package/src/cache/manager.ts +25 -30
  84. package/src/cli/index.ts +62 -141
  85. package/src/cli/option-resolution.ts +40 -0
  86. package/src/constants/index.ts +7 -0
  87. package/src/core/config-loader.ts +20 -22
  88. package/src/core/router.ts +535 -155
  89. package/src/core/server.ts +354 -45
  90. package/src/core/vector.ts +71 -61
  91. package/src/dev/route-generator.ts +1 -3
  92. package/src/dev/route-scanner.ts +38 -51
  93. package/src/http.ts +117 -187
  94. package/src/index.ts +3 -3
  95. package/src/middleware/manager.ts +8 -11
  96. package/src/openapi/assets/tailwindcdn.js +83 -0
  97. package/src/openapi/docs-ui.ts +1317 -0
  98. package/src/openapi/generator.ts +359 -0
  99. package/src/types/index.ts +104 -17
  100. package/src/types/standard-schema.ts +147 -0
  101. package/src/utils/cors.ts +101 -0
  102. package/src/utils/path.ts +19 -4
  103. package/src/utils/schema-validation.ts +123 -0
  104. package/src/utils/validation.ts +1 -0
@@ -1,71 +1,380 @@
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 { 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';
9
+ import type { VectorRouter } from './router';
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
+ }
10
74
 
11
75
  export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
12
76
  private server: Server | null = null;
13
77
  private router: VectorRouter<TTypes>;
14
78
  private config: VectorConfig<TTypes>;
15
- private corsHandler: any;
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;
16
89
 
17
90
  constructor(router: VectorRouter<TTypes>, config: VectorConfig<TTypes>) {
18
91
  this.router = router;
19
92
  this.config = config;
93
+ this.openapiConfig = this.normalizeOpenAPIConfig(config.openapi, config.development);
20
94
 
21
95
  if (config.cors) {
22
- const { preflight, corsify } = cors(
23
- this.normalizeCorsOptions(config.cors)
24
- );
96
+ const opts = this.normalizeCorsOptions(config.cors);
97
+ const { preflight, corsify } = cors(opts);
25
98
  this.corsHandler = { preflight, corsify };
99
+
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> = {
105
+ 'access-control-allow-origin': opts.origin,
106
+ 'access-control-allow-methods': opts.allowMethods,
107
+ 'access-control-allow-headers': opts.allowHeaders,
108
+ 'access-control-expose-headers': opts.exposeHeaders,
109
+ 'access-control-max-age': String(opts.maxAge),
110
+ };
111
+ if (opts.credentials) {
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);
26
120
  }
27
121
  }
28
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
+ );
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
+ });
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
+ });
317
+ }
318
+
319
+ return null;
320
+ }
321
+
29
322
  private normalizeCorsOptions(options: CorsOptions): any {
30
323
  return {
31
- origin: options.origin || "*",
324
+ origin: options.origin || '*',
32
325
  credentials: options.credentials !== false,
33
326
  allowHeaders: Array.isArray(options.allowHeaders)
34
- ? options.allowHeaders.join(", ")
35
- : options.allowHeaders || "Content-Type, Authorization",
327
+ ? options.allowHeaders.join(', ')
328
+ : options.allowHeaders || 'Content-Type, Authorization',
36
329
  allowMethods: Array.isArray(options.allowMethods)
37
- ? options.allowMethods.join(", ")
38
- : options.allowMethods || "GET, POST, PUT, PATCH, DELETE, OPTIONS",
330
+ ? options.allowMethods.join(', ')
331
+ : options.allowMethods || 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
39
332
  exposeHeaders: Array.isArray(options.exposeHeaders)
40
- ? options.exposeHeaders.join(", ")
41
- : options.exposeHeaders || "Authorization",
333
+ ? options.exposeHeaders.join(', ')
334
+ : options.exposeHeaders || 'Authorization',
42
335
  maxAge: options.maxAge || 86400,
43
336
  };
44
337
  }
45
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
+
46
354
  async start(): Promise<Server> {
47
- const port = this.config.port || 3000;
48
- const hostname = this.config.hostname || "localhost";
355
+ const port = this.config.port ?? 3000;
356
+ const hostname = this.config.hostname || 'localhost';
357
+
358
+ this.validateReservedOpenAPIPaths();
49
359
 
50
- const fetch = async (request: Request): Promise<Response> => {
360
+ const fallbackFetch = async (request: Request): Promise<Response> => {
51
361
  try {
52
- // Handle CORS preflight
53
- if (this.corsHandler && request.method === "OPTIONS") {
362
+ // Handle CORS preflight for any path
363
+ if (this.corsHandler && request.method === 'OPTIONS') {
54
364
  return this.corsHandler.preflight(request);
55
365
  }
56
366
 
57
- // Try to handle the request with our router
58
- let response = await this.router.handle(request);
59
-
60
- // Apply CORS headers if configured
61
- if (this.corsHandler) {
62
- 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);
63
371
  }
64
372
 
65
- return response;
373
+ // No route matched — return 404
374
+ return this.applyCors(STATIC_RESPONSES.NOT_FOUND.clone() as unknown as Response, request);
66
375
  } catch (error) {
67
- console.error("Server error:", error);
68
- return new Response("Internal Server Error", { status: 500 });
376
+ console.error('Server error:', error);
377
+ return this.applyCors(new Response('Internal Server Error', { status: 500 }), request);
69
378
  }
70
379
  };
71
380
 
@@ -74,24 +383,21 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
74
383
  port,
75
384
  hostname,
76
385
  reusePort: this.config.reusePort !== false,
77
- fetch,
386
+ routes: this.router.getRouteTable(),
387
+ fetch: fallbackFetch,
78
388
  idleTimeout: this.config.idleTimeout || 60,
79
- error: (error) => {
80
- console.error("[ERROR] Server error:", error);
81
- return new Response("Internal Server Error", { status: 500 });
389
+ error: (error, request?: Request) => {
390
+ console.error('[ERROR] Server error:', error);
391
+ return this.applyCors(new Response('Internal Server Error', { status: 500 }), request);
82
392
  },
83
393
  });
84
394
 
85
- // Validate that the server actually started
86
395
  if (!this.server || !this.server.port) {
87
396
  throw new Error(`Failed to start server on ${hostname}:${port} - server object is invalid`);
88
397
  }
89
398
 
90
- // Server logs are handled by CLI
91
-
92
399
  return this.server;
93
400
  } catch (error: any) {
94
- // Enhance error message with context for common issues
95
401
  if (error.code === 'EADDRINUSE' || error.message?.includes('address already in use')) {
96
402
  error.message = `Port ${port} is already in use`;
97
403
  error.port = port;
@@ -111,7 +417,10 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
111
417
  if (this.server) {
112
418
  this.server.stop();
113
419
  this.server = null;
114
- console.log("Server stopped");
420
+ this.openapiDocCache = null;
421
+ this.openapiDocsHtmlCache = null;
422
+ this.openapiWarningsLogged = false;
423
+ console.log('Server stopped');
115
424
  }
116
425
  }
117
426
 
@@ -120,11 +429,11 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
120
429
  }
121
430
 
122
431
  getPort(): number {
123
- return this.server?.port || this.config.port || 3000;
432
+ return this.server?.port ?? this.config.port ?? 3000;
124
433
  }
125
434
 
126
435
  getHostname(): string {
127
- return this.server?.hostname || this.config.hostname || "localhost";
436
+ return this.server?.hostname || this.config.hostname || 'localhost';
128
437
  }
129
438
 
130
439
  getUrl(): string {
@@ -1,22 +1,30 @@
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 { AuthManager } from '../auth/protected';
3
+ import { CacheManager } from '../cache/manager';
4
+ import { RouteGenerator } from '../dev/route-generator';
5
+ import { RouteScanner } from '../dev/route-scanner';
6
+ import { MiddlewareManager } from '../middleware/manager';
7
+ 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
- } from "../types";
18
- import { VectorRouter } from "./router";
19
- import { VectorServer } from "./server";
19
+ } from '../types';
20
+ import { VectorRouter } from './router';
21
+ import { VectorServer } from './server';
22
+
23
+ interface LoadedRouteDefinition<TTypes extends VectorTypes = DefaultVectorTypes> {
24
+ entry: { method: string; path: string };
25
+ options: RouteOptions<TTypes>;
26
+ handler: RouteHandler<TTypes>;
27
+ }
20
28
 
21
29
  // Internal-only class - not exposed to users
22
30
  export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
@@ -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,16 +76,20 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
72
76
  }
73
77
 
74
78
  // Internal method to add route
75
- addRoute(
76
- options: RouteOptions<TTypes>,
77
- handler: RouteHandler<TTypes>
78
- ): RouteEntry {
79
- 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);
80
85
  }
81
86
 
82
87
  // Internal method to start server - only called by CLI
83
88
  async startServer(config?: VectorConfig<TTypes>): Promise<Server> {
84
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);
85
93
 
86
94
  // Clear previous middleware to avoid accumulation across multiple starts
87
95
  this.middlewareManager.clear();
@@ -110,7 +118,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
110
118
  }
111
119
 
112
120
  private async discoverRoutes() {
113
- const routesDir = this.config.routesDir || "./routes";
121
+ const routesDir = this.config.routesDir || './routes';
114
122
  const excludePatterns = this.config.routeExcludePatterns;
115
123
 
116
124
  // Always create a new RouteScanner with the current config's routesDir
@@ -134,78 +142,80 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
134
142
  const importPath = toFileUrl(route.path);
135
143
 
136
144
  const module = await import(importPath);
137
- const exported =
138
- route.name === "default" ? module.default : module[route.name];
145
+ const exported = route.name === 'default' ? module.default : module[route.name];
139
146
 
140
147
  if (exported) {
141
148
  if (this.isRouteDefinition(exported)) {
142
149
  // Use router.route() to ensure middleware is applied
143
- const routeDef = exported as any;
144
- this.router.route(routeDef.options, routeDef.handler);
145
- this.logRouteLoaded(routeDef.options);
150
+ this.router.route(exported.options, exported.handler);
151
+ this.logRouteLoaded(exported.options);
146
152
  } else if (this.isRouteEntry(exported)) {
147
153
  // Legacy support for direct RouteEntry (won't have middleware)
148
- this.router.addRoute(exported as RouteEntry);
149
- this.logRouteLoaded(exported as RouteEntry);
150
- } else if (typeof exported === "function") {
151
- this.router.route(route.options as any, exported);
152
- this.logRouteLoaded(route.options);
154
+ this.router.addRoute(exported);
155
+ this.logRouteLoaded(exported);
156
+ } else if (typeof exported === 'function') {
157
+ this.router.route(route.options as RouteOptions<TTypes>, exported as RouteHandler<TTypes>);
158
+ this.logRouteLoaded(route.options as RouteOptions<TTypes>);
153
159
  }
154
160
  }
155
161
  } catch (error) {
156
- console.error(
157
- `Failed to load route ${route.name} from ${route.path}:`,
158
- error
159
- );
162
+ console.error(`Failed to load route ${route.name} from ${route.path}:`, error);
160
163
  }
161
164
  }
162
-
163
- // Ensure routes are properly sorted after loading all
164
- this.router.sortRoutes();
165
165
  }
166
166
  } 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);
167
+ if ((error as any).code !== 'ENOENT' && (error as any).code !== 'ENOTDIR') {
168
+ console.error('Failed to discover routes:', error);
172
169
  }
173
170
  }
174
171
  }
175
172
 
176
173
  async loadRoute(routeModule: any) {
177
- if (typeof routeModule === "function") {
174
+ if (typeof routeModule === 'function') {
178
175
  const routeEntry = routeModule();
179
- if (Array.isArray(routeEntry)) {
180
- this.router.addRoute(routeEntry as RouteEntry);
176
+ if (this.isRouteEntry(routeEntry)) {
177
+ this.router.addRoute(routeEntry);
181
178
  }
182
- } else if (routeModule && typeof routeModule === "object") {
179
+ } else if (routeModule && typeof routeModule === 'object') {
183
180
  for (const [, value] of Object.entries(routeModule)) {
184
- if (typeof value === "function") {
185
- const routeEntry = (value as any)();
186
- if (Array.isArray(routeEntry)) {
187
- this.router.addRoute(routeEntry as RouteEntry);
181
+ if (typeof value === 'function') {
182
+ const routeEntry = value();
183
+ if (this.isRouteEntry(routeEntry)) {
184
+ this.router.addRoute(routeEntry);
188
185
  }
189
186
  }
190
187
  }
191
188
  }
192
189
  }
193
190
 
194
- private isRouteEntry(value: any): boolean {
195
- 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
+ );
196
205
  }
197
206
 
198
- private isRouteDefinition(value: any): boolean {
207
+ private isRouteDefinition(value: unknown): value is LoadedRouteDefinition<TTypes> {
199
208
  return (
200
- value &&
201
- typeof value === "object" &&
202
- "entry" in value &&
203
- "options" in value &&
204
- "handler" in value
209
+ value !== null &&
210
+ typeof value === 'object' &&
211
+ 'entry' in value &&
212
+ 'options' in value &&
213
+ 'handler' in value &&
214
+ typeof (value as LoadedRouteDefinition<TTypes>).handler === 'function'
205
215
  );
206
216
  }
207
217
 
208
- private logRouteLoaded(_: RouteEntry | RouteOptions): void {
218
+ private logRouteLoaded(_: RouteOptions<TTypes> | LegacyRouteEntry): void {
209
219
  // Silent - no logging
210
220
  }
211
221