shokupan 0.7.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/analyzer-BqIe1p0R.js +35 -0
- package/dist/analyzer-BqIe1p0R.js.map +1 -0
- package/dist/analyzer-CKLGLFtx.cjs +35 -0
- package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
- package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
- package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
- package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +69 -22
- package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
- package/dist/http-server-BEMPIs33.cjs.map +1 -0
- package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
- package/dist/http-server-CCeagTyU.js.map +1 -0
- package/dist/index.cjs +2411 -329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2390 -308
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
- package/dist/plugins/application/api-explorer/static/style.css +767 -0
- package/dist/plugins/application/api-explorer/static/theme.css +128 -0
- package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
- package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
- package/dist/plugins/application/asyncapi/static/style.css +565 -0
- package/dist/plugins/application/asyncapi/static/theme.css +128 -0
- package/dist/plugins/application/auth.d.ts +3 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +14 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +25 -9
- package/dist/plugins/application/dashboard/static/charts.js +328 -0
- package/dist/plugins/application/dashboard/static/failures.js +85 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
- package/dist/plugins/application/dashboard/static/poll.js +146 -0
- package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
- package/dist/plugins/application/dashboard/static/registry.css +78 -0
- package/dist/plugins/application/dashboard/static/registry.js +269 -0
- package/dist/plugins/application/dashboard/static/requests.js +118 -0
- package/dist/plugins/application/dashboard/static/styles.css +184 -0
- package/dist/plugins/application/dashboard/static/tables.js +92 -0
- package/dist/plugins/application/dashboard/static/tabs.js +113 -0
- package/dist/plugins/application/dashboard/static/tabulator.css +118 -0
- package/dist/plugins/application/dashboard/static/theme.css +128 -0
- package/dist/plugins/application/graphql-apollo.d.ts +33 -0
- package/dist/plugins/application/graphql-yoga.d.ts +25 -0
- package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
- package/dist/plugins/application/scalar.d.ts +9 -2
- package/dist/plugins/application/socket-io.d.ts +14 -0
- package/dist/router.d.ts +92 -51
- package/dist/shokupan.d.ts +34 -8
- package/dist/util/datastore.d.ts +71 -6
- package/dist/util/decorators.d.ts +7 -2
- package/dist/util/http-error.d.ts +38 -0
- package/dist/util/http-status.d.ts +30 -0
- package/dist/util/request.d.ts +1 -1
- package/dist/util/symbol.d.ts +19 -0
- package/dist/util/types.d.ts +126 -4
- package/package.json +38 -15
- package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
- package/dist/analyzer-Ce_7JxZh.js.map +0 -1
- package/dist/http-server-0xH174zz.js.map +0 -1
- package/dist/http-server-DFhwlK8e.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -22,23 +22,32 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
24
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
const nanoid = require("nanoid");
|
|
25
26
|
const promises = require("node:fs/promises");
|
|
27
|
+
const node_util = require("node:util");
|
|
26
28
|
const api = require("@opentelemetry/api");
|
|
29
|
+
const jsYaml = require("js-yaml");
|
|
27
30
|
const node_async_hooks = require("node:async_hooks");
|
|
28
|
-
const
|
|
31
|
+
const surrealdb = require("surrealdb");
|
|
32
|
+
const eta$1 = require("eta");
|
|
29
33
|
const promises$1 = require("fs/promises");
|
|
30
34
|
const path = require("path");
|
|
31
35
|
const os = require("node:os");
|
|
32
|
-
const
|
|
33
|
-
const
|
|
36
|
+
const path$1 = require("node:path");
|
|
37
|
+
const node_url = require("node:url");
|
|
38
|
+
const renderToString = require("preact-render-to-string");
|
|
39
|
+
const jsxRuntime = require("preact/jsx-runtime");
|
|
34
40
|
const cluster = require("node:cluster");
|
|
35
41
|
const net = require("node:net");
|
|
36
|
-
const
|
|
42
|
+
const node_perf_hooks = require("node:perf_hooks");
|
|
43
|
+
const fs = require("node:fs");
|
|
44
|
+
const analyzer = require("./analyzer-CKLGLFtx.cjs");
|
|
37
45
|
const zlib = require("node:zlib");
|
|
38
46
|
const Ajv = require("ajv");
|
|
39
47
|
const addFormats = require("ajv-formats");
|
|
40
48
|
const crypto = require("crypto");
|
|
41
49
|
const events = require("events");
|
|
50
|
+
var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
|
|
42
51
|
function _interopNamespaceDefault(e) {
|
|
43
52
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
44
53
|
if (e) {
|
|
@@ -56,8 +65,37 @@ function _interopNamespaceDefault(e) {
|
|
|
56
65
|
return Object.freeze(n);
|
|
57
66
|
}
|
|
58
67
|
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
59
|
-
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
60
68
|
const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
|
|
69
|
+
const HTTP_STATUS = {
|
|
70
|
+
// 2xx Success
|
|
71
|
+
OK: 200,
|
|
72
|
+
CREATED: 201,
|
|
73
|
+
ACCEPTED: 202,
|
|
74
|
+
NO_CONTENT: 204,
|
|
75
|
+
// 3xx Redirection
|
|
76
|
+
MOVED_PERMANENTLY: 301,
|
|
77
|
+
FOUND: 302,
|
|
78
|
+
SEE_OTHER: 303,
|
|
79
|
+
NOT_MODIFIED: 304,
|
|
80
|
+
TEMPORARY_REDIRECT: 307,
|
|
81
|
+
PERMANENT_REDIRECT: 308,
|
|
82
|
+
// 4xx Client Errors
|
|
83
|
+
BAD_REQUEST: 400,
|
|
84
|
+
UNAUTHORIZED: 401,
|
|
85
|
+
FORBIDDEN: 403,
|
|
86
|
+
NOT_FOUND: 404,
|
|
87
|
+
METHOD_NOT_ALLOWED: 405,
|
|
88
|
+
REQUEST_TIMEOUT: 408,
|
|
89
|
+
CONFLICT: 409,
|
|
90
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
91
|
+
TOO_MANY_REQUESTS: 429,
|
|
92
|
+
// 5xx Server Errors
|
|
93
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
94
|
+
NOT_IMPLEMENTED: 501,
|
|
95
|
+
BAD_GATEWAY: 502,
|
|
96
|
+
SERVICE_UNAVAILABLE: 503,
|
|
97
|
+
GATEWAY_TIMEOUT: 504
|
|
98
|
+
};
|
|
61
99
|
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
62
100
|
100,
|
|
63
101
|
101,
|
|
@@ -187,6 +225,40 @@ class ShokupanResponse {
|
|
|
187
225
|
return this._headers !== null;
|
|
188
226
|
}
|
|
189
227
|
}
|
|
228
|
+
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
229
|
+
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
230
|
+
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
231
|
+
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
232
|
+
const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
|
|
233
|
+
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
234
|
+
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
235
|
+
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
236
|
+
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
237
|
+
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
238
|
+
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
239
|
+
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
240
|
+
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
241
|
+
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
242
|
+
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
243
|
+
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
244
|
+
const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
|
|
245
|
+
const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
|
|
246
|
+
const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
|
|
247
|
+
const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
|
|
248
|
+
const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
|
|
249
|
+
const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
|
|
250
|
+
const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
|
|
251
|
+
const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
|
|
252
|
+
const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
|
|
253
|
+
const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
|
|
254
|
+
const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
|
|
255
|
+
const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
|
|
256
|
+
const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
|
|
257
|
+
const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
|
|
258
|
+
const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
|
|
259
|
+
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
260
|
+
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
261
|
+
const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
|
|
190
262
|
function isValidCookieDomain(domain, currentHost) {
|
|
191
263
|
const hostWithoutPort = currentHost.split(":")[0];
|
|
192
264
|
if (domain === hostWithoutPort) return true;
|
|
@@ -224,23 +296,41 @@ class ShokupanContext {
|
|
|
224
296
|
state;
|
|
225
297
|
handlerStack = [];
|
|
226
298
|
response;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
299
|
+
[$debug];
|
|
300
|
+
[$finalResponse];
|
|
301
|
+
[$rawBody];
|
|
230
302
|
// Raw body for compression optimization
|
|
231
303
|
// Body caching to avoid double parsing
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
304
|
+
[$url];
|
|
305
|
+
[$cachedBody];
|
|
306
|
+
[$bodyType];
|
|
307
|
+
[$bodyParsed] = false;
|
|
308
|
+
[$bodyParseError];
|
|
309
|
+
[$routeMatched] = false;
|
|
238
310
|
// Cached URL properties to avoid repeated parsing
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
311
|
+
[$cachedHostname];
|
|
312
|
+
[$cachedProtocol];
|
|
313
|
+
[$cachedHost];
|
|
314
|
+
[$cachedOrigin];
|
|
315
|
+
[$cachedQuery];
|
|
316
|
+
disconnectCallbacks = [];
|
|
317
|
+
/**
|
|
318
|
+
* Registers a callback to be executed when the associated WebSocket disconnects.
|
|
319
|
+
* This is only applicable for requests that are part of a WebSocket interaction or upgrade.
|
|
320
|
+
*/
|
|
321
|
+
onSocketDisconnect(callback) {
|
|
322
|
+
this.disconnectCallbacks.push(callback);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* @internal
|
|
326
|
+
* Retrieves registered disconnect callbacks for execution.
|
|
327
|
+
*/
|
|
328
|
+
getDisconnectCallbacks() {
|
|
329
|
+
return this.disconnectCallbacks;
|
|
330
|
+
}
|
|
331
|
+
[$ws];
|
|
332
|
+
[$socket];
|
|
333
|
+
[$io];
|
|
244
334
|
/**
|
|
245
335
|
* JSX Rendering Function
|
|
246
336
|
*/
|
|
@@ -248,12 +338,30 @@ class ShokupanContext {
|
|
|
248
338
|
setRenderer(renderer) {
|
|
249
339
|
this.renderer = renderer;
|
|
250
340
|
}
|
|
341
|
+
[$requestId];
|
|
342
|
+
get requestId() {
|
|
343
|
+
return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid.nanoid();
|
|
344
|
+
}
|
|
345
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
346
|
+
const innerString = node_util.inspect({
|
|
347
|
+
method: this.request.method,
|
|
348
|
+
url: this.request.url,
|
|
349
|
+
requestHeaders: new Map(this.request.headers),
|
|
350
|
+
sessionId: this.sessionID,
|
|
351
|
+
state: this.state,
|
|
352
|
+
params: this.params,
|
|
353
|
+
response: this[$finalResponse]?.body,
|
|
354
|
+
responseHeaders: new Map(this[$finalResponse]?.headers),
|
|
355
|
+
handlerStack: this.handlerStack.map((h) => h.name === "anonymous" ? h.file + ":" + h.line : h.name)
|
|
356
|
+
}, { depth: null, colors: true, numericSeparator: true, customInspect: true });
|
|
357
|
+
return "Context(" + this.requestId + ") {" + innerString.slice(1, -2) + ",\n ...others\n}";
|
|
358
|
+
}
|
|
251
359
|
get url() {
|
|
252
|
-
if (!this
|
|
360
|
+
if (!this[$url]) {
|
|
253
361
|
const urlString = this.request.url || "http://localhost/";
|
|
254
|
-
this
|
|
362
|
+
this[$url] = new URL(urlString);
|
|
255
363
|
}
|
|
256
|
-
return this
|
|
364
|
+
return this[$url];
|
|
257
365
|
}
|
|
258
366
|
/**
|
|
259
367
|
* Base request
|
|
@@ -271,7 +379,7 @@ class ShokupanContext {
|
|
|
271
379
|
* Request path
|
|
272
380
|
*/
|
|
273
381
|
get path() {
|
|
274
|
-
if (this
|
|
382
|
+
if (this[$url]) return this[$url].pathname;
|
|
275
383
|
const url = this.request.url;
|
|
276
384
|
let queryIndex = url.indexOf("?");
|
|
277
385
|
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
@@ -296,13 +404,11 @@ class ShokupanContext {
|
|
|
296
404
|
* Request query params
|
|
297
405
|
*/
|
|
298
406
|
get query() {
|
|
299
|
-
if (this
|
|
407
|
+
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
300
408
|
const q = /* @__PURE__ */ Object.create(null);
|
|
301
409
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const [key, value] = entries[i];
|
|
305
|
-
if (blocklist.includes(key)) continue;
|
|
410
|
+
this.url.searchParams.forEach((value, key) => {
|
|
411
|
+
if (blocklist.includes(key)) return;
|
|
306
412
|
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
307
413
|
if (Array.isArray(q[key])) {
|
|
308
414
|
q[key].push(value);
|
|
@@ -312,8 +418,8 @@ class ShokupanContext {
|
|
|
312
418
|
} else {
|
|
313
419
|
q[key] = value;
|
|
314
420
|
}
|
|
315
|
-
}
|
|
316
|
-
this
|
|
421
|
+
});
|
|
422
|
+
this[$cachedQuery] = q;
|
|
317
423
|
return q;
|
|
318
424
|
}
|
|
319
425
|
/**
|
|
@@ -326,19 +432,19 @@ class ShokupanContext {
|
|
|
326
432
|
* Request hostname (e.g. "localhost")
|
|
327
433
|
*/
|
|
328
434
|
get hostname() {
|
|
329
|
-
return this
|
|
435
|
+
return this[$cachedHostname] ??= this.url.hostname;
|
|
330
436
|
}
|
|
331
437
|
/**
|
|
332
438
|
* Request host (e.g. "localhost:3000")
|
|
333
439
|
*/
|
|
334
440
|
get host() {
|
|
335
|
-
return this
|
|
441
|
+
return this[$cachedHost] ??= this.url.host;
|
|
336
442
|
}
|
|
337
443
|
/**
|
|
338
444
|
* Request protocol (e.g. "http:", "https:")
|
|
339
445
|
*/
|
|
340
446
|
get protocol() {
|
|
341
|
-
return this
|
|
447
|
+
return this[$cachedProtocol] ??= this.url.protocol;
|
|
342
448
|
}
|
|
343
449
|
/**
|
|
344
450
|
* Whether request is secure (https)
|
|
@@ -350,7 +456,7 @@ class ShokupanContext {
|
|
|
350
456
|
* Request origin (e.g. "http://localhost:3000")
|
|
351
457
|
*/
|
|
352
458
|
get origin() {
|
|
353
|
-
return this
|
|
459
|
+
return this[$cachedOrigin] ??= this.url.origin;
|
|
354
460
|
}
|
|
355
461
|
/**
|
|
356
462
|
* Request headers
|
|
@@ -371,6 +477,24 @@ class ShokupanContext {
|
|
|
371
477
|
get res() {
|
|
372
478
|
return this.response;
|
|
373
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Raw WebSocket connection
|
|
482
|
+
*/
|
|
483
|
+
get ws() {
|
|
484
|
+
return this[$ws];
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Socket.io socket
|
|
488
|
+
*/
|
|
489
|
+
get socket() {
|
|
490
|
+
return this[$socket];
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Socket.io server
|
|
494
|
+
*/
|
|
495
|
+
get io() {
|
|
496
|
+
return this[$io];
|
|
497
|
+
}
|
|
374
498
|
/**
|
|
375
499
|
* Helper to set a header on the response
|
|
376
500
|
* @param key Header key
|
|
@@ -380,6 +504,20 @@ class ShokupanContext {
|
|
|
380
504
|
this.response.set(key, value);
|
|
381
505
|
return this;
|
|
382
506
|
}
|
|
507
|
+
isUpgraded = false;
|
|
508
|
+
/**
|
|
509
|
+
* Upgrades the request to a WebSocket connection.
|
|
510
|
+
* @param options Upgrade options
|
|
511
|
+
* @returns true if upgraded, false otherwise
|
|
512
|
+
*/
|
|
513
|
+
upgrade(options) {
|
|
514
|
+
if (!this.server) return false;
|
|
515
|
+
const success = this.server.upgrade(this.req, options);
|
|
516
|
+
if (success) {
|
|
517
|
+
this.isUpgraded = true;
|
|
518
|
+
}
|
|
519
|
+
return success;
|
|
520
|
+
}
|
|
383
521
|
/**
|
|
384
522
|
* Set a cookie
|
|
385
523
|
* @param name Cookie name
|
|
@@ -457,18 +595,18 @@ class ShokupanContext {
|
|
|
457
595
|
* The body is only parsed once and cached for subsequent reads.
|
|
458
596
|
*/
|
|
459
597
|
async body() {
|
|
460
|
-
if (this
|
|
461
|
-
throw this
|
|
598
|
+
if (this[$bodyParseError]) {
|
|
599
|
+
throw this[$bodyParseError];
|
|
462
600
|
}
|
|
463
|
-
if (this
|
|
464
|
-
return this
|
|
601
|
+
if (this[$bodyParsed]) {
|
|
602
|
+
return this[$cachedBody];
|
|
465
603
|
}
|
|
466
604
|
const contentType = this.request.headers.get("content-type") || "";
|
|
467
605
|
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
468
606
|
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
469
607
|
if (parserType === "native") {
|
|
470
608
|
try {
|
|
471
|
-
this
|
|
609
|
+
this[$cachedBody] = await this.request.json();
|
|
472
610
|
} catch (e) {
|
|
473
611
|
throw e;
|
|
474
612
|
}
|
|
@@ -476,18 +614,18 @@ class ShokupanContext {
|
|
|
476
614
|
const rawText = await this.request.text();
|
|
477
615
|
const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
|
|
478
616
|
const parser = getJSONParser(parserType);
|
|
479
|
-
this
|
|
617
|
+
this[$cachedBody] = parser(rawText);
|
|
480
618
|
}
|
|
481
|
-
this
|
|
619
|
+
this[$bodyType] = "json";
|
|
482
620
|
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
483
|
-
this
|
|
484
|
-
this
|
|
621
|
+
this[$cachedBody] = await this.request.formData();
|
|
622
|
+
this[$bodyType] = "formData";
|
|
485
623
|
} else {
|
|
486
|
-
this
|
|
487
|
-
this
|
|
624
|
+
this[$cachedBody] = await this.request.text();
|
|
625
|
+
this[$bodyType] = "text";
|
|
488
626
|
}
|
|
489
|
-
this
|
|
490
|
-
return this
|
|
627
|
+
this[$bodyParsed] = true;
|
|
628
|
+
return this[$cachedBody];
|
|
491
629
|
}
|
|
492
630
|
/**
|
|
493
631
|
* Pre-parse the request body before handler execution.
|
|
@@ -495,7 +633,7 @@ class ShokupanContext {
|
|
|
495
633
|
* Errors are deferred until the body is actually accessed in the handler.
|
|
496
634
|
*/
|
|
497
635
|
async parseBody() {
|
|
498
|
-
if (this
|
|
636
|
+
if (this[$bodyParsed]) {
|
|
499
637
|
return;
|
|
500
638
|
}
|
|
501
639
|
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
@@ -504,7 +642,7 @@ class ShokupanContext {
|
|
|
504
642
|
try {
|
|
505
643
|
await this.body();
|
|
506
644
|
} catch (error) {
|
|
507
|
-
this
|
|
645
|
+
this[$bodyParseError] = error;
|
|
508
646
|
}
|
|
509
647
|
}
|
|
510
648
|
/**
|
|
@@ -554,116 +692,129 @@ class ShokupanContext {
|
|
|
554
692
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
555
693
|
}
|
|
556
694
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
557
|
-
this
|
|
695
|
+
this[$rawBody] = body;
|
|
696
|
+
}
|
|
697
|
+
return this[$finalResponse] ??= new Response(body, { status, headers });
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Emit an event to the client (WebSocket only)
|
|
701
|
+
* @param event Event name
|
|
702
|
+
* @param data Event data (Must be JSON serializable)
|
|
703
|
+
*/
|
|
704
|
+
emit(event, data) {
|
|
705
|
+
if (this[$ws]) {
|
|
706
|
+
this[$ws].send(JSON.stringify({ event, data }));
|
|
707
|
+
} else if (this[$socket]) {
|
|
708
|
+
this[$socket].emit(event, data);
|
|
558
709
|
}
|
|
559
|
-
this._finalResponse = new Response(body, { status, headers });
|
|
560
|
-
return this._finalResponse;
|
|
561
710
|
}
|
|
562
711
|
/**
|
|
563
712
|
* Respond with a JSON object
|
|
564
713
|
*/
|
|
565
|
-
json(data, status, headers) {
|
|
714
|
+
async json(data, status, headers) {
|
|
566
715
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
567
716
|
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
568
717
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
569
718
|
}
|
|
570
|
-
const jsonString = JSON.stringify(data);
|
|
571
|
-
this
|
|
719
|
+
const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
|
|
720
|
+
this[$rawBody] = jsonString;
|
|
572
721
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
573
|
-
this
|
|
722
|
+
this[$finalResponse] = new Response(jsonString, {
|
|
574
723
|
status: finalStatus,
|
|
575
724
|
headers: { "content-type": "application/json" }
|
|
576
725
|
});
|
|
577
|
-
return this
|
|
726
|
+
return this[$finalResponse];
|
|
578
727
|
}
|
|
579
728
|
const finalHeaders = this.mergeHeaders(headers);
|
|
580
729
|
finalHeaders.set("content-type", "application/json");
|
|
581
|
-
this
|
|
582
|
-
return this
|
|
730
|
+
this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
731
|
+
return this[$finalResponse];
|
|
583
732
|
}
|
|
584
733
|
/**
|
|
585
734
|
* Respond with a text string
|
|
586
735
|
*/
|
|
587
|
-
text(data, status, headers) {
|
|
736
|
+
async text(data, status, headers) {
|
|
588
737
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
589
738
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
590
739
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
591
740
|
}
|
|
592
|
-
this
|
|
741
|
+
this[$rawBody] = data instanceof Promise ? await data : data;
|
|
593
742
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
594
|
-
this
|
|
743
|
+
this[$finalResponse] = new Response(this[$rawBody], {
|
|
595
744
|
status: finalStatus,
|
|
596
745
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
597
746
|
});
|
|
598
|
-
return this
|
|
747
|
+
return this[$finalResponse];
|
|
599
748
|
}
|
|
600
749
|
const finalHeaders = this.mergeHeaders(headers);
|
|
601
750
|
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
602
|
-
this
|
|
603
|
-
return this
|
|
751
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
752
|
+
return this[$finalResponse];
|
|
604
753
|
}
|
|
605
754
|
/**
|
|
606
755
|
* Respond with HTML content
|
|
607
756
|
*/
|
|
608
|
-
html(html, status, headers) {
|
|
757
|
+
async html(html, status, headers) {
|
|
609
758
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
610
759
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
611
760
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
612
761
|
}
|
|
613
762
|
const finalHeaders = this.mergeHeaders(headers);
|
|
614
763
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
615
|
-
this
|
|
616
|
-
this
|
|
617
|
-
return this
|
|
764
|
+
this[$rawBody] = html instanceof Promise ? await html : html;
|
|
765
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
766
|
+
return this[$finalResponse];
|
|
618
767
|
}
|
|
619
768
|
/**
|
|
620
769
|
* Respond with a redirect
|
|
621
770
|
*/
|
|
622
|
-
redirect(url, status = 302) {
|
|
771
|
+
async redirect(url, status = 302) {
|
|
623
772
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
624
773
|
throw new Error(`Invalid redirect status code: ${status}`);
|
|
625
774
|
}
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
this
|
|
629
|
-
return this
|
|
775
|
+
const finalHeaders = this.mergeHeaders();
|
|
776
|
+
finalHeaders.set("Location", url instanceof Promise ? await url : url);
|
|
777
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
778
|
+
return this[$finalResponse];
|
|
630
779
|
}
|
|
631
780
|
/**
|
|
632
781
|
* Respond with a status code
|
|
633
782
|
* DOES NOT CHAIN!
|
|
634
783
|
*/
|
|
635
|
-
status(
|
|
784
|
+
async status(statusCode) {
|
|
785
|
+
const status = statusCode instanceof Promise ? await statusCode : statusCode;
|
|
636
786
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
637
787
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
638
788
|
}
|
|
639
|
-
const
|
|
640
|
-
this
|
|
641
|
-
return this
|
|
789
|
+
const finalHeaders = this.mergeHeaders();
|
|
790
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
791
|
+
return this[$finalResponse];
|
|
642
792
|
}
|
|
643
793
|
/**
|
|
644
794
|
* Respond with a file
|
|
645
795
|
*/
|
|
646
796
|
async file(path2, fileOptions, responseOptions) {
|
|
647
|
-
const
|
|
797
|
+
const finalHeaders = this.mergeHeaders(responseOptions?.headers);
|
|
648
798
|
const status = responseOptions?.status ?? this.response.status;
|
|
649
799
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
650
800
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
651
801
|
}
|
|
652
802
|
if (typeof Bun !== "undefined") {
|
|
653
|
-
this
|
|
654
|
-
return this
|
|
803
|
+
this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers: finalHeaders });
|
|
804
|
+
return this[$finalResponse];
|
|
655
805
|
} else {
|
|
656
806
|
const fileBuffer = await promises.readFile(path2);
|
|
657
807
|
if (fileOptions?.type) {
|
|
658
|
-
|
|
808
|
+
finalHeaders.set("content-type", fileOptions.type);
|
|
659
809
|
}
|
|
660
|
-
this
|
|
661
|
-
return this
|
|
810
|
+
this[$finalResponse] = new Response(fileBuffer, { status, headers: finalHeaders });
|
|
811
|
+
return this[$finalResponse];
|
|
662
812
|
}
|
|
663
813
|
}
|
|
664
814
|
/**
|
|
665
815
|
* Render a JSX element
|
|
666
816
|
* @param element JSX Element
|
|
817
|
+
* @param args JSX Element Args/Props
|
|
667
818
|
* @param status HTTP Status
|
|
668
819
|
* @param headers HTTP Headers
|
|
669
820
|
*/
|
|
@@ -694,10 +845,10 @@ const compose = (middleware) => {
|
|
|
694
845
|
return next ? next() : Promise.resolve();
|
|
695
846
|
}
|
|
696
847
|
const fn = middleware[i];
|
|
697
|
-
if (!context
|
|
848
|
+
if (!context[$debug]) {
|
|
698
849
|
return fn(context, () => runner(i + 1));
|
|
699
850
|
}
|
|
700
|
-
const debug = context
|
|
851
|
+
const debug = context[$debug];
|
|
701
852
|
const debugId = fn._debugId || fn.name || "anonymous";
|
|
702
853
|
const previousNode = debug.getCurrentNode();
|
|
703
854
|
debug.trackEdge(previousNode, debugId);
|
|
@@ -717,29 +868,6 @@ const compose = (middleware) => {
|
|
|
717
868
|
return runner(0);
|
|
718
869
|
};
|
|
719
870
|
};
|
|
720
|
-
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
721
|
-
function traceHandler(fn, name) {
|
|
722
|
-
return async function(...args) {
|
|
723
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
724
|
-
kind: api.SpanKind.INTERNAL,
|
|
725
|
-
attributes: {
|
|
726
|
-
"http.route": name,
|
|
727
|
-
"component": "shokupan.route"
|
|
728
|
-
}
|
|
729
|
-
}, async (span) => {
|
|
730
|
-
try {
|
|
731
|
-
const result = await fn.apply(this, args);
|
|
732
|
-
return result;
|
|
733
|
-
} catch (err) {
|
|
734
|
-
span.recordException(err);
|
|
735
|
-
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
736
|
-
throw err;
|
|
737
|
-
} finally {
|
|
738
|
-
span.end();
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
871
|
function isObject(item) {
|
|
744
872
|
return item && typeof item === "object" && !Array.isArray(item);
|
|
745
873
|
}
|
|
@@ -773,21 +901,6 @@ function deepMerge(target, ...sources) {
|
|
|
773
901
|
}
|
|
774
902
|
return deepMerge(target, ...sources);
|
|
775
903
|
}
|
|
776
|
-
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
777
|
-
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
778
|
-
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
779
|
-
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
780
|
-
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
781
|
-
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
782
|
-
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
783
|
-
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
784
|
-
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
785
|
-
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
786
|
-
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
787
|
-
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
788
|
-
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
789
|
-
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
790
|
-
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
791
904
|
const REGEX_PATTERNS = {
|
|
792
905
|
QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
|
|
793
906
|
QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
|
|
@@ -927,24 +1040,34 @@ function analyzeHandler(handler) {
|
|
|
927
1040
|
}
|
|
928
1041
|
return { inferredSpec };
|
|
929
1042
|
}
|
|
930
|
-
async function getAstRoutes(applications) {
|
|
1043
|
+
async function getAstRoutes$1(applications) {
|
|
931
1044
|
const astRoutes = [];
|
|
932
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1045
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
|
|
933
1046
|
if (seen.has(app.name)) return [];
|
|
934
1047
|
const newSeen = new Set(seen);
|
|
935
1048
|
newSeen.add(app.name);
|
|
936
1049
|
const expanded = [];
|
|
1050
|
+
let currentPrefix = prefix;
|
|
1051
|
+
if (app.controllerPrefix) {
|
|
1052
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1053
|
+
const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
|
|
1054
|
+
currentPrefix = cleanPrefix + cleanCont;
|
|
1055
|
+
}
|
|
937
1056
|
for (const route of app.routes) {
|
|
938
|
-
const cleanPrefix =
|
|
1057
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
939
1058
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
940
1059
|
let joined = cleanPrefix + cleanPath;
|
|
941
1060
|
if (joined.length > 1 && joined.endsWith("/")) {
|
|
942
1061
|
joined = joined.slice(0, -1);
|
|
943
1062
|
}
|
|
944
|
-
|
|
1063
|
+
const expandedRoute = {
|
|
945
1064
|
...route,
|
|
946
1065
|
path: joined || "/"
|
|
947
|
-
}
|
|
1066
|
+
};
|
|
1067
|
+
if (sourceOverride) {
|
|
1068
|
+
expandedRoute.sourceContext = sourceOverride;
|
|
1069
|
+
}
|
|
1070
|
+
expanded.push(expandedRoute);
|
|
948
1071
|
}
|
|
949
1072
|
if (app.mounted) {
|
|
950
1073
|
for (const mount of app.mounted) {
|
|
@@ -952,7 +1075,23 @@ async function getAstRoutes(applications) {
|
|
|
952
1075
|
if (targetApp) {
|
|
953
1076
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
954
1077
|
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
955
|
-
|
|
1078
|
+
let nextSourceOverride = sourceOverride;
|
|
1079
|
+
if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
|
|
1080
|
+
if (mount.sourceContext) {
|
|
1081
|
+
nextSourceOverride = {
|
|
1082
|
+
...mount.sourceContext,
|
|
1083
|
+
// Add highlight for the mount line to make it clear
|
|
1084
|
+
highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
|
|
1085
|
+
highlights: [{
|
|
1086
|
+
startLine: mount.sourceContext.startLine,
|
|
1087
|
+
endLine: mount.sourceContext.endLine,
|
|
1088
|
+
type: "return-success"
|
|
1089
|
+
// Use the success color (cyan) for the mount point
|
|
1090
|
+
}]
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
|
|
956
1095
|
}
|
|
957
1096
|
}
|
|
958
1097
|
}
|
|
@@ -980,13 +1119,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
980
1119
|
const defaultTagName = options.defaultTag || "Application";
|
|
981
1120
|
let astRoutes = [];
|
|
982
1121
|
try {
|
|
983
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
1122
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
|
|
984
1123
|
const analyzer2 = new OpenAPIAnalyzer(process.cwd());
|
|
985
1124
|
const { applications } = await analyzer2.analyze();
|
|
986
|
-
astRoutes = await getAstRoutes(applications);
|
|
1125
|
+
astRoutes = await getAstRoutes$1(applications);
|
|
987
1126
|
} catch (e) {
|
|
988
1127
|
}
|
|
989
|
-
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
1128
|
+
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
|
|
990
1129
|
let group = currentGroup;
|
|
991
1130
|
let tag = defaultTag;
|
|
992
1131
|
if (router.config?.group) group = router.config.group;
|
|
@@ -1003,21 +1142,33 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1003
1142
|
}
|
|
1004
1143
|
}
|
|
1005
1144
|
if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
|
|
1145
|
+
const routerMiddleware = router.middleware || [];
|
|
1006
1146
|
const routes = router[$routes] || [];
|
|
1007
1147
|
for (const route of routes) {
|
|
1148
|
+
if (!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"].includes(route.method.toUpperCase())) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1008
1151
|
const routeGroup = route.group || group;
|
|
1009
1152
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1010
1153
|
const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1011
1154
|
let fullPath = cleanPrefix + cleanSubPath || "/";
|
|
1012
|
-
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
1013
1155
|
if (fullPath.length > 1 && fullPath.endsWith("/")) {
|
|
1014
1156
|
fullPath = fullPath.slice(0, -1);
|
|
1015
1157
|
}
|
|
1158
|
+
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
1016
1159
|
if (!paths[fullPath]) paths[fullPath] = {};
|
|
1017
1160
|
const operation = {
|
|
1018
1161
|
responses: { "200": { description: "Successful response" } },
|
|
1019
1162
|
tags: [tag]
|
|
1020
1163
|
};
|
|
1164
|
+
const routeMiddleware = route.middleware || [];
|
|
1165
|
+
const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
|
|
1166
|
+
if (allMiddleware.length > 0) {
|
|
1167
|
+
operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
|
|
1168
|
+
name: mw.name || "middleware",
|
|
1169
|
+
metadata: mw.metadata
|
|
1170
|
+
}));
|
|
1171
|
+
}
|
|
1021
1172
|
if (route.guards) {
|
|
1022
1173
|
for (const guard of route.guards) {
|
|
1023
1174
|
if (guard.spec) {
|
|
@@ -1055,6 +1206,23 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1055
1206
|
if (astMatch.description) operation.description = astMatch.description;
|
|
1056
1207
|
if (astMatch.tags) operation.tags = astMatch.tags;
|
|
1057
1208
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1209
|
+
if (astMatch.sourceContext) {
|
|
1210
|
+
const sc = astMatch.sourceContext;
|
|
1211
|
+
operation["x-source-info"] = {
|
|
1212
|
+
file: sc.file,
|
|
1213
|
+
line: sc.startLine,
|
|
1214
|
+
snippet: sc.snippet || astMatch.handlerSource,
|
|
1215
|
+
// Fallback
|
|
1216
|
+
offset: sc.snippetStartLine || sc.startLine,
|
|
1217
|
+
highlightLines: [sc.startLine, sc.endLine],
|
|
1218
|
+
highlights: sc.highlights
|
|
1219
|
+
};
|
|
1220
|
+
operation["x-shokupan-source"] = {
|
|
1221
|
+
file: sc.file,
|
|
1222
|
+
line: sc.startLine,
|
|
1223
|
+
code: sc.snippet || astMatch.handlerSource || ""
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1058
1226
|
if (astMatch.requestTypes?.body) {
|
|
1059
1227
|
operation.requestBody = {
|
|
1060
1228
|
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
@@ -1066,10 +1234,12 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1066
1234
|
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1067
1235
|
};
|
|
1068
1236
|
} else if (astMatch.responseType) {
|
|
1069
|
-
|
|
1237
|
+
let contentType = "application/json";
|
|
1238
|
+
if (astMatch.responseType === "string") contentType = "text/plain";
|
|
1239
|
+
else if (astMatch.responseType === "html") contentType = "text/html";
|
|
1070
1240
|
operation.responses["200"] = {
|
|
1071
1241
|
description: "Successful response",
|
|
1072
|
-
content: { [contentType]: { schema: { type:
|
|
1242
|
+
content: { [contentType]: { schema: { type: "string" } } }
|
|
1073
1243
|
};
|
|
1074
1244
|
}
|
|
1075
1245
|
const params = [];
|
|
@@ -1081,6 +1251,26 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1081
1251
|
if (params.length > 0) {
|
|
1082
1252
|
operation.parameters = params;
|
|
1083
1253
|
}
|
|
1254
|
+
} else {
|
|
1255
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
1256
|
+
let file;
|
|
1257
|
+
let line;
|
|
1258
|
+
if (route.metadata?.file) {
|
|
1259
|
+
file = route.metadata.file;
|
|
1260
|
+
line = route.metadata.line || 1;
|
|
1261
|
+
}
|
|
1262
|
+
operation["x-source-info"] = {
|
|
1263
|
+
snippet: runtimeSource,
|
|
1264
|
+
isRuntime: true,
|
|
1265
|
+
...file ? { file, line: line || 1 } : {}
|
|
1266
|
+
};
|
|
1267
|
+
if (file) {
|
|
1268
|
+
operation["x-shokupan-source"] = {
|
|
1269
|
+
file,
|
|
1270
|
+
line: line || 1,
|
|
1271
|
+
code: runtimeSource
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1084
1274
|
}
|
|
1085
1275
|
if (route.keys.length > 0) {
|
|
1086
1276
|
const pathParams = route.keys.map((key) => ({
|
|
@@ -1150,7 +1340,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1150
1340
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1151
1341
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
1152
1342
|
const nextPrefix = cleanPrefix + cleanMount || "/";
|
|
1153
|
-
collect(child, nextPrefix, group, tag);
|
|
1343
|
+
collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
|
|
1154
1344
|
}
|
|
1155
1345
|
};
|
|
1156
1346
|
collect(rootRouter);
|
|
@@ -1174,7 +1364,36 @@ class RequestContextStore {
|
|
|
1174
1364
|
span;
|
|
1175
1365
|
}
|
|
1176
1366
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1177
|
-
|
|
1367
|
+
class HttpError extends Error {
|
|
1368
|
+
status;
|
|
1369
|
+
constructor(message, status) {
|
|
1370
|
+
super(message);
|
|
1371
|
+
this.name = "HttpError";
|
|
1372
|
+
this.status = status;
|
|
1373
|
+
if (Error.captureStackTrace) {
|
|
1374
|
+
Error.captureStackTrace(this, HttpError);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function getErrorStatus(err) {
|
|
1379
|
+
if (!err || typeof err !== "object") {
|
|
1380
|
+
return 500;
|
|
1381
|
+
}
|
|
1382
|
+
if (typeof err.status === "number") {
|
|
1383
|
+
return err.status;
|
|
1384
|
+
}
|
|
1385
|
+
if (typeof err.statusCode === "number") {
|
|
1386
|
+
return err.statusCode;
|
|
1387
|
+
}
|
|
1388
|
+
return 500;
|
|
1389
|
+
}
|
|
1390
|
+
class EventError extends HttpError {
|
|
1391
|
+
constructor(message = "Event Error") {
|
|
1392
|
+
super(message, 500);
|
|
1393
|
+
this.name = "EventError";
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const eta = new eta$1.Eta();
|
|
1178
1397
|
function serveStatic(config, prefix) {
|
|
1179
1398
|
const rootPath = path.resolve(config.root || ".");
|
|
1180
1399
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
@@ -1273,7 +1492,7 @@ function serveStatic(config, prefix) {
|
|
|
1273
1492
|
if (config.listDirectory) {
|
|
1274
1493
|
try {
|
|
1275
1494
|
const files = await promises$1.readdir(requestPath);
|
|
1276
|
-
const listing = eta
|
|
1495
|
+
const listing = eta.renderString(`
|
|
1277
1496
|
<!DOCTYPE html>
|
|
1278
1497
|
<html>
|
|
1279
1498
|
<head>
|
|
@@ -1313,7 +1532,7 @@ function serveStatic(config, prefix) {
|
|
|
1313
1532
|
if (typeof Bun !== "undefined") {
|
|
1314
1533
|
response = new Response(Bun.file(finalPath));
|
|
1315
1534
|
} else {
|
|
1316
|
-
const fileBuffer = await promises$1.readFile(finalPath);
|
|
1535
|
+
const fileBuffer = await promises$1.readFile(finalPath, { encoding: "binary" });
|
|
1317
1536
|
response = new Response(fileBuffer);
|
|
1318
1537
|
}
|
|
1319
1538
|
if (config.hooks?.onResponse) {
|
|
@@ -1326,69 +1545,6 @@ function serveStatic(config, prefix) {
|
|
|
1326
1545
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1327
1546
|
return serveStaticMiddleware;
|
|
1328
1547
|
}
|
|
1329
|
-
let db;
|
|
1330
|
-
let dbPromise = null;
|
|
1331
|
-
let RecordId;
|
|
1332
|
-
async function ensureDb() {
|
|
1333
|
-
if (db) return db;
|
|
1334
|
-
if (dbPromise) return dbPromise;
|
|
1335
|
-
dbPromise = (async () => {
|
|
1336
|
-
try {
|
|
1337
|
-
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1338
|
-
const surreal = await import("surrealdb");
|
|
1339
|
-
const Surreal = surreal.Surreal;
|
|
1340
|
-
RecordId = surreal.RecordId;
|
|
1341
|
-
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1342
|
-
const _db = new Surreal({
|
|
1343
|
-
engines: createNodeEngines()
|
|
1344
|
-
});
|
|
1345
|
-
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1346
|
-
await _db.query(`
|
|
1347
|
-
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1348
|
-
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1349
|
-
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1350
|
-
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1351
|
-
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1352
|
-
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1353
|
-
`);
|
|
1354
|
-
db = _db;
|
|
1355
|
-
return db;
|
|
1356
|
-
} catch (e) {
|
|
1357
|
-
dbPromise = null;
|
|
1358
|
-
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1359
|
-
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1360
|
-
}
|
|
1361
|
-
throw e;
|
|
1362
|
-
}
|
|
1363
|
-
})();
|
|
1364
|
-
return dbPromise;
|
|
1365
|
-
}
|
|
1366
|
-
const datastore = {
|
|
1367
|
-
async get(store, key) {
|
|
1368
|
-
await ensureDb();
|
|
1369
|
-
return db.select(new RecordId(store, key));
|
|
1370
|
-
},
|
|
1371
|
-
async set(store, key, value) {
|
|
1372
|
-
await ensureDb();
|
|
1373
|
-
return db.create(new RecordId(store, key)).content(value);
|
|
1374
|
-
},
|
|
1375
|
-
async query(query, vars) {
|
|
1376
|
-
await ensureDb();
|
|
1377
|
-
try {
|
|
1378
|
-
const r = await db.query(query, vars);
|
|
1379
|
-
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1380
|
-
} catch (e) {
|
|
1381
|
-
console.error("DS ERROR:", e);
|
|
1382
|
-
throw e;
|
|
1383
|
-
}
|
|
1384
|
-
},
|
|
1385
|
-
get ready() {
|
|
1386
|
-
return ensureDb().then(() => void 0);
|
|
1387
|
-
}
|
|
1388
|
-
};
|
|
1389
|
-
process.on("exit", async () => {
|
|
1390
|
-
if (db) await db.close();
|
|
1391
|
-
});
|
|
1392
1548
|
class Container {
|
|
1393
1549
|
static services = /* @__PURE__ */ new Map();
|
|
1394
1550
|
static register(target, instance) {
|
|
@@ -1422,6 +1578,29 @@ function Inject(token) {
|
|
|
1422
1578
|
});
|
|
1423
1579
|
};
|
|
1424
1580
|
}
|
|
1581
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1582
|
+
function traceHandler(fn, name) {
|
|
1583
|
+
return async function(...args) {
|
|
1584
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1585
|
+
kind: api.SpanKind.INTERNAL,
|
|
1586
|
+
attributes: {
|
|
1587
|
+
"http.route": name,
|
|
1588
|
+
"component": "shokupan.route"
|
|
1589
|
+
}
|
|
1590
|
+
}, async (span) => {
|
|
1591
|
+
try {
|
|
1592
|
+
const result = await fn.apply(this, args);
|
|
1593
|
+
return result;
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
span.recordException(err);
|
|
1596
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
1597
|
+
throw err;
|
|
1598
|
+
} finally {
|
|
1599
|
+
span.end();
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1425
1604
|
class ShokupanRequestBase {
|
|
1426
1605
|
method;
|
|
1427
1606
|
url;
|
|
@@ -1459,8 +1638,10 @@ function getCallerInfo(skipFrames = 1) {
|
|
|
1459
1638
|
if (!l.includes(":")) continue;
|
|
1460
1639
|
if (l.includes("node_modules")) continue;
|
|
1461
1640
|
if (l.includes("bun:main")) continue;
|
|
1641
|
+
if (l.includes("bun:wrap")) continue;
|
|
1462
1642
|
if (l.includes("src/util/stack.ts")) continue;
|
|
1463
1643
|
if (l.includes("src/router.ts")) continue;
|
|
1644
|
+
if (l.includes("src/util/decorators.ts")) continue;
|
|
1464
1645
|
if (l.includes("src/shokupan.ts")) continue;
|
|
1465
1646
|
found++;
|
|
1466
1647
|
if (found >= skipFrames) {
|
|
@@ -1603,6 +1784,9 @@ class ShokupanRouter {
|
|
|
1603
1784
|
[$parent] = null;
|
|
1604
1785
|
[$childRouters] = [];
|
|
1605
1786
|
[$childControllers] = [];
|
|
1787
|
+
get db() {
|
|
1788
|
+
return this.root?.db;
|
|
1789
|
+
}
|
|
1606
1790
|
hookCache = /* @__PURE__ */ new Map();
|
|
1607
1791
|
hooksInitialized = false;
|
|
1608
1792
|
middleware = [];
|
|
@@ -1618,6 +1802,15 @@ class ShokupanRouter {
|
|
|
1618
1802
|
metadata;
|
|
1619
1803
|
// Metadata for the router itself
|
|
1620
1804
|
currentGuards = [];
|
|
1805
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
1806
|
+
/**
|
|
1807
|
+
* Registers middleware for this router.
|
|
1808
|
+
* Middleware will run for all routes matched by this router.
|
|
1809
|
+
*/
|
|
1810
|
+
use(middleware) {
|
|
1811
|
+
this.middleware.push(middleware);
|
|
1812
|
+
return this;
|
|
1813
|
+
}
|
|
1621
1814
|
// Registry Accessor
|
|
1622
1815
|
getComponentRegistry() {
|
|
1623
1816
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
@@ -1678,6 +1871,42 @@ class ShokupanRouter {
|
|
|
1678
1871
|
isRouterInstance(target) {
|
|
1679
1872
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1680
1873
|
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Registers an event handler for WebSocket.
|
|
1876
|
+
*/
|
|
1877
|
+
event(name, handler) {
|
|
1878
|
+
const info = getCallerInfo();
|
|
1879
|
+
handler.source = { file: info.file, line: info.line };
|
|
1880
|
+
if (this.eventHandlers.has(name)) {
|
|
1881
|
+
const err = new EventError(`Event handler \`${name}\` already exists.`);
|
|
1882
|
+
console.warn(err);
|
|
1883
|
+
const handlers = this.eventHandlers.get(name);
|
|
1884
|
+
handlers.push(handler);
|
|
1885
|
+
this.eventHandlers.set(name, handlers);
|
|
1886
|
+
} else {
|
|
1887
|
+
this.eventHandlers.set(name, [handler]);
|
|
1888
|
+
}
|
|
1889
|
+
return this;
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Finds an event handler(s) by name.
|
|
1893
|
+
*/
|
|
1894
|
+
findEvent(name) {
|
|
1895
|
+
if (this.eventHandlers.has(name)) {
|
|
1896
|
+
return this.eventHandlers.get(name);
|
|
1897
|
+
}
|
|
1898
|
+
for (const child of this[$childRouters]) {
|
|
1899
|
+
const handler = child.findEvent(name);
|
|
1900
|
+
if (handler) return handler;
|
|
1901
|
+
}
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Returns all registered event handlers.
|
|
1906
|
+
*/
|
|
1907
|
+
getEventHandlers() {
|
|
1908
|
+
return this.eventHandlers;
|
|
1909
|
+
}
|
|
1681
1910
|
/**
|
|
1682
1911
|
* Mounts a controller instance to a path prefix.
|
|
1683
1912
|
*
|
|
@@ -1778,7 +2007,7 @@ class ShokupanRouter {
|
|
|
1778
2007
|
});
|
|
1779
2008
|
const ctx = new ShokupanContext(req);
|
|
1780
2009
|
let result = null;
|
|
1781
|
-
let status =
|
|
2010
|
+
let status = HTTP_STATUS.OK;
|
|
1782
2011
|
const headers = {};
|
|
1783
2012
|
const match = this.find(req.method, ctx.path);
|
|
1784
2013
|
if (match) {
|
|
@@ -1787,12 +2016,12 @@ class ShokupanRouter {
|
|
|
1787
2016
|
result = await match.handler(ctx);
|
|
1788
2017
|
} catch (err) {
|
|
1789
2018
|
console.error(err);
|
|
1790
|
-
status = err
|
|
2019
|
+
status = getErrorStatus(err);
|
|
1791
2020
|
result = { error: err.message || "Internal Server Error" };
|
|
1792
2021
|
if (err.errors) result.errors = err.errors;
|
|
1793
2022
|
}
|
|
1794
2023
|
} else {
|
|
1795
|
-
status =
|
|
2024
|
+
status = HTTP_STATUS.NOT_FOUND;
|
|
1796
2025
|
result = "Not Found";
|
|
1797
2026
|
}
|
|
1798
2027
|
if (result instanceof Response) {
|
|
@@ -1821,7 +2050,7 @@ class ShokupanRouter {
|
|
|
1821
2050
|
const originalHandler = handler;
|
|
1822
2051
|
const wrapped = async (ctx) => {
|
|
1823
2052
|
await this.runHooks("onRequestStart", ctx);
|
|
1824
|
-
const debug = ctx
|
|
2053
|
+
const debug = ctx[$debug];
|
|
1825
2054
|
let debugId;
|
|
1826
2055
|
let previousNode;
|
|
1827
2056
|
if (debug) {
|
|
@@ -1911,6 +2140,7 @@ class ShokupanRouter {
|
|
|
1911
2140
|
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
1912
2141
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1913
2142
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2143
|
+
const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
|
|
1914
2144
|
let routesAttached = 0;
|
|
1915
2145
|
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1916
2146
|
const name = Array.from(methods)[i];
|
|
@@ -1920,10 +2150,12 @@ class ShokupanRouter {
|
|
|
1920
2150
|
if (typeof originalHandler !== "function") continue;
|
|
1921
2151
|
let method;
|
|
1922
2152
|
let subPath = "";
|
|
2153
|
+
let methodSource;
|
|
1923
2154
|
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
1924
2155
|
const config = decoratedRoutes.get(name);
|
|
1925
2156
|
method = config.method;
|
|
1926
2157
|
subPath = config.path;
|
|
2158
|
+
methodSource = config.source;
|
|
1927
2159
|
} else {
|
|
1928
2160
|
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1929
2161
|
const m = HTTPMethods[j];
|
|
@@ -2053,7 +2285,54 @@ class ShokupanRouter {
|
|
|
2053
2285
|
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2054
2286
|
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2055
2287
|
const spec = { tags: [tagName], ...userSpec };
|
|
2056
|
-
this.add({
|
|
2288
|
+
this.add({
|
|
2289
|
+
method,
|
|
2290
|
+
path: normalizedPath,
|
|
2291
|
+
handler: finalHandler,
|
|
2292
|
+
spec,
|
|
2293
|
+
controller: instance,
|
|
2294
|
+
metadata: methodSource || instance.metadata,
|
|
2295
|
+
middleware: allMiddleware
|
|
2296
|
+
// Capture all resolved middleware
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
if (decoratedEvents?.has(name)) {
|
|
2300
|
+
routesAttached++;
|
|
2301
|
+
const config = decoratedEvents.get(name);
|
|
2302
|
+
const routeArgs = decoratedArgs?.get(name);
|
|
2303
|
+
const wrappedHandler = async (ctx) => {
|
|
2304
|
+
let args = [ctx];
|
|
2305
|
+
if (routeArgs?.length > 0) {
|
|
2306
|
+
args = [];
|
|
2307
|
+
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2308
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2309
|
+
const arg = sortedArgs[k];
|
|
2310
|
+
switch (arg.type) {
|
|
2311
|
+
case RouteParamType.BODY:
|
|
2312
|
+
args[arg.index] = await ctx.body();
|
|
2313
|
+
break;
|
|
2314
|
+
case RouteParamType.CONTEXT:
|
|
2315
|
+
args[arg.index] = ctx;
|
|
2316
|
+
break;
|
|
2317
|
+
case RouteParamType.REQUEST:
|
|
2318
|
+
args[arg.index] = ctx.req;
|
|
2319
|
+
break;
|
|
2320
|
+
case RouteParamType.HEADER:
|
|
2321
|
+
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2322
|
+
break;
|
|
2323
|
+
default:
|
|
2324
|
+
args[arg.index] = void 0;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return originalHandler.apply(instance, args);
|
|
2329
|
+
};
|
|
2330
|
+
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2331
|
+
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2332
|
+
const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
|
|
2333
|
+
wrappedHandler.spec = spec;
|
|
2334
|
+
wrappedHandler.originalHandler = originalHandler;
|
|
2335
|
+
this.event(config.eventName, wrappedHandler);
|
|
2057
2336
|
}
|
|
2058
2337
|
}
|
|
2059
2338
|
if (routesAttached === 0) {
|
|
@@ -2122,7 +2401,7 @@ class ShokupanRouter {
|
|
|
2122
2401
|
* @param arg.renderer - JSX renderer for the route
|
|
2123
2402
|
* @param arg.controller - Controller for the route
|
|
2124
2403
|
*/
|
|
2125
|
-
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2404
|
+
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller, metadata, middleware }) {
|
|
2126
2405
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
|
|
2127
2406
|
if (this.currentGuards.length > 0) {
|
|
2128
2407
|
spec = spec || {};
|
|
@@ -2140,7 +2419,13 @@ class ShokupanRouter {
|
|
|
2140
2419
|
}
|
|
2141
2420
|
}
|
|
2142
2421
|
}
|
|
2143
|
-
let wrappedHandler =
|
|
2422
|
+
let wrappedHandler = async (ctx) => {
|
|
2423
|
+
if (ctx.upgrade()) {
|
|
2424
|
+
return void 0;
|
|
2425
|
+
}
|
|
2426
|
+
return handler(ctx);
|
|
2427
|
+
};
|
|
2428
|
+
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
2144
2429
|
const routeGuards = [...this.currentGuards];
|
|
2145
2430
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
2146
2431
|
if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
|
|
@@ -2191,7 +2476,7 @@ class ShokupanRouter {
|
|
|
2191
2476
|
return innerHandler(ctx);
|
|
2192
2477
|
};
|
|
2193
2478
|
}
|
|
2194
|
-
const { file, line } = getCallerInfo();
|
|
2479
|
+
const { file, line } = metadata || getCallerInfo();
|
|
2195
2480
|
const trackingHandler = wrappedHandler;
|
|
2196
2481
|
wrappedHandler = async (ctx) => {
|
|
2197
2482
|
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
@@ -2217,9 +2502,13 @@ class ShokupanRouter {
|
|
|
2217
2502
|
const config = ctx.app.applicationConfig;
|
|
2218
2503
|
Promise.resolve().then(async () => {
|
|
2219
2504
|
try {
|
|
2505
|
+
const db = ctx.app?.db;
|
|
2506
|
+
if (!db) return;
|
|
2220
2507
|
const timestamp = Date.now();
|
|
2221
|
-
|
|
2222
|
-
|
|
2508
|
+
await db.upsert(new surrealdb.RecordId("middleware_tracking", {
|
|
2509
|
+
timestamp,
|
|
2510
|
+
name: handler.name || "anonymous"
|
|
2511
|
+
}), {
|
|
2223
2512
|
name: handler.name || "anonymous",
|
|
2224
2513
|
path: ctx.path,
|
|
2225
2514
|
timestamp,
|
|
@@ -2235,11 +2524,11 @@ class ShokupanRouter {
|
|
|
2235
2524
|
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2236
2525
|
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2237
2526
|
const cutoff = Date.now() - ttl;
|
|
2238
|
-
await
|
|
2239
|
-
const results = await
|
|
2240
|
-
if (results
|
|
2527
|
+
await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2528
|
+
const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2529
|
+
if (results?.[0]?.count > maxCapacity) {
|
|
2241
2530
|
const toDelete = results[0].count - maxCapacity;
|
|
2242
|
-
await
|
|
2531
|
+
await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2243
2532
|
}
|
|
2244
2533
|
} catch (datastoreError) {
|
|
2245
2534
|
console.error("Failed to store middleware tracking:", datastoreError);
|
|
@@ -2269,7 +2558,8 @@ class ShokupanRouter {
|
|
|
2269
2558
|
file,
|
|
2270
2559
|
line
|
|
2271
2560
|
},
|
|
2272
|
-
controller
|
|
2561
|
+
controller,
|
|
2562
|
+
middleware: middleware || []
|
|
2273
2563
|
});
|
|
2274
2564
|
this.trie.insert(method, path2, bakedHandler);
|
|
2275
2565
|
return this;
|
|
@@ -2314,7 +2604,7 @@ class ShokupanRouter {
|
|
|
2314
2604
|
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
2315
2605
|
);
|
|
2316
2606
|
if (callerLine) {
|
|
2317
|
-
const match = callerLine.match(/\((
|
|
2607
|
+
const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
|
|
2318
2608
|
if (match) {
|
|
2319
2609
|
file = match[1];
|
|
2320
2610
|
line = parseInt(match[2], 10);
|
|
@@ -2332,7 +2622,7 @@ class ShokupanRouter {
|
|
|
2332
2622
|
}
|
|
2333
2623
|
return guardHandler(ctx, next);
|
|
2334
2624
|
};
|
|
2335
|
-
trackedGuard.originalHandler = guardHandler.originalHandler
|
|
2625
|
+
trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
|
|
2336
2626
|
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
2337
2627
|
return this;
|
|
2338
2628
|
}
|
|
@@ -2398,7 +2688,8 @@ class ShokupanRouter {
|
|
|
2398
2688
|
method,
|
|
2399
2689
|
path: path2,
|
|
2400
2690
|
spec,
|
|
2401
|
-
handler: finalHandler
|
|
2691
|
+
handler: finalHandler,
|
|
2692
|
+
middleware: handlers.slice(0, handlers.length - 1)
|
|
2402
2693
|
});
|
|
2403
2694
|
}
|
|
2404
2695
|
/**
|
|
@@ -2438,16 +2729,16 @@ class ShokupanRouter {
|
|
|
2438
2729
|
}
|
|
2439
2730
|
this.hooksInitialized = true;
|
|
2440
2731
|
}
|
|
2441
|
-
|
|
2732
|
+
runHooks(name, ...args) {
|
|
2442
2733
|
if (!this.hooksInitialized) {
|
|
2443
2734
|
this.ensureHooksInitialized();
|
|
2444
2735
|
}
|
|
2445
2736
|
const fns = this.hookCache.get(name);
|
|
2446
2737
|
if (!fns) return;
|
|
2447
2738
|
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2448
|
-
const debug = ctx?.
|
|
2739
|
+
const debug = ctx?.[$debug];
|
|
2449
2740
|
if (debug) {
|
|
2450
|
-
|
|
2741
|
+
return Promise.all(fns.map(async (fn, index) => {
|
|
2451
2742
|
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2452
2743
|
const previousNode = debug.getCurrentNode();
|
|
2453
2744
|
debug.trackEdge(previousNode, hookId);
|
|
@@ -2466,7 +2757,7 @@ class ShokupanRouter {
|
|
|
2466
2757
|
}
|
|
2467
2758
|
}));
|
|
2468
2759
|
} else {
|
|
2469
|
-
|
|
2760
|
+
return Promise.all(fns.map((fn) => fn(...args)));
|
|
2470
2761
|
}
|
|
2471
2762
|
}
|
|
2472
2763
|
}
|
|
@@ -2513,46 +2804,161 @@ class SystemCpuMonitor {
|
|
|
2513
2804
|
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2514
2805
|
}
|
|
2515
2806
|
}
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
};
|
|
2523
|
-
api.trace.getTracer("shokupan.application");
|
|
2524
|
-
class Shokupan extends ShokupanRouter {
|
|
2525
|
-
applicationConfig = {};
|
|
2526
|
-
openApiSpec;
|
|
2527
|
-
composedMiddleware;
|
|
2528
|
-
cpuMonitor;
|
|
2529
|
-
get logger() {
|
|
2530
|
-
return this.applicationConfig.logger;
|
|
2807
|
+
class SurrealDatastore {
|
|
2808
|
+
constructor(db) {
|
|
2809
|
+
this.db = db;
|
|
2810
|
+
process.on("exit", async () => {
|
|
2811
|
+
await this.disconnect();
|
|
2812
|
+
});
|
|
2531
2813
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
line,
|
|
2543
|
-
name: "ShokupanApplication"
|
|
2544
|
-
};
|
|
2814
|
+
createSchema() {
|
|
2815
|
+
this.db.query(`
|
|
2816
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2817
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
2818
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
2819
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
2820
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
2821
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2822
|
+
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
2823
|
+
`).collect();
|
|
2545
2824
|
}
|
|
2546
2825
|
/**
|
|
2547
|
-
*
|
|
2826
|
+
* Select a record or contents of a table by its ID.
|
|
2548
2827
|
*/
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2828
|
+
async select(id) {
|
|
2829
|
+
return this.db.select(id);
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Merge update data into a record by its ID.
|
|
2833
|
+
*/
|
|
2834
|
+
async merge(id, data) {
|
|
2835
|
+
return this.db.update(id).merge(data).catch((err) => {
|
|
2836
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2837
|
+
return this.db.update(id).merge(data);
|
|
2838
|
+
}
|
|
2839
|
+
throw err;
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Create a record by its ID.
|
|
2844
|
+
*/
|
|
2845
|
+
async create(id, data) {
|
|
2846
|
+
return this.db.create(id).content(data).catch((err) => {
|
|
2847
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2848
|
+
return this.db.create(id).content(data);
|
|
2849
|
+
}
|
|
2850
|
+
throw err;
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Upsert a record by its ID.
|
|
2855
|
+
*/
|
|
2856
|
+
async upsert(id, data) {
|
|
2857
|
+
return this.db.upsert(id).content(data).catch((err) => {
|
|
2858
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2859
|
+
return this.db.upsert(id).content(data);
|
|
2860
|
+
}
|
|
2861
|
+
throw err;
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Delete a record by its ID.
|
|
2866
|
+
*/
|
|
2867
|
+
async delete(id) {
|
|
2868
|
+
return this.db.delete(id).catch((err) => {
|
|
2869
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2870
|
+
return this.db.delete(id);
|
|
2871
|
+
}
|
|
2872
|
+
throw err;
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Run a SurrealDB query.
|
|
2877
|
+
*/
|
|
2878
|
+
async query(query, vars) {
|
|
2879
|
+
return this.db.query(query, vars).collect().catch((err) => {
|
|
2880
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2881
|
+
return this.db.query(query, vars).collect();
|
|
2882
|
+
}
|
|
2883
|
+
throw err;
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Create a relationship between two records.
|
|
2888
|
+
*/
|
|
2889
|
+
async relate(fromId, edgeId, toId, data) {
|
|
2890
|
+
return this.db.relate(fromId, edgeId, toId, data).catch((err) => {
|
|
2891
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2892
|
+
return this.db.relate(fromId, edgeId, toId, data);
|
|
2893
|
+
}
|
|
2894
|
+
throw err;
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
disconnect() {
|
|
2898
|
+
return this.db.close();
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const defaults = {
|
|
2902
|
+
port: 3e3,
|
|
2903
|
+
hostname: "localhost",
|
|
2904
|
+
development: process.env.NODE_ENV !== "production",
|
|
2905
|
+
enableAsyncLocalStorage: false,
|
|
2906
|
+
enableHttpBridge: false,
|
|
2907
|
+
reusePort: false
|
|
2908
|
+
};
|
|
2909
|
+
api.trace.getTracer("shokupan.application");
|
|
2910
|
+
class Shokupan extends ShokupanRouter {
|
|
2911
|
+
applicationConfig = {};
|
|
2912
|
+
openApiSpec;
|
|
2913
|
+
asyncApiSpec;
|
|
2914
|
+
composedMiddleware;
|
|
2915
|
+
cpuMonitor;
|
|
2916
|
+
server;
|
|
2917
|
+
datastore;
|
|
2918
|
+
dbPromise;
|
|
2919
|
+
get db() {
|
|
2920
|
+
return this.datastore;
|
|
2921
|
+
}
|
|
2922
|
+
get logger() {
|
|
2923
|
+
return this.applicationConfig.logger;
|
|
2924
|
+
}
|
|
2925
|
+
constructor(applicationConfig = {}) {
|
|
2926
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2927
|
+
const { hooks, ...routerConfig } = config;
|
|
2928
|
+
super({ ...routerConfig, hooks });
|
|
2929
|
+
this[$isApplication] = true;
|
|
2930
|
+
this[$appRoot] = this;
|
|
2931
|
+
this.applicationConfig = config;
|
|
2932
|
+
const { file, line } = getCallerInfo();
|
|
2933
|
+
this.metadata = {
|
|
2934
|
+
file,
|
|
2935
|
+
line,
|
|
2936
|
+
name: "ShokupanApplication"
|
|
2937
|
+
};
|
|
2938
|
+
this.dbPromise = this.initDatastore();
|
|
2939
|
+
}
|
|
2940
|
+
async initDatastore() {
|
|
2941
|
+
const db = new surrealdb.Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
|
|
2942
|
+
this.datastore = new SurrealDatastore(db);
|
|
2943
|
+
await db.connect(
|
|
2944
|
+
this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
|
|
2945
|
+
this.applicationConfig.surreal?.connectOptions
|
|
2946
|
+
);
|
|
2947
|
+
await db.use({
|
|
2948
|
+
namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
|
|
2949
|
+
database: this.applicationConfig.surreal?.database ?? "shokupan"
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Adds middleware to the application.
|
|
2954
|
+
*/
|
|
2955
|
+
use(middleware) {
|
|
2956
|
+
const { file, line } = getCallerInfo();
|
|
2957
|
+
if (!middleware.metadata) {
|
|
2958
|
+
middleware.metadata = {
|
|
2959
|
+
file,
|
|
2960
|
+
line,
|
|
2961
|
+
name: middleware.name || "middleware",
|
|
2556
2962
|
isBuiltin: middleware.isBuiltin,
|
|
2557
2963
|
pluginName: middleware.pluginName
|
|
2558
2964
|
};
|
|
@@ -2621,19 +3027,76 @@ class Shokupan extends ShokupanRouter {
|
|
|
2621
3027
|
*/
|
|
2622
3028
|
async listen(port) {
|
|
2623
3029
|
const finalPort = port ?? this.applicationConfig.port ?? 3e3;
|
|
2624
|
-
if (finalPort < 0 || finalPort > 65535) {
|
|
3030
|
+
if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
|
|
2625
3031
|
throw new Error("Invalid port number");
|
|
2626
3032
|
}
|
|
2627
3033
|
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2628
3034
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2629
3035
|
this.openApiSpec = await generateOpenApi(this);
|
|
3036
|
+
this.get("/.well-known/openapi.yaml", (ctx) => {
|
|
3037
|
+
try {
|
|
3038
|
+
const yaml = jsYaml.dump(this.openApiSpec);
|
|
3039
|
+
return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
|
|
3040
|
+
} catch (e) {
|
|
3041
|
+
this.logger.error("Failed to generate OpenAPI YAML", { error: e });
|
|
3042
|
+
return ctx.text("Internal Server Error", 500);
|
|
3043
|
+
}
|
|
3044
|
+
});
|
|
3045
|
+
if (this.applicationConfig.aiPlugin?.enabled !== false) {
|
|
3046
|
+
this.get("/.well-known/ai-plugin.json", async (ctx) => {
|
|
3047
|
+
const config = this.applicationConfig.aiPlugin || {};
|
|
3048
|
+
let pkg = {};
|
|
3049
|
+
try {
|
|
3050
|
+
pkg = await Bun.file("package.json").json();
|
|
3051
|
+
} catch (e) {
|
|
3052
|
+
}
|
|
3053
|
+
const manifest = {
|
|
3054
|
+
schema_version: "v1",
|
|
3055
|
+
name_for_human: config.name_for_human || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3056
|
+
name_for_model: config.name_for_model || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3057
|
+
description_for_human: config.description_for_human || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3058
|
+
description_for_model: config.description_for_model || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3059
|
+
auth: config.auth || { type: "none" },
|
|
3060
|
+
api: config.api || {
|
|
3061
|
+
type: "openapi",
|
|
3062
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
|
|
3063
|
+
is_user_authenticated: false
|
|
3064
|
+
},
|
|
3065
|
+
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
|
|
3066
|
+
// Placeholder default
|
|
3067
|
+
contact_email: config.contact_email || pkg.author?.email || "support@example.com",
|
|
3068
|
+
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
|
|
3069
|
+
};
|
|
3070
|
+
return ctx.json(manifest);
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
if (this.applicationConfig.apiCatalog?.enabled !== false) {
|
|
3074
|
+
this.get("/.well-known/api-catalog", (ctx) => {
|
|
3075
|
+
const config = this.applicationConfig.apiCatalog || {};
|
|
3076
|
+
const catalog = {
|
|
3077
|
+
versions: config.versions || [
|
|
3078
|
+
{
|
|
3079
|
+
name: this.openApiSpec.info.version || "v1",
|
|
3080
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
|
|
3081
|
+
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
|
|
3082
|
+
}
|
|
3083
|
+
]
|
|
3084
|
+
};
|
|
3085
|
+
return ctx.json(catalog);
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
2630
3088
|
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2631
3089
|
}
|
|
3090
|
+
if (this.applicationConfig.enableAsyncApiGen) {
|
|
3091
|
+
const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
|
|
3092
|
+
this.asyncApiSpec = await generateAsyncApi2(this);
|
|
3093
|
+
}
|
|
2632
3094
|
if (port === 0 && process.platform === "linux") ;
|
|
2633
3095
|
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2634
3096
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2635
3097
|
this.cpuMonitor.start();
|
|
2636
3098
|
}
|
|
3099
|
+
const self = this;
|
|
2637
3100
|
const serveOptions = {
|
|
2638
3101
|
port: finalPort,
|
|
2639
3102
|
hostname: this.applicationConfig.hostname,
|
|
@@ -2645,25 +3108,116 @@ class Shokupan extends ShokupanRouter {
|
|
|
2645
3108
|
open(ws) {
|
|
2646
3109
|
ws.data?.handler?.open?.(ws);
|
|
2647
3110
|
},
|
|
2648
|
-
message(ws, message) {
|
|
2649
|
-
ws.data?.handler?.message
|
|
3111
|
+
async message(ws, message) {
|
|
3112
|
+
if (ws.data?.handler?.message) {
|
|
3113
|
+
return ws.data.handler.message(ws, message);
|
|
3114
|
+
}
|
|
3115
|
+
if (typeof message !== "string") return;
|
|
3116
|
+
try {
|
|
3117
|
+
const payload = JSON.parse(message);
|
|
3118
|
+
if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
|
|
3119
|
+
const { id, method, path: path2, headers, body } = payload;
|
|
3120
|
+
const url = new URL(path2, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
|
|
3121
|
+
const req = new Request(url.toString(), {
|
|
3122
|
+
method,
|
|
3123
|
+
headers,
|
|
3124
|
+
body: typeof body === "object" ? JSON.stringify(body) : body
|
|
3125
|
+
});
|
|
3126
|
+
const res = await self.fetch(req);
|
|
3127
|
+
const resBody = await res.json().catch((err) => res.text());
|
|
3128
|
+
const resHeaders = {};
|
|
3129
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
3130
|
+
ws.send(JSON.stringify({
|
|
3131
|
+
type: "RESPONSE",
|
|
3132
|
+
id,
|
|
3133
|
+
status: res.status,
|
|
3134
|
+
headers: resHeaders,
|
|
3135
|
+
body: resBody
|
|
3136
|
+
}));
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
|
|
3140
|
+
if (eventName) {
|
|
3141
|
+
const handlers = self.findEvent(eventName);
|
|
3142
|
+
const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
|
|
3143
|
+
if (handler) {
|
|
3144
|
+
const data = payload.data || payload.payload;
|
|
3145
|
+
const req = new ShokupanRequest({
|
|
3146
|
+
url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
|
|
3147
|
+
method: "POST",
|
|
3148
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
3149
|
+
body: JSON.stringify(data)
|
|
3150
|
+
});
|
|
3151
|
+
const ctx = new ShokupanContext(req, self.server);
|
|
3152
|
+
ctx[$ws] = ws;
|
|
3153
|
+
ws.data ??= {};
|
|
3154
|
+
ws.data["ctx"] = ctx;
|
|
3155
|
+
try {
|
|
3156
|
+
await handler(ctx);
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
if (self.applicationConfig["websocketErrorHandler"]) {
|
|
3159
|
+
await self.applicationConfig["websocketErrorHandler"](err, ctx);
|
|
3160
|
+
} else {
|
|
3161
|
+
console.error(`Error in event ${eventName}:`, err);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
} catch (e) {
|
|
3167
|
+
}
|
|
2650
3168
|
},
|
|
2651
3169
|
drain(ws) {
|
|
2652
3170
|
ws.data?.handler?.drain?.(ws);
|
|
2653
3171
|
},
|
|
2654
3172
|
close(ws, code, reason) {
|
|
2655
3173
|
ws.data?.handler?.close?.(ws, code, reason);
|
|
3174
|
+
const ctx = ws.data?.["ctx"];
|
|
3175
|
+
if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
|
|
3176
|
+
const callbacks = ctx.getDisconnectCallbacks();
|
|
3177
|
+
if (Array.isArray(callbacks) && callbacks.length > 0) {
|
|
3178
|
+
Promise.all(callbacks.map((cb) => cb())).catch((err) => {
|
|
3179
|
+
console.error("Error executing socket disconnect hook:", err);
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
2656
3183
|
}
|
|
2657
3184
|
}
|
|
2658
3185
|
};
|
|
2659
3186
|
let factory = this.applicationConfig.serverFactory;
|
|
2660
3187
|
if (!factory && typeof Bun === "undefined") {
|
|
2661
|
-
const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-
|
|
3188
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-BEMPIs33.cjs"));
|
|
2662
3189
|
factory = createHttpServer();
|
|
2663
3190
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
3191
|
+
this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
3192
|
+
return this.server;
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Stops the application server.
|
|
3196
|
+
*
|
|
3197
|
+
* This method gracefully shuts down the server and stops any running monitors.
|
|
3198
|
+
* Works transparently in both Bun and Node.js runtimes.
|
|
3199
|
+
*
|
|
3200
|
+
* @returns A promise that resolves when the server has been stopped.
|
|
3201
|
+
*
|
|
3202
|
+
* @example
|
|
3203
|
+
* ```typescript
|
|
3204
|
+
* const app = new Shokupan();
|
|
3205
|
+
* const server = await app.listen(3000);
|
|
3206
|
+
*
|
|
3207
|
+
* // Later, when you want to stop the server
|
|
3208
|
+
* await app.stop();
|
|
3209
|
+
* ```
|
|
3210
|
+
* @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
|
|
3211
|
+
*/
|
|
3212
|
+
async stop(closeActiveConnections) {
|
|
3213
|
+
if (this.cpuMonitor) {
|
|
3214
|
+
this.cpuMonitor.stop();
|
|
3215
|
+
this.cpuMonitor = void 0;
|
|
3216
|
+
}
|
|
3217
|
+
if (this.server) {
|
|
3218
|
+
await this.server.stop(closeActiveConnections);
|
|
3219
|
+
this.server = void 0;
|
|
3220
|
+
}
|
|
2667
3221
|
}
|
|
2668
3222
|
[$dispatch](req) {
|
|
2669
3223
|
return this.fetch(req);
|
|
@@ -2761,7 +3315,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2761
3315
|
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2762
3316
|
const match = this.find(req.method, ctx.path);
|
|
2763
3317
|
if (match) {
|
|
2764
|
-
ctx
|
|
3318
|
+
ctx[$routeMatched] = true;
|
|
2765
3319
|
ctx.params = match.params;
|
|
2766
3320
|
await bodyParsing;
|
|
2767
3321
|
return match.handler(ctx);
|
|
@@ -2771,18 +3325,23 @@ class Shokupan extends ShokupanRouter {
|
|
|
2771
3325
|
let response;
|
|
2772
3326
|
if (result instanceof Response) {
|
|
2773
3327
|
response = result;
|
|
2774
|
-
} else if ((result === null || result === void 0) && ctx
|
|
2775
|
-
response = ctx
|
|
3328
|
+
} else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
|
|
3329
|
+
response = ctx[$finalResponse];
|
|
2776
3330
|
} else if (result === null || result === void 0) {
|
|
2777
|
-
if (ctx
|
|
2778
|
-
response = ctx
|
|
2779
|
-
} else if (ctx.
|
|
3331
|
+
if (ctx[$finalResponse] instanceof Response) {
|
|
3332
|
+
response = ctx[$finalResponse];
|
|
3333
|
+
} else if (ctx.isUpgraded) {
|
|
3334
|
+
return void 0;
|
|
3335
|
+
} else if (ctx[$routeMatched]) {
|
|
2780
3336
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2781
3337
|
} else {
|
|
2782
|
-
if (ctx.
|
|
3338
|
+
if (ctx.upgrade()) {
|
|
3339
|
+
return void 0;
|
|
3340
|
+
}
|
|
3341
|
+
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
2783
3342
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2784
3343
|
} else {
|
|
2785
|
-
response = ctx.text("Not Found",
|
|
3344
|
+
response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
|
|
2786
3345
|
}
|
|
2787
3346
|
}
|
|
2788
3347
|
} else if (typeof result === "object") {
|
|
@@ -2791,13 +3350,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
2791
3350
|
response = ctx.text(String(result));
|
|
2792
3351
|
}
|
|
2793
3352
|
await this.runHooks("onRequestEnd", ctx);
|
|
3353
|
+
if (response instanceof Promise) {
|
|
3354
|
+
response = await response;
|
|
3355
|
+
}
|
|
2794
3356
|
await this.runHooks("onResponseStart", ctx, response);
|
|
2795
3357
|
return response;
|
|
2796
3358
|
} catch (err) {
|
|
2797
|
-
console.error(err);
|
|
2798
3359
|
const span = asyncContext.getStore()?.span;
|
|
2799
3360
|
if (span) span.setStatus({ code: 2 });
|
|
2800
|
-
const status = err
|
|
3361
|
+
const status = getErrorStatus(err);
|
|
2801
3362
|
const body = { error: err.message || "Internal Server Error" };
|
|
2802
3363
|
if (err.errors) body.errors = err.errors;
|
|
2803
3364
|
await this.runHooks("onError", ctx, err);
|
|
@@ -2819,10 +3380,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
2819
3380
|
}
|
|
2820
3381
|
return executionPromise.catch((err) => {
|
|
2821
3382
|
if (err.message === "Request Timeout") {
|
|
2822
|
-
return ctx.text("Request Timeout",
|
|
3383
|
+
return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
|
|
2823
3384
|
}
|
|
2824
3385
|
console.error("Unexpected error in request execution:", err);
|
|
2825
|
-
return ctx.text("Internal Server Error",
|
|
3386
|
+
return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
2826
3387
|
}).then(async (res) => {
|
|
2827
3388
|
await this.runHooks("onResponseEnd", ctx, res);
|
|
2828
3389
|
return res;
|
|
@@ -2901,8 +3462,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
2901
3462
|
}
|
|
2902
3463
|
}
|
|
2903
3464
|
const msg = typeof message === "function" ? message(ctx, key) : message;
|
|
2904
|
-
typeof msg === "object" ?
|
|
2905
|
-
const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
|
|
3465
|
+
const res = await (typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode));
|
|
2906
3466
|
if (headers) {
|
|
2907
3467
|
setHeaders(res);
|
|
2908
3468
|
res.headers.set("Retry-After", String(retryAfter));
|
|
@@ -2977,8 +3537,12 @@ function createMethodDecorator(method) {
|
|
|
2977
3537
|
}
|
|
2978
3538
|
target[$routeMethods].set(propertyKey, {
|
|
2979
3539
|
method,
|
|
2980
|
-
path: path2
|
|
3540
|
+
path: path2,
|
|
3541
|
+
source: getCallerInfo(2)
|
|
2981
3542
|
});
|
|
3543
|
+
if (path2.includes("/user")) {
|
|
3544
|
+
console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
|
|
3545
|
+
}
|
|
2982
3546
|
};
|
|
2983
3547
|
};
|
|
2984
3548
|
}
|
|
@@ -2990,18 +3554,583 @@ const Patch = createMethodDecorator("PATCH");
|
|
|
2990
3554
|
const Options = createMethodDecorator("OPTIONS");
|
|
2991
3555
|
const Head = createMethodDecorator("HEAD");
|
|
2992
3556
|
const All = createMethodDecorator("ALL");
|
|
3557
|
+
function Event(eventName) {
|
|
3558
|
+
return (target, propertyKey, descriptor) => {
|
|
3559
|
+
target[$eventMethods] ??= /* @__PURE__ */ new Map();
|
|
3560
|
+
target[$eventMethods].set(propertyKey, {
|
|
3561
|
+
eventName
|
|
3562
|
+
});
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
2993
3565
|
function RateLimit(options) {
|
|
2994
3566
|
return Use(RateLimitMiddleware(options));
|
|
2995
3567
|
}
|
|
3568
|
+
function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
3569
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
|
|
3570
|
+
/* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
|
|
3571
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
|
|
3572
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3573
|
+
/* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan AsyncAPI" }),
|
|
3574
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
3575
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
|
|
3576
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap", rel: "stylesheet" }),
|
|
3577
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
3578
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
3579
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
|
|
3580
|
+
__html: `
|
|
3581
|
+
window.INITIAL_SPEC = ${JSON.stringify(spec)};
|
|
3582
|
+
window.INITIAL_SERVER_URL = "${serverUrl}";
|
|
3583
|
+
window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
|
|
3584
|
+
`
|
|
3585
|
+
} })
|
|
3586
|
+
] }),
|
|
3587
|
+
/* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
|
|
3588
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "app-container", children: [
|
|
3589
|
+
/* @__PURE__ */ jsxRuntime.jsx(Sidebar, { navTree, disableSourceView }),
|
|
3590
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "resizer", id: "resizer-left" }),
|
|
3591
|
+
/* @__PURE__ */ jsxRuntime.jsx(MainContent, {}),
|
|
3592
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "resizer", id: "resizer-right" }),
|
|
3593
|
+
/* @__PURE__ */ jsxRuntime.jsx(ConsolePanel, { serverUrl })
|
|
3594
|
+
] }),
|
|
3595
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.socket.io/4.7.4/socket.io.min.js" }),
|
|
3596
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
|
|
3597
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/asyncapi-client.mjs`, type: "module" })
|
|
3598
|
+
] })
|
|
3599
|
+
] });
|
|
3600
|
+
}
|
|
3601
|
+
function Sidebar({ navTree, disableSourceView }) {
|
|
3602
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
|
|
3603
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
|
|
3604
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "AsyncAPI" }),
|
|
3605
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
|
|
3606
|
+
] }),
|
|
3607
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
3608
|
+
] });
|
|
3609
|
+
}
|
|
3610
|
+
function NavNode({ node, level, disableSourceView }) {
|
|
3611
|
+
const sortedEntries = Object.entries(node.children || {}).sort((a, b) => {
|
|
3612
|
+
const [aKey, aItem] = a;
|
|
3613
|
+
const [bKey, bItem] = b;
|
|
3614
|
+
const isWarningA = aItem.data?.op?.["x-warning"];
|
|
3615
|
+
const isWarningB = bItem.data?.op?.["x-warning"];
|
|
3616
|
+
if (isWarningA && !isWarningB) return -1;
|
|
3617
|
+
if (!isWarningA && isWarningB) return 1;
|
|
3618
|
+
if (aKey === bKey) return 0;
|
|
3619
|
+
if (aKey === "Warning" || aKey === "Warnings") return -1;
|
|
3620
|
+
if (bKey === "Warning" || bKey === "Warnings") return 1;
|
|
3621
|
+
if (aKey === "Application") return -1;
|
|
3622
|
+
if (bKey === "Application") return 1;
|
|
3623
|
+
if (aKey[0] === "/") return 1;
|
|
3624
|
+
if (bKey[0] === "/") return -1;
|
|
3625
|
+
return aKey.localeCompare(bKey);
|
|
3626
|
+
});
|
|
3627
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: sortedEntries.map(([key, item]) => {
|
|
3628
|
+
const hasChildren = Object.keys(item.children || {}).length > 0;
|
|
3629
|
+
if (level === 0) {
|
|
3630
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3631
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "group-label", children: key }),
|
|
3632
|
+
hasChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-node", style: "margin-left: 0", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3633
|
+
] }, key);
|
|
3634
|
+
}
|
|
3635
|
+
const isLeaf = item.isLeaf;
|
|
3636
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3637
|
+
isLeaf ? /* @__PURE__ */ jsxRuntime.jsx(LeafNode, { item, label: key, disableSourceView }) : /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-item", style: "color: var(--text-muted)", children: /* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: key }) }),
|
|
3638
|
+
hasChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-node", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3639
|
+
] }, key);
|
|
3640
|
+
}) });
|
|
3641
|
+
}
|
|
3642
|
+
function LeafNode({ item, label, disableSourceView }) {
|
|
3643
|
+
const isWarning = item.data?.op?.["x-warning"];
|
|
3644
|
+
const opId = item.data?.name;
|
|
3645
|
+
const sourceInfo = item.data?.op?.["x-source-info"];
|
|
3646
|
+
let content;
|
|
3647
|
+
if (isWarning) {
|
|
3648
|
+
content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3649
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: "margin-right: 6px;", children: "⚠️" }),
|
|
3650
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
|
|
3651
|
+
] });
|
|
3652
|
+
} else {
|
|
3653
|
+
const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
|
|
3654
|
+
content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3655
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
|
|
3656
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
|
|
3657
|
+
] });
|
|
3658
|
+
}
|
|
3659
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tree-item", "data-event": opId, style: isWarning ? "color: #fbbf24" : "", children: [
|
|
3660
|
+
content,
|
|
3661
|
+
sourceInfo && !disableSourceView && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3662
|
+
"a",
|
|
3663
|
+
{
|
|
3664
|
+
href: `vscode://file/${sourceInfo.file}:${sourceInfo.line}`,
|
|
3665
|
+
class: "source-link",
|
|
3666
|
+
onClick: (e) => {
|
|
3667
|
+
e.stopPropagation();
|
|
3668
|
+
},
|
|
3669
|
+
title: `${sourceInfo.file}:${sourceInfo.line}`,
|
|
3670
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", style: "display:block", children: [
|
|
3671
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 18 22 12 16 6" }),
|
|
3672
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 2 12 8 18" })
|
|
3673
|
+
] })
|
|
3674
|
+
}
|
|
3675
|
+
)
|
|
3676
|
+
] });
|
|
3677
|
+
}
|
|
3678
|
+
function MainContent() {
|
|
3679
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "main-wrapper", style: "flex: 1; min-width: 0; position: relative; overflow: hidden;", children: [
|
|
3680
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-expand-nav", class: "btn-icon floating-toggle left", title: "Expand Sidebar", style: "display:none;", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
|
|
3681
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-expand-console", class: "btn-icon floating-toggle right", title: "Expand Console", style: "display:none;", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" }) }) }),
|
|
3682
|
+
/* @__PURE__ */ jsxRuntime.jsx("main", { class: "main-content scroller", id: "doc-panel", style: "height: 100%;", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "empty-state", children: [
|
|
3683
|
+
/* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", children: [
|
|
3684
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
|
|
3685
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" })
|
|
3686
|
+
] }),
|
|
3687
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Select an event to view details" })
|
|
3688
|
+
] }) })
|
|
3689
|
+
] });
|
|
3690
|
+
}
|
|
3691
|
+
function ConsolePanel({ serverUrl }) {
|
|
3692
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "console-panel", id: "console-panel", children: [
|
|
3693
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "console-header", children: [
|
|
3694
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;", children: [
|
|
3695
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { style: "margin:0; font-size:1rem;", children: "Console" }),
|
|
3696
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display:flex; gap: 4px;", children: [
|
|
3697
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-maximize-console", class: "btn-icon", title: "Maximize Console", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }) }) }),
|
|
3698
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-collapse-console", class: "btn-icon", title: "Collapse Console", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) })
|
|
3699
|
+
] })
|
|
3700
|
+
] }),
|
|
3701
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "connection-bar", children: [
|
|
3702
|
+
/* @__PURE__ */ jsxRuntime.jsxs("select", { id: "protocol", children: [
|
|
3703
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "ws", children: "WS" }),
|
|
3704
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "wss", children: "WSS" }),
|
|
3705
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "socket.io", children: "Socket.IO" })
|
|
3706
|
+
] }),
|
|
3707
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "width: 1px; background: rgba(255,255,255,0.1); margin: 2px 0;" }),
|
|
3708
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", id: "url", value: serverUrl })
|
|
3709
|
+
] }),
|
|
3710
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display: grid; grid-template-columns: 1fr auto; gap: 8px;", children: [
|
|
3711
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "connect-btn", class: "btn", children: "Connect" }),
|
|
3712
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "clear-logs-btn", class: "btn secondary", title: "Clear Logs", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }) }) })
|
|
3713
|
+
] }),
|
|
3714
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "status-indicator", children: [
|
|
3715
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "status-dot", class: "dot" }),
|
|
3716
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "connection-status", children: "Disconnected" })
|
|
3717
|
+
] })
|
|
3718
|
+
] }),
|
|
3719
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "logs-container scroller", id: "logs", children: /* @__PURE__ */ jsxRuntime.jsx("div", { class: "log-shim", id: "log-shim" }) }),
|
|
3720
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "compose-area", children: [
|
|
3721
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "compose-header", children: [
|
|
3722
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Payload" }),
|
|
3723
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "target-event", style: "color: var(--primary);", children: "--" })
|
|
3724
|
+
] }),
|
|
3725
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "editor-container" }),
|
|
3726
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "send-bar", children: /* @__PURE__ */ jsxRuntime.jsx("button", { id: "send-btn", class: "btn", children: "Send Message" }) })
|
|
3727
|
+
] })
|
|
3728
|
+
] });
|
|
3729
|
+
}
|
|
3730
|
+
function buildNavTree(spec) {
|
|
3731
|
+
if (!spec || !spec.channels) return { children: {} };
|
|
3732
|
+
const root = { children: {} };
|
|
3733
|
+
Object.keys(spec.channels).forEach((name) => {
|
|
3734
|
+
const ch = spec.channels[name];
|
|
3735
|
+
const op = ch.publish || ch.subscribe;
|
|
3736
|
+
const type = ch.publish ? "publish" : "subscribe";
|
|
3737
|
+
const tag = op.tags && op.tags.length > 0 ? op.tags[0].name : "General";
|
|
3738
|
+
if (!root.children[tag]) root.children[tag] = { children: {} };
|
|
3739
|
+
const parts = name.split(/[\.\/]/);
|
|
3740
|
+
let current = root.children[tag];
|
|
3741
|
+
parts.forEach((part, i) => {
|
|
3742
|
+
if (!current.children[part]) current.children[part] = { children: {} };
|
|
3743
|
+
current = current.children[part];
|
|
3744
|
+
if (i === parts.length - 1) {
|
|
3745
|
+
current.isLeaf = true;
|
|
3746
|
+
current.data = { name, op, type };
|
|
3747
|
+
}
|
|
3748
|
+
});
|
|
3749
|
+
});
|
|
3750
|
+
return root;
|
|
3751
|
+
}
|
|
3752
|
+
async function getAstRoutes(applications) {
|
|
3753
|
+
const astRoutes = [];
|
|
3754
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
3755
|
+
if (seen.has(app.name)) return [];
|
|
3756
|
+
const newSeen = new Set(seen);
|
|
3757
|
+
newSeen.add(app.name);
|
|
3758
|
+
const expanded = [];
|
|
3759
|
+
for (const route of app.routes) {
|
|
3760
|
+
expanded.push({
|
|
3761
|
+
...route,
|
|
3762
|
+
// For events, path is the event name
|
|
3763
|
+
path: route.path.startsWith("/") ? route.path.slice(1) : route.path
|
|
3764
|
+
});
|
|
3765
|
+
}
|
|
3766
|
+
if (app.mounted) {
|
|
3767
|
+
for (const mount of app.mounted) {
|
|
3768
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
3769
|
+
if (targetApp) {
|
|
3770
|
+
expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return expanded;
|
|
3775
|
+
};
|
|
3776
|
+
applications.forEach((app) => {
|
|
3777
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
3778
|
+
});
|
|
3779
|
+
return astRoutes;
|
|
3780
|
+
}
|
|
3781
|
+
async function generateAsyncApi(rootRouter, options = {}) {
|
|
3782
|
+
const channels = {};
|
|
3783
|
+
let astRoutes = [];
|
|
3784
|
+
try {
|
|
3785
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
|
|
3786
|
+
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
3787
|
+
const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
3788
|
+
const { applications } = await analyzer2.analyze();
|
|
3789
|
+
astRoutes = await getAstRoutes(applications);
|
|
3790
|
+
} catch (e) {
|
|
3791
|
+
}
|
|
3792
|
+
const matchedAstRoutes = /* @__PURE__ */ new Set();
|
|
3793
|
+
const collect = async (router, prefix = "") => {
|
|
3794
|
+
const eventHandlers = router.getEventHandlers();
|
|
3795
|
+
let routerTag = "Other";
|
|
3796
|
+
if (router[$isApplication]) {
|
|
3797
|
+
routerTag = "Application";
|
|
3798
|
+
} else if (router.constructor.name && router.constructor.name !== "ShokupanRouter") {
|
|
3799
|
+
routerTag = router.constructor.name;
|
|
3800
|
+
} else {
|
|
3801
|
+
routerTag = router[$mountPath] || "Router";
|
|
3802
|
+
}
|
|
3803
|
+
if (eventHandlers) {
|
|
3804
|
+
for (const [eventName, handlers] of eventHandlers.entries()) {
|
|
3805
|
+
for (const handler of handlers) {
|
|
3806
|
+
const userSpec = handler.spec;
|
|
3807
|
+
let tags = userSpec?.tags;
|
|
3808
|
+
if (!tags && routerTag) {
|
|
3809
|
+
tags = [{ name: routerTag }];
|
|
3810
|
+
}
|
|
3811
|
+
let astMatch = astRoutes.find(
|
|
3812
|
+
(r) => (r.method === "EVENT" || r.method === "ON") && r.path === eventName
|
|
3813
|
+
);
|
|
3814
|
+
if (!astMatch) {
|
|
3815
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3816
|
+
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
3817
|
+
const normalize = (s) => stripComments(s).replace(/\s+/g, "");
|
|
3818
|
+
const runtimeHandlerSrc = normalize(runtimeSource);
|
|
3819
|
+
const eventRoutes = astRoutes.filter((r) => r.method === "EVENT" || r.method === "ON");
|
|
3820
|
+
astMatch = eventRoutes.find((r) => {
|
|
3821
|
+
const astHandlerSrc = normalize(r.handlerSource || r.handlerName || "");
|
|
3822
|
+
if (!astHandlerSrc || astHandlerSrc.length < 5) return false;
|
|
3823
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(normalize(r.handlerSource).substring(0, 50));
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
if (astMatch) matchedAstRoutes.add(astMatch);
|
|
3827
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3828
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3829
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3830
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3831
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3832
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3833
|
+
} : void 0;
|
|
3834
|
+
if (!channels[eventName]) {
|
|
3835
|
+
channels[eventName] = {
|
|
3836
|
+
publish: {
|
|
3837
|
+
operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
|
|
3838
|
+
tags,
|
|
3839
|
+
message: {
|
|
3840
|
+
payload: { type: "object" },
|
|
3841
|
+
...userSpec?.message ? userSpec.message : {}
|
|
3842
|
+
},
|
|
3843
|
+
...userSpec?.type === "publish" ? userSpec : {},
|
|
3844
|
+
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
3845
|
+
"x-shokupan-source": sourceInfo
|
|
3846
|
+
// Simplified
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
|
|
3850
|
+
if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
|
|
3851
|
+
} else {
|
|
3852
|
+
if (sourceInfo) {
|
|
3853
|
+
if (!channels[eventName].publish["x-source-info"]) {
|
|
3854
|
+
channels[eventName].publish["x-source-info"] = [];
|
|
3855
|
+
}
|
|
3856
|
+
const exists = channels[eventName].publish["x-source-info"].some(
|
|
3857
|
+
(s) => s.file === sourceInfo.file && s.line === sourceInfo.line
|
|
3858
|
+
);
|
|
3859
|
+
if (!exists) {
|
|
3860
|
+
channels[eventName].publish["x-source-info"].push(sourceInfo);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
let emits = astMatch?.emits || [];
|
|
3865
|
+
for (const emit of emits) {
|
|
3866
|
+
if (emit.event === "__DYNAMIC_EMIT__") {
|
|
3867
|
+
const warningKey = `${eventName}/Dynamic Emit`;
|
|
3868
|
+
channels[warningKey] = {
|
|
3869
|
+
subscribe: {
|
|
3870
|
+
operationId: `dynamicEmitWarning${eventName}`,
|
|
3871
|
+
summary: "Dynamic Emit Detected",
|
|
3872
|
+
description: "This handler emits an event with a dynamic name that could not be determined statically.",
|
|
3873
|
+
tags,
|
|
3874
|
+
"x-warning": true,
|
|
3875
|
+
"x-source-info": {
|
|
3876
|
+
file: astMatch?.sourceContext?.file,
|
|
3877
|
+
line: emit.location?.startLine,
|
|
3878
|
+
startLine: emit.location?.startLine,
|
|
3879
|
+
endLine: emit.location?.endLine,
|
|
3880
|
+
highlightLines: emit.location ? [emit.location.startLine, emit.location.endLine] : void 0
|
|
3881
|
+
},
|
|
3882
|
+
"x-shokupan-source": {
|
|
3883
|
+
file: astMatch?.sourceContext?.file,
|
|
3884
|
+
line: emit.location?.startLine
|
|
3885
|
+
},
|
|
3886
|
+
message: { payload: { type: "object" } }
|
|
3887
|
+
}
|
|
3888
|
+
};
|
|
3889
|
+
continue;
|
|
3890
|
+
}
|
|
3891
|
+
const emitStart = emit.location?.startLine;
|
|
3892
|
+
const emitEnd = emit.location?.endLine;
|
|
3893
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3894
|
+
file: sourceInfo.file,
|
|
3895
|
+
line: emitStart,
|
|
3896
|
+
startLine: emitStart,
|
|
3897
|
+
endLine: emitEnd,
|
|
3898
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3899
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3900
|
+
} : void 0;
|
|
3901
|
+
if (!channels[emit.event]) {
|
|
3902
|
+
channels[emit.event] = {
|
|
3903
|
+
subscribe: {
|
|
3904
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3905
|
+
tags,
|
|
3906
|
+
message: {
|
|
3907
|
+
payload: emit.payload || { type: "object" }
|
|
3908
|
+
},
|
|
3909
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3910
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3911
|
+
file: sourceInfo.file,
|
|
3912
|
+
line: emitStart
|
|
3913
|
+
} : void 0
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
} else {
|
|
3917
|
+
if (newSourceInfo) {
|
|
3918
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3919
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3920
|
+
}
|
|
3921
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3922
|
+
const exists = existing.some(
|
|
3923
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3924
|
+
);
|
|
3925
|
+
if (!exists) {
|
|
3926
|
+
existing.push(newSourceInfo);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
const httpRoutes = router[$routes];
|
|
3935
|
+
if (httpRoutes) {
|
|
3936
|
+
for (const route of httpRoutes) {
|
|
3937
|
+
const handler = route.handler;
|
|
3938
|
+
let tags = route.handlerSpec?.tags;
|
|
3939
|
+
if (!tags && routerTag) {
|
|
3940
|
+
tags = [{ name: routerTag }];
|
|
3941
|
+
}
|
|
3942
|
+
const methodUpper = route.method.toUpperCase();
|
|
3943
|
+
let astMatch = astRoutes.find(
|
|
3944
|
+
(r) => r.method === methodUpper && (r.path === route.path || r.path === "/" + route.path)
|
|
3945
|
+
);
|
|
3946
|
+
if (!astMatch) {
|
|
3947
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3948
|
+
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
3949
|
+
const sameMethodRoutes = astRoutes.filter((r) => r.method === methodUpper);
|
|
3950
|
+
astMatch = sameMethodRoutes.find((r) => {
|
|
3951
|
+
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
3952
|
+
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
3953
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3957
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3958
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3959
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3960
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3961
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3962
|
+
} : void 0;
|
|
3963
|
+
let emits = astMatch?.emits || [];
|
|
3964
|
+
for (const emit of emits) {
|
|
3965
|
+
const emitStart = emit.location?.startLine;
|
|
3966
|
+
const emitEnd = emit.location?.endLine;
|
|
3967
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3968
|
+
file: sourceInfo.file,
|
|
3969
|
+
line: emitStart,
|
|
3970
|
+
startLine: emitStart,
|
|
3971
|
+
endLine: emitEnd,
|
|
3972
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3973
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3974
|
+
} : void 0;
|
|
3975
|
+
if (!channels[emit.event]) {
|
|
3976
|
+
channels[emit.event] = {
|
|
3977
|
+
subscribe: {
|
|
3978
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3979
|
+
tags,
|
|
3980
|
+
message: {
|
|
3981
|
+
payload: emit.payload || { type: "object" }
|
|
3982
|
+
},
|
|
3983
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3984
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3985
|
+
file: sourceInfo.file,
|
|
3986
|
+
line: emitStart
|
|
3987
|
+
} : void 0
|
|
3988
|
+
}
|
|
3989
|
+
};
|
|
3990
|
+
} else {
|
|
3991
|
+
if (newSourceInfo) {
|
|
3992
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3993
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3994
|
+
}
|
|
3995
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3996
|
+
const exists = existing.some(
|
|
3997
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3998
|
+
);
|
|
3999
|
+
if (!exists) {
|
|
4000
|
+
existing.push(newSourceInfo);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
const childRouters = router[$childRouters];
|
|
4008
|
+
for (const child of childRouters) {
|
|
4009
|
+
await collect(child);
|
|
4010
|
+
}
|
|
4011
|
+
};
|
|
4012
|
+
await collect(rootRouter);
|
|
4013
|
+
const dynamicEvents = astRoutes.filter((r) => r.path === "__DYNAMIC_EVENT__" && !matchedAstRoutes.has(r));
|
|
4014
|
+
dynamicEvents.forEach((r, i) => {
|
|
4015
|
+
let prefix = "Anonymous";
|
|
4016
|
+
if (r.handlerName && !r.handlerName.includes("=>") && !r.handlerName.includes("{")) {
|
|
4017
|
+
const parts = r.handlerName.split(".");
|
|
4018
|
+
if (parts.length > 0) prefix = parts[0];
|
|
4019
|
+
}
|
|
4020
|
+
const key = `${prefix}.Dynamic Event ${i + 1}`;
|
|
4021
|
+
channels[key] = {
|
|
4022
|
+
publish: {
|
|
4023
|
+
operationId: `dynamicEventWarning${i}`,
|
|
4024
|
+
summary: "Dynamic Event Detected",
|
|
4025
|
+
description: `A dynamic event listener was detected in your source code but the event name could not be determined statically.`,
|
|
4026
|
+
tags: [{ name: "Warnings" }],
|
|
4027
|
+
"x-warning": true,
|
|
4028
|
+
"x-source-info": {
|
|
4029
|
+
file: r.sourceContext?.file,
|
|
4030
|
+
line: r.sourceContext?.startLine,
|
|
4031
|
+
startLine: r.sourceContext?.startLine,
|
|
4032
|
+
endLine: r.sourceContext?.endLine,
|
|
4033
|
+
highlightLines: r.sourceContext ? [r.sourceContext.startLine, r.sourceContext.endLine] : void 0
|
|
4034
|
+
},
|
|
4035
|
+
"x-shokupan-source": {
|
|
4036
|
+
file: r.sourceContext?.file,
|
|
4037
|
+
line: r.sourceContext?.startLine
|
|
4038
|
+
},
|
|
4039
|
+
message: { payload: { type: "object" } }
|
|
4040
|
+
}
|
|
4041
|
+
};
|
|
4042
|
+
});
|
|
4043
|
+
return {
|
|
4044
|
+
asyncapi: "3.0.0",
|
|
4045
|
+
info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
|
|
4046
|
+
channels
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
4050
|
+
__proto__: null,
|
|
4051
|
+
generateAsyncApi
|
|
4052
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
4053
|
+
class AsyncApiPlugin extends ShokupanRouter {
|
|
4054
|
+
constructor(pluginOptions = {}) {
|
|
4055
|
+
super({ renderer: renderToString });
|
|
4056
|
+
this.pluginOptions = pluginOptions;
|
|
4057
|
+
this.pluginOptions.path ??= "/asyncapi";
|
|
4058
|
+
this.init();
|
|
4059
|
+
}
|
|
4060
|
+
static getBasePath() {
|
|
4061
|
+
const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
4062
|
+
if (dir.endsWith("dist")) {
|
|
4063
|
+
return dir + "/plugins/application/asyncapi";
|
|
4064
|
+
}
|
|
4065
|
+
return dir;
|
|
4066
|
+
}
|
|
4067
|
+
onInit(app, options) {
|
|
4068
|
+
const path2 = this.pluginOptions.path || options?.path || "/asyncapi";
|
|
4069
|
+
app.mount(path2, this);
|
|
4070
|
+
if (app.applicationConfig.enableAsyncApiGen !== true) {
|
|
4071
|
+
console.warn("AsyncApiPlugin: enableAsyncApiGen is disabled. AsyncApiPlugin will not generate spec.");
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
init() {
|
|
4075
|
+
const serveFile = async (ctx, file, type) => {
|
|
4076
|
+
const content = await promises.readFile(path$1.join(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
|
|
4077
|
+
ctx.set("Content-Type", type);
|
|
4078
|
+
return ctx.send(content);
|
|
4079
|
+
};
|
|
4080
|
+
this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
|
|
4081
|
+
this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
|
|
4082
|
+
this.get("/asyncapi-client.mjs", (ctx) => serveFile(ctx, "asyncapi-client.mjs", "application/javascript"));
|
|
4083
|
+
this.get("/", async (ctx) => {
|
|
4084
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4085
|
+
if (!spec) {
|
|
4086
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4087
|
+
}
|
|
4088
|
+
if (this.pluginOptions.spec) {
|
|
4089
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4090
|
+
}
|
|
4091
|
+
const serverUrl = `${ctx.hostname}:${ctx.app?.applicationConfig.port}`;
|
|
4092
|
+
const base = this.pluginOptions.path;
|
|
4093
|
+
const disableSourceView = this.pluginOptions.disableSourceView;
|
|
4094
|
+
const navTree = buildNavTree(spec);
|
|
4095
|
+
return ctx.jsx(AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }));
|
|
4096
|
+
});
|
|
4097
|
+
this.get("/json", async (ctx) => {
|
|
4098
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4099
|
+
if (!spec) {
|
|
4100
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4101
|
+
}
|
|
4102
|
+
if (this.pluginOptions.spec) {
|
|
4103
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4104
|
+
}
|
|
4105
|
+
return ctx.json(spec);
|
|
4106
|
+
});
|
|
4107
|
+
this.get("/_code", async (ctx) => {
|
|
4108
|
+
const file = ctx.query["file"];
|
|
4109
|
+
if (!file || typeof file !== "string") {
|
|
4110
|
+
return ctx.text("Missing file parameter", 400);
|
|
4111
|
+
}
|
|
4112
|
+
try {
|
|
4113
|
+
const content = await promises.readFile(file, "utf8");
|
|
4114
|
+
return ctx.text(content);
|
|
4115
|
+
} catch (e) {
|
|
4116
|
+
return ctx.text("File not found: " + e.message, 404);
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
2996
4121
|
class AuthPlugin extends ShokupanRouter {
|
|
2997
4122
|
constructor(authConfig) {
|
|
2998
4123
|
super();
|
|
2999
4124
|
this.authConfig = authConfig;
|
|
3000
4125
|
this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
|
|
3001
|
-
this.init();
|
|
3002
4126
|
}
|
|
3003
4127
|
secret;
|
|
3004
|
-
|
|
4128
|
+
arctic;
|
|
4129
|
+
jose;
|
|
4130
|
+
async onInit(app, options) {
|
|
4131
|
+
this.arctic = await import("arctic");
|
|
4132
|
+
this.jose = await import("jose");
|
|
4133
|
+
this.init();
|
|
3005
4134
|
if (options?.path) {
|
|
3006
4135
|
app.mount(options.path, this);
|
|
3007
4136
|
} else {
|
|
@@ -3009,15 +4138,16 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3009
4138
|
}
|
|
3010
4139
|
}
|
|
3011
4140
|
getProviderInstance(name, p) {
|
|
4141
|
+
const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3012
4142
|
switch (name) {
|
|
3013
4143
|
case "github":
|
|
3014
|
-
return new
|
|
4144
|
+
return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
|
|
3015
4145
|
case "google":
|
|
3016
|
-
return new
|
|
4146
|
+
return new Google(p.clientId, p.clientSecret, p.redirectUri);
|
|
3017
4147
|
case "microsoft":
|
|
3018
|
-
return new
|
|
4148
|
+
return new MicrosoftEntraId(p.tenantId, p.clientId, p.clientSecret, p.redirectUri);
|
|
3019
4149
|
case "apple":
|
|
3020
|
-
return new
|
|
4150
|
+
return new Apple(
|
|
3021
4151
|
p.clientId,
|
|
3022
4152
|
p.teamId,
|
|
3023
4153
|
p.keyId,
|
|
@@ -3025,18 +4155,18 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3025
4155
|
p.redirectUri
|
|
3026
4156
|
);
|
|
3027
4157
|
case "auth0":
|
|
3028
|
-
return new
|
|
4158
|
+
return new Auth0(p.domain, p.clientId, p.clientSecret, p.redirectUri);
|
|
3029
4159
|
case "okta":
|
|
3030
|
-
return new
|
|
4160
|
+
return new Okta(p.domain, p.authUrl, p.clientId, p.clientSecret, p.redirectUri);
|
|
3031
4161
|
case "oauth2":
|
|
3032
|
-
return new
|
|
4162
|
+
return new OAuth2Client(p.clientId, p.clientSecret, p.redirectUri);
|
|
3033
4163
|
default:
|
|
3034
4164
|
return null;
|
|
3035
4165
|
}
|
|
3036
4166
|
}
|
|
3037
4167
|
async createSession(user, ctx) {
|
|
3038
4168
|
const alg = "HS256";
|
|
3039
|
-
const jwt = await new
|
|
4169
|
+
const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
3040
4170
|
const opts = this.authConfig.cookieOptions || {};
|
|
3041
4171
|
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
3042
4172
|
if (opts.secure) cookie += "; Secure";
|
|
@@ -3046,6 +4176,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3046
4176
|
return jwt;
|
|
3047
4177
|
}
|
|
3048
4178
|
init() {
|
|
4179
|
+
const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3049
4180
|
const providerEntries = Object.entries(this.authConfig.providers);
|
|
3050
4181
|
for (let i = 0; i < providerEntries.length; i++) {
|
|
3051
4182
|
const [providerName, providerConfig] = providerEntries[i];
|
|
@@ -3055,17 +4186,17 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3055
4186
|
continue;
|
|
3056
4187
|
}
|
|
3057
4188
|
this.get(`/auth/${providerName}/login`, async (ctx) => {
|
|
3058
|
-
const state =
|
|
3059
|
-
const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ?
|
|
4189
|
+
const state = generateState();
|
|
4190
|
+
const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ? generateCodeVerifier() : void 0;
|
|
3060
4191
|
const scopes = providerConfig.scopes || [];
|
|
3061
4192
|
let url;
|
|
3062
|
-
if (provider instanceof
|
|
4193
|
+
if (provider instanceof GitHub) {
|
|
3063
4194
|
url = await provider.createAuthorizationURL(state, scopes);
|
|
3064
|
-
} else if (provider instanceof
|
|
4195
|
+
} else if (provider instanceof Google || provider instanceof MicrosoftEntraId || provider instanceof Auth0 || provider instanceof Okta) {
|
|
3065
4196
|
url = await provider.createAuthorizationURL(state, codeVerifier, scopes);
|
|
3066
|
-
} else if (provider instanceof
|
|
4197
|
+
} else if (provider instanceof Apple) {
|
|
3067
4198
|
url = await provider.createAuthorizationURL(state, scopes);
|
|
3068
|
-
} else if (provider instanceof
|
|
4199
|
+
} else if (provider instanceof OAuth2Client) {
|
|
3069
4200
|
if (!providerConfig.authUrl) return ctx.text("Config error: authUrl required for oauth2", 500);
|
|
3070
4201
|
url = await provider.createAuthorizationURL(providerConfig.authUrl, state, scopes);
|
|
3071
4202
|
} else {
|
|
@@ -3091,17 +4222,17 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3091
4222
|
try {
|
|
3092
4223
|
let tokens;
|
|
3093
4224
|
let idToken;
|
|
3094
|
-
if (provider instanceof
|
|
4225
|
+
if (provider instanceof GitHub) {
|
|
3095
4226
|
tokens = await provider.validateAuthorizationCode(code);
|
|
3096
|
-
} else if (provider instanceof
|
|
4227
|
+
} else if (provider instanceof Google || provider instanceof MicrosoftEntraId) {
|
|
3097
4228
|
if (!storedVerifier) return ctx.text("Missing verifier", 400);
|
|
3098
4229
|
tokens = await provider.validateAuthorizationCode(code, storedVerifier);
|
|
3099
|
-
} else if (provider instanceof
|
|
4230
|
+
} else if (provider instanceof Auth0 || provider instanceof Okta) {
|
|
3100
4231
|
tokens = await provider.validateAuthorizationCode(code, storedVerifier || "");
|
|
3101
|
-
} else if (provider instanceof
|
|
4232
|
+
} else if (provider instanceof Apple) {
|
|
3102
4233
|
tokens = await provider.validateAuthorizationCode(code);
|
|
3103
4234
|
idToken = tokens.idToken;
|
|
3104
|
-
} else if (provider instanceof
|
|
4235
|
+
} else if (provider instanceof OAuth2Client) {
|
|
3105
4236
|
if (!providerConfig.tokenUrl) return ctx.text("Config error: tokenUrl required for oauth2", 500);
|
|
3106
4237
|
tokens = await provider.validateAuthorizationCode(providerConfig.tokenUrl, code, null);
|
|
3107
4238
|
}
|
|
@@ -3177,7 +4308,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3177
4308
|
};
|
|
3178
4309
|
} else if (provider === "apple") {
|
|
3179
4310
|
if (idToken) {
|
|
3180
|
-
const payload =
|
|
4311
|
+
const payload = this.jose.decodeJwt(idToken);
|
|
3181
4312
|
user = {
|
|
3182
4313
|
id: payload.sub,
|
|
3183
4314
|
email: payload["email"],
|
|
@@ -3208,6 +4339,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3208
4339
|
*/
|
|
3209
4340
|
getMiddleware() {
|
|
3210
4341
|
return async (ctx, next) => {
|
|
4342
|
+
if (!this.jose) {
|
|
4343
|
+
this.jose = await import("jose");
|
|
4344
|
+
}
|
|
3211
4345
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
3212
4346
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
3213
4347
|
if (!token) {
|
|
@@ -3216,7 +4350,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3216
4350
|
}
|
|
3217
4351
|
if (token) {
|
|
3218
4352
|
try {
|
|
3219
|
-
const { payload } = await
|
|
4353
|
+
const { payload } = await this.jose.jwtVerify(token, this.secret);
|
|
3220
4354
|
ctx.user = payload;
|
|
3221
4355
|
} catch {
|
|
3222
4356
|
}
|
|
@@ -3333,32 +4467,956 @@ class ClusterPlugin {
|
|
|
3333
4467
|
}
|
|
3334
4468
|
}
|
|
3335
4469
|
}
|
|
3336
|
-
|
|
4470
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
|
|
4471
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
|
|
4472
|
+
/* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
|
|
4473
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
|
|
4474
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
4475
|
+
/* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan Debug Dashboard" }),
|
|
4476
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
|
|
4477
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
|
|
4478
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
4479
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/styles.css` }),
|
|
4480
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/reactflow.css` }),
|
|
4481
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
|
|
4482
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
|
|
4483
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
|
|
4484
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
|
|
4485
|
+
] }),
|
|
4486
|
+
/* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
|
|
4487
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "container", children: [
|
|
4488
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
|
|
4489
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4490
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { children: "Dashboard" }),
|
|
4491
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary)", children: [
|
|
4492
|
+
"Uptime: ",
|
|
4493
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "uptime", children: uptime })
|
|
4494
|
+
] })
|
|
4495
|
+
] }),
|
|
4496
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tabs", children: [
|
|
4497
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
|
|
4498
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
|
|
4499
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
|
|
4500
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
|
|
4501
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
|
|
4502
|
+
integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
|
|
4503
|
+
integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
|
|
4504
|
+
] })
|
|
4505
|
+
] }),
|
|
4506
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
|
|
4507
|
+
/* @__PURE__ */ jsxRuntime.jsx(MetricsGrid, { metrics }),
|
|
4508
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
|
|
4509
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxRuntime.jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
|
|
4510
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1m", children: "1 Minute" }),
|
|
4511
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "5m", children: "5 Minutes" }),
|
|
4512
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "30m", children: "30 Minutes" }),
|
|
4513
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1h", children: "1 Hour" }),
|
|
4514
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "2h", children: "2 Hours" }),
|
|
4515
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "6h", children: "6 Hours" }),
|
|
4516
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "12h", children: "12 Hours" }),
|
|
4517
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1d", children: "1 Day" }),
|
|
4518
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "3d", children: "3 Days" }),
|
|
4519
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "7d", children: "7 Days" }),
|
|
4520
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "30d", children: "30 Days" })
|
|
4521
|
+
] }) }),
|
|
4522
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
|
|
4523
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
|
|
4524
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
|
|
4525
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
|
|
4526
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
|
|
4527
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
|
|
4528
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
|
|
4529
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
|
|
4530
|
+
] }),
|
|
4531
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
|
|
4532
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
|
|
4533
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
4534
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
4535
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
4536
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
4537
|
+
] }),
|
|
4538
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4539
|
+
] })
|
|
4540
|
+
] }),
|
|
4541
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
|
|
4542
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Component Registry" }),
|
|
4543
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
|
|
4544
|
+
] }) }),
|
|
4545
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-graph", class: "tab-content", children: [
|
|
4546
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
|
|
4547
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "cy" })
|
|
4548
|
+
] }),
|
|
4549
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-requests", class: "tab-content", children: [
|
|
4550
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4551
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
|
|
4552
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" }) })
|
|
4553
|
+
] }),
|
|
4554
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
|
|
4555
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
|
|
4556
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Request Details" }),
|
|
4557
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
|
|
4558
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
4559
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
|
|
4560
|
+
] })
|
|
4561
|
+
] }),
|
|
4562
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-failures", class: "tab-content", children: [
|
|
4563
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4564
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
|
|
4565
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4566
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "importFailure()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 8px;", children: "Import" }),
|
|
4567
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "fetchFailures()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" })
|
|
4568
|
+
] })
|
|
4569
|
+
] }),
|
|
4570
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "failures-table-container" })
|
|
4571
|
+
] }),
|
|
4572
|
+
integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
4573
|
+
integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
|
|
4574
|
+
] }),
|
|
4575
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
|
|
4576
|
+
__html: `
|
|
4577
|
+
// Injected function from server config
|
|
4578
|
+
const getRequestHeaders = ${getRequestHeadersSource};
|
|
4579
|
+
`
|
|
4580
|
+
} }),
|
|
4581
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/poll.js` }),
|
|
4582
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
|
|
4583
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
|
|
4584
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
|
|
4585
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/registry.js` }),
|
|
4586
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/failures.js` }),
|
|
4587
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/requests.js` }),
|
|
4588
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tabs.js` })
|
|
4589
|
+
] })
|
|
4590
|
+
] });
|
|
4591
|
+
}
|
|
4592
|
+
function MetricsGrid({ metrics }) {
|
|
4593
|
+
const total = metrics.totalRequests;
|
|
4594
|
+
const active = metrics.activeRequests;
|
|
4595
|
+
const finished = total - active;
|
|
4596
|
+
const successRate = finished ? Math.round(metrics.successfulRequests / finished * 100) : 100;
|
|
4597
|
+
const failRate = finished ? Math.round(metrics.failedRequests / finished * 100) : 0;
|
|
4598
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "metrics-grid", children: [
|
|
4599
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4600
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Total Requests" }),
|
|
4601
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value", id: "total-requests", children: metrics.totalRequests })
|
|
4602
|
+
] }),
|
|
4603
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4604
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Active Requests" }),
|
|
4605
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value", style: "color: var(--accent)", id: "active-requests", children: metrics.activeRequests })
|
|
4606
|
+
] }),
|
|
4607
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4608
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Success Rate" }),
|
|
4609
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value text-success", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { id: "success-rate", children: [
|
|
4610
|
+
successRate,
|
|
4611
|
+
"%"
|
|
4612
|
+
] }) }),
|
|
4613
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4614
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "successful-requests", children: metrics.successfulRequests }),
|
|
4615
|
+
" successful"
|
|
4616
|
+
] })
|
|
4617
|
+
] }),
|
|
4618
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4619
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Fail Rate" }),
|
|
4620
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value text-error", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { id: "fail-rate", children: [
|
|
4621
|
+
failRate,
|
|
4622
|
+
"%"
|
|
4623
|
+
] }) }),
|
|
4624
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4625
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "failed-requests", children: metrics.failedRequests }),
|
|
4626
|
+
" failed"
|
|
4627
|
+
] })
|
|
4628
|
+
] }),
|
|
4629
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4630
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Avg Latency" }),
|
|
4631
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-value", children: [
|
|
4632
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "avg-latency", children: metrics.averageTotalTime_ms.toFixed(2) }),
|
|
4633
|
+
" ",
|
|
4634
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: "font-size: 1rem; color: var(--text-secondary)", children: "ms" })
|
|
4635
|
+
] })
|
|
4636
|
+
] })
|
|
4637
|
+
] });
|
|
4638
|
+
}
|
|
4639
|
+
function ChartCard({ title, id }) {
|
|
4640
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "height: 300px;", children: [
|
|
4641
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: title }),
|
|
4642
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-chart", children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { id }) })
|
|
4643
|
+
] });
|
|
4644
|
+
}
|
|
4645
|
+
function Card({ title, contentId }) {
|
|
4646
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4647
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: title }),
|
|
4648
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: contentId })
|
|
4649
|
+
] });
|
|
4650
|
+
}
|
|
4651
|
+
const INTERVALS = [
|
|
4652
|
+
{ label: "10s", ms: 10 * 1e3 },
|
|
4653
|
+
{ label: "1m", ms: 60 * 1e3 },
|
|
4654
|
+
{ label: "5m", ms: 5 * 60 * 1e3 },
|
|
4655
|
+
{ label: "1h", ms: 60 * 60 * 1e3 },
|
|
4656
|
+
{ label: "2h", ms: 2 * 60 * 60 * 1e3 },
|
|
4657
|
+
{ label: "6h", ms: 6 * 60 * 60 * 1e3 },
|
|
4658
|
+
{ label: "12h", ms: 12 * 60 * 60 * 1e3 },
|
|
4659
|
+
{ label: "1d", ms: 24 * 60 * 60 * 1e3 },
|
|
4660
|
+
{ label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
|
|
4661
|
+
{ label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
|
|
4662
|
+
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
4663
|
+
];
|
|
4664
|
+
class MetricsCollector {
|
|
4665
|
+
constructor(db) {
|
|
4666
|
+
this.db = db;
|
|
4667
|
+
this.eventLoopHistogram.enable();
|
|
4668
|
+
const now = Date.now();
|
|
4669
|
+
INTERVALS.forEach((int) => {
|
|
4670
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
4671
|
+
this.pendingDetails[int.label] = [];
|
|
4672
|
+
});
|
|
4673
|
+
}
|
|
4674
|
+
currentIntervalStart = {};
|
|
4675
|
+
pendingDetails = {};
|
|
4676
|
+
eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
|
|
4677
|
+
timer = null;
|
|
4678
|
+
recordRequest(duration, isError) {
|
|
4679
|
+
INTERVALS.forEach((int) => {
|
|
4680
|
+
this.pendingDetails[int.label].push({ duration, isError });
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
alignTimestamp(ts, intervalMs) {
|
|
4684
|
+
return Math.floor(ts / intervalMs) * intervalMs;
|
|
4685
|
+
}
|
|
4686
|
+
async collect() {
|
|
4687
|
+
try {
|
|
4688
|
+
const now = Date.now();
|
|
4689
|
+
for (const int of INTERVALS) {
|
|
4690
|
+
const start = this.currentIntervalStart[int.label];
|
|
4691
|
+
if (now >= start + int.ms) {
|
|
4692
|
+
await this.flushInterval(int.label, start, int.ms);
|
|
4693
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
} catch (error) {
|
|
4697
|
+
console.error("[MetricsCollector] Error in collect():", error);
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
async flushInterval(label, timestamp, durationMs) {
|
|
4701
|
+
const reqs = this.pendingDetails[label];
|
|
4702
|
+
this.pendingDetails[label] = [];
|
|
4703
|
+
if (reqs.length === 0) {
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
const totalReqs = reqs.length;
|
|
4707
|
+
const errorReqs = reqs.filter((r) => r.isError).length;
|
|
4708
|
+
const successReqs = totalReqs - errorReqs;
|
|
4709
|
+
const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
|
|
4710
|
+
const rps = totalReqs / (durationMs / 1e3);
|
|
4711
|
+
const sum = duratons.reduce((a, b) => a + b, 0);
|
|
4712
|
+
const avg = totalReqs > 0 ? sum / totalReqs : 0;
|
|
4713
|
+
const getP = (p) => {
|
|
4714
|
+
if (duratons.length === 0) return 0;
|
|
4715
|
+
const idx = Math.floor(duratons.length * p);
|
|
4716
|
+
return duratons[idx];
|
|
4717
|
+
};
|
|
4718
|
+
const metric = {
|
|
4719
|
+
timestamp,
|
|
4720
|
+
interval: label,
|
|
4721
|
+
cpu: os__namespace.loadavg()[0],
|
|
4722
|
+
// Using load avg for simplicity as per requirements (Load)
|
|
4723
|
+
load: os__namespace.loadavg(),
|
|
4724
|
+
memory: {
|
|
4725
|
+
used: process.memoryUsage().rss,
|
|
4726
|
+
total: os__namespace.totalmem(),
|
|
4727
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
4728
|
+
heapTotal: process.memoryUsage().heapTotal
|
|
4729
|
+
},
|
|
4730
|
+
eventLoopLatency: {
|
|
4731
|
+
min: this.eventLoopHistogram.min / 1e6,
|
|
4732
|
+
max: this.eventLoopHistogram.max / 1e6,
|
|
4733
|
+
mean: this.eventLoopHistogram.mean / 1e6,
|
|
4734
|
+
p50: this.eventLoopHistogram.percentile(50) / 1e6,
|
|
4735
|
+
p95: this.eventLoopHistogram.percentile(95) / 1e6,
|
|
4736
|
+
p99: this.eventLoopHistogram.percentile(99) / 1e6
|
|
4737
|
+
},
|
|
4738
|
+
requests: {
|
|
4739
|
+
total: totalReqs,
|
|
4740
|
+
rps,
|
|
4741
|
+
success: successReqs,
|
|
4742
|
+
error: errorReqs
|
|
4743
|
+
},
|
|
4744
|
+
responseTime: {
|
|
4745
|
+
min: duratons[0] || 0,
|
|
4746
|
+
max: duratons[duratons.length - 1] || 0,
|
|
4747
|
+
avg,
|
|
4748
|
+
p50: getP(0.5),
|
|
4749
|
+
p95: getP(0.95),
|
|
4750
|
+
p99: getP(0.99)
|
|
4751
|
+
}
|
|
4752
|
+
};
|
|
4753
|
+
try {
|
|
4754
|
+
const recordId = new surrealdb.RecordId("metrics", timestamp);
|
|
4755
|
+
await this.db.upsert(recordId, metric);
|
|
4756
|
+
const test = await this.db.select(recordId);
|
|
4757
|
+
const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
4758
|
+
} catch (e) {
|
|
4759
|
+
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
// Cleanup if needed
|
|
4763
|
+
stop() {
|
|
4764
|
+
if (this.timer) clearInterval(this.timer);
|
|
4765
|
+
this.eventLoopHistogram.disable();
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
class Collector {
|
|
4769
|
+
constructor(dashboard) {
|
|
4770
|
+
this.dashboard = dashboard;
|
|
4771
|
+
}
|
|
4772
|
+
currentNode;
|
|
4773
|
+
trackStep(id, type, duration, status, error) {
|
|
4774
|
+
if (!id) return;
|
|
4775
|
+
this.dashboard.recordNodeMetric(id, type, duration, status === "error");
|
|
4776
|
+
}
|
|
4777
|
+
trackEdge(fromId, toId) {
|
|
4778
|
+
if (!fromId || !toId) return;
|
|
4779
|
+
this.dashboard.recordEdgeMetric(fromId, toId);
|
|
4780
|
+
}
|
|
4781
|
+
setNode(id) {
|
|
4782
|
+
this.currentNode = id;
|
|
4783
|
+
}
|
|
4784
|
+
getCurrentNode() {
|
|
4785
|
+
return this.currentNode;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
class Dashboard {
|
|
4789
|
+
constructor(dashboardConfig = {}) {
|
|
4790
|
+
this.dashboardConfig = dashboardConfig;
|
|
4791
|
+
}
|
|
4792
|
+
[$appRoot];
|
|
4793
|
+
router = new ShokupanRouter({ renderer: renderToString });
|
|
4794
|
+
metrics = {
|
|
4795
|
+
totalRequests: 0,
|
|
4796
|
+
successfulRequests: 0,
|
|
4797
|
+
failedRequests: 0,
|
|
4798
|
+
activeRequests: 0,
|
|
4799
|
+
averageTotalTime_ms: 0,
|
|
4800
|
+
recentTimings: [],
|
|
4801
|
+
logs: [],
|
|
4802
|
+
rateLimitedCounts: {},
|
|
4803
|
+
nodeMetrics: {},
|
|
4804
|
+
edgeMetrics: {}
|
|
4805
|
+
};
|
|
4806
|
+
startTime = Date.now();
|
|
4807
|
+
instrumented = false;
|
|
4808
|
+
metricsCollector;
|
|
4809
|
+
get db() {
|
|
4810
|
+
return this[$appRoot].db;
|
|
4811
|
+
}
|
|
4812
|
+
// ShokupanPlugin interface implementation
|
|
4813
|
+
onInit(app, options) {
|
|
4814
|
+
this[$appRoot] = app;
|
|
4815
|
+
this.metricsCollector = new MetricsCollector(this.db);
|
|
4816
|
+
const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
4817
|
+
const hooks = this.getHooks();
|
|
4818
|
+
if (!app.middleware) {
|
|
4819
|
+
app.middleware = [];
|
|
4820
|
+
}
|
|
4821
|
+
const hooksMiddleware = async (ctx, next) => {
|
|
4822
|
+
if (hooks.onRequestStart) {
|
|
4823
|
+
await hooks.onRequestStart(ctx);
|
|
4824
|
+
}
|
|
4825
|
+
await next();
|
|
4826
|
+
if (hooks.onResponseEnd) {
|
|
4827
|
+
const effectiveResponse = ctx._finalResponse || ctx.response || {};
|
|
4828
|
+
await hooks.onResponseEnd(ctx, effectiveResponse);
|
|
4829
|
+
}
|
|
4830
|
+
};
|
|
4831
|
+
app.use(hooksMiddleware);
|
|
4832
|
+
app.mount(mountPath, this.router);
|
|
4833
|
+
this.setupRoutes();
|
|
4834
|
+
}
|
|
4835
|
+
detectIntegrations() {
|
|
4836
|
+
const integrations = {};
|
|
4837
|
+
const routers = this[$appRoot]?.[$childRouters] || [];
|
|
4838
|
+
const checkConfig = (key) => {
|
|
4839
|
+
const conf = this.dashboardConfig.integrations?.[key];
|
|
4840
|
+
if (conf === false) return { enabled: false };
|
|
4841
|
+
if (typeof conf === "object" && conf.path) return { enabled: true, path: conf.path };
|
|
4842
|
+
return { enabled: true };
|
|
4843
|
+
};
|
|
4844
|
+
const scalarConf = checkConfig("scalar");
|
|
4845
|
+
if (scalarConf.enabled) {
|
|
4846
|
+
if (scalarConf.path) {
|
|
4847
|
+
integrations["scalar"] = scalarConf.path;
|
|
4848
|
+
} else {
|
|
4849
|
+
const plugin = routers.find((r) => r.constructor.name === "ScalarPlugin");
|
|
4850
|
+
if (plugin) {
|
|
4851
|
+
integrations["scalar"] = plugin[$mountPath];
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
const asyncApiConf = checkConfig("asyncapi");
|
|
4856
|
+
if (asyncApiConf.enabled) {
|
|
4857
|
+
if (asyncApiConf.path) {
|
|
4858
|
+
integrations["asyncapi"] = asyncApiConf.path;
|
|
4859
|
+
} else {
|
|
4860
|
+
const plugin = routers.find((r) => r.constructor.name === "AsyncApiPlugin");
|
|
4861
|
+
if (plugin) {
|
|
4862
|
+
integrations["asyncapi"] = plugin[$mountPath];
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
return integrations;
|
|
4867
|
+
}
|
|
4868
|
+
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
4869
|
+
static getBasePath() {
|
|
4870
|
+
const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
4871
|
+
if (dir.endsWith("dist")) {
|
|
4872
|
+
return dir + "/plugins/application/dashboard";
|
|
4873
|
+
}
|
|
4874
|
+
return dir;
|
|
4875
|
+
}
|
|
4876
|
+
setupRoutes() {
|
|
4877
|
+
this.router.get("/metrics", async (ctx) => {
|
|
4878
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
4879
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
4880
|
+
const interval = ctx.query["interval"];
|
|
4881
|
+
if (interval) {
|
|
4882
|
+
const intervalMap = {
|
|
4883
|
+
"10s": 10 * 1e3,
|
|
4884
|
+
"1m": 60 * 1e3,
|
|
4885
|
+
"5m": 5 * 60 * 1e3,
|
|
4886
|
+
"30m": 30 * 60 * 1e3,
|
|
4887
|
+
"1h": 60 * 60 * 1e3,
|
|
4888
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
4889
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
4890
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
4891
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
4892
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
4893
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
4894
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
4895
|
+
};
|
|
4896
|
+
const ms = intervalMap[interval] || 60 * 1e3;
|
|
4897
|
+
const startTime = Date.now() - ms;
|
|
4898
|
+
let stats;
|
|
4899
|
+
try {
|
|
4900
|
+
stats = await this.db.query(`
|
|
4901
|
+
SELECT
|
|
4902
|
+
count() as total,
|
|
4903
|
+
count(IF status < 400 THEN 1 END) as success,
|
|
4904
|
+
count(IF status >= 400 THEN 1 END) as failed,
|
|
4905
|
+
math::mean(duration) as avg_latency
|
|
4906
|
+
FROM requests
|
|
4907
|
+
WHERE timestamp >= $start
|
|
4908
|
+
GROUP ALL
|
|
4909
|
+
`, { start: startTime });
|
|
4910
|
+
} catch (error) {
|
|
4911
|
+
console.error("[Dashboard] Query failed at plugin.ts:180-191", {
|
|
4912
|
+
error,
|
|
4913
|
+
interval,
|
|
4914
|
+
startTime,
|
|
4915
|
+
query: "metrics interval stats",
|
|
4916
|
+
stack: new Error().stack
|
|
4917
|
+
});
|
|
4918
|
+
throw error;
|
|
4919
|
+
}
|
|
4920
|
+
const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
|
|
4921
|
+
return ctx.json({
|
|
4922
|
+
metrics: {
|
|
4923
|
+
totalRequests: s.total || 0,
|
|
4924
|
+
successfulRequests: s.success || 0,
|
|
4925
|
+
failedRequests: s.failed || 0,
|
|
4926
|
+
activeRequests: this.metrics.activeRequests,
|
|
4927
|
+
averageTotalTime_ms: s.avg_latency || 0,
|
|
4928
|
+
recentTimings: this.metrics.recentTimings,
|
|
4929
|
+
logs: [],
|
|
4930
|
+
rateLimitedCounts: this.metrics.rateLimitedCounts,
|
|
4931
|
+
nodeMetrics: this.metrics.nodeMetrics,
|
|
4932
|
+
edgeMetrics: this.metrics.edgeMetrics
|
|
4933
|
+
},
|
|
4934
|
+
uptime
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
return ctx.json({
|
|
4938
|
+
metrics: this.metrics,
|
|
4939
|
+
uptime
|
|
4940
|
+
});
|
|
4941
|
+
});
|
|
4942
|
+
this.router.get("/metrics/history", async (ctx) => {
|
|
4943
|
+
const interval = ctx.query["interval"] || "1m";
|
|
4944
|
+
const intervalMap = {
|
|
4945
|
+
"10s": 10 * 1e3,
|
|
4946
|
+
"1m": 60 * 1e3,
|
|
4947
|
+
"5m": 5 * 60 * 1e3,
|
|
4948
|
+
"30m": 30 * 60 * 1e3,
|
|
4949
|
+
"1h": 60 * 60 * 1e3,
|
|
4950
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
4951
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
4952
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
4953
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
4954
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
4955
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
4956
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
4957
|
+
};
|
|
4958
|
+
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
4959
|
+
const startTime = Date.now() - periodMs * 3;
|
|
4960
|
+
const endTime = Date.now();
|
|
4961
|
+
const result = await this.db.query(
|
|
4962
|
+
"SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
4963
|
+
{ start: startTime, end: endTime, interval }
|
|
4964
|
+
);
|
|
4965
|
+
return ctx.json({
|
|
4966
|
+
metrics: result[0] || []
|
|
4967
|
+
});
|
|
4968
|
+
});
|
|
4969
|
+
const getIntervalStartTime = (interval) => {
|
|
4970
|
+
if (!interval) return 0;
|
|
4971
|
+
const intervalMap = {
|
|
4972
|
+
"10s": 10 * 1e3,
|
|
4973
|
+
"1m": 60 * 1e3,
|
|
4974
|
+
"5m": 5 * 60 * 1e3,
|
|
4975
|
+
"30m": 30 * 60 * 1e3,
|
|
4976
|
+
"1h": 60 * 60 * 1e3,
|
|
4977
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
4978
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
4979
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
4980
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
4981
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
4982
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
4983
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
4984
|
+
};
|
|
4985
|
+
const ms = intervalMap[interval] || 0;
|
|
4986
|
+
return ms ? Date.now() - ms : 0;
|
|
4987
|
+
};
|
|
4988
|
+
this.router.get("/requests/top", async (ctx) => {
|
|
4989
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
4990
|
+
const result = await this.db.query(
|
|
4991
|
+
"SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
4992
|
+
{ start: startTime }
|
|
4993
|
+
);
|
|
4994
|
+
return ctx.json({ top: result[0] || [] });
|
|
4995
|
+
});
|
|
4996
|
+
this.router.get("/errors/top", async (ctx) => {
|
|
4997
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
4998
|
+
const result = await this.db.query(
|
|
4999
|
+
"SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
5000
|
+
{ start: startTime }
|
|
5001
|
+
);
|
|
5002
|
+
return ctx.json({ top: result[0] || [] });
|
|
5003
|
+
});
|
|
5004
|
+
this.router.get("/requests/failing", async (ctx) => {
|
|
5005
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5006
|
+
const result = await this.db.query(
|
|
5007
|
+
"SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
5008
|
+
{ start: startTime }
|
|
5009
|
+
);
|
|
5010
|
+
return ctx.json({ top: result[0] || [] });
|
|
5011
|
+
});
|
|
5012
|
+
this.router.get("/requests/slowest", async (ctx) => {
|
|
5013
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5014
|
+
const result = await this.db.query(
|
|
5015
|
+
"SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
5016
|
+
{ start: startTime }
|
|
5017
|
+
);
|
|
5018
|
+
return ctx.json({ slowest: result[0] || [] });
|
|
5019
|
+
});
|
|
5020
|
+
this.router.get("/registry", (ctx) => {
|
|
5021
|
+
const app = this[$appRoot];
|
|
5022
|
+
if (!this.instrumented && app) {
|
|
5023
|
+
this.instrumentApp(app);
|
|
5024
|
+
}
|
|
5025
|
+
const registry = app?.getComponentRegistry?.();
|
|
5026
|
+
if (registry) {
|
|
5027
|
+
this.assignIdsToRegistry(registry, "root");
|
|
5028
|
+
}
|
|
5029
|
+
return ctx.json({ registry: registry || {} });
|
|
5030
|
+
});
|
|
5031
|
+
this.router.get("/requests", async (ctx) => {
|
|
5032
|
+
const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
|
|
5033
|
+
return ctx.json({ requests: result[0] || [] });
|
|
5034
|
+
});
|
|
5035
|
+
this.router.get("/requests/:id", async (ctx) => {
|
|
5036
|
+
const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
|
|
5037
|
+
return ctx.json({ request: result[0]?.[0] });
|
|
5038
|
+
});
|
|
5039
|
+
this.router.get("/failures", async (ctx) => {
|
|
5040
|
+
const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
|
|
5041
|
+
return ctx.json({ failures: result[0] });
|
|
5042
|
+
});
|
|
5043
|
+
this.router.post("/replay", async (ctx) => {
|
|
5044
|
+
const body = await ctx.body();
|
|
5045
|
+
const app = this[$appRoot];
|
|
5046
|
+
if (!app) return unknownError(ctx);
|
|
5047
|
+
try {
|
|
5048
|
+
const result = await app.processRequest({
|
|
5049
|
+
method: body.method,
|
|
5050
|
+
path: body.url,
|
|
5051
|
+
// or path
|
|
5052
|
+
headers: body.headers,
|
|
5053
|
+
body: body.body
|
|
5054
|
+
});
|
|
5055
|
+
return ctx.json({
|
|
5056
|
+
status: result.status,
|
|
5057
|
+
headers: result.headers,
|
|
5058
|
+
data: result.data
|
|
5059
|
+
});
|
|
5060
|
+
} catch (e) {
|
|
5061
|
+
return ctx.json({ error: String(e) }, 500);
|
|
5062
|
+
}
|
|
5063
|
+
});
|
|
5064
|
+
this.router.get("/**", async (ctx) => {
|
|
5065
|
+
const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
|
|
5066
|
+
let relativePath = ctx.path;
|
|
5067
|
+
if (relativePath.startsWith(mountPath)) {
|
|
5068
|
+
relativePath = relativePath.slice(mountPath.length);
|
|
5069
|
+
}
|
|
5070
|
+
if (relativePath.startsWith("/")) {
|
|
5071
|
+
relativePath = relativePath.slice(1);
|
|
5072
|
+
}
|
|
5073
|
+
const path2 = relativePath;
|
|
5074
|
+
const staticFiles = [
|
|
5075
|
+
"charts.js",
|
|
5076
|
+
"failures.js",
|
|
5077
|
+
"graph.mjs",
|
|
5078
|
+
"poll.js",
|
|
5079
|
+
"reactflow.css",
|
|
5080
|
+
"registry.css",
|
|
5081
|
+
"registry.js",
|
|
5082
|
+
"requests.js",
|
|
5083
|
+
"styles.css",
|
|
5084
|
+
"tables.js",
|
|
5085
|
+
"tabs.js",
|
|
5086
|
+
"tabulator.css",
|
|
5087
|
+
"theme.css"
|
|
5088
|
+
];
|
|
5089
|
+
if (staticFiles.includes(path2)) {
|
|
5090
|
+
const content = await promises.readFile(path$1.join(Dashboard.getBasePath(), "static", path2), "utf-8");
|
|
5091
|
+
if (path2.endsWith(".css")) ctx.set("Content-Type", "text/css");
|
|
5092
|
+
else if (path2.endsWith(".js") || path2.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
|
|
5093
|
+
return ctx.send(content);
|
|
5094
|
+
}
|
|
5095
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
5096
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
5097
|
+
this.getLinkPattern();
|
|
5098
|
+
const integrations = this.detectIntegrations();
|
|
5099
|
+
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
5100
|
+
const html = renderToString(DashboardApp({
|
|
5101
|
+
metrics: this.metrics,
|
|
5102
|
+
uptime,
|
|
5103
|
+
rootPath: process.cwd(),
|
|
5104
|
+
integrations,
|
|
5105
|
+
base: mountPath,
|
|
5106
|
+
getRequestHeadersSource
|
|
5107
|
+
}));
|
|
5108
|
+
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
5111
|
+
instrumentApp(app) {
|
|
5112
|
+
if (!app.getComponentRegistry) return;
|
|
5113
|
+
const registry = app.getComponentRegistry();
|
|
5114
|
+
this.assignIdsToRegistry(registry, "root");
|
|
5115
|
+
this.instrumented = true;
|
|
5116
|
+
}
|
|
5117
|
+
// Traverses registry, generates IDs, and attaches them to the actual function objects
|
|
5118
|
+
assignIdsToRegistry(node, parentId) {
|
|
5119
|
+
if (!node) return;
|
|
5120
|
+
const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
5121
|
+
node.middleware?.forEach((mw, idx) => {
|
|
5122
|
+
const id = makeId("mw", parentId, idx, mw.name);
|
|
5123
|
+
mw.id = id;
|
|
5124
|
+
if (mw._fn) mw._fn._debugId = id;
|
|
5125
|
+
});
|
|
5126
|
+
node.controllers?.forEach((ctrl, idx) => {
|
|
5127
|
+
const id = makeId("ctrl", parentId, idx, ctrl.name);
|
|
5128
|
+
ctrl.id = id;
|
|
5129
|
+
});
|
|
5130
|
+
node.routes?.forEach((r, idx) => {
|
|
5131
|
+
const id = makeId("route", parentId, idx, r.handlerName || "handler");
|
|
5132
|
+
r.id = id;
|
|
5133
|
+
if (r._fn) r._fn._debugId = id;
|
|
5134
|
+
});
|
|
5135
|
+
node.routers?.forEach((r, idx) => {
|
|
5136
|
+
const id = makeId("router", parentId, idx, r.path);
|
|
5137
|
+
r.id = id;
|
|
5138
|
+
this.assignIdsToRegistry(r.children, id);
|
|
5139
|
+
});
|
|
5140
|
+
}
|
|
5141
|
+
recordNodeMetric(id, type, duration, isError) {
|
|
5142
|
+
if (!this.metrics.nodeMetrics[id]) {
|
|
5143
|
+
this.metrics.nodeMetrics[id] = {
|
|
5144
|
+
id,
|
|
5145
|
+
type,
|
|
5146
|
+
requests: 0,
|
|
5147
|
+
totalTime: 0,
|
|
5148
|
+
failures: 0,
|
|
5149
|
+
name: id
|
|
5150
|
+
// simplify
|
|
5151
|
+
};
|
|
5152
|
+
}
|
|
5153
|
+
const m = this.metrics.nodeMetrics[id];
|
|
5154
|
+
m.requests++;
|
|
5155
|
+
m.totalTime += duration;
|
|
5156
|
+
if (isError) m.failures++;
|
|
5157
|
+
}
|
|
5158
|
+
recordEdgeMetric(from, to) {
|
|
5159
|
+
const key = `${from}|${to}`;
|
|
5160
|
+
this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
|
|
5161
|
+
}
|
|
5162
|
+
getLinkPattern() {
|
|
5163
|
+
const term = process.env["TERM_PROGRAM"] || "";
|
|
5164
|
+
if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
|
|
5165
|
+
return "vscode://file/{{absolute}}:{{line}}";
|
|
5166
|
+
}
|
|
5167
|
+
return "file:///{{absolute}}:{{line}}";
|
|
5168
|
+
}
|
|
5169
|
+
getHooks() {
|
|
5170
|
+
return {
|
|
5171
|
+
onRequestStart: (ctx) => {
|
|
5172
|
+
const app = this[$appRoot];
|
|
5173
|
+
if (!this.instrumented && app) {
|
|
5174
|
+
this.instrumentApp(app);
|
|
5175
|
+
}
|
|
5176
|
+
this.metrics.totalRequests++;
|
|
5177
|
+
this.metrics.activeRequests++;
|
|
5178
|
+
ctx._debugStartTime = performance.now();
|
|
5179
|
+
ctx[$debug] = new Collector(this);
|
|
5180
|
+
},
|
|
5181
|
+
onResponseEnd: async (ctx, response) => {
|
|
5182
|
+
this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
|
|
5183
|
+
const start = ctx._debugStartTime;
|
|
5184
|
+
let duration = 0;
|
|
5185
|
+
if (start) {
|
|
5186
|
+
duration = performance.now() - start;
|
|
5187
|
+
this.updateTiming(duration);
|
|
5188
|
+
}
|
|
5189
|
+
const isError = response.status >= 400;
|
|
5190
|
+
this.metricsCollector.recordRequest(duration, isError);
|
|
5191
|
+
if (response.status >= 400) {
|
|
5192
|
+
this.metrics.failedRequests++;
|
|
5193
|
+
if (response.status === 429) {
|
|
5194
|
+
const path2 = ctx.path;
|
|
5195
|
+
this.metrics.rateLimitedCounts[path2] = (this.metrics.rateLimitedCounts[path2] || 0) + 1;
|
|
5196
|
+
}
|
|
5197
|
+
try {
|
|
5198
|
+
const headers = {};
|
|
5199
|
+
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
5200
|
+
ctx.request.headers.forEach((v, k) => {
|
|
5201
|
+
headers[k] = v;
|
|
5202
|
+
});
|
|
5203
|
+
}
|
|
5204
|
+
await this.db.upsert(new surrealdb.RecordId("failed_requests", ctx.requestId), {
|
|
5205
|
+
method: ctx.method,
|
|
5206
|
+
url: ctx.url.toString(),
|
|
5207
|
+
headers,
|
|
5208
|
+
status: response.status,
|
|
5209
|
+
timestamp: Date.now(),
|
|
5210
|
+
state: ctx.state
|
|
5211
|
+
// body?
|
|
5212
|
+
});
|
|
5213
|
+
} catch (e) {
|
|
5214
|
+
console.error("Failed to record failed request", e);
|
|
5215
|
+
}
|
|
5216
|
+
} else {
|
|
5217
|
+
this.metrics.successfulRequests++;
|
|
5218
|
+
}
|
|
5219
|
+
const logEntry = {
|
|
5220
|
+
method: ctx.method,
|
|
5221
|
+
url: ctx.url.toString(),
|
|
5222
|
+
status: response.status,
|
|
5223
|
+
duration,
|
|
5224
|
+
timestamp: Date.now(),
|
|
5225
|
+
handlerStack: ctx.handlerStack
|
|
5226
|
+
};
|
|
5227
|
+
this.metrics.logs.push(logEntry);
|
|
5228
|
+
try {
|
|
5229
|
+
await this.db.upsert(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
|
|
5230
|
+
} catch (e) {
|
|
5231
|
+
console.error("Failed to record request log", e);
|
|
5232
|
+
}
|
|
5233
|
+
const retention = this.dashboardConfig.retentionMs ?? 72e5;
|
|
5234
|
+
const cutoff = Date.now() - retention;
|
|
5235
|
+
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
5236
|
+
this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
};
|
|
5240
|
+
}
|
|
5241
|
+
updateTiming(duration) {
|
|
5242
|
+
const alpha = 0.1;
|
|
5243
|
+
if (this.metrics.averageTotalTime_ms === 0) {
|
|
5244
|
+
this.metrics.averageTotalTime_ms = duration;
|
|
5245
|
+
} else {
|
|
5246
|
+
this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
|
|
5247
|
+
}
|
|
5248
|
+
this.metrics.recentTimings.push(duration);
|
|
5249
|
+
if (this.metrics.recentTimings.length > 50) {
|
|
5250
|
+
this.metrics.recentTimings.shift();
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
function unknownError(ctx) {
|
|
5255
|
+
return ctx.json({ error: "Unknown Error" }, 500);
|
|
5256
|
+
}
|
|
5257
|
+
class GraphQLApolloPlugin extends ShokupanRouter {
|
|
5258
|
+
// Use generic any or verify type
|
|
5259
|
+
constructor(pluginOptions) {
|
|
5260
|
+
super();
|
|
5261
|
+
this.pluginOptions = pluginOptions;
|
|
5262
|
+
this.pluginOptions.path ??= "/graphql";
|
|
5263
|
+
}
|
|
5264
|
+
apolloServer;
|
|
5265
|
+
async onInit(app, options) {
|
|
5266
|
+
const { ApolloServer, HeaderMap } = await import("@apollo/server");
|
|
5267
|
+
this.apolloServer = new ApolloServer({
|
|
5268
|
+
typeDefs: this.pluginOptions.typeDefs,
|
|
5269
|
+
resolvers: this.pluginOptions.resolvers,
|
|
5270
|
+
...this.pluginOptions.apolloConfig || {}
|
|
5271
|
+
});
|
|
5272
|
+
const path2 = options?.path || this.pluginOptions.path || "/graphql";
|
|
5273
|
+
app.mount(path2, this);
|
|
5274
|
+
app.onStart(async () => {
|
|
5275
|
+
await this.apolloServer.start();
|
|
5276
|
+
});
|
|
5277
|
+
this.post("/", async (ctx) => {
|
|
5278
|
+
const body = await ctx.body();
|
|
5279
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5280
|
+
httpGraphQLRequest: {
|
|
5281
|
+
body,
|
|
5282
|
+
method: ctx.req.method,
|
|
5283
|
+
search: ctx.url.search,
|
|
5284
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5285
|
+
},
|
|
5286
|
+
// Pass the Shokupan Context as the GraphQL Context
|
|
5287
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5288
|
+
});
|
|
5289
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5290
|
+
ctx.set(key, value);
|
|
5291
|
+
}
|
|
5292
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5293
|
+
return ctx.send(httpGraphQLResponse.body.string, {
|
|
5294
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5295
|
+
});
|
|
5296
|
+
} else {
|
|
5297
|
+
let string = "";
|
|
5298
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5299
|
+
string += chunk;
|
|
5300
|
+
}
|
|
5301
|
+
return ctx.send(string, {
|
|
5302
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5303
|
+
});
|
|
5304
|
+
}
|
|
5305
|
+
});
|
|
5306
|
+
this.get("/", async (ctx) => {
|
|
5307
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5308
|
+
httpGraphQLRequest: {
|
|
5309
|
+
body: Object.keys(ctx.query).length > 0 ? ctx.query : void 0,
|
|
5310
|
+
method: ctx.req.method,
|
|
5311
|
+
search: ctx.url.search,
|
|
5312
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5313
|
+
},
|
|
5314
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5315
|
+
});
|
|
5316
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5317
|
+
ctx.set(key, value);
|
|
5318
|
+
}
|
|
5319
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5320
|
+
return ctx.html(httpGraphQLResponse.body.string, httpGraphQLResponse.status ?? 200);
|
|
5321
|
+
} else {
|
|
5322
|
+
let string = "";
|
|
5323
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5324
|
+
string += chunk;
|
|
5325
|
+
}
|
|
5326
|
+
return ctx.html(string, httpGraphQLResponse.status ?? 200);
|
|
5327
|
+
}
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
3337
5331
|
class ScalarPlugin extends ShokupanRouter {
|
|
3338
5332
|
constructor(pluginOptions = {}) {
|
|
3339
5333
|
pluginOptions.config ??= {};
|
|
3340
5334
|
super();
|
|
3341
5335
|
this.pluginOptions = pluginOptions;
|
|
3342
|
-
this.
|
|
5336
|
+
this.initRoutes();
|
|
5337
|
+
}
|
|
5338
|
+
eta;
|
|
5339
|
+
async onInit(app, options) {
|
|
5340
|
+
const { Eta } = await import("eta");
|
|
5341
|
+
this.eta = new Eta();
|
|
5342
|
+
const path2 = options?.path || this.pluginOptions.path || "/reference";
|
|
5343
|
+
app.mount(path2, this);
|
|
5344
|
+
this.onMount(app);
|
|
3343
5345
|
}
|
|
3344
|
-
|
|
3345
|
-
if (
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
app.mount(options.path ?? "/", this);
|
|
5346
|
+
async ensureEta() {
|
|
5347
|
+
if (!this.eta) {
|
|
5348
|
+
const { Eta } = await import("eta");
|
|
5349
|
+
this.eta = new Eta();
|
|
3349
5350
|
}
|
|
3350
|
-
this.onMount(app);
|
|
3351
5351
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
5352
|
+
initRoutes() {
|
|
5353
|
+
const bootId = Date.now().toString();
|
|
5354
|
+
this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
|
|
5355
|
+
this.get("/", async (ctx) => {
|
|
5356
|
+
await this.ensureEta();
|
|
3354
5357
|
let path2 = ctx.url.toString();
|
|
3355
5358
|
if (!path2.endsWith("/")) path2 += "/";
|
|
3356
|
-
|
|
3357
|
-
<
|
|
5359
|
+
const devScript = ctx.app?.applicationConfig.development ? `
|
|
5360
|
+
<script>
|
|
5361
|
+
(function() {
|
|
5362
|
+
const bootId = "${bootId}";
|
|
5363
|
+
let isDown = false;
|
|
5364
|
+
|
|
5365
|
+
setInterval(async () => {
|
|
5366
|
+
try {
|
|
5367
|
+
const res = await fetch('${path2}_lifecycle');
|
|
5368
|
+
if (!res.ok) throw new Error('Down');
|
|
5369
|
+
const data = await res.json();
|
|
5370
|
+
if (data.boot !== bootId) {
|
|
5371
|
+
console.log('Server restarted, reloading...');
|
|
5372
|
+
window.location.reload();
|
|
5373
|
+
}
|
|
5374
|
+
else if (isDown) {
|
|
5375
|
+
isDown = false;
|
|
5376
|
+
}
|
|
5377
|
+
} catch (e) {
|
|
5378
|
+
isDown = true;
|
|
5379
|
+
console.log('Connection lost...');
|
|
5380
|
+
}
|
|
5381
|
+
}, 2000);
|
|
5382
|
+
})();
|
|
5383
|
+
<\/script>
|
|
5384
|
+
` : "";
|
|
5385
|
+
let themeCss = "";
|
|
5386
|
+
try {
|
|
5387
|
+
try {
|
|
5388
|
+
themeCss = fs.readFileSync(path$1.join(process.cwd(), "src/theme.css"), "utf-8");
|
|
5389
|
+
} catch {
|
|
5390
|
+
}
|
|
5391
|
+
} catch (e) {
|
|
5392
|
+
}
|
|
5393
|
+
if (!this.eta) throw new Error("Eta not initialized");
|
|
5394
|
+
return ctx.html(this.eta.renderString(`<!doctype html>
|
|
5395
|
+
<html lang="en">
|
|
3358
5396
|
<head>
|
|
3359
5397
|
<title>API Reference</title>
|
|
3360
5398
|
<meta charset = "utf-8" />
|
|
3361
5399
|
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
5400
|
+
<style>
|
|
5401
|
+
${themeCss}
|
|
5402
|
+
|
|
5403
|
+
:root {
|
|
5404
|
+
--scalar-color-1: var(--primary);
|
|
5405
|
+
--scalar-color-2: var(--secondary);
|
|
5406
|
+
--scalar-color-3: var(--accent);
|
|
5407
|
+
--scalar-color-accent: var(--accent);
|
|
5408
|
+
|
|
5409
|
+
--scalar-background-1: var(--bg-primary);
|
|
5410
|
+
--scalar-background-2: var(--bg-secondary);
|
|
5411
|
+
--scalar-background-3: var(--bg-card);
|
|
5412
|
+
|
|
5413
|
+
--scalar-text-1: var(--text-primary);
|
|
5414
|
+
--scalar-text-2: var(--text-secondary);
|
|
5415
|
+
--scalar-text-3: var(--text-muted);
|
|
5416
|
+
|
|
5417
|
+
--scalar-border-color: var(--border-color);
|
|
5418
|
+
}
|
|
5419
|
+
</style>
|
|
3362
5420
|
</head>
|
|
3363
5421
|
|
|
3364
5422
|
<body>
|
|
@@ -3370,9 +5428,10 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3370
5428
|
}
|
|
3371
5429
|
])
|
|
3372
5430
|
<\/script>
|
|
5431
|
+
<%~ it.devScript %>
|
|
3373
5432
|
</body>
|
|
3374
5433
|
|
|
3375
|
-
</html>`, { path: path2, config: this.pluginOptions }));
|
|
5434
|
+
</html>`, { path: path2, config: this.pluginOptions, devScript }));
|
|
3376
5435
|
});
|
|
3377
5436
|
this.get("/openapi.json", async (ctx) => {
|
|
3378
5437
|
let spec;
|
|
@@ -3431,23 +5490,23 @@ function Compression(options = {}) {
|
|
|
3431
5490
|
return next();
|
|
3432
5491
|
}
|
|
3433
5492
|
let response = await next();
|
|
3434
|
-
if (!(response instanceof Response) && ctx
|
|
3435
|
-
response = ctx
|
|
5493
|
+
if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
|
|
5494
|
+
response = ctx[$finalResponse];
|
|
3436
5495
|
}
|
|
3437
5496
|
if (response instanceof Response) {
|
|
3438
5497
|
if (response.headers.has("Content-Encoding")) return response;
|
|
3439
5498
|
let body;
|
|
3440
5499
|
let bodySize;
|
|
3441
|
-
if (ctx
|
|
3442
|
-
if (typeof ctx
|
|
3443
|
-
const encoded = new TextEncoder().encode(ctx
|
|
5500
|
+
if (ctx[$rawBody] !== void 0) {
|
|
5501
|
+
if (typeof ctx[$rawBody] === "string") {
|
|
5502
|
+
const encoded = new TextEncoder().encode(ctx[$rawBody]);
|
|
3444
5503
|
body = encoded;
|
|
3445
5504
|
bodySize = encoded.byteLength;
|
|
3446
|
-
} else if (ctx
|
|
3447
|
-
body = ctx
|
|
3448
|
-
bodySize = ctx.
|
|
5505
|
+
} else if (ctx[$rawBody] instanceof Uint8Array) {
|
|
5506
|
+
body = ctx[$rawBody];
|
|
5507
|
+
bodySize = ctx[$rawBody].byteLength;
|
|
3449
5508
|
} else {
|
|
3450
|
-
body = ctx
|
|
5509
|
+
body = ctx[$rawBody];
|
|
3451
5510
|
bodySize = body.byteLength;
|
|
3452
5511
|
}
|
|
3453
5512
|
} else {
|
|
@@ -3575,7 +5634,7 @@ function Cors(options = {}) {
|
|
|
3575
5634
|
}
|
|
3576
5635
|
const response = await next();
|
|
3577
5636
|
if (response instanceof Response) {
|
|
3578
|
-
const headerEntries =
|
|
5637
|
+
const headerEntries = Object.entries(headers);
|
|
3579
5638
|
for (let i = 0; i < headerEntries.length; i++) {
|
|
3580
5639
|
const [key, value] = headerEntries[i];
|
|
3581
5640
|
response.headers.set(key, value);
|
|
@@ -4312,21 +6371,41 @@ function Session(options) {
|
|
|
4312
6371
|
return sessionMiddleware;
|
|
4313
6372
|
}
|
|
4314
6373
|
exports.$appRoot = $appRoot;
|
|
6374
|
+
exports.$bodyParseError = $bodyParseError;
|
|
6375
|
+
exports.$bodyParsed = $bodyParsed;
|
|
6376
|
+
exports.$bodyType = $bodyType;
|
|
6377
|
+
exports.$cachedBody = $cachedBody;
|
|
6378
|
+
exports.$cachedHost = $cachedHost;
|
|
6379
|
+
exports.$cachedHostname = $cachedHostname;
|
|
6380
|
+
exports.$cachedOrigin = $cachedOrigin;
|
|
6381
|
+
exports.$cachedProtocol = $cachedProtocol;
|
|
6382
|
+
exports.$cachedQuery = $cachedQuery;
|
|
4315
6383
|
exports.$childControllers = $childControllers;
|
|
4316
6384
|
exports.$childRouters = $childRouters;
|
|
4317
6385
|
exports.$controllerPath = $controllerPath;
|
|
6386
|
+
exports.$debug = $debug;
|
|
4318
6387
|
exports.$dispatch = $dispatch;
|
|
6388
|
+
exports.$eventMethods = $eventMethods;
|
|
6389
|
+
exports.$finalResponse = $finalResponse;
|
|
6390
|
+
exports.$io = $io;
|
|
4319
6391
|
exports.$isApplication = $isApplication;
|
|
4320
6392
|
exports.$isMounted = $isMounted;
|
|
4321
6393
|
exports.$isRouter = $isRouter;
|
|
4322
6394
|
exports.$middleware = $middleware;
|
|
4323
6395
|
exports.$mountPath = $mountPath;
|
|
4324
6396
|
exports.$parent = $parent;
|
|
6397
|
+
exports.$rawBody = $rawBody;
|
|
6398
|
+
exports.$requestId = $requestId;
|
|
4325
6399
|
exports.$routeArgs = $routeArgs;
|
|
6400
|
+
exports.$routeMatched = $routeMatched;
|
|
4326
6401
|
exports.$routeMethods = $routeMethods;
|
|
4327
6402
|
exports.$routeSpec = $routeSpec;
|
|
4328
6403
|
exports.$routes = $routes;
|
|
6404
|
+
exports.$socket = $socket;
|
|
6405
|
+
exports.$url = $url;
|
|
6406
|
+
exports.$ws = $ws;
|
|
4329
6407
|
exports.All = All;
|
|
6408
|
+
exports.AsyncApiPlugin = AsyncApiPlugin;
|
|
4330
6409
|
exports.AuthPlugin = AuthPlugin;
|
|
4331
6410
|
exports.Body = Body;
|
|
4332
6411
|
exports.ClusterPlugin = ClusterPlugin;
|
|
@@ -4335,8 +6414,11 @@ exports.Container = Container;
|
|
|
4335
6414
|
exports.Controller = Controller;
|
|
4336
6415
|
exports.Cors = Cors;
|
|
4337
6416
|
exports.Ctx = Ctx;
|
|
6417
|
+
exports.Dashboard = Dashboard;
|
|
4338
6418
|
exports.Delete = Delete;
|
|
6419
|
+
exports.Event = Event;
|
|
4339
6420
|
exports.Get = Get;
|
|
6421
|
+
exports.GraphQLApolloPlugin = GraphQLApolloPlugin;
|
|
4340
6422
|
exports.HTTPMethods = HTTPMethods;
|
|
4341
6423
|
exports.Head = Head;
|
|
4342
6424
|
exports.Headers = Headers$1;
|