vector-framework 1.2.0 → 1.2.2
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 +19 -0
- package/dist/auth/protected.d.ts +1 -0
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js +3 -0
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +1 -0
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +3 -0
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/graceful-shutdown.d.ts +15 -0
- package/dist/cli/graceful-shutdown.d.ts.map +1 -0
- package/dist/cli/graceful-shutdown.js +42 -0
- package/dist/cli/graceful-shutdown.js.map +1 -0
- package/dist/cli/index.js +37 -43
- package/dist/cli/index.js.map +1 -1
- package/dist/cli.js +967 -222
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +5 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/server.d.ts +4 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +240 -9
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +4 -2
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +32 -2
- package/dist/core/vector.js.map +1 -1
- package/dist/errors/index.cjs +2 -0
- package/dist/index.cjs +1434 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1327
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +153 -46
- package/dist/openapi/docs-ui.d.ts +1 -1
- package/dist/openapi/docs-ui.d.ts.map +1 -1
- package/dist/openapi/docs-ui.js +147 -35
- package/dist/openapi/docs-ui.js.map +1 -1
- package/dist/openapi/generator.d.ts.map +1 -1
- package/dist/openapi/generator.js +318 -6
- package/dist/openapi/generator.js.map +1 -1
- package/dist/start-vector.d.ts +3 -0
- package/dist/start-vector.d.ts.map +1 -0
- package/dist/start-vector.js +38 -0
- package/dist/start-vector.js.map +1 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/logger.js +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +2 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +10 -14
- package/src/auth/protected.ts +4 -0
- package/src/cache/manager.ts +4 -0
- package/src/cli/graceful-shutdown.ts +60 -0
- package/src/cli/index.ts +42 -49
- package/src/core/config-loader.ts +5 -2
- package/src/core/server.ts +304 -9
- package/src/core/vector.ts +38 -4
- package/src/index.ts +4 -3
- package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
- package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
- package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
- package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
- package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
- package/src/openapi/assets/favicon/favicon.ico +0 -0
- package/src/openapi/assets/favicon/site.webmanifest +11 -0
- package/src/openapi/assets/logo.svg +12 -0
- package/src/openapi/assets/logo_dark.svg +6 -0
- package/src/openapi/assets/logo_icon.png +0 -0
- package/src/openapi/assets/logo_white.svg +6 -0
- package/src/openapi/docs-ui.ts +153 -35
- package/src/openapi/generator.ts +341 -6
- package/src/start-vector.ts +50 -0
- package/src/types/index.ts +34 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/validation.ts +2 -0
package/dist/cli.js
CHANGED
|
@@ -5,29 +5,164 @@
|
|
|
5
5
|
import { watch } from "fs";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.protectedHandler = handler;
|
|
8
|
+
// src/cli/option-resolution.ts
|
|
9
|
+
function resolveRoutesDir(configRoutesDir, hasRoutesOption, cliRoutes) {
|
|
10
|
+
if (hasRoutesOption) {
|
|
11
|
+
return cliRoutes;
|
|
13
12
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
return configRoutesDir ?? cliRoutes;
|
|
14
|
+
}
|
|
15
|
+
function parseAndValidatePort(value) {
|
|
16
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
17
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
18
|
+
throw new Error(`Invalid port value: ${String(value)}`);
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
function resolvePort(configPort, hasPortOption, cliPort) {
|
|
23
|
+
if (hasPortOption) {
|
|
24
|
+
return parseAndValidatePort(cliPort);
|
|
25
|
+
}
|
|
26
|
+
const resolved = configPort ?? cliPort;
|
|
27
|
+
return parseAndValidatePort(resolved);
|
|
28
|
+
}
|
|
29
|
+
function resolveHost(configHost, hasHostOption, cliHost) {
|
|
30
|
+
const resolved = hasHostOption ? cliHost : configHost ?? cliHost;
|
|
31
|
+
if (typeof resolved !== "string" || resolved.length === 0) {
|
|
32
|
+
throw new Error(`Invalid host value: ${String(resolved)}`);
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/cli/graceful-shutdown.ts
|
|
38
|
+
function installGracefulShutdownHandlers(options) {
|
|
39
|
+
const on = options.on ?? ((event, listener) => process.on(event, listener));
|
|
40
|
+
const off = options.off ?? ((event, listener) => process.off(event, listener));
|
|
41
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
42
|
+
const logError = options.logError ?? console.error;
|
|
43
|
+
let shuttingDown = false;
|
|
44
|
+
const handleSignal = async (signal) => {
|
|
45
|
+
if (shuttingDown) {
|
|
46
|
+
return;
|
|
17
47
|
}
|
|
48
|
+
shuttingDown = true;
|
|
18
49
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
50
|
+
const target = options.getTarget();
|
|
51
|
+
if (target) {
|
|
52
|
+
if (typeof target.shutdown === "function") {
|
|
53
|
+
await target.shutdown();
|
|
54
|
+
} else if (typeof target.stop === "function") {
|
|
55
|
+
target.stop();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exit(0);
|
|
22
59
|
} catch (error) {
|
|
23
|
-
|
|
60
|
+
logError(`[vector] Graceful shutdown failed after ${signal}:`, error);
|
|
61
|
+
exit(1);
|
|
24
62
|
}
|
|
63
|
+
};
|
|
64
|
+
const onSigint = () => {
|
|
65
|
+
handleSignal("SIGINT");
|
|
66
|
+
};
|
|
67
|
+
const onSigterm = () => {
|
|
68
|
+
handleSignal("SIGTERM");
|
|
69
|
+
};
|
|
70
|
+
on("SIGINT", onSigint);
|
|
71
|
+
on("SIGTERM", onSigterm);
|
|
72
|
+
return () => {
|
|
73
|
+
off("SIGINT", onSigint);
|
|
74
|
+
off("SIGTERM", onSigterm);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/core/config-loader.ts
|
|
79
|
+
import { existsSync } from "fs";
|
|
80
|
+
import { resolve, isAbsolute } from "path";
|
|
81
|
+
|
|
82
|
+
// src/utils/path.ts
|
|
83
|
+
function toFileUrl(path) {
|
|
84
|
+
return process.platform === "win32" ? `file:///${path.replace(/\\/g, "/")}` : path;
|
|
85
|
+
}
|
|
86
|
+
function buildRouteRegex(path) {
|
|
87
|
+
return RegExp(`^${path.replace(/\/+(\/|$)/g, "$1").replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>[\\s\\S]+))").replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))").replace(/\./g, "\\.").replace(/(\/?)\*/g, "($1.*)?")}/*$`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/core/config-loader.ts
|
|
91
|
+
class ConfigLoader {
|
|
92
|
+
configPath;
|
|
93
|
+
config = null;
|
|
94
|
+
configSource = "default";
|
|
95
|
+
constructor(configPath) {
|
|
96
|
+
const path = configPath || "vector.config.ts";
|
|
97
|
+
this.configPath = isAbsolute(path) ? path : resolve(process.cwd(), path);
|
|
25
98
|
}
|
|
26
|
-
|
|
27
|
-
|
|
99
|
+
async load() {
|
|
100
|
+
if (existsSync(this.configPath)) {
|
|
101
|
+
try {
|
|
102
|
+
const userConfigPath = toFileUrl(this.configPath);
|
|
103
|
+
const userConfig = await import(userConfigPath);
|
|
104
|
+
this.config = userConfig.default || userConfig;
|
|
105
|
+
this.configSource = "user";
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
108
|
+
console.error(`[vector] Failed to load config from ${this.configPath}: ${msg}`);
|
|
109
|
+
console.error("[vector] Server is using default configuration. Fix your config file and restart.");
|
|
110
|
+
this.config = {};
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
this.config = {};
|
|
114
|
+
}
|
|
115
|
+
return await this.buildLegacyConfig();
|
|
28
116
|
}
|
|
29
|
-
|
|
30
|
-
return
|
|
117
|
+
getConfigSource() {
|
|
118
|
+
return this.configSource;
|
|
119
|
+
}
|
|
120
|
+
async buildLegacyConfig() {
|
|
121
|
+
const config = {};
|
|
122
|
+
if (this.config) {
|
|
123
|
+
config.port = this.config.port;
|
|
124
|
+
config.hostname = this.config.hostname;
|
|
125
|
+
config.reusePort = this.config.reusePort;
|
|
126
|
+
config.development = this.config.development;
|
|
127
|
+
config.routesDir = this.config.routesDir || "./routes";
|
|
128
|
+
config.routeExcludePatterns = this.config.routeExcludePatterns;
|
|
129
|
+
config.idleTimeout = this.config.idleTimeout;
|
|
130
|
+
config.defaults = this.config.defaults;
|
|
131
|
+
config.openapi = this.config.openapi;
|
|
132
|
+
config.startup = this.config.startup;
|
|
133
|
+
config.shutdown = this.config.shutdown;
|
|
134
|
+
}
|
|
135
|
+
config.autoDiscover = true;
|
|
136
|
+
if (this.config?.cors) {
|
|
137
|
+
if (typeof this.config.cors === "boolean") {
|
|
138
|
+
config.cors = this.config.cors ? {
|
|
139
|
+
origin: "*",
|
|
140
|
+
credentials: true,
|
|
141
|
+
allowHeaders: "Content-Type, Authorization",
|
|
142
|
+
allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
143
|
+
exposeHeaders: "Authorization",
|
|
144
|
+
maxAge: 86400
|
|
145
|
+
} : undefined;
|
|
146
|
+
} else {
|
|
147
|
+
config.cors = this.config.cors;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (this.config?.before) {
|
|
151
|
+
config.before = this.config.before;
|
|
152
|
+
}
|
|
153
|
+
if (this.config?.after) {
|
|
154
|
+
config.finally = this.config.after;
|
|
155
|
+
}
|
|
156
|
+
return config;
|
|
157
|
+
}
|
|
158
|
+
async loadAuthHandler() {
|
|
159
|
+
return this.config?.auth || null;
|
|
160
|
+
}
|
|
161
|
+
async loadCacheHandler() {
|
|
162
|
+
return this.config?.cache || null;
|
|
163
|
+
}
|
|
164
|
+
getConfig() {
|
|
165
|
+
return this.config;
|
|
31
166
|
}
|
|
32
167
|
}
|
|
33
168
|
|
|
@@ -113,6 +248,35 @@ var STATIC_RESPONSES = {
|
|
|
113
248
|
})
|
|
114
249
|
};
|
|
115
250
|
|
|
251
|
+
// src/auth/protected.ts
|
|
252
|
+
class AuthManager {
|
|
253
|
+
protectedHandler = null;
|
|
254
|
+
setProtectedHandler(handler) {
|
|
255
|
+
this.protectedHandler = handler;
|
|
256
|
+
}
|
|
257
|
+
clearProtectedHandler() {
|
|
258
|
+
this.protectedHandler = null;
|
|
259
|
+
}
|
|
260
|
+
async authenticate(request) {
|
|
261
|
+
if (!this.protectedHandler) {
|
|
262
|
+
throw new Error("Protected handler not configured. Use vector.protected() to set authentication handler.");
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const authUser = await this.protectedHandler(request);
|
|
266
|
+
request.authUser = authUser;
|
|
267
|
+
return authUser;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new Error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
isAuthenticated(request) {
|
|
273
|
+
return !!request.authUser;
|
|
274
|
+
}
|
|
275
|
+
getUser(request) {
|
|
276
|
+
return request.authUser || null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
116
280
|
// src/cache/manager.ts
|
|
117
281
|
class CacheManager {
|
|
118
282
|
cacheHandler = null;
|
|
@@ -122,6 +286,9 @@ class CacheManager {
|
|
|
122
286
|
setCacheHandler(handler) {
|
|
123
287
|
this.cacheHandler = handler;
|
|
124
288
|
}
|
|
289
|
+
clearCacheHandler() {
|
|
290
|
+
this.cacheHandler = null;
|
|
291
|
+
}
|
|
125
292
|
async get(key, factory, ttl = DEFAULT_CONFIG.CACHE_TTL) {
|
|
126
293
|
if (ttl <= 0) {
|
|
127
294
|
return factory();
|
|
@@ -294,8 +461,8 @@ ${routeEntries.join(`,
|
|
|
294
461
|
}
|
|
295
462
|
|
|
296
463
|
// src/dev/route-scanner.ts
|
|
297
|
-
import { existsSync, promises as fs2 } from "fs";
|
|
298
|
-
import { join, relative as relative2, resolve, sep } from "path";
|
|
464
|
+
import { existsSync as existsSync2, promises as fs2 } from "fs";
|
|
465
|
+
import { join, relative as relative2, resolve as resolve2, sep } from "path";
|
|
299
466
|
|
|
300
467
|
class RouteScanner {
|
|
301
468
|
routesDir;
|
|
@@ -317,12 +484,12 @@ class RouteScanner {
|
|
|
317
484
|
"*.d.ts"
|
|
318
485
|
];
|
|
319
486
|
constructor(routesDir = "./routes", excludePatterns) {
|
|
320
|
-
this.routesDir =
|
|
487
|
+
this.routesDir = resolve2(process.cwd(), routesDir);
|
|
321
488
|
this.excludePatterns = excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
|
|
322
489
|
}
|
|
323
490
|
async scan() {
|
|
324
491
|
const routes = [];
|
|
325
|
-
if (!
|
|
492
|
+
if (!existsSync2(this.routesDir)) {
|
|
326
493
|
return [];
|
|
327
494
|
}
|
|
328
495
|
try {
|
|
@@ -465,14 +632,6 @@ class MiddlewareManager {
|
|
|
465
632
|
}
|
|
466
633
|
}
|
|
467
634
|
|
|
468
|
-
// src/utils/path.ts
|
|
469
|
-
function toFileUrl(path) {
|
|
470
|
-
return process.platform === "win32" ? `file:///${path.replace(/\\/g, "/")}` : path;
|
|
471
|
-
}
|
|
472
|
-
function buildRouteRegex(path) {
|
|
473
|
-
return RegExp(`^${path.replace(/\/+(\/|$)/g, "$1").replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>[\\s\\S]+))").replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))").replace(/\./g, "\\.").replace(/(\/?)\*/g, "($1.*)?")}/*$`);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
635
|
// src/http.ts
|
|
477
636
|
function stringifyData(data) {
|
|
478
637
|
const val = data ?? null;
|
|
@@ -1143,7 +1302,7 @@ class VectorRouter {
|
|
|
1143
1302
|
}
|
|
1144
1303
|
|
|
1145
1304
|
// src/core/server.ts
|
|
1146
|
-
import { existsSync as
|
|
1305
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1147
1306
|
import { join as join2 } from "path";
|
|
1148
1307
|
|
|
1149
1308
|
// src/utils/cors.ts
|
|
@@ -1231,16 +1390,26 @@ function cors(config) {
|
|
|
1231
1390
|
}
|
|
1232
1391
|
|
|
1233
1392
|
// src/openapi/docs-ui.ts
|
|
1234
|
-
function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
1393
|
+
function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPath, logoWhitePath, appleTouchIconPath, favicon32Path, favicon16Path, webManifestPath) {
|
|
1235
1394
|
const specJson = JSON.stringify(spec).replace(/<\/script/gi, "<\\/script");
|
|
1236
1395
|
const openapiPathJson = JSON.stringify(openapiPath);
|
|
1237
1396
|
const tailwindScriptPathJson = JSON.stringify(tailwindScriptPath);
|
|
1397
|
+
const logoDarkPathJson = JSON.stringify(logoDarkPath);
|
|
1398
|
+
const logoWhitePathJson = JSON.stringify(logoWhitePath);
|
|
1399
|
+
const appleTouchIconPathJson = JSON.stringify(appleTouchIconPath);
|
|
1400
|
+
const favicon32PathJson = JSON.stringify(favicon32Path);
|
|
1401
|
+
const favicon16PathJson = JSON.stringify(favicon16Path);
|
|
1402
|
+
const webManifestPathJson = JSON.stringify(webManifestPath);
|
|
1238
1403
|
return `<!DOCTYPE html>
|
|
1239
1404
|
<html lang="en">
|
|
1240
1405
|
<head>
|
|
1241
1406
|
<meta charset="UTF-8">
|
|
1242
1407
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1243
1408
|
<title>Vector API Documentation</title>
|
|
1409
|
+
<link rel="apple-touch-icon" sizes="180x180" href=${appleTouchIconPathJson}>
|
|
1410
|
+
<link rel="icon" type="image/png" sizes="32x32" href=${favicon32PathJson}>
|
|
1411
|
+
<link rel="icon" type="image/png" sizes="16x16" href=${favicon16PathJson}>
|
|
1412
|
+
<link rel="manifest" href=${webManifestPathJson}>
|
|
1244
1413
|
<script>
|
|
1245
1414
|
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
1246
1415
|
document.documentElement.classList.add('dark');
|
|
@@ -1256,7 +1425,12 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1256
1425
|
theme: {
|
|
1257
1426
|
extend: {
|
|
1258
1427
|
colors: {
|
|
1259
|
-
brand:
|
|
1428
|
+
brand: {
|
|
1429
|
+
DEFAULT: '#00A1FF',
|
|
1430
|
+
mint: '#00FF8F',
|
|
1431
|
+
soft: '#E4F5FF',
|
|
1432
|
+
deep: '#007BC5',
|
|
1433
|
+
},
|
|
1260
1434
|
dark: { bg: '#0A0A0A', surface: '#111111', border: '#1F1F1F', text: '#EDEDED' },
|
|
1261
1435
|
light: { bg: '#FFFFFF', surface: '#F9F9F9', border: '#E5E5E5', text: '#111111' }
|
|
1262
1436
|
},
|
|
@@ -1323,32 +1497,44 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1323
1497
|
from { opacity: 0; transform: translateX(-6px); }
|
|
1324
1498
|
to { opacity: 1; transform: translateX(0); }
|
|
1325
1499
|
}
|
|
1500
|
+
@keyframes spin {
|
|
1501
|
+
to { transform: rotate(360deg); }
|
|
1502
|
+
}
|
|
1503
|
+
.button-spinner {
|
|
1504
|
+
display: inline-block;
|
|
1505
|
+
width: 0.875rem;
|
|
1506
|
+
height: 0.875rem;
|
|
1507
|
+
border: 2px solid currentColor;
|
|
1508
|
+
border-right-color: transparent;
|
|
1509
|
+
border-radius: 9999px;
|
|
1510
|
+
animation: spin 700ms linear infinite;
|
|
1511
|
+
}
|
|
1326
1512
|
@media (prefers-reduced-motion: reduce) {
|
|
1327
1513
|
*, *::before, *::after {
|
|
1328
1514
|
animation: none !important;
|
|
1329
1515
|
transition: none !important;
|
|
1330
1516
|
}
|
|
1331
1517
|
}
|
|
1332
|
-
.json-key { color: #
|
|
1333
|
-
.json-string { color: #
|
|
1334
|
-
.json-number { color: #
|
|
1335
|
-
.json-boolean { color: #
|
|
1336
|
-
.json-null { color: #
|
|
1337
|
-
.dark .json-key { color: #
|
|
1338
|
-
.dark .json-string { color: #
|
|
1339
|
-
.dark .json-number { color: #
|
|
1340
|
-
.dark .json-boolean { color: #
|
|
1341
|
-
.dark .json-null { color: #
|
|
1518
|
+
.json-key { color: #007bc5; }
|
|
1519
|
+
.json-string { color: #334155; }
|
|
1520
|
+
.json-number { color: #00a1ff; }
|
|
1521
|
+
.json-boolean { color: #475569; }
|
|
1522
|
+
.json-null { color: #64748b; }
|
|
1523
|
+
.dark .json-key { color: #7dc9ff; }
|
|
1524
|
+
.dark .json-string { color: #d1d9e6; }
|
|
1525
|
+
.dark .json-number { color: #7dc9ff; }
|
|
1526
|
+
.dark .json-boolean { color: #93a4bf; }
|
|
1527
|
+
.dark .json-null { color: #7c8ba3; }
|
|
1342
1528
|
</style>
|
|
1343
1529
|
</head>
|
|
1344
1530
|
<body class="bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans antialiased flex h-screen overflow-hidden">
|
|
1345
1531
|
<div id="mobile-backdrop" class="fixed inset-0 z-30 bg-black/40 opacity-0 pointer-events-none transition-opacity duration-300 md:hidden"></div>
|
|
1346
1532
|
<aside id="docs-sidebar" class="fixed inset-y-0 left-0 z-40 w-72 md:w-64 border-r border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface flex flex-col flex-shrink-0 transition-transform duration-300 ease-out -translate-x-full md:translate-x-0 md:static md:z-auto transition-colors duration-150">
|
|
1347
1533
|
<div class="h-14 flex items-center px-5 border-b border-light-border dark:border-dark-border">
|
|
1348
|
-
<
|
|
1349
|
-
<
|
|
1350
|
-
|
|
1351
|
-
|
|
1534
|
+
<div class="flex items-center">
|
|
1535
|
+
<img src=${logoDarkPathJson} alt="Vector" class="h-6 w-auto block dark:hidden" />
|
|
1536
|
+
<img src=${logoWhitePathJson} alt="Vector" class="h-6 w-auto hidden dark:block" />
|
|
1537
|
+
</div>
|
|
1352
1538
|
<button id="sidebar-close" class="ml-auto p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 transition md:hidden" aria-label="Close Menu" title="Close Menu">
|
|
1353
1539
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1354
1540
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
@@ -1379,8 +1565,8 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1379
1565
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
1380
1566
|
</svg>
|
|
1381
1567
|
</button>
|
|
1382
|
-
<
|
|
1383
|
-
<
|
|
1568
|
+
<img src=${logoDarkPathJson} alt="Vector" class="h-5 w-auto block dark:hidden" />
|
|
1569
|
+
<img src=${logoWhitePathJson} alt="Vector" class="h-5 w-auto hidden dark:block" />
|
|
1384
1570
|
</div>
|
|
1385
1571
|
<div class="flex-1"></div>
|
|
1386
1572
|
<button id="theme-toggle" class="p-2 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors" aria-label="Toggle Dark Mode">
|
|
@@ -1405,17 +1591,22 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1405
1591
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
|
|
1406
1592
|
<div class="lg:col-span-5 space-y-8" id="params-column"></div>
|
|
1407
1593
|
<div class="lg:col-span-7">
|
|
1408
|
-
<div class="rounded-lg border border-light-border dark:border-
|
|
1409
|
-
<div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-
|
|
1410
|
-
<span class="text-xs font-mono text-light-text/70 dark:text-
|
|
1411
|
-
<button class="text-xs text-light-text/50 hover:text-light-text dark:text-
|
|
1594
|
+
<div class="rounded-lg border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg overflow-hidden group">
|
|
1595
|
+
<div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
|
|
1596
|
+
<span class="text-xs font-mono text-light-text/70 dark:text-dark-text/70">cURL</span>
|
|
1597
|
+
<button class="text-xs text-light-text/50 hover:text-light-text dark:text-dark-text/50 dark:hover:text-dark-text transition-colors" id="copy-curl">Copy</button>
|
|
1412
1598
|
</div>
|
|
1413
|
-
<pre class="p-4 text-sm font-mono text-light-text dark:text-
|
|
1599
|
+
<pre class="p-4 text-sm font-mono text-light-text dark:text-dark-text overflow-x-auto leading-relaxed"><code id="curl-code"></code></pre>
|
|
1414
1600
|
</div>
|
|
1415
1601
|
<div class="mt-4 p-4 rounded-lg border border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
|
|
1416
1602
|
<div class="flex items-center justify-between mb-3">
|
|
1417
1603
|
<h4 class="text-sm font-medium">Try it out</h4>
|
|
1418
|
-
<button id="send-btn" class="px-4 py-1.5 bg-
|
|
1604
|
+
<button id="send-btn" class="px-4 py-1.5 bg-brand text-white text-sm font-semibold rounded hover:bg-brand-deep transition-colors">
|
|
1605
|
+
<span class="inline-flex items-center gap-2">
|
|
1606
|
+
<span id="send-btn-spinner" class="button-spinner hidden" aria-hidden="true"></span>
|
|
1607
|
+
<span id="send-btn-label">Submit</span>
|
|
1608
|
+
</span>
|
|
1609
|
+
</button>
|
|
1419
1610
|
</div>
|
|
1420
1611
|
<div class="space-y-4">
|
|
1421
1612
|
<div>
|
|
@@ -1449,7 +1640,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1449
1640
|
</div>
|
|
1450
1641
|
</div>
|
|
1451
1642
|
|
|
1452
|
-
<div>
|
|
1643
|
+
<div id="response-section">
|
|
1453
1644
|
<div class="flex items-center justify-between mb-2">
|
|
1454
1645
|
<p class="text-xs font-semibold uppercase tracking-wider opacity-60">Response</p>
|
|
1455
1646
|
</div>
|
|
@@ -1476,7 +1667,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1476
1667
|
<div class="flex items-center justify-between mb-3">
|
|
1477
1668
|
<h3 id="expand-modal-title" class="text-sm font-semibold">Expanded View</h3>
|
|
1478
1669
|
<div class="flex items-center gap-2">
|
|
1479
|
-
<button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-
|
|
1670
|
+
<button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-brand text-white font-semibold hover:bg-brand-deep transition-colors">Apply</button>
|
|
1480
1671
|
<button id="expand-close" class="p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 hover:border-brand/60 transition-colors" aria-label="Close Modal" title="Close Modal">
|
|
1481
1672
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1482
1673
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
@@ -1495,12 +1686,13 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1495
1686
|
<script>
|
|
1496
1687
|
const spec = ${specJson};
|
|
1497
1688
|
const openapiPath = ${openapiPathJson};
|
|
1689
|
+
const methodBadgeDefault = "bg-black/5 text-light-text/80 dark:bg-white/10 dark:text-dark-text/80";
|
|
1498
1690
|
const methodBadge = {
|
|
1499
|
-
GET: "bg-
|
|
1500
|
-
POST: "bg-
|
|
1501
|
-
PUT: "bg-
|
|
1502
|
-
PATCH: "bg-
|
|
1503
|
-
DELETE: "bg-
|
|
1691
|
+
GET: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
1692
|
+
POST: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
1693
|
+
PUT: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
1694
|
+
PATCH: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
1695
|
+
DELETE: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
|
|
1504
1696
|
};
|
|
1505
1697
|
|
|
1506
1698
|
function getOperations() {
|
|
@@ -1754,6 +1946,16 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1754
1946
|
return;
|
|
1755
1947
|
}
|
|
1756
1948
|
for (const [tag, ops] of groups.entries()) {
|
|
1949
|
+
ops.sort((a, b) => {
|
|
1950
|
+
const byName = a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
1951
|
+
if (byName !== 0) return byName;
|
|
1952
|
+
|
|
1953
|
+
const byPath = a.path.localeCompare(b.path, undefined, { sensitivity: "base" });
|
|
1954
|
+
if (byPath !== 0) return byPath;
|
|
1955
|
+
|
|
1956
|
+
return a.method.localeCompare(b.method, undefined, { sensitivity: "base" });
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1757
1959
|
const block = document.createElement("div");
|
|
1758
1960
|
block.innerHTML = '<h3 class="px-2 mb-2 font-semibold text-xs uppercase tracking-wider opacity-50"></h3><ul class="space-y-0.5"></ul>';
|
|
1759
1961
|
block.querySelector("h3").textContent = tag;
|
|
@@ -1765,14 +1967,14 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1765
1967
|
const a = document.createElement("a");
|
|
1766
1968
|
a.href = "#";
|
|
1767
1969
|
a.className = op === selected
|
|
1768
|
-
? "block px-2 py-1.5 rounded-md bg-
|
|
1970
|
+
? "block px-2 py-1.5 rounded-md bg-brand-soft/70 dark:bg-brand/20 text-brand-deep dark:text-brand font-medium transition-colors"
|
|
1769
1971
|
: "block px-2 py-1.5 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors";
|
|
1770
1972
|
|
|
1771
1973
|
const row = document.createElement("span");
|
|
1772
1974
|
row.className = "flex items-center gap-2";
|
|
1773
1975
|
|
|
1774
1976
|
const method = document.createElement("span");
|
|
1775
|
-
method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] ||
|
|
1977
|
+
method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] || methodBadgeDefault);
|
|
1776
1978
|
method.textContent = op.method;
|
|
1777
1979
|
|
|
1778
1980
|
const name = document.createElement("span");
|
|
@@ -1911,6 +2113,55 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
1911
2113
|
);
|
|
1912
2114
|
}
|
|
1913
2115
|
|
|
2116
|
+
function renderResponseSchemasSection(responses) {
|
|
2117
|
+
if (!responses || typeof responses !== "object") return "";
|
|
2118
|
+
|
|
2119
|
+
const statusCodes = Object.keys(responses).sort((a, b) => {
|
|
2120
|
+
const aNum = Number(a);
|
|
2121
|
+
const bNum = Number(b);
|
|
2122
|
+
if (Number.isInteger(aNum) && Number.isInteger(bNum)) return aNum - bNum;
|
|
2123
|
+
if (Number.isInteger(aNum)) return -1;
|
|
2124
|
+
if (Number.isInteger(bNum)) return 1;
|
|
2125
|
+
return a.localeCompare(b);
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
let sections = "";
|
|
2129
|
+
for (const statusCode of statusCodes) {
|
|
2130
|
+
const responseDef = responses[statusCode];
|
|
2131
|
+
if (!responseDef || typeof responseDef !== "object") continue;
|
|
2132
|
+
|
|
2133
|
+
const jsonSchema =
|
|
2134
|
+
responseDef.content &&
|
|
2135
|
+
responseDef.content["application/json"] &&
|
|
2136
|
+
responseDef.content["application/json"].schema;
|
|
2137
|
+
|
|
2138
|
+
if (!jsonSchema || typeof jsonSchema !== "object") continue;
|
|
2139
|
+
|
|
2140
|
+
const rootChildren = buildSchemaChildren(jsonSchema);
|
|
2141
|
+
if (!rootChildren.length) continue;
|
|
2142
|
+
|
|
2143
|
+
let rows = "";
|
|
2144
|
+
for (const child of rootChildren) {
|
|
2145
|
+
rows += renderSchemaFieldNode(child, 0);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
sections +=
|
|
2149
|
+
'<div class="mb-4"><h4 class="text-xs font-mono uppercase tracking-wider opacity-70 mb-2">Status ' +
|
|
2150
|
+
escapeHtml(statusCode) +
|
|
2151
|
+
"</h4>" +
|
|
2152
|
+
rows +
|
|
2153
|
+
"</div>";
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
if (!sections) return "";
|
|
2157
|
+
|
|
2158
|
+
return (
|
|
2159
|
+
'<div><h3 class="text-sm font-semibold mb-3 flex items-center border-b border-light-border dark:border-dark-border pb-2">Response Schemas</h3>' +
|
|
2160
|
+
sections +
|
|
2161
|
+
"</div>"
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
1914
2165
|
function renderTryItParameterInputs(pathParams, queryParams) {
|
|
1915
2166
|
const container = document.getElementById("request-param-inputs");
|
|
1916
2167
|
if (!container || !selected) return;
|
|
@@ -2194,6 +2445,19 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
2194
2445
|
}
|
|
2195
2446
|
}
|
|
2196
2447
|
|
|
2448
|
+
function setSubmitLoading(isLoading) {
|
|
2449
|
+
const sendButton = document.getElementById("send-btn");
|
|
2450
|
+
const spinner = document.getElementById("send-btn-spinner");
|
|
2451
|
+
const label = document.getElementById("send-btn-label");
|
|
2452
|
+
if (!sendButton) return;
|
|
2453
|
+
|
|
2454
|
+
sendButton.disabled = isLoading;
|
|
2455
|
+
sendButton.classList.toggle("opacity-80", isLoading);
|
|
2456
|
+
sendButton.classList.toggle("cursor-wait", isLoading);
|
|
2457
|
+
if (spinner) spinner.classList.toggle("hidden", !isLoading);
|
|
2458
|
+
if (label) label.textContent = isLoading ? "Sending..." : "Submit";
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2197
2461
|
function updateRequestPreview() {
|
|
2198
2462
|
if (!selected) return;
|
|
2199
2463
|
|
|
@@ -2257,7 +2521,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
2257
2521
|
document.getElementById("tag-description").textContent = op.description || "Interactive API documentation.";
|
|
2258
2522
|
const methodNode = document.getElementById("endpoint-method");
|
|
2259
2523
|
methodNode.textContent = selected.method;
|
|
2260
|
-
methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] ||
|
|
2524
|
+
methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || methodBadgeDefault);
|
|
2261
2525
|
document.getElementById("endpoint-title").textContent = selected.name;
|
|
2262
2526
|
document.getElementById("endpoint-path").textContent = selected.path;
|
|
2263
2527
|
|
|
@@ -2270,6 +2534,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
2270
2534
|
html += renderParamSection("Header Parameters", headers);
|
|
2271
2535
|
|
|
2272
2536
|
html += renderRequestBodySchemaSection(reqSchema);
|
|
2537
|
+
html += renderResponseSchemasSection(op.responses);
|
|
2273
2538
|
document.getElementById("params-column").innerHTML = html || '<div class="text-sm opacity-70">No parameters</div>';
|
|
2274
2539
|
renderTryItParameterInputs(path, query);
|
|
2275
2540
|
renderHeaderInputs();
|
|
@@ -2320,14 +2585,18 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
2320
2585
|
headers["Content-Type"] = "application/json";
|
|
2321
2586
|
}
|
|
2322
2587
|
|
|
2588
|
+
setSubmitLoading(true);
|
|
2323
2589
|
try {
|
|
2590
|
+
const requestStart = performance.now();
|
|
2324
2591
|
const response = await fetch(requestPath, { method: selected.method, headers, body: body || undefined });
|
|
2325
2592
|
const text = await response.text();
|
|
2593
|
+
const responseTimeMs = Math.round(performance.now() - requestStart);
|
|
2326
2594
|
const contentType = response.headers.get("content-type") || "unknown";
|
|
2327
2595
|
const formattedResponse = formatResponseText(text);
|
|
2328
2596
|
const headerText =
|
|
2329
2597
|
"Status: " + response.status + " " + response.statusText + "\\n" +
|
|
2330
|
-
"Content-Type: " + contentType + "\\n
|
|
2598
|
+
"Content-Type: " + contentType + "\\n" +
|
|
2599
|
+
"Response Time: " + responseTimeMs + " ms\\n\\n";
|
|
2331
2600
|
setResponseContent(
|
|
2332
2601
|
headerText,
|
|
2333
2602
|
formattedResponse.text,
|
|
@@ -2335,6 +2604,8 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
|
|
|
2335
2604
|
);
|
|
2336
2605
|
} catch (error) {
|
|
2337
2606
|
setResponseContent("", "Request failed: " + String(error), false);
|
|
2607
|
+
} finally {
|
|
2608
|
+
setSubmitLoading(false);
|
|
2338
2609
|
}
|
|
2339
2610
|
});
|
|
2340
2611
|
|
|
@@ -2630,29 +2901,325 @@ function getResponseDescription(status) {
|
|
|
2630
2901
|
}
|
|
2631
2902
|
function convertInputSchema(routePath, inputSchema, target, warnings) {
|
|
2632
2903
|
if (!isJSONSchemaCapable(inputSchema)) {
|
|
2633
|
-
|
|
2904
|
+
const fallback = buildFallbackJSONSchema(inputSchema);
|
|
2905
|
+
return isEmptyObjectSchema(fallback) ? null : fallback;
|
|
2634
2906
|
}
|
|
2635
2907
|
try {
|
|
2636
2908
|
return inputSchema["~standard"].jsonSchema.input({ target });
|
|
2637
2909
|
} catch (error) {
|
|
2638
|
-
|
|
2639
|
-
|
|
2910
|
+
const alternate = tryAlternateTargetConversion(inputSchema, "input", target, error, routePath, undefined, warnings);
|
|
2911
|
+
if (alternate) {
|
|
2912
|
+
return alternate;
|
|
2913
|
+
}
|
|
2914
|
+
warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
|
|
2915
|
+
const fallback = buildFallbackJSONSchema(inputSchema);
|
|
2916
|
+
return isEmptyObjectSchema(fallback) ? null : fallback;
|
|
2640
2917
|
}
|
|
2641
2918
|
}
|
|
2642
2919
|
function convertOutputSchema(routePath, statusCode, outputSchema, target, warnings) {
|
|
2643
2920
|
if (!isJSONSchemaCapable(outputSchema)) {
|
|
2644
|
-
|
|
2921
|
+
const fallback = buildFallbackJSONSchema(outputSchema);
|
|
2922
|
+
return isEmptyObjectSchema(fallback) ? null : fallback;
|
|
2645
2923
|
}
|
|
2646
2924
|
try {
|
|
2647
2925
|
return outputSchema["~standard"].jsonSchema.output({ target });
|
|
2648
2926
|
} catch (error) {
|
|
2649
|
-
|
|
2650
|
-
|
|
2927
|
+
const alternate = tryAlternateTargetConversion(outputSchema, "output", target, error, routePath, statusCode, warnings);
|
|
2928
|
+
if (alternate) {
|
|
2929
|
+
return alternate;
|
|
2930
|
+
}
|
|
2931
|
+
warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
|
|
2932
|
+
return buildFallbackJSONSchema(outputSchema);
|
|
2651
2933
|
}
|
|
2652
2934
|
}
|
|
2653
2935
|
function isRecord(value) {
|
|
2654
2936
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2655
2937
|
}
|
|
2938
|
+
function isEmptyObjectSchema(value) {
|
|
2939
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
2940
|
+
}
|
|
2941
|
+
function tryAlternateTargetConversion(schema, kind, target, originalError, routePath, statusCode, warnings) {
|
|
2942
|
+
if (!isJSONSchemaCapable(schema)) {
|
|
2943
|
+
return null;
|
|
2944
|
+
}
|
|
2945
|
+
const message = originalError instanceof Error ? originalError.message : String(originalError);
|
|
2946
|
+
const unsupportedOpenAPITarget = target === "openapi-3.0" && message.includes("target 'openapi-3.0' is not supported") && message.includes("draft-2020-12") && message.includes("draft-07");
|
|
2947
|
+
if (!unsupportedOpenAPITarget) {
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
try {
|
|
2951
|
+
const converted = schema["~standard"].jsonSchema[kind]({ target: "draft-07" });
|
|
2952
|
+
warnings.push(kind === "input" ? `[OpenAPI] ${routePath} converter does not support openapi-3.0 target; using draft-07 conversion output.` : `[OpenAPI] ${routePath} (${statusCode}) converter does not support openapi-3.0 target; using draft-07 conversion output.`);
|
|
2953
|
+
return converted;
|
|
2954
|
+
} catch {
|
|
2955
|
+
return null;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
function getValidatorSchemaDef(schema) {
|
|
2959
|
+
if (!schema || typeof schema !== "object")
|
|
2960
|
+
return null;
|
|
2961
|
+
const value = schema;
|
|
2962
|
+
if (isRecord(value._def))
|
|
2963
|
+
return value._def;
|
|
2964
|
+
if (isRecord(value._zod) && isRecord(value._zod.def)) {
|
|
2965
|
+
return value._zod.def;
|
|
2966
|
+
}
|
|
2967
|
+
if (value.kind === "schema" && typeof value.type === "string") {
|
|
2968
|
+
return value;
|
|
2969
|
+
}
|
|
2970
|
+
return null;
|
|
2971
|
+
}
|
|
2972
|
+
function getSchemaKind(def) {
|
|
2973
|
+
if (!def)
|
|
2974
|
+
return null;
|
|
2975
|
+
const typeName = def.typeName;
|
|
2976
|
+
if (typeof typeName === "string")
|
|
2977
|
+
return typeName;
|
|
2978
|
+
const type = def.type;
|
|
2979
|
+
if (typeof type === "string")
|
|
2980
|
+
return type;
|
|
2981
|
+
return null;
|
|
2982
|
+
}
|
|
2983
|
+
function pickSchemaChild(def) {
|
|
2984
|
+
const candidates = ["innerType", "schema", "type", "out", "in", "left", "right", "wrapped", "element"];
|
|
2985
|
+
for (const key of candidates) {
|
|
2986
|
+
if (key in def)
|
|
2987
|
+
return def[key];
|
|
2988
|
+
}
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
function pickSchemaObjectCandidate(def, keys) {
|
|
2992
|
+
for (const key of keys) {
|
|
2993
|
+
const value = def[key];
|
|
2994
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2995
|
+
return value;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
function isOptionalWrapperKind(kind) {
|
|
3001
|
+
if (!kind)
|
|
3002
|
+
return false;
|
|
3003
|
+
const lower = kind.toLowerCase();
|
|
3004
|
+
return lower.includes("optional") || lower.includes("default") || lower.includes("catch");
|
|
3005
|
+
}
|
|
3006
|
+
function unwrapOptionalForRequired(schema) {
|
|
3007
|
+
let current = schema;
|
|
3008
|
+
let optional = false;
|
|
3009
|
+
let guard = 0;
|
|
3010
|
+
while (guard < 8) {
|
|
3011
|
+
guard += 1;
|
|
3012
|
+
const def = getValidatorSchemaDef(current);
|
|
3013
|
+
const kind = getSchemaKind(def);
|
|
3014
|
+
if (!def || !isOptionalWrapperKind(kind))
|
|
3015
|
+
break;
|
|
3016
|
+
optional = true;
|
|
3017
|
+
const inner = pickSchemaChild(def);
|
|
3018
|
+
if (!inner)
|
|
3019
|
+
break;
|
|
3020
|
+
current = inner;
|
|
3021
|
+
}
|
|
3022
|
+
return { schema: current, optional };
|
|
3023
|
+
}
|
|
3024
|
+
function getObjectShape(def) {
|
|
3025
|
+
const entries = def.entries;
|
|
3026
|
+
if (isRecord(entries)) {
|
|
3027
|
+
return entries;
|
|
3028
|
+
}
|
|
3029
|
+
const rawShape = def.shape;
|
|
3030
|
+
if (typeof rawShape === "function") {
|
|
3031
|
+
try {
|
|
3032
|
+
const resolved = rawShape();
|
|
3033
|
+
return isRecord(resolved) ? resolved : {};
|
|
3034
|
+
} catch {
|
|
3035
|
+
return {};
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
return isRecord(rawShape) ? rawShape : {};
|
|
3039
|
+
}
|
|
3040
|
+
function extractEnumValues(def) {
|
|
3041
|
+
const values = def.values;
|
|
3042
|
+
if (Array.isArray(values))
|
|
3043
|
+
return values;
|
|
3044
|
+
if (values && typeof values === "object")
|
|
3045
|
+
return Object.values(values);
|
|
3046
|
+
const entries = def.entries;
|
|
3047
|
+
if (entries && typeof entries === "object")
|
|
3048
|
+
return Object.values(entries);
|
|
3049
|
+
const enumObject = def.enum;
|
|
3050
|
+
if (enumObject && typeof enumObject === "object")
|
|
3051
|
+
return Object.values(enumObject);
|
|
3052
|
+
const options = def.options;
|
|
3053
|
+
if (Array.isArray(options)) {
|
|
3054
|
+
return options.map((item) => {
|
|
3055
|
+
if (item && typeof item === "object" && "unit" in item) {
|
|
3056
|
+
return item.unit;
|
|
3057
|
+
}
|
|
3058
|
+
return item;
|
|
3059
|
+
}).filter((item) => item !== undefined);
|
|
3060
|
+
}
|
|
3061
|
+
return [];
|
|
3062
|
+
}
|
|
3063
|
+
function mapPrimitiveKind(kind) {
|
|
3064
|
+
const lower = kind.toLowerCase();
|
|
3065
|
+
if (lower.includes("string"))
|
|
3066
|
+
return { type: "string" };
|
|
3067
|
+
if (lower.includes("number"))
|
|
3068
|
+
return { type: "number" };
|
|
3069
|
+
if (lower.includes("boolean"))
|
|
3070
|
+
return { type: "boolean" };
|
|
3071
|
+
if (lower.includes("bigint"))
|
|
3072
|
+
return { type: "string" };
|
|
3073
|
+
if (lower === "null" || lower.includes("zodnull"))
|
|
3074
|
+
return { type: "null" };
|
|
3075
|
+
if (lower.includes("any") || lower.includes("unknown") || lower.includes("never"))
|
|
3076
|
+
return {};
|
|
3077
|
+
if (lower.includes("date"))
|
|
3078
|
+
return { type: "string", format: "date-time" };
|
|
3079
|
+
if (lower.includes("custom"))
|
|
3080
|
+
return { type: "object", additionalProperties: true };
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
function buildIntrospectedFallbackJSONSchema(schema, seen = new WeakSet) {
|
|
3084
|
+
if (!schema || typeof schema !== "object")
|
|
3085
|
+
return {};
|
|
3086
|
+
if (seen.has(schema))
|
|
3087
|
+
return {};
|
|
3088
|
+
seen.add(schema);
|
|
3089
|
+
const def = getValidatorSchemaDef(schema);
|
|
3090
|
+
const kind = getSchemaKind(def);
|
|
3091
|
+
if (!def || !kind)
|
|
3092
|
+
return {};
|
|
3093
|
+
const primitive = mapPrimitiveKind(kind);
|
|
3094
|
+
if (primitive)
|
|
3095
|
+
return primitive;
|
|
3096
|
+
const lower = kind.toLowerCase();
|
|
3097
|
+
if (lower.includes("object")) {
|
|
3098
|
+
const shape = getObjectShape(def);
|
|
3099
|
+
const properties = {};
|
|
3100
|
+
const required = [];
|
|
3101
|
+
for (const [key, child2] of Object.entries(shape)) {
|
|
3102
|
+
const unwrapped = unwrapOptionalForRequired(child2);
|
|
3103
|
+
properties[key] = buildIntrospectedFallbackJSONSchema(unwrapped.schema, seen);
|
|
3104
|
+
if (!unwrapped.optional)
|
|
3105
|
+
required.push(key);
|
|
3106
|
+
}
|
|
3107
|
+
const out = {
|
|
3108
|
+
type: "object",
|
|
3109
|
+
properties,
|
|
3110
|
+
additionalProperties: true
|
|
3111
|
+
};
|
|
3112
|
+
if (required.length > 0) {
|
|
3113
|
+
out.required = required;
|
|
3114
|
+
}
|
|
3115
|
+
return out;
|
|
3116
|
+
}
|
|
3117
|
+
if (lower.includes("array")) {
|
|
3118
|
+
const itemSchema = pickSchemaObjectCandidate(def, ["element", "items", "innerType", "type"]) ?? {};
|
|
3119
|
+
return {
|
|
3120
|
+
type: "array",
|
|
3121
|
+
items: buildIntrospectedFallbackJSONSchema(itemSchema, seen)
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
if (lower.includes("record")) {
|
|
3125
|
+
const valueType = def.valueType ?? def.valueSchema;
|
|
3126
|
+
return {
|
|
3127
|
+
type: "object",
|
|
3128
|
+
additionalProperties: valueType ? buildIntrospectedFallbackJSONSchema(valueType, seen) : true
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
if (lower.includes("tuple")) {
|
|
3132
|
+
const items = Array.isArray(def.items) ? def.items : [];
|
|
3133
|
+
const prefixItems = items.map((item) => buildIntrospectedFallbackJSONSchema(item, seen));
|
|
3134
|
+
return {
|
|
3135
|
+
type: "array",
|
|
3136
|
+
prefixItems,
|
|
3137
|
+
minItems: prefixItems.length,
|
|
3138
|
+
maxItems: prefixItems.length
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
if (lower.includes("union")) {
|
|
3142
|
+
const options = def.options ?? def.schemas ?? [];
|
|
3143
|
+
if (!Array.isArray(options) || options.length === 0)
|
|
3144
|
+
return {};
|
|
3145
|
+
return {
|
|
3146
|
+
anyOf: options.map((option) => buildIntrospectedFallbackJSONSchema(option, seen))
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
if (lower.includes("intersection")) {
|
|
3150
|
+
const left = def.left;
|
|
3151
|
+
const right = def.right;
|
|
3152
|
+
if (!left || !right)
|
|
3153
|
+
return {};
|
|
3154
|
+
return {
|
|
3155
|
+
allOf: [buildIntrospectedFallbackJSONSchema(left, seen), buildIntrospectedFallbackJSONSchema(right, seen)]
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
if (lower.includes("enum")) {
|
|
3159
|
+
const enumValues = extractEnumValues(def);
|
|
3160
|
+
if (enumValues.length > 0) {
|
|
3161
|
+
const allString = enumValues.every((v) => typeof v === "string");
|
|
3162
|
+
const allNumber = enumValues.every((v) => typeof v === "number");
|
|
3163
|
+
const allBoolean = enumValues.every((v) => typeof v === "boolean");
|
|
3164
|
+
if (allString)
|
|
3165
|
+
return { type: "string", enum: enumValues };
|
|
3166
|
+
if (allNumber)
|
|
3167
|
+
return { type: "number", enum: enumValues };
|
|
3168
|
+
if (allBoolean)
|
|
3169
|
+
return { type: "boolean", enum: enumValues };
|
|
3170
|
+
return { enum: enumValues };
|
|
3171
|
+
}
|
|
3172
|
+
return {};
|
|
3173
|
+
}
|
|
3174
|
+
if (lower.includes("picklist")) {
|
|
3175
|
+
const enumValues = extractEnumValues(def);
|
|
3176
|
+
if (enumValues.length > 0) {
|
|
3177
|
+
const allString = enumValues.every((v) => typeof v === "string");
|
|
3178
|
+
if (allString)
|
|
3179
|
+
return { type: "string", enum: enumValues };
|
|
3180
|
+
return { enum: enumValues };
|
|
3181
|
+
}
|
|
3182
|
+
return {};
|
|
3183
|
+
}
|
|
3184
|
+
if (lower.includes("literal")) {
|
|
3185
|
+
const value = def.value;
|
|
3186
|
+
if (value === undefined)
|
|
3187
|
+
return {};
|
|
3188
|
+
const valueType = value === null ? "null" : typeof value;
|
|
3189
|
+
if (valueType === "string" || valueType === "number" || valueType === "boolean" || valueType === "null") {
|
|
3190
|
+
return { type: valueType, const: value };
|
|
3191
|
+
}
|
|
3192
|
+
return { const: value };
|
|
3193
|
+
}
|
|
3194
|
+
if (lower.includes("nullable")) {
|
|
3195
|
+
const inner = pickSchemaChild(def);
|
|
3196
|
+
if (!inner)
|
|
3197
|
+
return {};
|
|
3198
|
+
return {
|
|
3199
|
+
anyOf: [buildIntrospectedFallbackJSONSchema(inner, seen), { type: "null" }]
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
if (lower.includes("lazy")) {
|
|
3203
|
+
const getter = def.getter;
|
|
3204
|
+
if (typeof getter !== "function")
|
|
3205
|
+
return {};
|
|
3206
|
+
try {
|
|
3207
|
+
return buildIntrospectedFallbackJSONSchema(getter(), seen);
|
|
3208
|
+
} catch {
|
|
3209
|
+
return {};
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
const child = pickSchemaChild(def);
|
|
3213
|
+
if (child)
|
|
3214
|
+
return buildIntrospectedFallbackJSONSchema(child, seen);
|
|
3215
|
+
return {};
|
|
3216
|
+
}
|
|
3217
|
+
function buildFallbackJSONSchema(schema) {
|
|
3218
|
+
const def = getValidatorSchemaDef(schema);
|
|
3219
|
+
if (!def)
|
|
3220
|
+
return {};
|
|
3221
|
+
return buildIntrospectedFallbackJSONSchema(schema);
|
|
3222
|
+
}
|
|
2656
3223
|
function addStructuredInputToOperation(operation, inputJSONSchema) {
|
|
2657
3224
|
if (!isRecord(inputJSONSchema))
|
|
2658
3225
|
return;
|
|
@@ -2803,38 +3370,155 @@ function generateOpenAPIDocument(routes, options) {
|
|
|
2803
3370
|
|
|
2804
3371
|
// src/core/server.ts
|
|
2805
3372
|
var OPENAPI_TAILWIND_ASSET_PATH = "/_vector/openapi/tailwindcdn.js";
|
|
3373
|
+
var OPENAPI_LOGO_DARK_ASSET_PATH = "/_vector/openapi/logo_dark.svg";
|
|
3374
|
+
var OPENAPI_LOGO_WHITE_ASSET_PATH = "/_vector/openapi/logo_white.svg";
|
|
3375
|
+
var OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH = "/_vector/openapi/favicon/apple-touch-icon.png";
|
|
3376
|
+
var OPENAPI_FAVICON_32_ASSET_PATH = "/_vector/openapi/favicon/favicon-32x32.png";
|
|
3377
|
+
var OPENAPI_FAVICON_16_ASSET_PATH = "/_vector/openapi/favicon/favicon-16x16.png";
|
|
3378
|
+
var OPENAPI_FAVICON_ICO_ASSET_PATH = "/_vector/openapi/favicon/favicon.ico";
|
|
3379
|
+
var OPENAPI_WEBMANIFEST_ASSET_PATH = "/_vector/openapi/favicon/site.webmanifest";
|
|
3380
|
+
var OPENAPI_ANDROID_192_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-192x192.png";
|
|
3381
|
+
var OPENAPI_ANDROID_512_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-512x512.png";
|
|
2806
3382
|
var OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES = [
|
|
2807
3383
|
"../openapi/assets/tailwindcdn.js",
|
|
2808
3384
|
"../src/openapi/assets/tailwindcdn.js",
|
|
2809
3385
|
"../../src/openapi/assets/tailwindcdn.js"
|
|
2810
3386
|
];
|
|
3387
|
+
var OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES = [
|
|
3388
|
+
"../openapi/assets/logo_dark.svg",
|
|
3389
|
+
"../src/openapi/assets/logo_dark.svg",
|
|
3390
|
+
"../../src/openapi/assets/logo_dark.svg"
|
|
3391
|
+
];
|
|
3392
|
+
var OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES = [
|
|
3393
|
+
"../openapi/assets/logo_white.svg",
|
|
3394
|
+
"../src/openapi/assets/logo_white.svg",
|
|
3395
|
+
"../../src/openapi/assets/logo_white.svg"
|
|
3396
|
+
];
|
|
2811
3397
|
var OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES = [
|
|
2812
3398
|
"src/openapi/assets/tailwindcdn.js",
|
|
2813
3399
|
"openapi/assets/tailwindcdn.js",
|
|
2814
3400
|
"dist/openapi/assets/tailwindcdn.js"
|
|
2815
3401
|
];
|
|
3402
|
+
var OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES = [
|
|
3403
|
+
"src/openapi/assets/logo_dark.svg",
|
|
3404
|
+
"openapi/assets/logo_dark.svg",
|
|
3405
|
+
"dist/openapi/assets/logo_dark.svg"
|
|
3406
|
+
];
|
|
3407
|
+
var OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES = [
|
|
3408
|
+
"src/openapi/assets/logo_white.svg",
|
|
3409
|
+
"openapi/assets/logo_white.svg",
|
|
3410
|
+
"dist/openapi/assets/logo_white.svg"
|
|
3411
|
+
];
|
|
3412
|
+
var OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES = [
|
|
3413
|
+
"../openapi/assets/favicon",
|
|
3414
|
+
"../src/openapi/assets/favicon",
|
|
3415
|
+
"../../src/openapi/assets/favicon"
|
|
3416
|
+
];
|
|
3417
|
+
var OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES = [
|
|
3418
|
+
"src/openapi/assets/favicon",
|
|
3419
|
+
"openapi/assets/favicon",
|
|
3420
|
+
"dist/openapi/assets/favicon"
|
|
3421
|
+
];
|
|
2816
3422
|
var OPENAPI_TAILWIND_ASSET_INLINE_FALLBACK = "/* OpenAPI docs runtime asset missing: tailwind disabled */";
|
|
2817
|
-
function
|
|
2818
|
-
|
|
3423
|
+
function buildOpenAPIAssetCandidatePaths(bases, filename) {
|
|
3424
|
+
return bases.map((base) => `${base}/${filename}`);
|
|
3425
|
+
}
|
|
3426
|
+
function resolveOpenAPIAssetFile(relativeCandidates, cwdCandidates) {
|
|
3427
|
+
for (const relativePath of relativeCandidates) {
|
|
2819
3428
|
try {
|
|
2820
3429
|
const fileUrl = new URL(relativePath, import.meta.url);
|
|
2821
|
-
if (
|
|
3430
|
+
if (existsSync3(fileUrl)) {
|
|
2822
3431
|
return Bun.file(fileUrl);
|
|
2823
3432
|
}
|
|
2824
3433
|
} catch {}
|
|
2825
3434
|
}
|
|
2826
3435
|
const cwd = process.cwd();
|
|
2827
|
-
for (const relativePath of
|
|
3436
|
+
for (const relativePath of cwdCandidates) {
|
|
2828
3437
|
const absolutePath = join2(cwd, relativePath);
|
|
2829
|
-
if (
|
|
3438
|
+
if (existsSync3(absolutePath)) {
|
|
2830
3439
|
return Bun.file(absolutePath);
|
|
2831
3440
|
}
|
|
2832
3441
|
}
|
|
2833
3442
|
return null;
|
|
2834
3443
|
}
|
|
2835
|
-
var OPENAPI_TAILWIND_ASSET_FILE =
|
|
3444
|
+
var OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES, OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES);
|
|
3445
|
+
var OPENAPI_LOGO_DARK_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES);
|
|
3446
|
+
var OPENAPI_LOGO_WHITE_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES);
|
|
3447
|
+
var OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "apple-touch-icon.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "apple-touch-icon.png"));
|
|
3448
|
+
var OPENAPI_FAVICON_32_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "favicon-32x32.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "favicon-32x32.png"));
|
|
3449
|
+
var OPENAPI_FAVICON_16_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "favicon-16x16.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "favicon-16x16.png"));
|
|
3450
|
+
var OPENAPI_FAVICON_ICO_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "favicon.ico"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "favicon.ico"));
|
|
3451
|
+
var OPENAPI_WEBMANIFEST_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "site.webmanifest"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "site.webmanifest"));
|
|
3452
|
+
var OPENAPI_ANDROID_192_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "android-chrome-192x192.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "android-chrome-192x192.png"));
|
|
3453
|
+
var OPENAPI_ANDROID_512_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "android-chrome-512x512.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "android-chrome-512x512.png"));
|
|
3454
|
+
var OPENAPI_FAVICON_ASSETS = [
|
|
3455
|
+
{
|
|
3456
|
+
path: OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH,
|
|
3457
|
+
file: OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE,
|
|
3458
|
+
contentType: "image/png",
|
|
3459
|
+
filename: "apple-touch-icon.png"
|
|
3460
|
+
},
|
|
3461
|
+
{
|
|
3462
|
+
path: OPENAPI_FAVICON_32_ASSET_PATH,
|
|
3463
|
+
file: OPENAPI_FAVICON_32_ASSET_FILE,
|
|
3464
|
+
contentType: "image/png",
|
|
3465
|
+
filename: "favicon-32x32.png"
|
|
3466
|
+
},
|
|
3467
|
+
{
|
|
3468
|
+
path: OPENAPI_FAVICON_16_ASSET_PATH,
|
|
3469
|
+
file: OPENAPI_FAVICON_16_ASSET_FILE,
|
|
3470
|
+
contentType: "image/png",
|
|
3471
|
+
filename: "favicon-16x16.png"
|
|
3472
|
+
},
|
|
3473
|
+
{
|
|
3474
|
+
path: OPENAPI_FAVICON_ICO_ASSET_PATH,
|
|
3475
|
+
file: OPENAPI_FAVICON_ICO_ASSET_FILE,
|
|
3476
|
+
contentType: "image/x-icon",
|
|
3477
|
+
filename: "favicon.ico"
|
|
3478
|
+
},
|
|
3479
|
+
{
|
|
3480
|
+
path: OPENAPI_WEBMANIFEST_ASSET_PATH,
|
|
3481
|
+
file: OPENAPI_WEBMANIFEST_ASSET_FILE,
|
|
3482
|
+
contentType: "application/manifest+json; charset=utf-8",
|
|
3483
|
+
filename: "site.webmanifest"
|
|
3484
|
+
},
|
|
3485
|
+
{
|
|
3486
|
+
path: OPENAPI_ANDROID_192_ASSET_PATH,
|
|
3487
|
+
file: OPENAPI_ANDROID_192_ASSET_FILE,
|
|
3488
|
+
contentType: "image/png",
|
|
3489
|
+
filename: "android-chrome-192x192.png"
|
|
3490
|
+
},
|
|
3491
|
+
{
|
|
3492
|
+
path: OPENAPI_ANDROID_512_ASSET_PATH,
|
|
3493
|
+
file: OPENAPI_ANDROID_512_ASSET_FILE,
|
|
3494
|
+
contentType: "image/png",
|
|
3495
|
+
filename: "android-chrome-512x512.png"
|
|
3496
|
+
}
|
|
3497
|
+
];
|
|
2836
3498
|
var DOCS_HTML_CACHE_CONTROL = "public, max-age=0, must-revalidate";
|
|
2837
3499
|
var DOCS_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
3500
|
+
var DOCS_ASSET_ERROR_CACHE_CONTROL = "no-store";
|
|
3501
|
+
function escapeRegex(value) {
|
|
3502
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3503
|
+
}
|
|
3504
|
+
function wildcardPatternToRegex(pattern) {
|
|
3505
|
+
let regexSource = "^";
|
|
3506
|
+
for (const char of pattern) {
|
|
3507
|
+
if (char === "*") {
|
|
3508
|
+
regexSource += ".*";
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
regexSource += escapeRegex(char);
|
|
3512
|
+
}
|
|
3513
|
+
regexSource += "$";
|
|
3514
|
+
return new RegExp(regexSource);
|
|
3515
|
+
}
|
|
3516
|
+
function matchesExposePath(path, exposePathPattern) {
|
|
3517
|
+
if (!exposePathPattern.includes("*")) {
|
|
3518
|
+
return path === exposePathPattern;
|
|
3519
|
+
}
|
|
3520
|
+
return wildcardPatternToRegex(exposePathPattern).test(path);
|
|
3521
|
+
}
|
|
2838
3522
|
|
|
2839
3523
|
class VectorServer {
|
|
2840
3524
|
server = null;
|
|
@@ -2845,6 +3529,8 @@ class VectorServer {
|
|
|
2845
3529
|
openapiDocsHtmlCache = null;
|
|
2846
3530
|
openapiWarningsLogged = false;
|
|
2847
3531
|
openapiTailwindMissingLogged = false;
|
|
3532
|
+
openapiLogoDarkMissingLogged = false;
|
|
3533
|
+
openapiLogoWhiteMissingLogged = false;
|
|
2848
3534
|
corsHandler = null;
|
|
2849
3535
|
corsHeadersEntries = null;
|
|
2850
3536
|
constructor(router, config) {
|
|
@@ -2894,9 +3580,10 @@ class VectorServer {
|
|
|
2894
3580
|
}
|
|
2895
3581
|
const openapiObject = openapi || {};
|
|
2896
3582
|
const docsValue = openapiObject.docs;
|
|
2897
|
-
const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs" } : {
|
|
3583
|
+
const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs", exposePaths: undefined } : {
|
|
2898
3584
|
enabled: docsValue?.enabled === true,
|
|
2899
|
-
path: docsValue?.path || "/docs"
|
|
3585
|
+
path: docsValue?.path || "/docs",
|
|
3586
|
+
exposePaths: Array.isArray(docsValue?.exposePaths) ? docsValue.exposePaths.map((path) => typeof path === "string" ? path.trim() : "").filter((path) => path.length > 0) : undefined
|
|
2900
3587
|
};
|
|
2901
3588
|
return {
|
|
2902
3589
|
enabled: openapiObject.enabled ?? defaultEnabled,
|
|
@@ -2909,6 +3596,15 @@ class VectorServer {
|
|
|
2909
3596
|
isDocsReservedPath(path) {
|
|
2910
3597
|
return path === this.openapiConfig.path || this.openapiConfig.docs.enabled && path === this.openapiConfig.docs.path;
|
|
2911
3598
|
}
|
|
3599
|
+
shouldLogOpenAPIConversionWarnings() {
|
|
3600
|
+
const nodeEnv = "development";
|
|
3601
|
+
const isDevelopment = this.config.development !== false && nodeEnv !== "production";
|
|
3602
|
+
if (!isDevelopment) {
|
|
3603
|
+
return false;
|
|
3604
|
+
}
|
|
3605
|
+
const logLevel = process.env.LOG_LEVEL;
|
|
3606
|
+
return typeof logLevel === "string" && logLevel.toLowerCase() === "debug";
|
|
3607
|
+
}
|
|
2912
3608
|
getOpenAPIDocument() {
|
|
2913
3609
|
if (this.openapiDocCache) {
|
|
2914
3610
|
return this.openapiDocCache;
|
|
@@ -2919,19 +3615,39 @@ class VectorServer {
|
|
|
2919
3615
|
info: this.openapiConfig.info
|
|
2920
3616
|
});
|
|
2921
3617
|
if (!this.openapiWarningsLogged && result.warnings.length > 0) {
|
|
2922
|
-
|
|
2923
|
-
|
|
3618
|
+
if (this.shouldLogOpenAPIConversionWarnings()) {
|
|
3619
|
+
for (const warning of result.warnings) {
|
|
3620
|
+
console.warn(warning);
|
|
3621
|
+
}
|
|
2924
3622
|
}
|
|
2925
3623
|
this.openapiWarningsLogged = true;
|
|
2926
3624
|
}
|
|
2927
3625
|
this.openapiDocCache = result.document;
|
|
2928
3626
|
return this.openapiDocCache;
|
|
2929
3627
|
}
|
|
3628
|
+
getOpenAPIDocumentForDocs() {
|
|
3629
|
+
const exposePaths = this.openapiConfig.docs.exposePaths;
|
|
3630
|
+
const document = this.getOpenAPIDocument();
|
|
3631
|
+
if (!Array.isArray(exposePaths) || exposePaths.length === 0) {
|
|
3632
|
+
return document;
|
|
3633
|
+
}
|
|
3634
|
+
const existingPaths = document.paths && typeof document.paths === "object" && !Array.isArray(document.paths) ? document.paths : {};
|
|
3635
|
+
const filteredPaths = {};
|
|
3636
|
+
for (const [path, value] of Object.entries(existingPaths)) {
|
|
3637
|
+
if (exposePaths.some((pattern) => matchesExposePath(path, pattern))) {
|
|
3638
|
+
filteredPaths[path] = value;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
return {
|
|
3642
|
+
...document,
|
|
3643
|
+
paths: filteredPaths
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
2930
3646
|
getOpenAPIDocsHtmlCacheEntry() {
|
|
2931
3647
|
if (this.openapiDocsHtmlCache) {
|
|
2932
3648
|
return this.openapiDocsHtmlCache;
|
|
2933
3649
|
}
|
|
2934
|
-
const html = renderOpenAPIDocsHtml(this.
|
|
3650
|
+
const html = renderOpenAPIDocsHtml(this.getOpenAPIDocumentForDocs(), this.openapiConfig.path, OPENAPI_TAILWIND_ASSET_PATH, OPENAPI_LOGO_DARK_ASSET_PATH, OPENAPI_LOGO_WHITE_ASSET_PATH, OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH, OPENAPI_FAVICON_32_ASSET_PATH, OPENAPI_FAVICON_16_ASSET_PATH, OPENAPI_WEBMANIFEST_ASSET_PATH);
|
|
2935
3651
|
const gzip = Bun.gzipSync(html);
|
|
2936
3652
|
const etag = `"${Bun.hash(html).toString(16)}"`;
|
|
2937
3653
|
this.openapiDocsHtmlCache = { html, gzip, etag };
|
|
@@ -2949,6 +3665,11 @@ class VectorServer {
|
|
|
2949
3665
|
if (this.openapiConfig.docs.enabled) {
|
|
2950
3666
|
reserved.add(this.openapiConfig.docs.path);
|
|
2951
3667
|
reserved.add(OPENAPI_TAILWIND_ASSET_PATH);
|
|
3668
|
+
reserved.add(OPENAPI_LOGO_DARK_ASSET_PATH);
|
|
3669
|
+
reserved.add(OPENAPI_LOGO_WHITE_ASSET_PATH);
|
|
3670
|
+
for (const asset of OPENAPI_FAVICON_ASSETS) {
|
|
3671
|
+
reserved.add(asset.path);
|
|
3672
|
+
}
|
|
2952
3673
|
}
|
|
2953
3674
|
const methodConflicts = this.router.getRouteDefinitions().filter((route) => reserved.has(route.path)).map((route) => `${route.method} ${route.path}`);
|
|
2954
3675
|
const staticConflicts = Object.entries(this.router.getRouteTable()).filter(([path, value]) => reserved.has(path) && value instanceof Response).map(([path]) => `STATIC ${path}`);
|
|
@@ -3021,6 +3742,71 @@ class VectorServer {
|
|
|
3021
3742
|
}
|
|
3022
3743
|
});
|
|
3023
3744
|
}
|
|
3745
|
+
if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_DARK_ASSET_PATH) {
|
|
3746
|
+
if (!OPENAPI_LOGO_DARK_ASSET_FILE) {
|
|
3747
|
+
if (!this.openapiLogoDarkMissingLogged) {
|
|
3748
|
+
this.openapiLogoDarkMissingLogged = true;
|
|
3749
|
+
console.warn('[OpenAPI] Missing docs runtime asset "logo_dark.svg".');
|
|
3750
|
+
}
|
|
3751
|
+
return new Response("OpenAPI docs runtime asset missing: logo_dark.svg", {
|
|
3752
|
+
status: 404,
|
|
3753
|
+
headers: {
|
|
3754
|
+
"content-type": "text/plain; charset=utf-8",
|
|
3755
|
+
"cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
|
|
3756
|
+
}
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
return new Response(OPENAPI_LOGO_DARK_ASSET_FILE, {
|
|
3760
|
+
status: 200,
|
|
3761
|
+
headers: {
|
|
3762
|
+
"content-type": "image/svg+xml; charset=utf-8",
|
|
3763
|
+
"cache-control": DOCS_ASSET_CACHE_CONTROL
|
|
3764
|
+
}
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_WHITE_ASSET_PATH) {
|
|
3768
|
+
if (!OPENAPI_LOGO_WHITE_ASSET_FILE) {
|
|
3769
|
+
if (!this.openapiLogoWhiteMissingLogged) {
|
|
3770
|
+
this.openapiLogoWhiteMissingLogged = true;
|
|
3771
|
+
console.warn('[OpenAPI] Missing docs runtime asset "logo_white.svg".');
|
|
3772
|
+
}
|
|
3773
|
+
return new Response("OpenAPI docs runtime asset missing: logo_white.svg", {
|
|
3774
|
+
status: 404,
|
|
3775
|
+
headers: {
|
|
3776
|
+
"content-type": "text/plain; charset=utf-8",
|
|
3777
|
+
"cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
return new Response(OPENAPI_LOGO_WHITE_ASSET_FILE, {
|
|
3782
|
+
status: 200,
|
|
3783
|
+
headers: {
|
|
3784
|
+
"content-type": "image/svg+xml; charset=utf-8",
|
|
3785
|
+
"cache-control": DOCS_ASSET_CACHE_CONTROL
|
|
3786
|
+
}
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
if (this.openapiConfig.docs.enabled) {
|
|
3790
|
+
const faviconAsset = OPENAPI_FAVICON_ASSETS.find((asset) => asset.path === pathname);
|
|
3791
|
+
if (faviconAsset) {
|
|
3792
|
+
if (!faviconAsset.file) {
|
|
3793
|
+
return new Response(`OpenAPI docs runtime asset missing: ${faviconAsset.filename}`, {
|
|
3794
|
+
status: 404,
|
|
3795
|
+
headers: {
|
|
3796
|
+
"content-type": "text/plain; charset=utf-8",
|
|
3797
|
+
"cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
|
|
3798
|
+
}
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
return new Response(faviconAsset.file, {
|
|
3802
|
+
status: 200,
|
|
3803
|
+
headers: {
|
|
3804
|
+
"content-type": faviconAsset.contentType,
|
|
3805
|
+
"cache-control": DOCS_ASSET_CACHE_CONTROL
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3024
3810
|
return null;
|
|
3025
3811
|
}
|
|
3026
3812
|
normalizeCorsOptions(options) {
|
|
@@ -3071,7 +3857,7 @@ class VectorServer {
|
|
|
3071
3857
|
reusePort: this.config.reusePort !== false,
|
|
3072
3858
|
routes: this.router.getRouteTable(),
|
|
3073
3859
|
fetch: fallbackFetch,
|
|
3074
|
-
idleTimeout: this.config.idleTimeout
|
|
3860
|
+
idleTimeout: this.config.idleTimeout ?? 60,
|
|
3075
3861
|
error: (error, request) => {
|
|
3076
3862
|
console.error("[ERROR] Server error:", error);
|
|
3077
3863
|
return this.applyCors(new Response("Internal Server Error", { status: 500 }), request);
|
|
@@ -3134,6 +3920,7 @@ class Vector {
|
|
|
3134
3920
|
routeGenerator = null;
|
|
3135
3921
|
_protectedHandler = null;
|
|
3136
3922
|
_cacheHandler = null;
|
|
3923
|
+
shutdownPromise = null;
|
|
3137
3924
|
constructor() {
|
|
3138
3925
|
this.middlewareManager = new MiddlewareManager;
|
|
3139
3926
|
this.authManager = new AuthManager;
|
|
@@ -3148,14 +3935,22 @@ class Vector {
|
|
|
3148
3935
|
}
|
|
3149
3936
|
setProtectedHandler(handler) {
|
|
3150
3937
|
this._protectedHandler = handler;
|
|
3151
|
-
|
|
3938
|
+
if (handler) {
|
|
3939
|
+
this.authManager.setProtectedHandler(handler);
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
this.authManager.clearProtectedHandler();
|
|
3152
3943
|
}
|
|
3153
3944
|
getProtectedHandler() {
|
|
3154
3945
|
return this._protectedHandler;
|
|
3155
3946
|
}
|
|
3156
3947
|
setCacheHandler(handler) {
|
|
3157
3948
|
this._cacheHandler = handler;
|
|
3158
|
-
|
|
3949
|
+
if (handler) {
|
|
3950
|
+
this.cacheManager.setCacheHandler(handler);
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3953
|
+
this.cacheManager.clearCacheHandler();
|
|
3159
3954
|
}
|
|
3160
3955
|
getCacheHandler() {
|
|
3161
3956
|
return this._cacheHandler;
|
|
@@ -3178,6 +3973,9 @@ class Vector {
|
|
|
3178
3973
|
if (config?.finally) {
|
|
3179
3974
|
this.middlewareManager.addFinally(...config.finally);
|
|
3180
3975
|
}
|
|
3976
|
+
if (typeof this.config.startup === "function") {
|
|
3977
|
+
await this.config.startup();
|
|
3978
|
+
}
|
|
3181
3979
|
if (this.config.autoDiscover !== false) {
|
|
3182
3980
|
await this.discoverRoutes();
|
|
3183
3981
|
}
|
|
@@ -3260,6 +4058,22 @@ class Vector {
|
|
|
3260
4058
|
this.server = null;
|
|
3261
4059
|
}
|
|
3262
4060
|
}
|
|
4061
|
+
async shutdown() {
|
|
4062
|
+
if (this.shutdownPromise) {
|
|
4063
|
+
return this.shutdownPromise;
|
|
4064
|
+
}
|
|
4065
|
+
this.shutdownPromise = (async () => {
|
|
4066
|
+
this.stop();
|
|
4067
|
+
if (typeof this.config.shutdown === "function") {
|
|
4068
|
+
await this.config.shutdown();
|
|
4069
|
+
}
|
|
4070
|
+
})();
|
|
4071
|
+
try {
|
|
4072
|
+
await this.shutdownPromise;
|
|
4073
|
+
} finally {
|
|
4074
|
+
this.shutdownPromise = null;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
3263
4077
|
getServer() {
|
|
3264
4078
|
return this.server;
|
|
3265
4079
|
}
|
|
@@ -3278,111 +4092,40 @@ class Vector {
|
|
|
3278
4092
|
}
|
|
3279
4093
|
var getVectorInstance = Vector.getInstance;
|
|
3280
4094
|
|
|
3281
|
-
// src/
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
config =
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
if (this.config) {
|
|
3316
|
-
config.port = this.config.port;
|
|
3317
|
-
config.hostname = this.config.hostname;
|
|
3318
|
-
config.reusePort = this.config.reusePort;
|
|
3319
|
-
config.development = this.config.development;
|
|
3320
|
-
config.routesDir = this.config.routesDir || "./routes";
|
|
3321
|
-
config.idleTimeout = this.config.idleTimeout;
|
|
3322
|
-
config.defaults = this.config.defaults;
|
|
3323
|
-
config.openapi = this.config.openapi;
|
|
3324
|
-
}
|
|
3325
|
-
config.autoDiscover = true;
|
|
3326
|
-
if (this.config?.cors) {
|
|
3327
|
-
if (typeof this.config.cors === "boolean") {
|
|
3328
|
-
config.cors = this.config.cors ? {
|
|
3329
|
-
origin: "*",
|
|
3330
|
-
credentials: true,
|
|
3331
|
-
allowHeaders: "Content-Type, Authorization",
|
|
3332
|
-
allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
3333
|
-
exposeHeaders: "Authorization",
|
|
3334
|
-
maxAge: 86400
|
|
3335
|
-
} : undefined;
|
|
3336
|
-
} else {
|
|
3337
|
-
config.cors = this.config.cors;
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
if (this.config?.before) {
|
|
3341
|
-
config.before = this.config.before;
|
|
3342
|
-
}
|
|
3343
|
-
if (this.config?.after) {
|
|
3344
|
-
config.finally = this.config.after;
|
|
3345
|
-
}
|
|
3346
|
-
return config;
|
|
3347
|
-
}
|
|
3348
|
-
async loadAuthHandler() {
|
|
3349
|
-
return this.config?.auth || null;
|
|
3350
|
-
}
|
|
3351
|
-
async loadCacheHandler() {
|
|
3352
|
-
return this.config?.cache || null;
|
|
3353
|
-
}
|
|
3354
|
-
getConfig() {
|
|
3355
|
-
return this.config;
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
// src/cli/option-resolution.ts
|
|
3360
|
-
function resolveRoutesDir(configRoutesDir, hasRoutesOption, cliRoutes) {
|
|
3361
|
-
if (hasRoutesOption) {
|
|
3362
|
-
return cliRoutes;
|
|
3363
|
-
}
|
|
3364
|
-
return configRoutesDir ?? cliRoutes;
|
|
3365
|
-
}
|
|
3366
|
-
function parseAndValidatePort(value) {
|
|
3367
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
3368
|
-
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
3369
|
-
throw new Error(`Invalid port value: ${String(value)}`);
|
|
3370
|
-
}
|
|
3371
|
-
return parsed;
|
|
3372
|
-
}
|
|
3373
|
-
function resolvePort(configPort, hasPortOption, cliPort) {
|
|
3374
|
-
if (hasPortOption) {
|
|
3375
|
-
return parseAndValidatePort(cliPort);
|
|
3376
|
-
}
|
|
3377
|
-
const resolved = configPort ?? cliPort;
|
|
3378
|
-
return parseAndValidatePort(resolved);
|
|
3379
|
-
}
|
|
3380
|
-
function resolveHost(configHost, hasHostOption, cliHost) {
|
|
3381
|
-
const resolved = hasHostOption ? cliHost : configHost ?? cliHost;
|
|
3382
|
-
if (typeof resolved !== "string" || resolved.length === 0) {
|
|
3383
|
-
throw new Error(`Invalid host value: ${String(resolved)}`);
|
|
3384
|
-
}
|
|
3385
|
-
return resolved;
|
|
4095
|
+
// src/start-vector.ts
|
|
4096
|
+
async function startVector(options = {}) {
|
|
4097
|
+
const configLoader = new ConfigLoader(options.configPath);
|
|
4098
|
+
const loadedConfig = await configLoader.load();
|
|
4099
|
+
const configSource = configLoader.getConfigSource();
|
|
4100
|
+
let config = { ...loadedConfig };
|
|
4101
|
+
if (options.mutateConfig) {
|
|
4102
|
+
config = await options.mutateConfig(config, { configSource });
|
|
4103
|
+
}
|
|
4104
|
+
if (options.config) {
|
|
4105
|
+
config = { ...config, ...options.config };
|
|
4106
|
+
}
|
|
4107
|
+
if (options.autoDiscover !== undefined) {
|
|
4108
|
+
config.autoDiscover = options.autoDiscover;
|
|
4109
|
+
}
|
|
4110
|
+
const vector = getVectorInstance();
|
|
4111
|
+
const resolvedProtectedHandler = options.protectedHandler !== undefined ? options.protectedHandler : await configLoader.loadAuthHandler();
|
|
4112
|
+
const resolvedCacheHandler = options.cacheHandler !== undefined ? options.cacheHandler : await configLoader.loadCacheHandler();
|
|
4113
|
+
vector.setProtectedHandler(resolvedProtectedHandler ?? null);
|
|
4114
|
+
vector.setCacheHandler(resolvedCacheHandler ?? null);
|
|
4115
|
+
const server = await vector.startServer(config);
|
|
4116
|
+
const effectiveConfig = {
|
|
4117
|
+
...config,
|
|
4118
|
+
port: server.port ?? config.port ?? DEFAULT_CONFIG.PORT,
|
|
4119
|
+
hostname: server.hostname || config.hostname || DEFAULT_CONFIG.HOSTNAME,
|
|
4120
|
+
reusePort: config.reusePort !== false,
|
|
4121
|
+
idleTimeout: config.idleTimeout ?? 60
|
|
4122
|
+
};
|
|
4123
|
+
return {
|
|
4124
|
+
server,
|
|
4125
|
+
config: effectiveConfig,
|
|
4126
|
+
stop: () => vector.stop(),
|
|
4127
|
+
shutdown: () => vector.shutdown()
|
|
4128
|
+
};
|
|
3386
4129
|
}
|
|
3387
4130
|
|
|
3388
4131
|
// src/cli/index.ts
|
|
@@ -3429,7 +4172,8 @@ var hasPortOption = args.some((arg) => arg === "--port" || arg === "-p" || arg.s
|
|
|
3429
4172
|
async function runDev() {
|
|
3430
4173
|
const isDev = command === "dev";
|
|
3431
4174
|
let server = null;
|
|
3432
|
-
let
|
|
4175
|
+
let app = null;
|
|
4176
|
+
let removeShutdownHandlers = null;
|
|
3433
4177
|
async function startServer() {
|
|
3434
4178
|
const timeoutPromise = new Promise((_, reject) => {
|
|
3435
4179
|
setTimeout(() => {
|
|
@@ -3438,34 +4182,30 @@ async function runDev() {
|
|
|
3438
4182
|
});
|
|
3439
4183
|
const serverStartPromise = (async () => {
|
|
3440
4184
|
const explicitConfigPath = values.config;
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
const
|
|
3465
|
-
if (cacheHandler) {
|
|
3466
|
-
vector.setCacheHandler(cacheHandler);
|
|
3467
|
-
}
|
|
3468
|
-
server = await vector.startServer(config);
|
|
4185
|
+
app = await startVector({
|
|
4186
|
+
configPath: explicitConfigPath,
|
|
4187
|
+
mutateConfig: (loadedConfig) => {
|
|
4188
|
+
const config2 = { ...loadedConfig };
|
|
4189
|
+
config2.port = resolvePort(config2.port, hasPortOption, values.port);
|
|
4190
|
+
config2.hostname = resolveHost(config2.hostname, hasHostOption, values.host);
|
|
4191
|
+
config2.routesDir = resolveRoutesDir(config2.routesDir, hasRoutesOption, values.routes);
|
|
4192
|
+
config2.development = config2.development ?? isDev;
|
|
4193
|
+
config2.autoDiscover = true;
|
|
4194
|
+
if (config2.cors === undefined && values.cors) {
|
|
4195
|
+
config2.cors = {
|
|
4196
|
+
origin: "*",
|
|
4197
|
+
credentials: true,
|
|
4198
|
+
allowHeaders: "Content-Type, Authorization",
|
|
4199
|
+
allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
4200
|
+
exposeHeaders: "Authorization",
|
|
4201
|
+
maxAge: 86400
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
return config2;
|
|
4205
|
+
}
|
|
4206
|
+
});
|
|
4207
|
+
server = app.server;
|
|
4208
|
+
const config = app.config;
|
|
3469
4209
|
if (!server || !server.port) {
|
|
3470
4210
|
throw new Error("Server started but is not responding correctly");
|
|
3471
4211
|
}
|
|
@@ -3474,13 +4214,18 @@ async function runDev() {
|
|
|
3474
4214
|
console.log(`
|
|
3475
4215
|
Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
|
|
3476
4216
|
`);
|
|
3477
|
-
return { server,
|
|
4217
|
+
return { server, app, config };
|
|
3478
4218
|
})();
|
|
3479
4219
|
return await Promise.race([serverStartPromise, timeoutPromise]);
|
|
3480
4220
|
}
|
|
3481
4221
|
try {
|
|
3482
4222
|
const result = await startServer();
|
|
3483
4223
|
server = result.server;
|
|
4224
|
+
if (!removeShutdownHandlers) {
|
|
4225
|
+
removeShutdownHandlers = installGracefulShutdownHandlers({
|
|
4226
|
+
getTarget: () => app
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
3484
4229
|
if (isDev && values.watch) {
|
|
3485
4230
|
try {
|
|
3486
4231
|
let reloadTimeout = null;
|
|
@@ -3504,14 +4249,14 @@ Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
|
|
|
3504
4249
|
isReloading = true;
|
|
3505
4250
|
lastReloadTime = Date.now();
|
|
3506
4251
|
changedFiles.clear();
|
|
3507
|
-
if (
|
|
3508
|
-
|
|
4252
|
+
if (app) {
|
|
4253
|
+
app.stop();
|
|
3509
4254
|
}
|
|
3510
4255
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
3511
4256
|
try {
|
|
3512
4257
|
const result2 = await startServer();
|
|
3513
4258
|
server = result2.server;
|
|
3514
|
-
|
|
4259
|
+
app = result2.app;
|
|
3515
4260
|
} catch (error) {
|
|
3516
4261
|
console.error(`
|
|
3517
4262
|
[Reload Error]`, error.message || error);
|