vector-framework 1.2.0 → 1.2.1
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 +877 -218
- 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 +3 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +227 -7
- 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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +147 -41
- 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 +233 -4
- 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 +3 -1
- 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 +289 -7
- 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 +231 -4
- 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);
|
|
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);
|
|
98
|
+
}
|
|
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 = {};
|
|
24
114
|
}
|
|
115
|
+
return await this.buildLegacyConfig();
|
|
25
116
|
}
|
|
26
|
-
|
|
27
|
-
return
|
|
117
|
+
getConfigSource() {
|
|
118
|
+
return this.configSource;
|
|
28
119
|
}
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
|
@@ -2635,8 +2906,9 @@ function convertInputSchema(routePath, inputSchema, target, warnings) {
|
|
|
2635
2906
|
try {
|
|
2636
2907
|
return inputSchema["~standard"].jsonSchema.input({ target });
|
|
2637
2908
|
} catch (error) {
|
|
2638
|
-
warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}
|
|
2639
|
-
|
|
2909
|
+
warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
|
|
2910
|
+
const fallback = buildFallbackJSONSchema(inputSchema);
|
|
2911
|
+
return isEmptyObjectSchema(fallback) ? null : fallback;
|
|
2640
2912
|
}
|
|
2641
2913
|
}
|
|
2642
2914
|
function convertOutputSchema(routePath, statusCode, outputSchema, target, warnings) {
|
|
@@ -2646,13 +2918,233 @@ function convertOutputSchema(routePath, statusCode, outputSchema, target, warnin
|
|
|
2646
2918
|
try {
|
|
2647
2919
|
return outputSchema["~standard"].jsonSchema.output({ target });
|
|
2648
2920
|
} catch (error) {
|
|
2649
|
-
warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}
|
|
2650
|
-
return
|
|
2921
|
+
warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
|
|
2922
|
+
return buildFallbackJSONSchema(outputSchema);
|
|
2651
2923
|
}
|
|
2652
2924
|
}
|
|
2653
2925
|
function isRecord(value) {
|
|
2654
2926
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2655
2927
|
}
|
|
2928
|
+
function isEmptyObjectSchema(value) {
|
|
2929
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
2930
|
+
}
|
|
2931
|
+
function getValidatorSchemaDef(schema) {
|
|
2932
|
+
if (!schema || typeof schema !== "object")
|
|
2933
|
+
return null;
|
|
2934
|
+
const value = schema;
|
|
2935
|
+
if (isRecord(value._def))
|
|
2936
|
+
return value._def;
|
|
2937
|
+
if (isRecord(value._zod) && isRecord(value._zod.def)) {
|
|
2938
|
+
return value._zod.def;
|
|
2939
|
+
}
|
|
2940
|
+
return null;
|
|
2941
|
+
}
|
|
2942
|
+
function getSchemaKind(def) {
|
|
2943
|
+
if (!def)
|
|
2944
|
+
return null;
|
|
2945
|
+
const typeName = def.typeName;
|
|
2946
|
+
if (typeof typeName === "string")
|
|
2947
|
+
return typeName;
|
|
2948
|
+
const type = def.type;
|
|
2949
|
+
if (typeof type === "string")
|
|
2950
|
+
return type;
|
|
2951
|
+
return null;
|
|
2952
|
+
}
|
|
2953
|
+
function pickSchemaChild(def) {
|
|
2954
|
+
const candidates = ["innerType", "schema", "type", "out", "in", "left", "right"];
|
|
2955
|
+
for (const key of candidates) {
|
|
2956
|
+
if (key in def)
|
|
2957
|
+
return def[key];
|
|
2958
|
+
}
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
function pickSchemaObjectCandidate(def, keys) {
|
|
2962
|
+
for (const key of keys) {
|
|
2963
|
+
const value = def[key];
|
|
2964
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2965
|
+
return value;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
function isOptionalWrapperKind(kind) {
|
|
2971
|
+
if (!kind)
|
|
2972
|
+
return false;
|
|
2973
|
+
const lower = kind.toLowerCase();
|
|
2974
|
+
return lower.includes("optional") || lower.includes("default") || lower.includes("catch");
|
|
2975
|
+
}
|
|
2976
|
+
function unwrapOptionalForRequired(schema) {
|
|
2977
|
+
let current = schema;
|
|
2978
|
+
let optional = false;
|
|
2979
|
+
let guard = 0;
|
|
2980
|
+
while (guard < 8) {
|
|
2981
|
+
guard += 1;
|
|
2982
|
+
const def = getValidatorSchemaDef(current);
|
|
2983
|
+
const kind = getSchemaKind(def);
|
|
2984
|
+
if (!def || !isOptionalWrapperKind(kind))
|
|
2985
|
+
break;
|
|
2986
|
+
optional = true;
|
|
2987
|
+
const inner = pickSchemaChild(def);
|
|
2988
|
+
if (!inner)
|
|
2989
|
+
break;
|
|
2990
|
+
current = inner;
|
|
2991
|
+
}
|
|
2992
|
+
return { schema: current, optional };
|
|
2993
|
+
}
|
|
2994
|
+
function getObjectShape(def) {
|
|
2995
|
+
const rawShape = def.shape;
|
|
2996
|
+
if (typeof rawShape === "function") {
|
|
2997
|
+
try {
|
|
2998
|
+
const resolved = rawShape();
|
|
2999
|
+
return isRecord(resolved) ? resolved : {};
|
|
3000
|
+
} catch {
|
|
3001
|
+
return {};
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
return isRecord(rawShape) ? rawShape : {};
|
|
3005
|
+
}
|
|
3006
|
+
function mapPrimitiveKind(kind) {
|
|
3007
|
+
const lower = kind.toLowerCase();
|
|
3008
|
+
if (lower.includes("string"))
|
|
3009
|
+
return { type: "string" };
|
|
3010
|
+
if (lower.includes("number"))
|
|
3011
|
+
return { type: "number" };
|
|
3012
|
+
if (lower.includes("boolean"))
|
|
3013
|
+
return { type: "boolean" };
|
|
3014
|
+
if (lower.includes("bigint"))
|
|
3015
|
+
return { type: "string" };
|
|
3016
|
+
if (lower.includes("null"))
|
|
3017
|
+
return { type: "null" };
|
|
3018
|
+
if (lower.includes("any") || lower.includes("unknown") || lower.includes("never"))
|
|
3019
|
+
return {};
|
|
3020
|
+
if (lower.includes("date"))
|
|
3021
|
+
return { type: "string", format: "date-time" };
|
|
3022
|
+
if (lower.includes("custom"))
|
|
3023
|
+
return { type: "object", additionalProperties: true };
|
|
3024
|
+
return null;
|
|
3025
|
+
}
|
|
3026
|
+
function buildIntrospectedFallbackJSONSchema(schema, seen = new WeakSet) {
|
|
3027
|
+
if (!schema || typeof schema !== "object")
|
|
3028
|
+
return {};
|
|
3029
|
+
if (seen.has(schema))
|
|
3030
|
+
return {};
|
|
3031
|
+
seen.add(schema);
|
|
3032
|
+
const def = getValidatorSchemaDef(schema);
|
|
3033
|
+
const kind = getSchemaKind(def);
|
|
3034
|
+
if (!def || !kind)
|
|
3035
|
+
return {};
|
|
3036
|
+
const primitive = mapPrimitiveKind(kind);
|
|
3037
|
+
if (primitive)
|
|
3038
|
+
return primitive;
|
|
3039
|
+
const lower = kind.toLowerCase();
|
|
3040
|
+
if (lower.includes("object")) {
|
|
3041
|
+
const shape = getObjectShape(def);
|
|
3042
|
+
const properties = {};
|
|
3043
|
+
const required = [];
|
|
3044
|
+
for (const [key, child2] of Object.entries(shape)) {
|
|
3045
|
+
const unwrapped = unwrapOptionalForRequired(child2);
|
|
3046
|
+
properties[key] = buildIntrospectedFallbackJSONSchema(unwrapped.schema, seen);
|
|
3047
|
+
if (!unwrapped.optional)
|
|
3048
|
+
required.push(key);
|
|
3049
|
+
}
|
|
3050
|
+
const out = {
|
|
3051
|
+
type: "object",
|
|
3052
|
+
properties,
|
|
3053
|
+
additionalProperties: true
|
|
3054
|
+
};
|
|
3055
|
+
if (required.length > 0) {
|
|
3056
|
+
out.required = required;
|
|
3057
|
+
}
|
|
3058
|
+
return out;
|
|
3059
|
+
}
|
|
3060
|
+
if (lower.includes("array")) {
|
|
3061
|
+
const itemSchema = pickSchemaObjectCandidate(def, ["element", "items", "innerType", "type"]) ?? {};
|
|
3062
|
+
return {
|
|
3063
|
+
type: "array",
|
|
3064
|
+
items: buildIntrospectedFallbackJSONSchema(itemSchema, seen)
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
if (lower.includes("record")) {
|
|
3068
|
+
const valueType = def.valueType ?? def.valueSchema;
|
|
3069
|
+
return {
|
|
3070
|
+
type: "object",
|
|
3071
|
+
additionalProperties: valueType ? buildIntrospectedFallbackJSONSchema(valueType, seen) : true
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
if (lower.includes("tuple")) {
|
|
3075
|
+
const items = Array.isArray(def.items) ? def.items : [];
|
|
3076
|
+
const prefixItems = items.map((item) => buildIntrospectedFallbackJSONSchema(item, seen));
|
|
3077
|
+
return {
|
|
3078
|
+
type: "array",
|
|
3079
|
+
prefixItems,
|
|
3080
|
+
minItems: prefixItems.length,
|
|
3081
|
+
maxItems: prefixItems.length
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
if (lower.includes("union")) {
|
|
3085
|
+
const options = def.options ?? def.schemas ?? [];
|
|
3086
|
+
if (!Array.isArray(options) || options.length === 0)
|
|
3087
|
+
return {};
|
|
3088
|
+
return {
|
|
3089
|
+
anyOf: options.map((option) => buildIntrospectedFallbackJSONSchema(option, seen))
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
if (lower.includes("intersection")) {
|
|
3093
|
+
const left = def.left;
|
|
3094
|
+
const right = def.right;
|
|
3095
|
+
if (!left || !right)
|
|
3096
|
+
return {};
|
|
3097
|
+
return {
|
|
3098
|
+
allOf: [buildIntrospectedFallbackJSONSchema(left, seen), buildIntrospectedFallbackJSONSchema(right, seen)]
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
if (lower.includes("enum")) {
|
|
3102
|
+
const values = def.values;
|
|
3103
|
+
if (Array.isArray(values))
|
|
3104
|
+
return { enum: values };
|
|
3105
|
+
if (values && typeof values === "object")
|
|
3106
|
+
return { enum: Object.values(values) };
|
|
3107
|
+
return {};
|
|
3108
|
+
}
|
|
3109
|
+
if (lower.includes("literal")) {
|
|
3110
|
+
const value = def.value;
|
|
3111
|
+
if (value === undefined)
|
|
3112
|
+
return {};
|
|
3113
|
+
const valueType = value === null ? "null" : typeof value;
|
|
3114
|
+
if (valueType === "string" || valueType === "number" || valueType === "boolean" || valueType === "null") {
|
|
3115
|
+
return { type: valueType, const: value };
|
|
3116
|
+
}
|
|
3117
|
+
return { const: value };
|
|
3118
|
+
}
|
|
3119
|
+
if (lower.includes("nullable")) {
|
|
3120
|
+
const inner = pickSchemaChild(def);
|
|
3121
|
+
if (!inner)
|
|
3122
|
+
return {};
|
|
3123
|
+
return {
|
|
3124
|
+
anyOf: [buildIntrospectedFallbackJSONSchema(inner, seen), { type: "null" }]
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
if (lower.includes("lazy")) {
|
|
3128
|
+
const getter = def.getter;
|
|
3129
|
+
if (typeof getter !== "function")
|
|
3130
|
+
return {};
|
|
3131
|
+
try {
|
|
3132
|
+
return buildIntrospectedFallbackJSONSchema(getter(), seen);
|
|
3133
|
+
} catch {
|
|
3134
|
+
return {};
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const child = pickSchemaChild(def);
|
|
3138
|
+
if (child)
|
|
3139
|
+
return buildIntrospectedFallbackJSONSchema(child, seen);
|
|
3140
|
+
return {};
|
|
3141
|
+
}
|
|
3142
|
+
function buildFallbackJSONSchema(schema) {
|
|
3143
|
+
const def = getValidatorSchemaDef(schema);
|
|
3144
|
+
if (!def)
|
|
3145
|
+
return {};
|
|
3146
|
+
return buildIntrospectedFallbackJSONSchema(schema);
|
|
3147
|
+
}
|
|
2656
3148
|
function addStructuredInputToOperation(operation, inputJSONSchema) {
|
|
2657
3149
|
if (!isRecord(inputJSONSchema))
|
|
2658
3150
|
return;
|
|
@@ -2803,38 +3295,155 @@ function generateOpenAPIDocument(routes, options) {
|
|
|
2803
3295
|
|
|
2804
3296
|
// src/core/server.ts
|
|
2805
3297
|
var OPENAPI_TAILWIND_ASSET_PATH = "/_vector/openapi/tailwindcdn.js";
|
|
3298
|
+
var OPENAPI_LOGO_DARK_ASSET_PATH = "/_vector/openapi/logo_dark.svg";
|
|
3299
|
+
var OPENAPI_LOGO_WHITE_ASSET_PATH = "/_vector/openapi/logo_white.svg";
|
|
3300
|
+
var OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH = "/_vector/openapi/favicon/apple-touch-icon.png";
|
|
3301
|
+
var OPENAPI_FAVICON_32_ASSET_PATH = "/_vector/openapi/favicon/favicon-32x32.png";
|
|
3302
|
+
var OPENAPI_FAVICON_16_ASSET_PATH = "/_vector/openapi/favicon/favicon-16x16.png";
|
|
3303
|
+
var OPENAPI_FAVICON_ICO_ASSET_PATH = "/_vector/openapi/favicon/favicon.ico";
|
|
3304
|
+
var OPENAPI_WEBMANIFEST_ASSET_PATH = "/_vector/openapi/favicon/site.webmanifest";
|
|
3305
|
+
var OPENAPI_ANDROID_192_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-192x192.png";
|
|
3306
|
+
var OPENAPI_ANDROID_512_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-512x512.png";
|
|
2806
3307
|
var OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES = [
|
|
2807
3308
|
"../openapi/assets/tailwindcdn.js",
|
|
2808
3309
|
"../src/openapi/assets/tailwindcdn.js",
|
|
2809
3310
|
"../../src/openapi/assets/tailwindcdn.js"
|
|
2810
3311
|
];
|
|
3312
|
+
var OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES = [
|
|
3313
|
+
"../openapi/assets/logo_dark.svg",
|
|
3314
|
+
"../src/openapi/assets/logo_dark.svg",
|
|
3315
|
+
"../../src/openapi/assets/logo_dark.svg"
|
|
3316
|
+
];
|
|
3317
|
+
var OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES = [
|
|
3318
|
+
"../openapi/assets/logo_white.svg",
|
|
3319
|
+
"../src/openapi/assets/logo_white.svg",
|
|
3320
|
+
"../../src/openapi/assets/logo_white.svg"
|
|
3321
|
+
];
|
|
2811
3322
|
var OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES = [
|
|
2812
3323
|
"src/openapi/assets/tailwindcdn.js",
|
|
2813
3324
|
"openapi/assets/tailwindcdn.js",
|
|
2814
3325
|
"dist/openapi/assets/tailwindcdn.js"
|
|
2815
3326
|
];
|
|
3327
|
+
var OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES = [
|
|
3328
|
+
"src/openapi/assets/logo_dark.svg",
|
|
3329
|
+
"openapi/assets/logo_dark.svg",
|
|
3330
|
+
"dist/openapi/assets/logo_dark.svg"
|
|
3331
|
+
];
|
|
3332
|
+
var OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES = [
|
|
3333
|
+
"src/openapi/assets/logo_white.svg",
|
|
3334
|
+
"openapi/assets/logo_white.svg",
|
|
3335
|
+
"dist/openapi/assets/logo_white.svg"
|
|
3336
|
+
];
|
|
3337
|
+
var OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES = [
|
|
3338
|
+
"../openapi/assets/favicon",
|
|
3339
|
+
"../src/openapi/assets/favicon",
|
|
3340
|
+
"../../src/openapi/assets/favicon"
|
|
3341
|
+
];
|
|
3342
|
+
var OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES = [
|
|
3343
|
+
"src/openapi/assets/favicon",
|
|
3344
|
+
"openapi/assets/favicon",
|
|
3345
|
+
"dist/openapi/assets/favicon"
|
|
3346
|
+
];
|
|
2816
3347
|
var OPENAPI_TAILWIND_ASSET_INLINE_FALLBACK = "/* OpenAPI docs runtime asset missing: tailwind disabled */";
|
|
2817
|
-
function
|
|
2818
|
-
|
|
3348
|
+
function buildOpenAPIAssetCandidatePaths(bases, filename) {
|
|
3349
|
+
return bases.map((base) => `${base}/${filename}`);
|
|
3350
|
+
}
|
|
3351
|
+
function resolveOpenAPIAssetFile(relativeCandidates, cwdCandidates) {
|
|
3352
|
+
for (const relativePath of relativeCandidates) {
|
|
2819
3353
|
try {
|
|
2820
3354
|
const fileUrl = new URL(relativePath, import.meta.url);
|
|
2821
|
-
if (
|
|
3355
|
+
if (existsSync3(fileUrl)) {
|
|
2822
3356
|
return Bun.file(fileUrl);
|
|
2823
3357
|
}
|
|
2824
3358
|
} catch {}
|
|
2825
3359
|
}
|
|
2826
3360
|
const cwd = process.cwd();
|
|
2827
|
-
for (const relativePath of
|
|
3361
|
+
for (const relativePath of cwdCandidates) {
|
|
2828
3362
|
const absolutePath = join2(cwd, relativePath);
|
|
2829
|
-
if (
|
|
3363
|
+
if (existsSync3(absolutePath)) {
|
|
2830
3364
|
return Bun.file(absolutePath);
|
|
2831
3365
|
}
|
|
2832
3366
|
}
|
|
2833
3367
|
return null;
|
|
2834
3368
|
}
|
|
2835
|
-
var OPENAPI_TAILWIND_ASSET_FILE =
|
|
3369
|
+
var OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES, OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES);
|
|
3370
|
+
var OPENAPI_LOGO_DARK_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES);
|
|
3371
|
+
var OPENAPI_LOGO_WHITE_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES);
|
|
3372
|
+
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"));
|
|
3373
|
+
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"));
|
|
3374
|
+
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"));
|
|
3375
|
+
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"));
|
|
3376
|
+
var OPENAPI_WEBMANIFEST_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "site.webmanifest"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "site.webmanifest"));
|
|
3377
|
+
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"));
|
|
3378
|
+
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"));
|
|
3379
|
+
var OPENAPI_FAVICON_ASSETS = [
|
|
3380
|
+
{
|
|
3381
|
+
path: OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH,
|
|
3382
|
+
file: OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE,
|
|
3383
|
+
contentType: "image/png",
|
|
3384
|
+
filename: "apple-touch-icon.png"
|
|
3385
|
+
},
|
|
3386
|
+
{
|
|
3387
|
+
path: OPENAPI_FAVICON_32_ASSET_PATH,
|
|
3388
|
+
file: OPENAPI_FAVICON_32_ASSET_FILE,
|
|
3389
|
+
contentType: "image/png",
|
|
3390
|
+
filename: "favicon-32x32.png"
|
|
3391
|
+
},
|
|
3392
|
+
{
|
|
3393
|
+
path: OPENAPI_FAVICON_16_ASSET_PATH,
|
|
3394
|
+
file: OPENAPI_FAVICON_16_ASSET_FILE,
|
|
3395
|
+
contentType: "image/png",
|
|
3396
|
+
filename: "favicon-16x16.png"
|
|
3397
|
+
},
|
|
3398
|
+
{
|
|
3399
|
+
path: OPENAPI_FAVICON_ICO_ASSET_PATH,
|
|
3400
|
+
file: OPENAPI_FAVICON_ICO_ASSET_FILE,
|
|
3401
|
+
contentType: "image/x-icon",
|
|
3402
|
+
filename: "favicon.ico"
|
|
3403
|
+
},
|
|
3404
|
+
{
|
|
3405
|
+
path: OPENAPI_WEBMANIFEST_ASSET_PATH,
|
|
3406
|
+
file: OPENAPI_WEBMANIFEST_ASSET_FILE,
|
|
3407
|
+
contentType: "application/manifest+json; charset=utf-8",
|
|
3408
|
+
filename: "site.webmanifest"
|
|
3409
|
+
},
|
|
3410
|
+
{
|
|
3411
|
+
path: OPENAPI_ANDROID_192_ASSET_PATH,
|
|
3412
|
+
file: OPENAPI_ANDROID_192_ASSET_FILE,
|
|
3413
|
+
contentType: "image/png",
|
|
3414
|
+
filename: "android-chrome-192x192.png"
|
|
3415
|
+
},
|
|
3416
|
+
{
|
|
3417
|
+
path: OPENAPI_ANDROID_512_ASSET_PATH,
|
|
3418
|
+
file: OPENAPI_ANDROID_512_ASSET_FILE,
|
|
3419
|
+
contentType: "image/png",
|
|
3420
|
+
filename: "android-chrome-512x512.png"
|
|
3421
|
+
}
|
|
3422
|
+
];
|
|
2836
3423
|
var DOCS_HTML_CACHE_CONTROL = "public, max-age=0, must-revalidate";
|
|
2837
3424
|
var DOCS_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
3425
|
+
var DOCS_ASSET_ERROR_CACHE_CONTROL = "no-store";
|
|
3426
|
+
function escapeRegex(value) {
|
|
3427
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3428
|
+
}
|
|
3429
|
+
function wildcardPatternToRegex(pattern) {
|
|
3430
|
+
let regexSource = "^";
|
|
3431
|
+
for (const char of pattern) {
|
|
3432
|
+
if (char === "*") {
|
|
3433
|
+
regexSource += ".*";
|
|
3434
|
+
continue;
|
|
3435
|
+
}
|
|
3436
|
+
regexSource += escapeRegex(char);
|
|
3437
|
+
}
|
|
3438
|
+
regexSource += "$";
|
|
3439
|
+
return new RegExp(regexSource);
|
|
3440
|
+
}
|
|
3441
|
+
function matchesExposePath(path, exposePathPattern) {
|
|
3442
|
+
if (!exposePathPattern.includes("*")) {
|
|
3443
|
+
return path === exposePathPattern;
|
|
3444
|
+
}
|
|
3445
|
+
return wildcardPatternToRegex(exposePathPattern).test(path);
|
|
3446
|
+
}
|
|
2838
3447
|
|
|
2839
3448
|
class VectorServer {
|
|
2840
3449
|
server = null;
|
|
@@ -2845,6 +3454,8 @@ class VectorServer {
|
|
|
2845
3454
|
openapiDocsHtmlCache = null;
|
|
2846
3455
|
openapiWarningsLogged = false;
|
|
2847
3456
|
openapiTailwindMissingLogged = false;
|
|
3457
|
+
openapiLogoDarkMissingLogged = false;
|
|
3458
|
+
openapiLogoWhiteMissingLogged = false;
|
|
2848
3459
|
corsHandler = null;
|
|
2849
3460
|
corsHeadersEntries = null;
|
|
2850
3461
|
constructor(router, config) {
|
|
@@ -2894,9 +3505,10 @@ class VectorServer {
|
|
|
2894
3505
|
}
|
|
2895
3506
|
const openapiObject = openapi || {};
|
|
2896
3507
|
const docsValue = openapiObject.docs;
|
|
2897
|
-
const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs" } : {
|
|
3508
|
+
const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs", exposePaths: undefined } : {
|
|
2898
3509
|
enabled: docsValue?.enabled === true,
|
|
2899
|
-
path: docsValue?.path || "/docs"
|
|
3510
|
+
path: docsValue?.path || "/docs",
|
|
3511
|
+
exposePaths: Array.isArray(docsValue?.exposePaths) ? docsValue.exposePaths.map((path) => typeof path === "string" ? path.trim() : "").filter((path) => path.length > 0) : undefined
|
|
2900
3512
|
};
|
|
2901
3513
|
return {
|
|
2902
3514
|
enabled: openapiObject.enabled ?? defaultEnabled,
|
|
@@ -2927,11 +3539,29 @@ class VectorServer {
|
|
|
2927
3539
|
this.openapiDocCache = result.document;
|
|
2928
3540
|
return this.openapiDocCache;
|
|
2929
3541
|
}
|
|
3542
|
+
getOpenAPIDocumentForDocs() {
|
|
3543
|
+
const exposePaths = this.openapiConfig.docs.exposePaths;
|
|
3544
|
+
const document = this.getOpenAPIDocument();
|
|
3545
|
+
if (!Array.isArray(exposePaths) || exposePaths.length === 0) {
|
|
3546
|
+
return document;
|
|
3547
|
+
}
|
|
3548
|
+
const existingPaths = document.paths && typeof document.paths === "object" && !Array.isArray(document.paths) ? document.paths : {};
|
|
3549
|
+
const filteredPaths = {};
|
|
3550
|
+
for (const [path, value] of Object.entries(existingPaths)) {
|
|
3551
|
+
if (exposePaths.some((pattern) => matchesExposePath(path, pattern))) {
|
|
3552
|
+
filteredPaths[path] = value;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
return {
|
|
3556
|
+
...document,
|
|
3557
|
+
paths: filteredPaths
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
2930
3560
|
getOpenAPIDocsHtmlCacheEntry() {
|
|
2931
3561
|
if (this.openapiDocsHtmlCache) {
|
|
2932
3562
|
return this.openapiDocsHtmlCache;
|
|
2933
3563
|
}
|
|
2934
|
-
const html = renderOpenAPIDocsHtml(this.
|
|
3564
|
+
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
3565
|
const gzip = Bun.gzipSync(html);
|
|
2936
3566
|
const etag = `"${Bun.hash(html).toString(16)}"`;
|
|
2937
3567
|
this.openapiDocsHtmlCache = { html, gzip, etag };
|
|
@@ -2949,6 +3579,11 @@ class VectorServer {
|
|
|
2949
3579
|
if (this.openapiConfig.docs.enabled) {
|
|
2950
3580
|
reserved.add(this.openapiConfig.docs.path);
|
|
2951
3581
|
reserved.add(OPENAPI_TAILWIND_ASSET_PATH);
|
|
3582
|
+
reserved.add(OPENAPI_LOGO_DARK_ASSET_PATH);
|
|
3583
|
+
reserved.add(OPENAPI_LOGO_WHITE_ASSET_PATH);
|
|
3584
|
+
for (const asset of OPENAPI_FAVICON_ASSETS) {
|
|
3585
|
+
reserved.add(asset.path);
|
|
3586
|
+
}
|
|
2952
3587
|
}
|
|
2953
3588
|
const methodConflicts = this.router.getRouteDefinitions().filter((route) => reserved.has(route.path)).map((route) => `${route.method} ${route.path}`);
|
|
2954
3589
|
const staticConflicts = Object.entries(this.router.getRouteTable()).filter(([path, value]) => reserved.has(path) && value instanceof Response).map(([path]) => `STATIC ${path}`);
|
|
@@ -3021,6 +3656,71 @@ class VectorServer {
|
|
|
3021
3656
|
}
|
|
3022
3657
|
});
|
|
3023
3658
|
}
|
|
3659
|
+
if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_DARK_ASSET_PATH) {
|
|
3660
|
+
if (!OPENAPI_LOGO_DARK_ASSET_FILE) {
|
|
3661
|
+
if (!this.openapiLogoDarkMissingLogged) {
|
|
3662
|
+
this.openapiLogoDarkMissingLogged = true;
|
|
3663
|
+
console.warn('[OpenAPI] Missing docs runtime asset "logo_dark.svg".');
|
|
3664
|
+
}
|
|
3665
|
+
return new Response("OpenAPI docs runtime asset missing: logo_dark.svg", {
|
|
3666
|
+
status: 404,
|
|
3667
|
+
headers: {
|
|
3668
|
+
"content-type": "text/plain; charset=utf-8",
|
|
3669
|
+
"cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
|
|
3670
|
+
}
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
return new Response(OPENAPI_LOGO_DARK_ASSET_FILE, {
|
|
3674
|
+
status: 200,
|
|
3675
|
+
headers: {
|
|
3676
|
+
"content-type": "image/svg+xml; charset=utf-8",
|
|
3677
|
+
"cache-control": DOCS_ASSET_CACHE_CONTROL
|
|
3678
|
+
}
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_WHITE_ASSET_PATH) {
|
|
3682
|
+
if (!OPENAPI_LOGO_WHITE_ASSET_FILE) {
|
|
3683
|
+
if (!this.openapiLogoWhiteMissingLogged) {
|
|
3684
|
+
this.openapiLogoWhiteMissingLogged = true;
|
|
3685
|
+
console.warn('[OpenAPI] Missing docs runtime asset "logo_white.svg".');
|
|
3686
|
+
}
|
|
3687
|
+
return new Response("OpenAPI docs runtime asset missing: logo_white.svg", {
|
|
3688
|
+
status: 404,
|
|
3689
|
+
headers: {
|
|
3690
|
+
"content-type": "text/plain; charset=utf-8",
|
|
3691
|
+
"cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
|
|
3692
|
+
}
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
return new Response(OPENAPI_LOGO_WHITE_ASSET_FILE, {
|
|
3696
|
+
status: 200,
|
|
3697
|
+
headers: {
|
|
3698
|
+
"content-type": "image/svg+xml; charset=utf-8",
|
|
3699
|
+
"cache-control": DOCS_ASSET_CACHE_CONTROL
|
|
3700
|
+
}
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
if (this.openapiConfig.docs.enabled) {
|
|
3704
|
+
const faviconAsset = OPENAPI_FAVICON_ASSETS.find((asset) => asset.path === pathname);
|
|
3705
|
+
if (faviconAsset) {
|
|
3706
|
+
if (!faviconAsset.file) {
|
|
3707
|
+
return new Response(`OpenAPI docs runtime asset missing: ${faviconAsset.filename}`, {
|
|
3708
|
+
status: 404,
|
|
3709
|
+
headers: {
|
|
3710
|
+
"content-type": "text/plain; charset=utf-8",
|
|
3711
|
+
"cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
|
|
3712
|
+
}
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
return new Response(faviconAsset.file, {
|
|
3716
|
+
status: 200,
|
|
3717
|
+
headers: {
|
|
3718
|
+
"content-type": faviconAsset.contentType,
|
|
3719
|
+
"cache-control": DOCS_ASSET_CACHE_CONTROL
|
|
3720
|
+
}
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3024
3724
|
return null;
|
|
3025
3725
|
}
|
|
3026
3726
|
normalizeCorsOptions(options) {
|
|
@@ -3071,7 +3771,7 @@ class VectorServer {
|
|
|
3071
3771
|
reusePort: this.config.reusePort !== false,
|
|
3072
3772
|
routes: this.router.getRouteTable(),
|
|
3073
3773
|
fetch: fallbackFetch,
|
|
3074
|
-
idleTimeout: this.config.idleTimeout
|
|
3774
|
+
idleTimeout: this.config.idleTimeout ?? 60,
|
|
3075
3775
|
error: (error, request) => {
|
|
3076
3776
|
console.error("[ERROR] Server error:", error);
|
|
3077
3777
|
return this.applyCors(new Response("Internal Server Error", { status: 500 }), request);
|
|
@@ -3134,6 +3834,7 @@ class Vector {
|
|
|
3134
3834
|
routeGenerator = null;
|
|
3135
3835
|
_protectedHandler = null;
|
|
3136
3836
|
_cacheHandler = null;
|
|
3837
|
+
shutdownPromise = null;
|
|
3137
3838
|
constructor() {
|
|
3138
3839
|
this.middlewareManager = new MiddlewareManager;
|
|
3139
3840
|
this.authManager = new AuthManager;
|
|
@@ -3148,14 +3849,22 @@ class Vector {
|
|
|
3148
3849
|
}
|
|
3149
3850
|
setProtectedHandler(handler) {
|
|
3150
3851
|
this._protectedHandler = handler;
|
|
3151
|
-
|
|
3852
|
+
if (handler) {
|
|
3853
|
+
this.authManager.setProtectedHandler(handler);
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
this.authManager.clearProtectedHandler();
|
|
3152
3857
|
}
|
|
3153
3858
|
getProtectedHandler() {
|
|
3154
3859
|
return this._protectedHandler;
|
|
3155
3860
|
}
|
|
3156
3861
|
setCacheHandler(handler) {
|
|
3157
3862
|
this._cacheHandler = handler;
|
|
3158
|
-
|
|
3863
|
+
if (handler) {
|
|
3864
|
+
this.cacheManager.setCacheHandler(handler);
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
this.cacheManager.clearCacheHandler();
|
|
3159
3868
|
}
|
|
3160
3869
|
getCacheHandler() {
|
|
3161
3870
|
return this._cacheHandler;
|
|
@@ -3178,6 +3887,9 @@ class Vector {
|
|
|
3178
3887
|
if (config?.finally) {
|
|
3179
3888
|
this.middlewareManager.addFinally(...config.finally);
|
|
3180
3889
|
}
|
|
3890
|
+
if (typeof this.config.startup === "function") {
|
|
3891
|
+
await this.config.startup();
|
|
3892
|
+
}
|
|
3181
3893
|
if (this.config.autoDiscover !== false) {
|
|
3182
3894
|
await this.discoverRoutes();
|
|
3183
3895
|
}
|
|
@@ -3260,6 +3972,22 @@ class Vector {
|
|
|
3260
3972
|
this.server = null;
|
|
3261
3973
|
}
|
|
3262
3974
|
}
|
|
3975
|
+
async shutdown() {
|
|
3976
|
+
if (this.shutdownPromise) {
|
|
3977
|
+
return this.shutdownPromise;
|
|
3978
|
+
}
|
|
3979
|
+
this.shutdownPromise = (async () => {
|
|
3980
|
+
this.stop();
|
|
3981
|
+
if (typeof this.config.shutdown === "function") {
|
|
3982
|
+
await this.config.shutdown();
|
|
3983
|
+
}
|
|
3984
|
+
})();
|
|
3985
|
+
try {
|
|
3986
|
+
await this.shutdownPromise;
|
|
3987
|
+
} finally {
|
|
3988
|
+
this.shutdownPromise = null;
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3263
3991
|
getServer() {
|
|
3264
3992
|
return this.server;
|
|
3265
3993
|
}
|
|
@@ -3278,111 +4006,40 @@ class Vector {
|
|
|
3278
4006
|
}
|
|
3279
4007
|
var getVectorInstance = Vector.getInstance;
|
|
3280
4008
|
|
|
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;
|
|
4009
|
+
// src/start-vector.ts
|
|
4010
|
+
async function startVector(options = {}) {
|
|
4011
|
+
const configLoader = new ConfigLoader(options.configPath);
|
|
4012
|
+
const loadedConfig = await configLoader.load();
|
|
4013
|
+
const configSource = configLoader.getConfigSource();
|
|
4014
|
+
let config = { ...loadedConfig };
|
|
4015
|
+
if (options.mutateConfig) {
|
|
4016
|
+
config = await options.mutateConfig(config, { configSource });
|
|
4017
|
+
}
|
|
4018
|
+
if (options.config) {
|
|
4019
|
+
config = { ...config, ...options.config };
|
|
4020
|
+
}
|
|
4021
|
+
if (options.autoDiscover !== undefined) {
|
|
4022
|
+
config.autoDiscover = options.autoDiscover;
|
|
4023
|
+
}
|
|
4024
|
+
const vector = getVectorInstance();
|
|
4025
|
+
const resolvedProtectedHandler = options.protectedHandler !== undefined ? options.protectedHandler : await configLoader.loadAuthHandler();
|
|
4026
|
+
const resolvedCacheHandler = options.cacheHandler !== undefined ? options.cacheHandler : await configLoader.loadCacheHandler();
|
|
4027
|
+
vector.setProtectedHandler(resolvedProtectedHandler ?? null);
|
|
4028
|
+
vector.setCacheHandler(resolvedCacheHandler ?? null);
|
|
4029
|
+
const server = await vector.startServer(config);
|
|
4030
|
+
const effectiveConfig = {
|
|
4031
|
+
...config,
|
|
4032
|
+
port: server.port ?? config.port ?? DEFAULT_CONFIG.PORT,
|
|
4033
|
+
hostname: server.hostname || config.hostname || DEFAULT_CONFIG.HOSTNAME,
|
|
4034
|
+
reusePort: config.reusePort !== false,
|
|
4035
|
+
idleTimeout: config.idleTimeout ?? 60
|
|
4036
|
+
};
|
|
4037
|
+
return {
|
|
4038
|
+
server,
|
|
4039
|
+
config: effectiveConfig,
|
|
4040
|
+
stop: () => vector.stop(),
|
|
4041
|
+
shutdown: () => vector.shutdown()
|
|
4042
|
+
};
|
|
3386
4043
|
}
|
|
3387
4044
|
|
|
3388
4045
|
// src/cli/index.ts
|
|
@@ -3429,7 +4086,8 @@ var hasPortOption = args.some((arg) => arg === "--port" || arg === "-p" || arg.s
|
|
|
3429
4086
|
async function runDev() {
|
|
3430
4087
|
const isDev = command === "dev";
|
|
3431
4088
|
let server = null;
|
|
3432
|
-
let
|
|
4089
|
+
let app = null;
|
|
4090
|
+
let removeShutdownHandlers = null;
|
|
3433
4091
|
async function startServer() {
|
|
3434
4092
|
const timeoutPromise = new Promise((_, reject) => {
|
|
3435
4093
|
setTimeout(() => {
|
|
@@ -3438,34 +4096,30 @@ async function runDev() {
|
|
|
3438
4096
|
});
|
|
3439
4097
|
const serverStartPromise = (async () => {
|
|
3440
4098
|
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);
|
|
4099
|
+
app = await startVector({
|
|
4100
|
+
configPath: explicitConfigPath,
|
|
4101
|
+
mutateConfig: (loadedConfig) => {
|
|
4102
|
+
const config2 = { ...loadedConfig };
|
|
4103
|
+
config2.port = resolvePort(config2.port, hasPortOption, values.port);
|
|
4104
|
+
config2.hostname = resolveHost(config2.hostname, hasHostOption, values.host);
|
|
4105
|
+
config2.routesDir = resolveRoutesDir(config2.routesDir, hasRoutesOption, values.routes);
|
|
4106
|
+
config2.development = config2.development ?? isDev;
|
|
4107
|
+
config2.autoDiscover = true;
|
|
4108
|
+
if (config2.cors === undefined && values.cors) {
|
|
4109
|
+
config2.cors = {
|
|
4110
|
+
origin: "*",
|
|
4111
|
+
credentials: true,
|
|
4112
|
+
allowHeaders: "Content-Type, Authorization",
|
|
4113
|
+
allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
4114
|
+
exposeHeaders: "Authorization",
|
|
4115
|
+
maxAge: 86400
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
return config2;
|
|
4119
|
+
}
|
|
4120
|
+
});
|
|
4121
|
+
server = app.server;
|
|
4122
|
+
const config = app.config;
|
|
3469
4123
|
if (!server || !server.port) {
|
|
3470
4124
|
throw new Error("Server started but is not responding correctly");
|
|
3471
4125
|
}
|
|
@@ -3474,13 +4128,18 @@ async function runDev() {
|
|
|
3474
4128
|
console.log(`
|
|
3475
4129
|
Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
|
|
3476
4130
|
`);
|
|
3477
|
-
return { server,
|
|
4131
|
+
return { server, app, config };
|
|
3478
4132
|
})();
|
|
3479
4133
|
return await Promise.race([serverStartPromise, timeoutPromise]);
|
|
3480
4134
|
}
|
|
3481
4135
|
try {
|
|
3482
4136
|
const result = await startServer();
|
|
3483
4137
|
server = result.server;
|
|
4138
|
+
if (!removeShutdownHandlers) {
|
|
4139
|
+
removeShutdownHandlers = installGracefulShutdownHandlers({
|
|
4140
|
+
getTarget: () => app
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
3484
4143
|
if (isDev && values.watch) {
|
|
3485
4144
|
try {
|
|
3486
4145
|
let reloadTimeout = null;
|
|
@@ -3504,14 +4163,14 @@ Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
|
|
|
3504
4163
|
isReloading = true;
|
|
3505
4164
|
lastReloadTime = Date.now();
|
|
3506
4165
|
changedFiles.clear();
|
|
3507
|
-
if (
|
|
3508
|
-
|
|
4166
|
+
if (app) {
|
|
4167
|
+
app.stop();
|
|
3509
4168
|
}
|
|
3510
4169
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
3511
4170
|
try {
|
|
3512
4171
|
const result2 = await startServer();
|
|
3513
4172
|
server = result2.server;
|
|
3514
|
-
|
|
4173
|
+
app = result2.app;
|
|
3515
4174
|
} catch (error) {
|
|
3516
4175
|
console.error(`
|
|
3517
4176
|
[Reload Error]`, error.message || error);
|