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.
- package/README.md +87 -635
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +2 -7
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/index.js +17 -62
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/option-resolution.d.ts +4 -0
- package/dist/cli/option-resolution.d.ts.map +1 -0
- package/dist/cli/option-resolution.js +28 -0
- package/dist/cli/option-resolution.js.map +1 -0
- package/dist/cli.js +2721 -617
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +2 -0
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +41 -17
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +432 -153
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +14 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +250 -30
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +4 -3
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +21 -12
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-generator.d.ts.map +1 -1
- package/dist/dev/route-generator.js.map +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +1 -5
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +14 -14
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +34 -41
- package/dist/http.js.map +1 -1
- package/dist/index.js +1314 -8
- package/dist/index.mjs +1314 -8
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +4 -0
- package/dist/middleware/manager.js.map +1 -1
- package/dist/openapi/docs-ui.d.ts +2 -0
- package/dist/openapi/docs-ui.d.ts.map +1 -0
- package/dist/openapi/docs-ui.js +1313 -0
- package/dist/openapi/docs-ui.js.map +1 -0
- package/dist/openapi/generator.d.ts +12 -0
- package/dist/openapi/generator.d.ts.map +1 -0
- package/dist/openapi/generator.js +273 -0
- package/dist/openapi/generator.js.map +1 -0
- package/dist/types/index.d.ts +70 -11
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/standard-schema.d.ts +118 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/utils/cors.d.ts +13 -0
- package/dist/utils/cors.d.ts.map +1 -0
- package/dist/utils/cors.js +89 -0
- package/dist/utils/cors.js.map +1 -0
- package/dist/utils/path.d.ts +6 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +5 -0
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/schema-validation.d.ts +31 -0
- package/dist/utils/schema-validation.d.ts.map +1 -0
- package/dist/utils/schema-validation.js +77 -0
- package/dist/utils/schema-validation.js.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +1 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +13 -12
- package/src/auth/protected.ts +3 -13
- package/src/cache/manager.ts +4 -18
- package/src/cli/index.ts +19 -75
- package/src/cli/option-resolution.ts +40 -0
- package/src/constants/index.ts +7 -0
- package/src/core/config-loader.ts +3 -3
- package/src/core/router.ts +502 -156
- package/src/core/server.ts +327 -32
- package/src/core/vector.ts +49 -29
- package/src/dev/route-generator.ts +1 -3
- package/src/dev/route-scanner.ts +2 -9
- package/src/http.ts +85 -125
- package/src/middleware/manager.ts +4 -0
- package/src/openapi/assets/tailwindcdn.js +83 -0
- package/src/openapi/docs-ui.ts +1317 -0
- package/src/openapi/generator.ts +359 -0
- package/src/types/index.ts +104 -17
- package/src/types/standard-schema.ts +147 -0
- package/src/utils/cors.ts +101 -0
- package/src/utils/path.ts +6 -0
- package/src/utils/schema-validation.ts +123 -0
- package/src/utils/validation.ts +1 -0
package/src/core/server.ts
CHANGED
|
@@ -1,28 +1,107 @@
|
|
|
1
1
|
import type { Server } from 'bun';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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
|
|
11
|
-
private
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
355
|
+
const port = this.config.port ?? 3000;
|
|
58
356
|
const hostname = this.config.hostname || 'localhost';
|
|
59
357
|
|
|
60
|
-
|
|
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
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
432
|
+
return this.server?.port ?? this.config.port ?? 3000;
|
|
138
433
|
}
|
|
139
434
|
|
|
140
435
|
getHostname(): string {
|
package/src/core/vector.ts
CHANGED
|
@@ -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
|
|
76
|
-
|
|
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
|
-
|
|
140
|
-
this.
|
|
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
|
|
145
|
-
this.logRouteLoaded(exported
|
|
154
|
+
this.router.addRoute(exported);
|
|
155
|
+
this.logRouteLoaded(exported);
|
|
146
156
|
} else if (typeof exported === 'function') {
|
|
147
|
-
this.router.route(route.options as
|
|
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 (
|
|
170
|
-
this.router.addRoute(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 =
|
|
176
|
-
if (
|
|
177
|
-
this.router.addRoute(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:
|
|
185
|
-
|
|
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:
|
|
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(_:
|
|
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
|
}
|
package/src/dev/route-scanner.ts
CHANGED
|
@@ -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,
|