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.
- package/README.md +87 -634
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +5 -2
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +21 -12
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/index.js +60 -126
- 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 +2774 -599
- 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 +2 -2
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +18 -18
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +41 -15
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +465 -150
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +17 -3
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +274 -33
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +9 -8
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +40 -32
- 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 +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +37 -43
- 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 +84 -84
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1314 -8
- package/dist/index.mjs +1314 -8
- package/dist/middleware/manager.d.ts +1 -1
- 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 +7 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +14 -3
- 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 +24 -19
- package/src/auth/protected.ts +3 -13
- package/src/cache/manager.ts +25 -30
- package/src/cli/index.ts +62 -141
- package/src/cli/option-resolution.ts +40 -0
- package/src/constants/index.ts +7 -0
- package/src/core/config-loader.ts +20 -22
- package/src/core/router.ts +535 -155
- package/src/core/server.ts +354 -45
- package/src/core/vector.ts +71 -61
- package/src/dev/route-generator.ts +1 -3
- package/src/dev/route-scanner.ts +38 -51
- package/src/http.ts +117 -187
- package/src/index.ts +3 -3
- package/src/middleware/manager.ts +8 -11
- 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 +19 -4
- package/src/utils/schema-validation.ts +123 -0
- package/src/utils/validation.ts +1 -0
package/src/core/server.ts
CHANGED
|
@@ -1,71 +1,380 @@
|
|
|
1
|
-
import type { Server } from
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from
|
|
9
|
-
import type { VectorRouter } from
|
|
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
|
|
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
|
|
23
|
-
|
|
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 ||
|
|
327
|
+
? options.allowHeaders.join(', ')
|
|
328
|
+
: options.allowHeaders || 'Content-Type, Authorization',
|
|
36
329
|
allowMethods: Array.isArray(options.allowMethods)
|
|
37
|
-
? options.allowMethods.join(
|
|
38
|
-
: options.allowMethods ||
|
|
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 ||
|
|
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
|
|
48
|
-
const hostname = this.config.hostname ||
|
|
355
|
+
const port = this.config.port ?? 3000;
|
|
356
|
+
const hostname = this.config.hostname || 'localhost';
|
|
357
|
+
|
|
358
|
+
this.validateReservedOpenAPIPaths();
|
|
49
359
|
|
|
50
|
-
const
|
|
360
|
+
const fallbackFetch = async (request: Request): Promise<Response> => {
|
|
51
361
|
try {
|
|
52
|
-
// Handle CORS preflight
|
|
53
|
-
if (this.corsHandler && request.method ===
|
|
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
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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(
|
|
68
|
-
return new Response(
|
|
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
|
-
|
|
386
|
+
routes: this.router.getRouteTable(),
|
|
387
|
+
fetch: fallbackFetch,
|
|
78
388
|
idleTimeout: this.config.idleTimeout || 60,
|
|
79
|
-
error: (error) => {
|
|
80
|
-
console.error(
|
|
81
|
-
return new Response(
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
436
|
+
return this.server?.hostname || this.config.hostname || 'localhost';
|
|
128
437
|
}
|
|
129
438
|
|
|
130
439
|
getUrl(): string {
|
package/src/core/vector.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import type { Server } from
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
|
18
|
-
import { VectorRouter } from
|
|
19
|
-
import { VectorServer } from
|
|
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
|
-
):
|
|
79
|
-
|
|
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 ||
|
|
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
|
-
|
|
144
|
-
this.
|
|
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
|
|
149
|
-
this.logRouteLoaded(exported
|
|
150
|
-
} else if (typeof exported ===
|
|
151
|
-
this.router.route(route.options as
|
|
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
|
-
(
|
|
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 ===
|
|
174
|
+
if (typeof routeModule === 'function') {
|
|
178
175
|
const routeEntry = routeModule();
|
|
179
|
-
if (
|
|
180
|
-
this.router.addRoute(routeEntry
|
|
176
|
+
if (this.isRouteEntry(routeEntry)) {
|
|
177
|
+
this.router.addRoute(routeEntry);
|
|
181
178
|
}
|
|
182
|
-
} else if (routeModule && typeof routeModule ===
|
|
179
|
+
} else if (routeModule && typeof routeModule === 'object') {
|
|
183
180
|
for (const [, value] of Object.entries(routeModule)) {
|
|
184
|
-
if (typeof value ===
|
|
185
|
-
const routeEntry =
|
|
186
|
-
if (
|
|
187
|
-
this.router.addRoute(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:
|
|
195
|
-
|
|
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:
|
|
207
|
+
private isRouteDefinition(value: unknown): value is LoadedRouteDefinition<TTypes> {
|
|
199
208
|
return (
|
|
200
|
-
value &&
|
|
201
|
-
typeof value ===
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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(_:
|
|
218
|
+
private logRouteLoaded(_: RouteOptions<TTypes> | LegacyRouteEntry): void {
|
|
209
219
|
// Silent - no logging
|
|
210
220
|
}
|
|
211
221
|
|