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.js
CHANGED
|
@@ -1,21 +1,59 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { inspect } from "node:util";
|
|
2
4
|
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
5
|
+
import { dump } from "js-yaml";
|
|
3
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import { RecordId, Surreal } from "surrealdb";
|
|
4
8
|
import { Eta } from "eta";
|
|
5
9
|
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
6
10
|
import { resolve, join, sep, basename } from "path";
|
|
7
11
|
import * as os from "node:os";
|
|
8
12
|
import os__default from "node:os";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
13
|
+
import { dirname, join as join$1 } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import renderToString from "preact-render-to-string";
|
|
16
|
+
import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
|
|
11
17
|
import cluster from "node:cluster";
|
|
12
18
|
import net from "node:net";
|
|
13
|
-
import {
|
|
19
|
+
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { OpenAPIAnalyzer } from "./analyzer-BqIe1p0R.js";
|
|
14
22
|
import * as zlib from "node:zlib";
|
|
15
23
|
import Ajv from "ajv";
|
|
16
24
|
import addFormats from "ajv-formats";
|
|
17
25
|
import { randomUUID, createHmac } from "crypto";
|
|
18
26
|
import { EventEmitter } from "events";
|
|
27
|
+
const HTTP_STATUS = {
|
|
28
|
+
// 2xx Success
|
|
29
|
+
OK: 200,
|
|
30
|
+
CREATED: 201,
|
|
31
|
+
ACCEPTED: 202,
|
|
32
|
+
NO_CONTENT: 204,
|
|
33
|
+
// 3xx Redirection
|
|
34
|
+
MOVED_PERMANENTLY: 301,
|
|
35
|
+
FOUND: 302,
|
|
36
|
+
SEE_OTHER: 303,
|
|
37
|
+
NOT_MODIFIED: 304,
|
|
38
|
+
TEMPORARY_REDIRECT: 307,
|
|
39
|
+
PERMANENT_REDIRECT: 308,
|
|
40
|
+
// 4xx Client Errors
|
|
41
|
+
BAD_REQUEST: 400,
|
|
42
|
+
UNAUTHORIZED: 401,
|
|
43
|
+
FORBIDDEN: 403,
|
|
44
|
+
NOT_FOUND: 404,
|
|
45
|
+
METHOD_NOT_ALLOWED: 405,
|
|
46
|
+
REQUEST_TIMEOUT: 408,
|
|
47
|
+
CONFLICT: 409,
|
|
48
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
49
|
+
TOO_MANY_REQUESTS: 429,
|
|
50
|
+
// 5xx Server Errors
|
|
51
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
52
|
+
NOT_IMPLEMENTED: 501,
|
|
53
|
+
BAD_GATEWAY: 502,
|
|
54
|
+
SERVICE_UNAVAILABLE: 503,
|
|
55
|
+
GATEWAY_TIMEOUT: 504
|
|
56
|
+
};
|
|
19
57
|
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
20
58
|
100,
|
|
21
59
|
101,
|
|
@@ -145,6 +183,40 @@ class ShokupanResponse {
|
|
|
145
183
|
return this._headers !== null;
|
|
146
184
|
}
|
|
147
185
|
}
|
|
186
|
+
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
187
|
+
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
188
|
+
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
189
|
+
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
190
|
+
const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
|
|
191
|
+
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
192
|
+
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
193
|
+
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
194
|
+
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
195
|
+
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
196
|
+
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
197
|
+
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
198
|
+
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
199
|
+
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
200
|
+
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
201
|
+
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
202
|
+
const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
|
|
203
|
+
const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
|
|
204
|
+
const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
|
|
205
|
+
const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
|
|
206
|
+
const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
|
|
207
|
+
const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
|
|
208
|
+
const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
|
|
209
|
+
const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
|
|
210
|
+
const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
|
|
211
|
+
const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
|
|
212
|
+
const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
|
|
213
|
+
const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
|
|
214
|
+
const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
|
|
215
|
+
const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
|
|
216
|
+
const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
|
|
217
|
+
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
218
|
+
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
219
|
+
const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
|
|
148
220
|
function isValidCookieDomain(domain, currentHost) {
|
|
149
221
|
const hostWithoutPort = currentHost.split(":")[0];
|
|
150
222
|
if (domain === hostWithoutPort) return true;
|
|
@@ -182,23 +254,41 @@ class ShokupanContext {
|
|
|
182
254
|
state;
|
|
183
255
|
handlerStack = [];
|
|
184
256
|
response;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
257
|
+
[$debug];
|
|
258
|
+
[$finalResponse];
|
|
259
|
+
[$rawBody];
|
|
188
260
|
// Raw body for compression optimization
|
|
189
261
|
// Body caching to avoid double parsing
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
262
|
+
[$url];
|
|
263
|
+
[$cachedBody];
|
|
264
|
+
[$bodyType];
|
|
265
|
+
[$bodyParsed] = false;
|
|
266
|
+
[$bodyParseError];
|
|
267
|
+
[$routeMatched] = false;
|
|
196
268
|
// Cached URL properties to avoid repeated parsing
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
269
|
+
[$cachedHostname];
|
|
270
|
+
[$cachedProtocol];
|
|
271
|
+
[$cachedHost];
|
|
272
|
+
[$cachedOrigin];
|
|
273
|
+
[$cachedQuery];
|
|
274
|
+
disconnectCallbacks = [];
|
|
275
|
+
/**
|
|
276
|
+
* Registers a callback to be executed when the associated WebSocket disconnects.
|
|
277
|
+
* This is only applicable for requests that are part of a WebSocket interaction or upgrade.
|
|
278
|
+
*/
|
|
279
|
+
onSocketDisconnect(callback) {
|
|
280
|
+
this.disconnectCallbacks.push(callback);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* @internal
|
|
284
|
+
* Retrieves registered disconnect callbacks for execution.
|
|
285
|
+
*/
|
|
286
|
+
getDisconnectCallbacks() {
|
|
287
|
+
return this.disconnectCallbacks;
|
|
288
|
+
}
|
|
289
|
+
[$ws];
|
|
290
|
+
[$socket];
|
|
291
|
+
[$io];
|
|
202
292
|
/**
|
|
203
293
|
* JSX Rendering Function
|
|
204
294
|
*/
|
|
@@ -206,12 +296,30 @@ class ShokupanContext {
|
|
|
206
296
|
setRenderer(renderer) {
|
|
207
297
|
this.renderer = renderer;
|
|
208
298
|
}
|
|
299
|
+
[$requestId];
|
|
300
|
+
get requestId() {
|
|
301
|
+
return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
|
|
302
|
+
}
|
|
303
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
304
|
+
const innerString = inspect({
|
|
305
|
+
method: this.request.method,
|
|
306
|
+
url: this.request.url,
|
|
307
|
+
requestHeaders: new Map(this.request.headers),
|
|
308
|
+
sessionId: this.sessionID,
|
|
309
|
+
state: this.state,
|
|
310
|
+
params: this.params,
|
|
311
|
+
response: this[$finalResponse]?.body,
|
|
312
|
+
responseHeaders: new Map(this[$finalResponse]?.headers),
|
|
313
|
+
handlerStack: this.handlerStack.map((h) => h.name === "anonymous" ? h.file + ":" + h.line : h.name)
|
|
314
|
+
}, { depth: null, colors: true, numericSeparator: true, customInspect: true });
|
|
315
|
+
return "Context(" + this.requestId + ") {" + innerString.slice(1, -2) + ",\n ...others\n}";
|
|
316
|
+
}
|
|
209
317
|
get url() {
|
|
210
|
-
if (!this
|
|
318
|
+
if (!this[$url]) {
|
|
211
319
|
const urlString = this.request.url || "http://localhost/";
|
|
212
|
-
this
|
|
320
|
+
this[$url] = new URL(urlString);
|
|
213
321
|
}
|
|
214
|
-
return this
|
|
322
|
+
return this[$url];
|
|
215
323
|
}
|
|
216
324
|
/**
|
|
217
325
|
* Base request
|
|
@@ -229,7 +337,7 @@ class ShokupanContext {
|
|
|
229
337
|
* Request path
|
|
230
338
|
*/
|
|
231
339
|
get path() {
|
|
232
|
-
if (this
|
|
340
|
+
if (this[$url]) return this[$url].pathname;
|
|
233
341
|
const url = this.request.url;
|
|
234
342
|
let queryIndex = url.indexOf("?");
|
|
235
343
|
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
@@ -254,13 +362,11 @@ class ShokupanContext {
|
|
|
254
362
|
* Request query params
|
|
255
363
|
*/
|
|
256
364
|
get query() {
|
|
257
|
-
if (this
|
|
365
|
+
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
258
366
|
const q = /* @__PURE__ */ Object.create(null);
|
|
259
367
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const [key, value] = entries[i];
|
|
263
|
-
if (blocklist.includes(key)) continue;
|
|
368
|
+
this.url.searchParams.forEach((value, key) => {
|
|
369
|
+
if (blocklist.includes(key)) return;
|
|
264
370
|
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
265
371
|
if (Array.isArray(q[key])) {
|
|
266
372
|
q[key].push(value);
|
|
@@ -270,8 +376,8 @@ class ShokupanContext {
|
|
|
270
376
|
} else {
|
|
271
377
|
q[key] = value;
|
|
272
378
|
}
|
|
273
|
-
}
|
|
274
|
-
this
|
|
379
|
+
});
|
|
380
|
+
this[$cachedQuery] = q;
|
|
275
381
|
return q;
|
|
276
382
|
}
|
|
277
383
|
/**
|
|
@@ -284,19 +390,19 @@ class ShokupanContext {
|
|
|
284
390
|
* Request hostname (e.g. "localhost")
|
|
285
391
|
*/
|
|
286
392
|
get hostname() {
|
|
287
|
-
return this
|
|
393
|
+
return this[$cachedHostname] ??= this.url.hostname;
|
|
288
394
|
}
|
|
289
395
|
/**
|
|
290
396
|
* Request host (e.g. "localhost:3000")
|
|
291
397
|
*/
|
|
292
398
|
get host() {
|
|
293
|
-
return this
|
|
399
|
+
return this[$cachedHost] ??= this.url.host;
|
|
294
400
|
}
|
|
295
401
|
/**
|
|
296
402
|
* Request protocol (e.g. "http:", "https:")
|
|
297
403
|
*/
|
|
298
404
|
get protocol() {
|
|
299
|
-
return this
|
|
405
|
+
return this[$cachedProtocol] ??= this.url.protocol;
|
|
300
406
|
}
|
|
301
407
|
/**
|
|
302
408
|
* Whether request is secure (https)
|
|
@@ -308,7 +414,7 @@ class ShokupanContext {
|
|
|
308
414
|
* Request origin (e.g. "http://localhost:3000")
|
|
309
415
|
*/
|
|
310
416
|
get origin() {
|
|
311
|
-
return this
|
|
417
|
+
return this[$cachedOrigin] ??= this.url.origin;
|
|
312
418
|
}
|
|
313
419
|
/**
|
|
314
420
|
* Request headers
|
|
@@ -329,6 +435,24 @@ class ShokupanContext {
|
|
|
329
435
|
get res() {
|
|
330
436
|
return this.response;
|
|
331
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Raw WebSocket connection
|
|
440
|
+
*/
|
|
441
|
+
get ws() {
|
|
442
|
+
return this[$ws];
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Socket.io socket
|
|
446
|
+
*/
|
|
447
|
+
get socket() {
|
|
448
|
+
return this[$socket];
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Socket.io server
|
|
452
|
+
*/
|
|
453
|
+
get io() {
|
|
454
|
+
return this[$io];
|
|
455
|
+
}
|
|
332
456
|
/**
|
|
333
457
|
* Helper to set a header on the response
|
|
334
458
|
* @param key Header key
|
|
@@ -338,6 +462,20 @@ class ShokupanContext {
|
|
|
338
462
|
this.response.set(key, value);
|
|
339
463
|
return this;
|
|
340
464
|
}
|
|
465
|
+
isUpgraded = false;
|
|
466
|
+
/**
|
|
467
|
+
* Upgrades the request to a WebSocket connection.
|
|
468
|
+
* @param options Upgrade options
|
|
469
|
+
* @returns true if upgraded, false otherwise
|
|
470
|
+
*/
|
|
471
|
+
upgrade(options) {
|
|
472
|
+
if (!this.server) return false;
|
|
473
|
+
const success = this.server.upgrade(this.req, options);
|
|
474
|
+
if (success) {
|
|
475
|
+
this.isUpgraded = true;
|
|
476
|
+
}
|
|
477
|
+
return success;
|
|
478
|
+
}
|
|
341
479
|
/**
|
|
342
480
|
* Set a cookie
|
|
343
481
|
* @param name Cookie name
|
|
@@ -415,18 +553,18 @@ class ShokupanContext {
|
|
|
415
553
|
* The body is only parsed once and cached for subsequent reads.
|
|
416
554
|
*/
|
|
417
555
|
async body() {
|
|
418
|
-
if (this
|
|
419
|
-
throw this
|
|
556
|
+
if (this[$bodyParseError]) {
|
|
557
|
+
throw this[$bodyParseError];
|
|
420
558
|
}
|
|
421
|
-
if (this
|
|
422
|
-
return this
|
|
559
|
+
if (this[$bodyParsed]) {
|
|
560
|
+
return this[$cachedBody];
|
|
423
561
|
}
|
|
424
562
|
const contentType = this.request.headers.get("content-type") || "";
|
|
425
563
|
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
426
564
|
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
427
565
|
if (parserType === "native") {
|
|
428
566
|
try {
|
|
429
|
-
this
|
|
567
|
+
this[$cachedBody] = await this.request.json();
|
|
430
568
|
} catch (e) {
|
|
431
569
|
throw e;
|
|
432
570
|
}
|
|
@@ -434,18 +572,18 @@ class ShokupanContext {
|
|
|
434
572
|
const rawText = await this.request.text();
|
|
435
573
|
const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
|
|
436
574
|
const parser = getJSONParser(parserType);
|
|
437
|
-
this
|
|
575
|
+
this[$cachedBody] = parser(rawText);
|
|
438
576
|
}
|
|
439
|
-
this
|
|
577
|
+
this[$bodyType] = "json";
|
|
440
578
|
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
441
|
-
this
|
|
442
|
-
this
|
|
579
|
+
this[$cachedBody] = await this.request.formData();
|
|
580
|
+
this[$bodyType] = "formData";
|
|
443
581
|
} else {
|
|
444
|
-
this
|
|
445
|
-
this
|
|
582
|
+
this[$cachedBody] = await this.request.text();
|
|
583
|
+
this[$bodyType] = "text";
|
|
446
584
|
}
|
|
447
|
-
this
|
|
448
|
-
return this
|
|
585
|
+
this[$bodyParsed] = true;
|
|
586
|
+
return this[$cachedBody];
|
|
449
587
|
}
|
|
450
588
|
/**
|
|
451
589
|
* Pre-parse the request body before handler execution.
|
|
@@ -453,7 +591,7 @@ class ShokupanContext {
|
|
|
453
591
|
* Errors are deferred until the body is actually accessed in the handler.
|
|
454
592
|
*/
|
|
455
593
|
async parseBody() {
|
|
456
|
-
if (this
|
|
594
|
+
if (this[$bodyParsed]) {
|
|
457
595
|
return;
|
|
458
596
|
}
|
|
459
597
|
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
@@ -462,7 +600,7 @@ class ShokupanContext {
|
|
|
462
600
|
try {
|
|
463
601
|
await this.body();
|
|
464
602
|
} catch (error) {
|
|
465
|
-
this
|
|
603
|
+
this[$bodyParseError] = error;
|
|
466
604
|
}
|
|
467
605
|
}
|
|
468
606
|
/**
|
|
@@ -512,116 +650,129 @@ class ShokupanContext {
|
|
|
512
650
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
513
651
|
}
|
|
514
652
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
515
|
-
this
|
|
653
|
+
this[$rawBody] = body;
|
|
654
|
+
}
|
|
655
|
+
return this[$finalResponse] ??= new Response(body, { status, headers });
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Emit an event to the client (WebSocket only)
|
|
659
|
+
* @param event Event name
|
|
660
|
+
* @param data Event data (Must be JSON serializable)
|
|
661
|
+
*/
|
|
662
|
+
emit(event, data) {
|
|
663
|
+
if (this[$ws]) {
|
|
664
|
+
this[$ws].send(JSON.stringify({ event, data }));
|
|
665
|
+
} else if (this[$socket]) {
|
|
666
|
+
this[$socket].emit(event, data);
|
|
516
667
|
}
|
|
517
|
-
this._finalResponse = new Response(body, { status, headers });
|
|
518
|
-
return this._finalResponse;
|
|
519
668
|
}
|
|
520
669
|
/**
|
|
521
670
|
* Respond with a JSON object
|
|
522
671
|
*/
|
|
523
|
-
json(data, status, headers) {
|
|
672
|
+
async json(data, status, headers) {
|
|
524
673
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
525
674
|
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
526
675
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
527
676
|
}
|
|
528
|
-
const jsonString = JSON.stringify(data);
|
|
529
|
-
this
|
|
677
|
+
const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
|
|
678
|
+
this[$rawBody] = jsonString;
|
|
530
679
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
531
|
-
this
|
|
680
|
+
this[$finalResponse] = new Response(jsonString, {
|
|
532
681
|
status: finalStatus,
|
|
533
682
|
headers: { "content-type": "application/json" }
|
|
534
683
|
});
|
|
535
|
-
return this
|
|
684
|
+
return this[$finalResponse];
|
|
536
685
|
}
|
|
537
686
|
const finalHeaders = this.mergeHeaders(headers);
|
|
538
687
|
finalHeaders.set("content-type", "application/json");
|
|
539
|
-
this
|
|
540
|
-
return this
|
|
688
|
+
this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
689
|
+
return this[$finalResponse];
|
|
541
690
|
}
|
|
542
691
|
/**
|
|
543
692
|
* Respond with a text string
|
|
544
693
|
*/
|
|
545
|
-
text(data, status, headers) {
|
|
694
|
+
async text(data, status, headers) {
|
|
546
695
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
547
696
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
548
697
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
549
698
|
}
|
|
550
|
-
this
|
|
699
|
+
this[$rawBody] = data instanceof Promise ? await data : data;
|
|
551
700
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
552
|
-
this
|
|
701
|
+
this[$finalResponse] = new Response(this[$rawBody], {
|
|
553
702
|
status: finalStatus,
|
|
554
703
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
555
704
|
});
|
|
556
|
-
return this
|
|
705
|
+
return this[$finalResponse];
|
|
557
706
|
}
|
|
558
707
|
const finalHeaders = this.mergeHeaders(headers);
|
|
559
708
|
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
560
|
-
this
|
|
561
|
-
return this
|
|
709
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
710
|
+
return this[$finalResponse];
|
|
562
711
|
}
|
|
563
712
|
/**
|
|
564
713
|
* Respond with HTML content
|
|
565
714
|
*/
|
|
566
|
-
html(html, status, headers) {
|
|
715
|
+
async html(html, status, headers) {
|
|
567
716
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
568
717
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
569
718
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
570
719
|
}
|
|
571
720
|
const finalHeaders = this.mergeHeaders(headers);
|
|
572
721
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
573
|
-
this
|
|
574
|
-
this
|
|
575
|
-
return this
|
|
722
|
+
this[$rawBody] = html instanceof Promise ? await html : html;
|
|
723
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
724
|
+
return this[$finalResponse];
|
|
576
725
|
}
|
|
577
726
|
/**
|
|
578
727
|
* Respond with a redirect
|
|
579
728
|
*/
|
|
580
|
-
redirect(url, status = 302) {
|
|
729
|
+
async redirect(url, status = 302) {
|
|
581
730
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
582
731
|
throw new Error(`Invalid redirect status code: ${status}`);
|
|
583
732
|
}
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
this
|
|
587
|
-
return this
|
|
733
|
+
const finalHeaders = this.mergeHeaders();
|
|
734
|
+
finalHeaders.set("Location", url instanceof Promise ? await url : url);
|
|
735
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
736
|
+
return this[$finalResponse];
|
|
588
737
|
}
|
|
589
738
|
/**
|
|
590
739
|
* Respond with a status code
|
|
591
740
|
* DOES NOT CHAIN!
|
|
592
741
|
*/
|
|
593
|
-
status(
|
|
742
|
+
async status(statusCode) {
|
|
743
|
+
const status = statusCode instanceof Promise ? await statusCode : statusCode;
|
|
594
744
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
595
745
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
596
746
|
}
|
|
597
|
-
const
|
|
598
|
-
this
|
|
599
|
-
return this
|
|
747
|
+
const finalHeaders = this.mergeHeaders();
|
|
748
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
749
|
+
return this[$finalResponse];
|
|
600
750
|
}
|
|
601
751
|
/**
|
|
602
752
|
* Respond with a file
|
|
603
753
|
*/
|
|
604
754
|
async file(path, fileOptions, responseOptions) {
|
|
605
|
-
const
|
|
755
|
+
const finalHeaders = this.mergeHeaders(responseOptions?.headers);
|
|
606
756
|
const status = responseOptions?.status ?? this.response.status;
|
|
607
757
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
608
758
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
609
759
|
}
|
|
610
760
|
if (typeof Bun !== "undefined") {
|
|
611
|
-
this
|
|
612
|
-
return this
|
|
761
|
+
this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers: finalHeaders });
|
|
762
|
+
return this[$finalResponse];
|
|
613
763
|
} else {
|
|
614
764
|
const fileBuffer = await readFile(path);
|
|
615
765
|
if (fileOptions?.type) {
|
|
616
|
-
|
|
766
|
+
finalHeaders.set("content-type", fileOptions.type);
|
|
617
767
|
}
|
|
618
|
-
this
|
|
619
|
-
return this
|
|
768
|
+
this[$finalResponse] = new Response(fileBuffer, { status, headers: finalHeaders });
|
|
769
|
+
return this[$finalResponse];
|
|
620
770
|
}
|
|
621
771
|
}
|
|
622
772
|
/**
|
|
623
773
|
* Render a JSX element
|
|
624
774
|
* @param element JSX Element
|
|
775
|
+
* @param args JSX Element Args/Props
|
|
625
776
|
* @param status HTTP Status
|
|
626
777
|
* @param headers HTTP Headers
|
|
627
778
|
*/
|
|
@@ -652,10 +803,10 @@ const compose = (middleware) => {
|
|
|
652
803
|
return next ? next() : Promise.resolve();
|
|
653
804
|
}
|
|
654
805
|
const fn = middleware[i];
|
|
655
|
-
if (!context2
|
|
806
|
+
if (!context2[$debug]) {
|
|
656
807
|
return fn(context2, () => runner(i + 1));
|
|
657
808
|
}
|
|
658
|
-
const debug = context2
|
|
809
|
+
const debug = context2[$debug];
|
|
659
810
|
const debugId = fn._debugId || fn.name || "anonymous";
|
|
660
811
|
const previousNode = debug.getCurrentNode();
|
|
661
812
|
debug.trackEdge(previousNode, debugId);
|
|
@@ -675,29 +826,6 @@ const compose = (middleware) => {
|
|
|
675
826
|
return runner(0);
|
|
676
827
|
};
|
|
677
828
|
};
|
|
678
|
-
const tracer = trace.getTracer("shokupan.middleware");
|
|
679
|
-
function traceHandler(fn, name) {
|
|
680
|
-
return async function(...args) {
|
|
681
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
682
|
-
kind: SpanKind.INTERNAL,
|
|
683
|
-
attributes: {
|
|
684
|
-
"http.route": name,
|
|
685
|
-
"component": "shokupan.route"
|
|
686
|
-
}
|
|
687
|
-
}, async (span) => {
|
|
688
|
-
try {
|
|
689
|
-
const result = await fn.apply(this, args);
|
|
690
|
-
return result;
|
|
691
|
-
} catch (err) {
|
|
692
|
-
span.recordException(err);
|
|
693
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
694
|
-
throw err;
|
|
695
|
-
} finally {
|
|
696
|
-
span.end();
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
829
|
function isObject(item) {
|
|
702
830
|
return item && typeof item === "object" && !Array.isArray(item);
|
|
703
831
|
}
|
|
@@ -731,21 +859,6 @@ function deepMerge(target, ...sources) {
|
|
|
731
859
|
}
|
|
732
860
|
return deepMerge(target, ...sources);
|
|
733
861
|
}
|
|
734
|
-
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
735
|
-
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
736
|
-
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
737
|
-
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
738
|
-
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
739
|
-
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
740
|
-
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
741
|
-
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
742
|
-
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
743
|
-
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
744
|
-
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
745
|
-
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
746
|
-
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
747
|
-
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
748
|
-
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
749
862
|
const REGEX_PATTERNS = {
|
|
750
863
|
QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
|
|
751
864
|
QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
|
|
@@ -885,24 +998,34 @@ function analyzeHandler(handler) {
|
|
|
885
998
|
}
|
|
886
999
|
return { inferredSpec };
|
|
887
1000
|
}
|
|
888
|
-
async function getAstRoutes(applications) {
|
|
1001
|
+
async function getAstRoutes$1(applications) {
|
|
889
1002
|
const astRoutes = [];
|
|
890
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1003
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
|
|
891
1004
|
if (seen.has(app.name)) return [];
|
|
892
1005
|
const newSeen = new Set(seen);
|
|
893
1006
|
newSeen.add(app.name);
|
|
894
1007
|
const expanded = [];
|
|
1008
|
+
let currentPrefix = prefix;
|
|
1009
|
+
if (app.controllerPrefix) {
|
|
1010
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1011
|
+
const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
|
|
1012
|
+
currentPrefix = cleanPrefix + cleanCont;
|
|
1013
|
+
}
|
|
895
1014
|
for (const route of app.routes) {
|
|
896
|
-
const cleanPrefix =
|
|
1015
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
897
1016
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
898
1017
|
let joined = cleanPrefix + cleanPath;
|
|
899
1018
|
if (joined.length > 1 && joined.endsWith("/")) {
|
|
900
1019
|
joined = joined.slice(0, -1);
|
|
901
1020
|
}
|
|
902
|
-
|
|
1021
|
+
const expandedRoute = {
|
|
903
1022
|
...route,
|
|
904
1023
|
path: joined || "/"
|
|
905
|
-
}
|
|
1024
|
+
};
|
|
1025
|
+
if (sourceOverride) {
|
|
1026
|
+
expandedRoute.sourceContext = sourceOverride;
|
|
1027
|
+
}
|
|
1028
|
+
expanded.push(expandedRoute);
|
|
906
1029
|
}
|
|
907
1030
|
if (app.mounted) {
|
|
908
1031
|
for (const mount of app.mounted) {
|
|
@@ -910,7 +1033,23 @@ async function getAstRoutes(applications) {
|
|
|
910
1033
|
if (targetApp) {
|
|
911
1034
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
912
1035
|
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
913
|
-
|
|
1036
|
+
let nextSourceOverride = sourceOverride;
|
|
1037
|
+
if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
|
|
1038
|
+
if (mount.sourceContext) {
|
|
1039
|
+
nextSourceOverride = {
|
|
1040
|
+
...mount.sourceContext,
|
|
1041
|
+
// Add highlight for the mount line to make it clear
|
|
1042
|
+
highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
|
|
1043
|
+
highlights: [{
|
|
1044
|
+
startLine: mount.sourceContext.startLine,
|
|
1045
|
+
endLine: mount.sourceContext.endLine,
|
|
1046
|
+
type: "return-success"
|
|
1047
|
+
// Use the success color (cyan) for the mount point
|
|
1048
|
+
}]
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
|
|
914
1053
|
}
|
|
915
1054
|
}
|
|
916
1055
|
}
|
|
@@ -938,13 +1077,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
938
1077
|
const defaultTagName = options.defaultTag || "Application";
|
|
939
1078
|
let astRoutes = [];
|
|
940
1079
|
try {
|
|
941
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
1080
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
|
|
942
1081
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
943
1082
|
const { applications } = await analyzer.analyze();
|
|
944
|
-
astRoutes = await getAstRoutes(applications);
|
|
1083
|
+
astRoutes = await getAstRoutes$1(applications);
|
|
945
1084
|
} catch (e) {
|
|
946
1085
|
}
|
|
947
|
-
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
1086
|
+
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
|
|
948
1087
|
let group = currentGroup;
|
|
949
1088
|
let tag = defaultTag;
|
|
950
1089
|
if (router.config?.group) group = router.config.group;
|
|
@@ -961,21 +1100,33 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
961
1100
|
}
|
|
962
1101
|
}
|
|
963
1102
|
if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
|
|
1103
|
+
const routerMiddleware = router.middleware || [];
|
|
964
1104
|
const routes = router[$routes] || [];
|
|
965
1105
|
for (const route of routes) {
|
|
1106
|
+
if (!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"].includes(route.method.toUpperCase())) {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
966
1109
|
const routeGroup = route.group || group;
|
|
967
1110
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
968
1111
|
const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
969
1112
|
let fullPath = cleanPrefix + cleanSubPath || "/";
|
|
970
|
-
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
971
1113
|
if (fullPath.length > 1 && fullPath.endsWith("/")) {
|
|
972
1114
|
fullPath = fullPath.slice(0, -1);
|
|
973
1115
|
}
|
|
1116
|
+
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
974
1117
|
if (!paths[fullPath]) paths[fullPath] = {};
|
|
975
1118
|
const operation = {
|
|
976
1119
|
responses: { "200": { description: "Successful response" } },
|
|
977
1120
|
tags: [tag]
|
|
978
1121
|
};
|
|
1122
|
+
const routeMiddleware = route.middleware || [];
|
|
1123
|
+
const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
|
|
1124
|
+
if (allMiddleware.length > 0) {
|
|
1125
|
+
operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
|
|
1126
|
+
name: mw.name || "middleware",
|
|
1127
|
+
metadata: mw.metadata
|
|
1128
|
+
}));
|
|
1129
|
+
}
|
|
979
1130
|
if (route.guards) {
|
|
980
1131
|
for (const guard of route.guards) {
|
|
981
1132
|
if (guard.spec) {
|
|
@@ -1013,6 +1164,23 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1013
1164
|
if (astMatch.description) operation.description = astMatch.description;
|
|
1014
1165
|
if (astMatch.tags) operation.tags = astMatch.tags;
|
|
1015
1166
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1167
|
+
if (astMatch.sourceContext) {
|
|
1168
|
+
const sc = astMatch.sourceContext;
|
|
1169
|
+
operation["x-source-info"] = {
|
|
1170
|
+
file: sc.file,
|
|
1171
|
+
line: sc.startLine,
|
|
1172
|
+
snippet: sc.snippet || astMatch.handlerSource,
|
|
1173
|
+
// Fallback
|
|
1174
|
+
offset: sc.snippetStartLine || sc.startLine,
|
|
1175
|
+
highlightLines: [sc.startLine, sc.endLine],
|
|
1176
|
+
highlights: sc.highlights
|
|
1177
|
+
};
|
|
1178
|
+
operation["x-shokupan-source"] = {
|
|
1179
|
+
file: sc.file,
|
|
1180
|
+
line: sc.startLine,
|
|
1181
|
+
code: sc.snippet || astMatch.handlerSource || ""
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1016
1184
|
if (astMatch.requestTypes?.body) {
|
|
1017
1185
|
operation.requestBody = {
|
|
1018
1186
|
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
@@ -1024,10 +1192,12 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1024
1192
|
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1025
1193
|
};
|
|
1026
1194
|
} else if (astMatch.responseType) {
|
|
1027
|
-
|
|
1195
|
+
let contentType = "application/json";
|
|
1196
|
+
if (astMatch.responseType === "string") contentType = "text/plain";
|
|
1197
|
+
else if (astMatch.responseType === "html") contentType = "text/html";
|
|
1028
1198
|
operation.responses["200"] = {
|
|
1029
1199
|
description: "Successful response",
|
|
1030
|
-
content: { [contentType]: { schema: { type:
|
|
1200
|
+
content: { [contentType]: { schema: { type: "string" } } }
|
|
1031
1201
|
};
|
|
1032
1202
|
}
|
|
1033
1203
|
const params = [];
|
|
@@ -1039,6 +1209,26 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1039
1209
|
if (params.length > 0) {
|
|
1040
1210
|
operation.parameters = params;
|
|
1041
1211
|
}
|
|
1212
|
+
} else {
|
|
1213
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
1214
|
+
let file;
|
|
1215
|
+
let line;
|
|
1216
|
+
if (route.metadata?.file) {
|
|
1217
|
+
file = route.metadata.file;
|
|
1218
|
+
line = route.metadata.line || 1;
|
|
1219
|
+
}
|
|
1220
|
+
operation["x-source-info"] = {
|
|
1221
|
+
snippet: runtimeSource,
|
|
1222
|
+
isRuntime: true,
|
|
1223
|
+
...file ? { file, line: line || 1 } : {}
|
|
1224
|
+
};
|
|
1225
|
+
if (file) {
|
|
1226
|
+
operation["x-shokupan-source"] = {
|
|
1227
|
+
file,
|
|
1228
|
+
line: line || 1,
|
|
1229
|
+
code: runtimeSource
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1042
1232
|
}
|
|
1043
1233
|
if (route.keys.length > 0) {
|
|
1044
1234
|
const pathParams = route.keys.map((key) => ({
|
|
@@ -1108,7 +1298,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1108
1298
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1109
1299
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
1110
1300
|
const nextPrefix = cleanPrefix + cleanMount || "/";
|
|
1111
|
-
collect(child, nextPrefix, group, tag);
|
|
1301
|
+
collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
|
|
1112
1302
|
}
|
|
1113
1303
|
};
|
|
1114
1304
|
collect(rootRouter);
|
|
@@ -1132,7 +1322,36 @@ class RequestContextStore {
|
|
|
1132
1322
|
span;
|
|
1133
1323
|
}
|
|
1134
1324
|
const asyncContext = new AsyncLocalStorage();
|
|
1135
|
-
|
|
1325
|
+
class HttpError extends Error {
|
|
1326
|
+
status;
|
|
1327
|
+
constructor(message, status) {
|
|
1328
|
+
super(message);
|
|
1329
|
+
this.name = "HttpError";
|
|
1330
|
+
this.status = status;
|
|
1331
|
+
if (Error.captureStackTrace) {
|
|
1332
|
+
Error.captureStackTrace(this, HttpError);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function getErrorStatus(err) {
|
|
1337
|
+
if (!err || typeof err !== "object") {
|
|
1338
|
+
return 500;
|
|
1339
|
+
}
|
|
1340
|
+
if (typeof err.status === "number") {
|
|
1341
|
+
return err.status;
|
|
1342
|
+
}
|
|
1343
|
+
if (typeof err.statusCode === "number") {
|
|
1344
|
+
return err.statusCode;
|
|
1345
|
+
}
|
|
1346
|
+
return 500;
|
|
1347
|
+
}
|
|
1348
|
+
class EventError extends HttpError {
|
|
1349
|
+
constructor(message = "Event Error") {
|
|
1350
|
+
super(message, 500);
|
|
1351
|
+
this.name = "EventError";
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
const eta = new Eta();
|
|
1136
1355
|
function serveStatic(config, prefix) {
|
|
1137
1356
|
const rootPath = resolve(config.root || ".");
|
|
1138
1357
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
@@ -1231,7 +1450,7 @@ function serveStatic(config, prefix) {
|
|
|
1231
1450
|
if (config.listDirectory) {
|
|
1232
1451
|
try {
|
|
1233
1452
|
const files = await readdir(requestPath);
|
|
1234
|
-
const listing = eta
|
|
1453
|
+
const listing = eta.renderString(`
|
|
1235
1454
|
<!DOCTYPE html>
|
|
1236
1455
|
<html>
|
|
1237
1456
|
<head>
|
|
@@ -1271,7 +1490,7 @@ function serveStatic(config, prefix) {
|
|
|
1271
1490
|
if (typeof Bun !== "undefined") {
|
|
1272
1491
|
response = new Response(Bun.file(finalPath));
|
|
1273
1492
|
} else {
|
|
1274
|
-
const fileBuffer = await readFile$1(finalPath);
|
|
1493
|
+
const fileBuffer = await readFile$1(finalPath, { encoding: "binary" });
|
|
1275
1494
|
response = new Response(fileBuffer);
|
|
1276
1495
|
}
|
|
1277
1496
|
if (config.hooks?.onResponse) {
|
|
@@ -1284,69 +1503,6 @@ function serveStatic(config, prefix) {
|
|
|
1284
1503
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1285
1504
|
return serveStaticMiddleware;
|
|
1286
1505
|
}
|
|
1287
|
-
let db;
|
|
1288
|
-
let dbPromise = null;
|
|
1289
|
-
let RecordId;
|
|
1290
|
-
async function ensureDb() {
|
|
1291
|
-
if (db) return db;
|
|
1292
|
-
if (dbPromise) return dbPromise;
|
|
1293
|
-
dbPromise = (async () => {
|
|
1294
|
-
try {
|
|
1295
|
-
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1296
|
-
const surreal = await import("surrealdb");
|
|
1297
|
-
const Surreal = surreal.Surreal;
|
|
1298
|
-
RecordId = surreal.RecordId;
|
|
1299
|
-
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1300
|
-
const _db = new Surreal({
|
|
1301
|
-
engines: createNodeEngines()
|
|
1302
|
-
});
|
|
1303
|
-
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1304
|
-
await _db.query(`
|
|
1305
|
-
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1306
|
-
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1307
|
-
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1308
|
-
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1309
|
-
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1310
|
-
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1311
|
-
`);
|
|
1312
|
-
db = _db;
|
|
1313
|
-
return db;
|
|
1314
|
-
} catch (e) {
|
|
1315
|
-
dbPromise = null;
|
|
1316
|
-
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1317
|
-
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1318
|
-
}
|
|
1319
|
-
throw e;
|
|
1320
|
-
}
|
|
1321
|
-
})();
|
|
1322
|
-
return dbPromise;
|
|
1323
|
-
}
|
|
1324
|
-
const datastore = {
|
|
1325
|
-
async get(store, key) {
|
|
1326
|
-
await ensureDb();
|
|
1327
|
-
return db.select(new RecordId(store, key));
|
|
1328
|
-
},
|
|
1329
|
-
async set(store, key, value) {
|
|
1330
|
-
await ensureDb();
|
|
1331
|
-
return db.create(new RecordId(store, key)).content(value);
|
|
1332
|
-
},
|
|
1333
|
-
async query(query, vars) {
|
|
1334
|
-
await ensureDb();
|
|
1335
|
-
try {
|
|
1336
|
-
const r = await db.query(query, vars);
|
|
1337
|
-
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1338
|
-
} catch (e) {
|
|
1339
|
-
console.error("DS ERROR:", e);
|
|
1340
|
-
throw e;
|
|
1341
|
-
}
|
|
1342
|
-
},
|
|
1343
|
-
get ready() {
|
|
1344
|
-
return ensureDb().then(() => void 0);
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
process.on("exit", async () => {
|
|
1348
|
-
if (db) await db.close();
|
|
1349
|
-
});
|
|
1350
1506
|
class Container {
|
|
1351
1507
|
static services = /* @__PURE__ */ new Map();
|
|
1352
1508
|
static register(target, instance) {
|
|
@@ -1380,6 +1536,29 @@ function Inject(token) {
|
|
|
1380
1536
|
});
|
|
1381
1537
|
};
|
|
1382
1538
|
}
|
|
1539
|
+
const tracer = trace.getTracer("shokupan.middleware");
|
|
1540
|
+
function traceHandler(fn, name) {
|
|
1541
|
+
return async function(...args) {
|
|
1542
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1543
|
+
kind: SpanKind.INTERNAL,
|
|
1544
|
+
attributes: {
|
|
1545
|
+
"http.route": name,
|
|
1546
|
+
"component": "shokupan.route"
|
|
1547
|
+
}
|
|
1548
|
+
}, async (span) => {
|
|
1549
|
+
try {
|
|
1550
|
+
const result = await fn.apply(this, args);
|
|
1551
|
+
return result;
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
span.recordException(err);
|
|
1554
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
1555
|
+
throw err;
|
|
1556
|
+
} finally {
|
|
1557
|
+
span.end();
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1383
1562
|
class ShokupanRequestBase {
|
|
1384
1563
|
method;
|
|
1385
1564
|
url;
|
|
@@ -1417,8 +1596,10 @@ function getCallerInfo(skipFrames = 1) {
|
|
|
1417
1596
|
if (!l.includes(":")) continue;
|
|
1418
1597
|
if (l.includes("node_modules")) continue;
|
|
1419
1598
|
if (l.includes("bun:main")) continue;
|
|
1599
|
+
if (l.includes("bun:wrap")) continue;
|
|
1420
1600
|
if (l.includes("src/util/stack.ts")) continue;
|
|
1421
1601
|
if (l.includes("src/router.ts")) continue;
|
|
1602
|
+
if (l.includes("src/util/decorators.ts")) continue;
|
|
1422
1603
|
if (l.includes("src/shokupan.ts")) continue;
|
|
1423
1604
|
found++;
|
|
1424
1605
|
if (found >= skipFrames) {
|
|
@@ -1561,6 +1742,9 @@ class ShokupanRouter {
|
|
|
1561
1742
|
[$parent] = null;
|
|
1562
1743
|
[$childRouters] = [];
|
|
1563
1744
|
[$childControllers] = [];
|
|
1745
|
+
get db() {
|
|
1746
|
+
return this.root?.db;
|
|
1747
|
+
}
|
|
1564
1748
|
hookCache = /* @__PURE__ */ new Map();
|
|
1565
1749
|
hooksInitialized = false;
|
|
1566
1750
|
middleware = [];
|
|
@@ -1576,6 +1760,15 @@ class ShokupanRouter {
|
|
|
1576
1760
|
metadata;
|
|
1577
1761
|
// Metadata for the router itself
|
|
1578
1762
|
currentGuards = [];
|
|
1763
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
1764
|
+
/**
|
|
1765
|
+
* Registers middleware for this router.
|
|
1766
|
+
* Middleware will run for all routes matched by this router.
|
|
1767
|
+
*/
|
|
1768
|
+
use(middleware) {
|
|
1769
|
+
this.middleware.push(middleware);
|
|
1770
|
+
return this;
|
|
1771
|
+
}
|
|
1579
1772
|
// Registry Accessor
|
|
1580
1773
|
getComponentRegistry() {
|
|
1581
1774
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
@@ -1636,6 +1829,42 @@ class ShokupanRouter {
|
|
|
1636
1829
|
isRouterInstance(target) {
|
|
1637
1830
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1638
1831
|
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Registers an event handler for WebSocket.
|
|
1834
|
+
*/
|
|
1835
|
+
event(name, handler) {
|
|
1836
|
+
const info = getCallerInfo();
|
|
1837
|
+
handler.source = { file: info.file, line: info.line };
|
|
1838
|
+
if (this.eventHandlers.has(name)) {
|
|
1839
|
+
const err = new EventError(`Event handler \`${name}\` already exists.`);
|
|
1840
|
+
console.warn(err);
|
|
1841
|
+
const handlers = this.eventHandlers.get(name);
|
|
1842
|
+
handlers.push(handler);
|
|
1843
|
+
this.eventHandlers.set(name, handlers);
|
|
1844
|
+
} else {
|
|
1845
|
+
this.eventHandlers.set(name, [handler]);
|
|
1846
|
+
}
|
|
1847
|
+
return this;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Finds an event handler(s) by name.
|
|
1851
|
+
*/
|
|
1852
|
+
findEvent(name) {
|
|
1853
|
+
if (this.eventHandlers.has(name)) {
|
|
1854
|
+
return this.eventHandlers.get(name);
|
|
1855
|
+
}
|
|
1856
|
+
for (const child of this[$childRouters]) {
|
|
1857
|
+
const handler = child.findEvent(name);
|
|
1858
|
+
if (handler) return handler;
|
|
1859
|
+
}
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Returns all registered event handlers.
|
|
1864
|
+
*/
|
|
1865
|
+
getEventHandlers() {
|
|
1866
|
+
return this.eventHandlers;
|
|
1867
|
+
}
|
|
1639
1868
|
/**
|
|
1640
1869
|
* Mounts a controller instance to a path prefix.
|
|
1641
1870
|
*
|
|
@@ -1736,7 +1965,7 @@ class ShokupanRouter {
|
|
|
1736
1965
|
});
|
|
1737
1966
|
const ctx = new ShokupanContext(req);
|
|
1738
1967
|
let result = null;
|
|
1739
|
-
let status =
|
|
1968
|
+
let status = HTTP_STATUS.OK;
|
|
1740
1969
|
const headers = {};
|
|
1741
1970
|
const match = this.find(req.method, ctx.path);
|
|
1742
1971
|
if (match) {
|
|
@@ -1745,12 +1974,12 @@ class ShokupanRouter {
|
|
|
1745
1974
|
result = await match.handler(ctx);
|
|
1746
1975
|
} catch (err) {
|
|
1747
1976
|
console.error(err);
|
|
1748
|
-
status = err
|
|
1977
|
+
status = getErrorStatus(err);
|
|
1749
1978
|
result = { error: err.message || "Internal Server Error" };
|
|
1750
1979
|
if (err.errors) result.errors = err.errors;
|
|
1751
1980
|
}
|
|
1752
1981
|
} else {
|
|
1753
|
-
status =
|
|
1982
|
+
status = HTTP_STATUS.NOT_FOUND;
|
|
1754
1983
|
result = "Not Found";
|
|
1755
1984
|
}
|
|
1756
1985
|
if (result instanceof Response) {
|
|
@@ -1779,7 +2008,7 @@ class ShokupanRouter {
|
|
|
1779
2008
|
const originalHandler = handler;
|
|
1780
2009
|
const wrapped = async (ctx) => {
|
|
1781
2010
|
await this.runHooks("onRequestStart", ctx);
|
|
1782
|
-
const debug = ctx
|
|
2011
|
+
const debug = ctx[$debug];
|
|
1783
2012
|
let debugId;
|
|
1784
2013
|
let previousNode;
|
|
1785
2014
|
if (debug) {
|
|
@@ -1869,6 +2098,7 @@ class ShokupanRouter {
|
|
|
1869
2098
|
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
1870
2099
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1871
2100
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2101
|
+
const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
|
|
1872
2102
|
let routesAttached = 0;
|
|
1873
2103
|
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1874
2104
|
const name = Array.from(methods)[i];
|
|
@@ -1878,10 +2108,12 @@ class ShokupanRouter {
|
|
|
1878
2108
|
if (typeof originalHandler !== "function") continue;
|
|
1879
2109
|
let method;
|
|
1880
2110
|
let subPath = "";
|
|
2111
|
+
let methodSource;
|
|
1881
2112
|
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
1882
2113
|
const config = decoratedRoutes.get(name);
|
|
1883
2114
|
method = config.method;
|
|
1884
2115
|
subPath = config.path;
|
|
2116
|
+
methodSource = config.source;
|
|
1885
2117
|
} else {
|
|
1886
2118
|
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1887
2119
|
const m = HTTPMethods[j];
|
|
@@ -2011,7 +2243,54 @@ class ShokupanRouter {
|
|
|
2011
2243
|
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2012
2244
|
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2013
2245
|
const spec = { tags: [tagName], ...userSpec };
|
|
2014
|
-
this.add({
|
|
2246
|
+
this.add({
|
|
2247
|
+
method,
|
|
2248
|
+
path: normalizedPath,
|
|
2249
|
+
handler: finalHandler,
|
|
2250
|
+
spec,
|
|
2251
|
+
controller: instance,
|
|
2252
|
+
metadata: methodSource || instance.metadata,
|
|
2253
|
+
middleware: allMiddleware
|
|
2254
|
+
// Capture all resolved middleware
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
if (decoratedEvents?.has(name)) {
|
|
2258
|
+
routesAttached++;
|
|
2259
|
+
const config = decoratedEvents.get(name);
|
|
2260
|
+
const routeArgs = decoratedArgs?.get(name);
|
|
2261
|
+
const wrappedHandler = async (ctx) => {
|
|
2262
|
+
let args = [ctx];
|
|
2263
|
+
if (routeArgs?.length > 0) {
|
|
2264
|
+
args = [];
|
|
2265
|
+
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2266
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2267
|
+
const arg = sortedArgs[k];
|
|
2268
|
+
switch (arg.type) {
|
|
2269
|
+
case RouteParamType.BODY:
|
|
2270
|
+
args[arg.index] = await ctx.body();
|
|
2271
|
+
break;
|
|
2272
|
+
case RouteParamType.CONTEXT:
|
|
2273
|
+
args[arg.index] = ctx;
|
|
2274
|
+
break;
|
|
2275
|
+
case RouteParamType.REQUEST:
|
|
2276
|
+
args[arg.index] = ctx.req;
|
|
2277
|
+
break;
|
|
2278
|
+
case RouteParamType.HEADER:
|
|
2279
|
+
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2280
|
+
break;
|
|
2281
|
+
default:
|
|
2282
|
+
args[arg.index] = void 0;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return originalHandler.apply(instance, args);
|
|
2287
|
+
};
|
|
2288
|
+
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2289
|
+
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2290
|
+
const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
|
|
2291
|
+
wrappedHandler.spec = spec;
|
|
2292
|
+
wrappedHandler.originalHandler = originalHandler;
|
|
2293
|
+
this.event(config.eventName, wrappedHandler);
|
|
2015
2294
|
}
|
|
2016
2295
|
}
|
|
2017
2296
|
if (routesAttached === 0) {
|
|
@@ -2080,7 +2359,7 @@ class ShokupanRouter {
|
|
|
2080
2359
|
* @param arg.renderer - JSX renderer for the route
|
|
2081
2360
|
* @param arg.controller - Controller for the route
|
|
2082
2361
|
*/
|
|
2083
|
-
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2362
|
+
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller, metadata, middleware }) {
|
|
2084
2363
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
|
|
2085
2364
|
if (this.currentGuards.length > 0) {
|
|
2086
2365
|
spec = spec || {};
|
|
@@ -2098,7 +2377,13 @@ class ShokupanRouter {
|
|
|
2098
2377
|
}
|
|
2099
2378
|
}
|
|
2100
2379
|
}
|
|
2101
|
-
let wrappedHandler =
|
|
2380
|
+
let wrappedHandler = async (ctx) => {
|
|
2381
|
+
if (ctx.upgrade()) {
|
|
2382
|
+
return void 0;
|
|
2383
|
+
}
|
|
2384
|
+
return handler(ctx);
|
|
2385
|
+
};
|
|
2386
|
+
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
2102
2387
|
const routeGuards = [...this.currentGuards];
|
|
2103
2388
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
2104
2389
|
if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
|
|
@@ -2149,7 +2434,7 @@ class ShokupanRouter {
|
|
|
2149
2434
|
return innerHandler(ctx);
|
|
2150
2435
|
};
|
|
2151
2436
|
}
|
|
2152
|
-
const { file, line } = getCallerInfo();
|
|
2437
|
+
const { file, line } = metadata || getCallerInfo();
|
|
2153
2438
|
const trackingHandler = wrappedHandler;
|
|
2154
2439
|
wrappedHandler = async (ctx) => {
|
|
2155
2440
|
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
@@ -2175,9 +2460,13 @@ class ShokupanRouter {
|
|
|
2175
2460
|
const config = ctx.app.applicationConfig;
|
|
2176
2461
|
Promise.resolve().then(async () => {
|
|
2177
2462
|
try {
|
|
2463
|
+
const db = ctx.app?.db;
|
|
2464
|
+
if (!db) return;
|
|
2178
2465
|
const timestamp = Date.now();
|
|
2179
|
-
|
|
2180
|
-
|
|
2466
|
+
await db.upsert(new RecordId("middleware_tracking", {
|
|
2467
|
+
timestamp,
|
|
2468
|
+
name: handler.name || "anonymous"
|
|
2469
|
+
}), {
|
|
2181
2470
|
name: handler.name || "anonymous",
|
|
2182
2471
|
path: ctx.path,
|
|
2183
2472
|
timestamp,
|
|
@@ -2193,11 +2482,11 @@ class ShokupanRouter {
|
|
|
2193
2482
|
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2194
2483
|
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2195
2484
|
const cutoff = Date.now() - ttl;
|
|
2196
|
-
await
|
|
2197
|
-
const results = await
|
|
2198
|
-
if (results
|
|
2485
|
+
await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2486
|
+
const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2487
|
+
if (results?.[0]?.count > maxCapacity) {
|
|
2199
2488
|
const toDelete = results[0].count - maxCapacity;
|
|
2200
|
-
await
|
|
2489
|
+
await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2201
2490
|
}
|
|
2202
2491
|
} catch (datastoreError) {
|
|
2203
2492
|
console.error("Failed to store middleware tracking:", datastoreError);
|
|
@@ -2227,7 +2516,8 @@ class ShokupanRouter {
|
|
|
2227
2516
|
file,
|
|
2228
2517
|
line
|
|
2229
2518
|
},
|
|
2230
|
-
controller
|
|
2519
|
+
controller,
|
|
2520
|
+
middleware: middleware || []
|
|
2231
2521
|
});
|
|
2232
2522
|
this.trie.insert(method, path, bakedHandler);
|
|
2233
2523
|
return this;
|
|
@@ -2272,7 +2562,7 @@ class ShokupanRouter {
|
|
|
2272
2562
|
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
2273
2563
|
);
|
|
2274
2564
|
if (callerLine) {
|
|
2275
|
-
const match = callerLine.match(/\((
|
|
2565
|
+
const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
|
|
2276
2566
|
if (match) {
|
|
2277
2567
|
file = match[1];
|
|
2278
2568
|
line = parseInt(match[2], 10);
|
|
@@ -2290,7 +2580,7 @@ class ShokupanRouter {
|
|
|
2290
2580
|
}
|
|
2291
2581
|
return guardHandler(ctx, next);
|
|
2292
2582
|
};
|
|
2293
|
-
trackedGuard.originalHandler = guardHandler.originalHandler
|
|
2583
|
+
trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
|
|
2294
2584
|
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
2295
2585
|
return this;
|
|
2296
2586
|
}
|
|
@@ -2356,7 +2646,8 @@ class ShokupanRouter {
|
|
|
2356
2646
|
method,
|
|
2357
2647
|
path,
|
|
2358
2648
|
spec,
|
|
2359
|
-
handler: finalHandler
|
|
2649
|
+
handler: finalHandler,
|
|
2650
|
+
middleware: handlers.slice(0, handlers.length - 1)
|
|
2360
2651
|
});
|
|
2361
2652
|
}
|
|
2362
2653
|
/**
|
|
@@ -2396,16 +2687,16 @@ class ShokupanRouter {
|
|
|
2396
2687
|
}
|
|
2397
2688
|
this.hooksInitialized = true;
|
|
2398
2689
|
}
|
|
2399
|
-
|
|
2690
|
+
runHooks(name, ...args) {
|
|
2400
2691
|
if (!this.hooksInitialized) {
|
|
2401
2692
|
this.ensureHooksInitialized();
|
|
2402
2693
|
}
|
|
2403
2694
|
const fns = this.hookCache.get(name);
|
|
2404
2695
|
if (!fns) return;
|
|
2405
2696
|
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2406
|
-
const debug = ctx?.
|
|
2697
|
+
const debug = ctx?.[$debug];
|
|
2407
2698
|
if (debug) {
|
|
2408
|
-
|
|
2699
|
+
return Promise.all(fns.map(async (fn, index) => {
|
|
2409
2700
|
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2410
2701
|
const previousNode = debug.getCurrentNode();
|
|
2411
2702
|
debug.trackEdge(previousNode, hookId);
|
|
@@ -2424,7 +2715,7 @@ class ShokupanRouter {
|
|
|
2424
2715
|
}
|
|
2425
2716
|
}));
|
|
2426
2717
|
} else {
|
|
2427
|
-
|
|
2718
|
+
return Promise.all(fns.map((fn) => fn(...args)));
|
|
2428
2719
|
}
|
|
2429
2720
|
}
|
|
2430
2721
|
}
|
|
@@ -2471,45 +2762,160 @@ class SystemCpuMonitor {
|
|
|
2471
2762
|
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2472
2763
|
}
|
|
2473
2764
|
}
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
};
|
|
2481
|
-
trace.getTracer("shokupan.application");
|
|
2482
|
-
class Shokupan extends ShokupanRouter {
|
|
2483
|
-
applicationConfig = {};
|
|
2484
|
-
openApiSpec;
|
|
2485
|
-
composedMiddleware;
|
|
2486
|
-
cpuMonitor;
|
|
2487
|
-
get logger() {
|
|
2488
|
-
return this.applicationConfig.logger;
|
|
2765
|
+
class SurrealDatastore {
|
|
2766
|
+
constructor(db) {
|
|
2767
|
+
this.db = db;
|
|
2768
|
+
process.on("exit", async () => {
|
|
2769
|
+
await this.disconnect();
|
|
2770
|
+
});
|
|
2489
2771
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
line,
|
|
2501
|
-
name: "ShokupanApplication"
|
|
2502
|
-
};
|
|
2772
|
+
createSchema() {
|
|
2773
|
+
this.db.query(`
|
|
2774
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2775
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
2776
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
2777
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
2778
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
2779
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2780
|
+
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
2781
|
+
`).collect();
|
|
2503
2782
|
}
|
|
2504
2783
|
/**
|
|
2505
|
-
*
|
|
2784
|
+
* Select a record or contents of a table by its ID.
|
|
2506
2785
|
*/
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2786
|
+
async select(id) {
|
|
2787
|
+
return this.db.select(id);
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Merge update data into a record by its ID.
|
|
2791
|
+
*/
|
|
2792
|
+
async merge(id, data) {
|
|
2793
|
+
return this.db.update(id).merge(data).catch((err) => {
|
|
2794
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2795
|
+
return this.db.update(id).merge(data);
|
|
2796
|
+
}
|
|
2797
|
+
throw err;
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Create a record by its ID.
|
|
2802
|
+
*/
|
|
2803
|
+
async create(id, data) {
|
|
2804
|
+
return this.db.create(id).content(data).catch((err) => {
|
|
2805
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2806
|
+
return this.db.create(id).content(data);
|
|
2807
|
+
}
|
|
2808
|
+
throw err;
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Upsert a record by its ID.
|
|
2813
|
+
*/
|
|
2814
|
+
async upsert(id, data) {
|
|
2815
|
+
return this.db.upsert(id).content(data).catch((err) => {
|
|
2816
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2817
|
+
return this.db.upsert(id).content(data);
|
|
2818
|
+
}
|
|
2819
|
+
throw err;
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Delete a record by its ID.
|
|
2824
|
+
*/
|
|
2825
|
+
async delete(id) {
|
|
2826
|
+
return this.db.delete(id).catch((err) => {
|
|
2827
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2828
|
+
return this.db.delete(id);
|
|
2829
|
+
}
|
|
2830
|
+
throw err;
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Run a SurrealDB query.
|
|
2835
|
+
*/
|
|
2836
|
+
async query(query, vars) {
|
|
2837
|
+
return this.db.query(query, vars).collect().catch((err) => {
|
|
2838
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2839
|
+
return this.db.query(query, vars).collect();
|
|
2840
|
+
}
|
|
2841
|
+
throw err;
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Create a relationship between two records.
|
|
2846
|
+
*/
|
|
2847
|
+
async relate(fromId, edgeId, toId, data) {
|
|
2848
|
+
return this.db.relate(fromId, edgeId, toId, data).catch((err) => {
|
|
2849
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2850
|
+
return this.db.relate(fromId, edgeId, toId, data);
|
|
2851
|
+
}
|
|
2852
|
+
throw err;
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
disconnect() {
|
|
2856
|
+
return this.db.close();
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
const defaults = {
|
|
2860
|
+
port: 3e3,
|
|
2861
|
+
hostname: "localhost",
|
|
2862
|
+
development: process.env.NODE_ENV !== "production",
|
|
2863
|
+
enableAsyncLocalStorage: false,
|
|
2864
|
+
enableHttpBridge: false,
|
|
2865
|
+
reusePort: false
|
|
2866
|
+
};
|
|
2867
|
+
trace.getTracer("shokupan.application");
|
|
2868
|
+
class Shokupan extends ShokupanRouter {
|
|
2869
|
+
applicationConfig = {};
|
|
2870
|
+
openApiSpec;
|
|
2871
|
+
asyncApiSpec;
|
|
2872
|
+
composedMiddleware;
|
|
2873
|
+
cpuMonitor;
|
|
2874
|
+
server;
|
|
2875
|
+
datastore;
|
|
2876
|
+
dbPromise;
|
|
2877
|
+
get db() {
|
|
2878
|
+
return this.datastore;
|
|
2879
|
+
}
|
|
2880
|
+
get logger() {
|
|
2881
|
+
return this.applicationConfig.logger;
|
|
2882
|
+
}
|
|
2883
|
+
constructor(applicationConfig = {}) {
|
|
2884
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2885
|
+
const { hooks, ...routerConfig } = config;
|
|
2886
|
+
super({ ...routerConfig, hooks });
|
|
2887
|
+
this[$isApplication] = true;
|
|
2888
|
+
this[$appRoot] = this;
|
|
2889
|
+
this.applicationConfig = config;
|
|
2890
|
+
const { file, line } = getCallerInfo();
|
|
2891
|
+
this.metadata = {
|
|
2892
|
+
file,
|
|
2893
|
+
line,
|
|
2894
|
+
name: "ShokupanApplication"
|
|
2895
|
+
};
|
|
2896
|
+
this.dbPromise = this.initDatastore();
|
|
2897
|
+
}
|
|
2898
|
+
async initDatastore() {
|
|
2899
|
+
const db = new Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
|
|
2900
|
+
this.datastore = new SurrealDatastore(db);
|
|
2901
|
+
await db.connect(
|
|
2902
|
+
this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
|
|
2903
|
+
this.applicationConfig.surreal?.connectOptions
|
|
2904
|
+
);
|
|
2905
|
+
await db.use({
|
|
2906
|
+
namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
|
|
2907
|
+
database: this.applicationConfig.surreal?.database ?? "shokupan"
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Adds middleware to the application.
|
|
2912
|
+
*/
|
|
2913
|
+
use(middleware) {
|
|
2914
|
+
const { file, line } = getCallerInfo();
|
|
2915
|
+
if (!middleware.metadata) {
|
|
2916
|
+
middleware.metadata = {
|
|
2917
|
+
file,
|
|
2918
|
+
line,
|
|
2513
2919
|
name: middleware.name || "middleware",
|
|
2514
2920
|
isBuiltin: middleware.isBuiltin,
|
|
2515
2921
|
pluginName: middleware.pluginName
|
|
@@ -2579,19 +2985,76 @@ class Shokupan extends ShokupanRouter {
|
|
|
2579
2985
|
*/
|
|
2580
2986
|
async listen(port) {
|
|
2581
2987
|
const finalPort = port ?? this.applicationConfig.port ?? 3e3;
|
|
2582
|
-
if (finalPort < 0 || finalPort > 65535) {
|
|
2988
|
+
if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
|
|
2583
2989
|
throw new Error("Invalid port number");
|
|
2584
2990
|
}
|
|
2585
2991
|
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2586
2992
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2587
2993
|
this.openApiSpec = await generateOpenApi(this);
|
|
2994
|
+
this.get("/.well-known/openapi.yaml", (ctx) => {
|
|
2995
|
+
try {
|
|
2996
|
+
const yaml = dump(this.openApiSpec);
|
|
2997
|
+
return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
|
|
2998
|
+
} catch (e) {
|
|
2999
|
+
this.logger.error("Failed to generate OpenAPI YAML", { error: e });
|
|
3000
|
+
return ctx.text("Internal Server Error", 500);
|
|
3001
|
+
}
|
|
3002
|
+
});
|
|
3003
|
+
if (this.applicationConfig.aiPlugin?.enabled !== false) {
|
|
3004
|
+
this.get("/.well-known/ai-plugin.json", async (ctx) => {
|
|
3005
|
+
const config = this.applicationConfig.aiPlugin || {};
|
|
3006
|
+
let pkg = {};
|
|
3007
|
+
try {
|
|
3008
|
+
pkg = await Bun.file("package.json").json();
|
|
3009
|
+
} catch (e) {
|
|
3010
|
+
}
|
|
3011
|
+
const manifest = {
|
|
3012
|
+
schema_version: "v1",
|
|
3013
|
+
name_for_human: config.name_for_human || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3014
|
+
name_for_model: config.name_for_model || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3015
|
+
description_for_human: config.description_for_human || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3016
|
+
description_for_model: config.description_for_model || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3017
|
+
auth: config.auth || { type: "none" },
|
|
3018
|
+
api: config.api || {
|
|
3019
|
+
type: "openapi",
|
|
3020
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
|
|
3021
|
+
is_user_authenticated: false
|
|
3022
|
+
},
|
|
3023
|
+
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
|
|
3024
|
+
// Placeholder default
|
|
3025
|
+
contact_email: config.contact_email || pkg.author?.email || "support@example.com",
|
|
3026
|
+
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
|
|
3027
|
+
};
|
|
3028
|
+
return ctx.json(manifest);
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
if (this.applicationConfig.apiCatalog?.enabled !== false) {
|
|
3032
|
+
this.get("/.well-known/api-catalog", (ctx) => {
|
|
3033
|
+
const config = this.applicationConfig.apiCatalog || {};
|
|
3034
|
+
const catalog = {
|
|
3035
|
+
versions: config.versions || [
|
|
3036
|
+
{
|
|
3037
|
+
name: this.openApiSpec.info.version || "v1",
|
|
3038
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
|
|
3039
|
+
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
|
|
3040
|
+
}
|
|
3041
|
+
]
|
|
3042
|
+
};
|
|
3043
|
+
return ctx.json(catalog);
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
2588
3046
|
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2589
3047
|
}
|
|
3048
|
+
if (this.applicationConfig.enableAsyncApiGen) {
|
|
3049
|
+
const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
|
|
3050
|
+
this.asyncApiSpec = await generateAsyncApi2(this);
|
|
3051
|
+
}
|
|
2590
3052
|
if (port === 0 && process.platform === "linux") ;
|
|
2591
3053
|
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2592
3054
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2593
3055
|
this.cpuMonitor.start();
|
|
2594
3056
|
}
|
|
3057
|
+
const self = this;
|
|
2595
3058
|
const serveOptions = {
|
|
2596
3059
|
port: finalPort,
|
|
2597
3060
|
hostname: this.applicationConfig.hostname,
|
|
@@ -2603,25 +3066,116 @@ class Shokupan extends ShokupanRouter {
|
|
|
2603
3066
|
open(ws) {
|
|
2604
3067
|
ws.data?.handler?.open?.(ws);
|
|
2605
3068
|
},
|
|
2606
|
-
message(ws, message) {
|
|
2607
|
-
ws.data?.handler?.message
|
|
3069
|
+
async message(ws, message) {
|
|
3070
|
+
if (ws.data?.handler?.message) {
|
|
3071
|
+
return ws.data.handler.message(ws, message);
|
|
3072
|
+
}
|
|
3073
|
+
if (typeof message !== "string") return;
|
|
3074
|
+
try {
|
|
3075
|
+
const payload = JSON.parse(message);
|
|
3076
|
+
if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
|
|
3077
|
+
const { id, method, path, headers, body } = payload;
|
|
3078
|
+
const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
|
|
3079
|
+
const req = new Request(url.toString(), {
|
|
3080
|
+
method,
|
|
3081
|
+
headers,
|
|
3082
|
+
body: typeof body === "object" ? JSON.stringify(body) : body
|
|
3083
|
+
});
|
|
3084
|
+
const res = await self.fetch(req);
|
|
3085
|
+
const resBody = await res.json().catch((err) => res.text());
|
|
3086
|
+
const resHeaders = {};
|
|
3087
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
3088
|
+
ws.send(JSON.stringify({
|
|
3089
|
+
type: "RESPONSE",
|
|
3090
|
+
id,
|
|
3091
|
+
status: res.status,
|
|
3092
|
+
headers: resHeaders,
|
|
3093
|
+
body: resBody
|
|
3094
|
+
}));
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
|
|
3098
|
+
if (eventName) {
|
|
3099
|
+
const handlers = self.findEvent(eventName);
|
|
3100
|
+
const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
|
|
3101
|
+
if (handler) {
|
|
3102
|
+
const data = payload.data || payload.payload;
|
|
3103
|
+
const req = new ShokupanRequest({
|
|
3104
|
+
url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
|
|
3105
|
+
method: "POST",
|
|
3106
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
3107
|
+
body: JSON.stringify(data)
|
|
3108
|
+
});
|
|
3109
|
+
const ctx = new ShokupanContext(req, self.server);
|
|
3110
|
+
ctx[$ws] = ws;
|
|
3111
|
+
ws.data ??= {};
|
|
3112
|
+
ws.data["ctx"] = ctx;
|
|
3113
|
+
try {
|
|
3114
|
+
await handler(ctx);
|
|
3115
|
+
} catch (err) {
|
|
3116
|
+
if (self.applicationConfig["websocketErrorHandler"]) {
|
|
3117
|
+
await self.applicationConfig["websocketErrorHandler"](err, ctx);
|
|
3118
|
+
} else {
|
|
3119
|
+
console.error(`Error in event ${eventName}:`, err);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
} catch (e) {
|
|
3125
|
+
}
|
|
2608
3126
|
},
|
|
2609
3127
|
drain(ws) {
|
|
2610
3128
|
ws.data?.handler?.drain?.(ws);
|
|
2611
3129
|
},
|
|
2612
3130
|
close(ws, code, reason) {
|
|
2613
3131
|
ws.data?.handler?.close?.(ws, code, reason);
|
|
3132
|
+
const ctx = ws.data?.["ctx"];
|
|
3133
|
+
if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
|
|
3134
|
+
const callbacks = ctx.getDisconnectCallbacks();
|
|
3135
|
+
if (Array.isArray(callbacks) && callbacks.length > 0) {
|
|
3136
|
+
Promise.all(callbacks.map((cb) => cb())).catch((err) => {
|
|
3137
|
+
console.error("Error executing socket disconnect hook:", err);
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
2614
3141
|
}
|
|
2615
3142
|
}
|
|
2616
3143
|
};
|
|
2617
3144
|
let factory = this.applicationConfig.serverFactory;
|
|
2618
3145
|
if (!factory && typeof Bun === "undefined") {
|
|
2619
|
-
const { createHttpServer } = await import("./http-server-
|
|
3146
|
+
const { createHttpServer } = await import("./http-server-CCeagTyU.js");
|
|
2620
3147
|
factory = createHttpServer();
|
|
2621
3148
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
3149
|
+
this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
3150
|
+
return this.server;
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Stops the application server.
|
|
3154
|
+
*
|
|
3155
|
+
* This method gracefully shuts down the server and stops any running monitors.
|
|
3156
|
+
* Works transparently in both Bun and Node.js runtimes.
|
|
3157
|
+
*
|
|
3158
|
+
* @returns A promise that resolves when the server has been stopped.
|
|
3159
|
+
*
|
|
3160
|
+
* @example
|
|
3161
|
+
* ```typescript
|
|
3162
|
+
* const app = new Shokupan();
|
|
3163
|
+
* const server = await app.listen(3000);
|
|
3164
|
+
*
|
|
3165
|
+
* // Later, when you want to stop the server
|
|
3166
|
+
* await app.stop();
|
|
3167
|
+
* ```
|
|
3168
|
+
* @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
|
|
3169
|
+
*/
|
|
3170
|
+
async stop(closeActiveConnections) {
|
|
3171
|
+
if (this.cpuMonitor) {
|
|
3172
|
+
this.cpuMonitor.stop();
|
|
3173
|
+
this.cpuMonitor = void 0;
|
|
3174
|
+
}
|
|
3175
|
+
if (this.server) {
|
|
3176
|
+
await this.server.stop(closeActiveConnections);
|
|
3177
|
+
this.server = void 0;
|
|
3178
|
+
}
|
|
2625
3179
|
}
|
|
2626
3180
|
[$dispatch](req) {
|
|
2627
3181
|
return this.fetch(req);
|
|
@@ -2719,7 +3273,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2719
3273
|
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2720
3274
|
const match = this.find(req.method, ctx.path);
|
|
2721
3275
|
if (match) {
|
|
2722
|
-
ctx
|
|
3276
|
+
ctx[$routeMatched] = true;
|
|
2723
3277
|
ctx.params = match.params;
|
|
2724
3278
|
await bodyParsing;
|
|
2725
3279
|
return match.handler(ctx);
|
|
@@ -2729,18 +3283,23 @@ class Shokupan extends ShokupanRouter {
|
|
|
2729
3283
|
let response;
|
|
2730
3284
|
if (result instanceof Response) {
|
|
2731
3285
|
response = result;
|
|
2732
|
-
} else if ((result === null || result === void 0) && ctx
|
|
2733
|
-
response = ctx
|
|
3286
|
+
} else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
|
|
3287
|
+
response = ctx[$finalResponse];
|
|
2734
3288
|
} else if (result === null || result === void 0) {
|
|
2735
|
-
if (ctx
|
|
2736
|
-
response = ctx
|
|
2737
|
-
} else if (ctx.
|
|
3289
|
+
if (ctx[$finalResponse] instanceof Response) {
|
|
3290
|
+
response = ctx[$finalResponse];
|
|
3291
|
+
} else if (ctx.isUpgraded) {
|
|
3292
|
+
return void 0;
|
|
3293
|
+
} else if (ctx[$routeMatched]) {
|
|
2738
3294
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2739
3295
|
} else {
|
|
2740
|
-
if (ctx.
|
|
3296
|
+
if (ctx.upgrade()) {
|
|
3297
|
+
return void 0;
|
|
3298
|
+
}
|
|
3299
|
+
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
2741
3300
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2742
3301
|
} else {
|
|
2743
|
-
response = ctx.text("Not Found",
|
|
3302
|
+
response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
|
|
2744
3303
|
}
|
|
2745
3304
|
}
|
|
2746
3305
|
} else if (typeof result === "object") {
|
|
@@ -2749,13 +3308,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
2749
3308
|
response = ctx.text(String(result));
|
|
2750
3309
|
}
|
|
2751
3310
|
await this.runHooks("onRequestEnd", ctx);
|
|
3311
|
+
if (response instanceof Promise) {
|
|
3312
|
+
response = await response;
|
|
3313
|
+
}
|
|
2752
3314
|
await this.runHooks("onResponseStart", ctx, response);
|
|
2753
3315
|
return response;
|
|
2754
3316
|
} catch (err) {
|
|
2755
|
-
console.error(err);
|
|
2756
3317
|
const span = asyncContext.getStore()?.span;
|
|
2757
3318
|
if (span) span.setStatus({ code: 2 });
|
|
2758
|
-
const status = err
|
|
3319
|
+
const status = getErrorStatus(err);
|
|
2759
3320
|
const body = { error: err.message || "Internal Server Error" };
|
|
2760
3321
|
if (err.errors) body.errors = err.errors;
|
|
2761
3322
|
await this.runHooks("onError", ctx, err);
|
|
@@ -2777,10 +3338,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
2777
3338
|
}
|
|
2778
3339
|
return executionPromise.catch((err) => {
|
|
2779
3340
|
if (err.message === "Request Timeout") {
|
|
2780
|
-
return ctx.text("Request Timeout",
|
|
3341
|
+
return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
|
|
2781
3342
|
}
|
|
2782
3343
|
console.error("Unexpected error in request execution:", err);
|
|
2783
|
-
return ctx.text("Internal Server Error",
|
|
3344
|
+
return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
2784
3345
|
}).then(async (res) => {
|
|
2785
3346
|
await this.runHooks("onResponseEnd", ctx, res);
|
|
2786
3347
|
return res;
|
|
@@ -2859,8 +3420,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
2859
3420
|
}
|
|
2860
3421
|
}
|
|
2861
3422
|
const msg = typeof message === "function" ? message(ctx, key) : message;
|
|
2862
|
-
typeof msg === "object" ?
|
|
2863
|
-
const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
|
|
3423
|
+
const res = await (typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode));
|
|
2864
3424
|
if (headers) {
|
|
2865
3425
|
setHeaders(res);
|
|
2866
3426
|
res.headers.set("Retry-After", String(retryAfter));
|
|
@@ -2935,8 +3495,12 @@ function createMethodDecorator(method) {
|
|
|
2935
3495
|
}
|
|
2936
3496
|
target[$routeMethods].set(propertyKey, {
|
|
2937
3497
|
method,
|
|
2938
|
-
path
|
|
3498
|
+
path,
|
|
3499
|
+
source: getCallerInfo(2)
|
|
2939
3500
|
});
|
|
3501
|
+
if (path.includes("/user")) {
|
|
3502
|
+
console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
|
|
3503
|
+
}
|
|
2940
3504
|
};
|
|
2941
3505
|
};
|
|
2942
3506
|
}
|
|
@@ -2948,18 +3512,583 @@ const Patch = createMethodDecorator("PATCH");
|
|
|
2948
3512
|
const Options = createMethodDecorator("OPTIONS");
|
|
2949
3513
|
const Head = createMethodDecorator("HEAD");
|
|
2950
3514
|
const All = createMethodDecorator("ALL");
|
|
3515
|
+
function Event(eventName) {
|
|
3516
|
+
return (target, propertyKey, descriptor) => {
|
|
3517
|
+
target[$eventMethods] ??= /* @__PURE__ */ new Map();
|
|
3518
|
+
target[$eventMethods].set(propertyKey, {
|
|
3519
|
+
eventName
|
|
3520
|
+
});
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
2951
3523
|
function RateLimit(options) {
|
|
2952
3524
|
return Use(RateLimitMiddleware(options));
|
|
2953
3525
|
}
|
|
3526
|
+
function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
3527
|
+
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
3528
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
3529
|
+
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
3530
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3531
|
+
/* @__PURE__ */ jsx("title", { children: "Shokupan AsyncAPI" }),
|
|
3532
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
3533
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
|
|
3534
|
+
/* @__PURE__ */ 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" }),
|
|
3535
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
3536
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
3537
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
3538
|
+
__html: `
|
|
3539
|
+
window.INITIAL_SPEC = ${JSON.stringify(spec)};
|
|
3540
|
+
window.INITIAL_SERVER_URL = "${serverUrl}";
|
|
3541
|
+
window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
|
|
3542
|
+
`
|
|
3543
|
+
} })
|
|
3544
|
+
] }),
|
|
3545
|
+
/* @__PURE__ */ jsxs("body", { children: [
|
|
3546
|
+
/* @__PURE__ */ jsxs("div", { class: "app-container", children: [
|
|
3547
|
+
/* @__PURE__ */ jsx(Sidebar, { navTree, disableSourceView }),
|
|
3548
|
+
/* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-left" }),
|
|
3549
|
+
/* @__PURE__ */ jsx(MainContent, {}),
|
|
3550
|
+
/* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-right" }),
|
|
3551
|
+
/* @__PURE__ */ jsx(ConsolePanel, { serverUrl })
|
|
3552
|
+
] }),
|
|
3553
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.socket.io/4.7.4/socket.io.min.js" }),
|
|
3554
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
|
|
3555
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/asyncapi-client.mjs`, type: "module" })
|
|
3556
|
+
] })
|
|
3557
|
+
] });
|
|
3558
|
+
}
|
|
3559
|
+
function Sidebar({ navTree, disableSourceView }) {
|
|
3560
|
+
return /* @__PURE__ */ jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
|
|
3561
|
+
/* @__PURE__ */ jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
|
|
3562
|
+
/* @__PURE__ */ jsx("h2", { children: "AsyncAPI" }),
|
|
3563
|
+
/* @__PURE__ */ jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
|
|
3564
|
+
] }),
|
|
3565
|
+
/* @__PURE__ */ jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
3566
|
+
] });
|
|
3567
|
+
}
|
|
3568
|
+
function NavNode({ node, level, disableSourceView }) {
|
|
3569
|
+
const sortedEntries = Object.entries(node.children || {}).sort((a, b) => {
|
|
3570
|
+
const [aKey, aItem] = a;
|
|
3571
|
+
const [bKey, bItem] = b;
|
|
3572
|
+
const isWarningA = aItem.data?.op?.["x-warning"];
|
|
3573
|
+
const isWarningB = bItem.data?.op?.["x-warning"];
|
|
3574
|
+
if (isWarningA && !isWarningB) return -1;
|
|
3575
|
+
if (!isWarningA && isWarningB) return 1;
|
|
3576
|
+
if (aKey === bKey) return 0;
|
|
3577
|
+
if (aKey === "Warning" || aKey === "Warnings") return -1;
|
|
3578
|
+
if (bKey === "Warning" || bKey === "Warnings") return 1;
|
|
3579
|
+
if (aKey === "Application") return -1;
|
|
3580
|
+
if (bKey === "Application") return 1;
|
|
3581
|
+
if (aKey[0] === "/") return 1;
|
|
3582
|
+
if (bKey[0] === "/") return -1;
|
|
3583
|
+
return aKey.localeCompare(bKey);
|
|
3584
|
+
});
|
|
3585
|
+
return /* @__PURE__ */ jsx(Fragment, { children: sortedEntries.map(([key, item]) => {
|
|
3586
|
+
const hasChildren = Object.keys(item.children || {}).length > 0;
|
|
3587
|
+
if (level === 0) {
|
|
3588
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
3589
|
+
/* @__PURE__ */ jsx("div", { class: "group-label", children: key }),
|
|
3590
|
+
hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", style: "margin-left: 0", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3591
|
+
] }, key);
|
|
3592
|
+
}
|
|
3593
|
+
const isLeaf = item.isLeaf;
|
|
3594
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
3595
|
+
isLeaf ? /* @__PURE__ */ jsx(LeafNode, { item, label: key, disableSourceView }) : /* @__PURE__ */ jsx("div", { class: "tree-item", style: "color: var(--text-muted)", children: /* @__PURE__ */ jsx("span", { class: "tree-label", children: key }) }),
|
|
3596
|
+
hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3597
|
+
] }, key);
|
|
3598
|
+
}) });
|
|
3599
|
+
}
|
|
3600
|
+
function LeafNode({ item, label, disableSourceView }) {
|
|
3601
|
+
const isWarning = item.data?.op?.["x-warning"];
|
|
3602
|
+
const opId = item.data?.name;
|
|
3603
|
+
const sourceInfo = item.data?.op?.["x-source-info"];
|
|
3604
|
+
let content;
|
|
3605
|
+
if (isWarning) {
|
|
3606
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3607
|
+
/* @__PURE__ */ jsx("span", { style: "margin-right: 6px;", children: "⚠️" }),
|
|
3608
|
+
/* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
|
|
3609
|
+
] });
|
|
3610
|
+
} else {
|
|
3611
|
+
const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
|
|
3612
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3613
|
+
/* @__PURE__ */ jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
|
|
3614
|
+
/* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
|
|
3615
|
+
] });
|
|
3616
|
+
}
|
|
3617
|
+
return /* @__PURE__ */ jsxs("div", { class: "tree-item", "data-event": opId, style: isWarning ? "color: #fbbf24" : "", children: [
|
|
3618
|
+
content,
|
|
3619
|
+
sourceInfo && !disableSourceView && /* @__PURE__ */ jsx(
|
|
3620
|
+
"a",
|
|
3621
|
+
{
|
|
3622
|
+
href: `vscode://file/${sourceInfo.file}:${sourceInfo.line}`,
|
|
3623
|
+
class: "source-link",
|
|
3624
|
+
onClick: (e) => {
|
|
3625
|
+
e.stopPropagation();
|
|
3626
|
+
},
|
|
3627
|
+
title: `${sourceInfo.file}:${sourceInfo.line}`,
|
|
3628
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", style: "display:block", children: [
|
|
3629
|
+
/* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
|
|
3630
|
+
/* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
|
|
3631
|
+
] })
|
|
3632
|
+
}
|
|
3633
|
+
)
|
|
3634
|
+
] });
|
|
3635
|
+
}
|
|
3636
|
+
function MainContent() {
|
|
3637
|
+
return /* @__PURE__ */ jsxs("div", { id: "main-wrapper", style: "flex: 1; min-width: 0; position: relative; overflow: hidden;", children: [
|
|
3638
|
+
/* @__PURE__ */ jsx("button", { id: "btn-expand-nav", class: "btn-icon floating-toggle left", title: "Expand Sidebar", style: "display:none;", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
|
|
3639
|
+
/* @__PURE__ */ jsx("button", { id: "btn-expand-console", class: "btn-icon floating-toggle right", title: "Expand Console", style: "display:none;", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) }),
|
|
3640
|
+
/* @__PURE__ */ jsx("main", { class: "main-content scroller", id: "doc-panel", style: "height: 100%;", children: /* @__PURE__ */ jsxs("div", { class: "empty-state", children: [
|
|
3641
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", children: [
|
|
3642
|
+
/* @__PURE__ */ jsx("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
|
|
3643
|
+
/* @__PURE__ */ 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" })
|
|
3644
|
+
] }),
|
|
3645
|
+
/* @__PURE__ */ jsx("h3", { children: "Select an event to view details" })
|
|
3646
|
+
] }) })
|
|
3647
|
+
] });
|
|
3648
|
+
}
|
|
3649
|
+
function ConsolePanel({ serverUrl }) {
|
|
3650
|
+
return /* @__PURE__ */ jsxs("div", { class: "console-panel", id: "console-panel", children: [
|
|
3651
|
+
/* @__PURE__ */ jsxs("div", { class: "console-header", children: [
|
|
3652
|
+
/* @__PURE__ */ jsxs("div", { style: "display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;", children: [
|
|
3653
|
+
/* @__PURE__ */ jsx("h3", { style: "margin:0; font-size:1rem;", children: "Console" }),
|
|
3654
|
+
/* @__PURE__ */ jsxs("div", { style: "display:flex; gap: 4px;", children: [
|
|
3655
|
+
/* @__PURE__ */ jsx("button", { id: "btn-maximize-console", class: "btn-icon", title: "Maximize Console", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }) }) }),
|
|
3656
|
+
/* @__PURE__ */ jsx("button", { id: "btn-collapse-console", class: "btn-icon", title: "Collapse Console", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) })
|
|
3657
|
+
] })
|
|
3658
|
+
] }),
|
|
3659
|
+
/* @__PURE__ */ jsxs("div", { class: "connection-bar", children: [
|
|
3660
|
+
/* @__PURE__ */ jsxs("select", { id: "protocol", children: [
|
|
3661
|
+
/* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
|
|
3662
|
+
/* @__PURE__ */ jsx("option", { value: "wss", children: "WSS" }),
|
|
3663
|
+
/* @__PURE__ */ jsx("option", { value: "socket.io", children: "Socket.IO" })
|
|
3664
|
+
] }),
|
|
3665
|
+
/* @__PURE__ */ jsx("div", { style: "width: 1px; background: rgba(255,255,255,0.1); margin: 2px 0;" }),
|
|
3666
|
+
/* @__PURE__ */ jsx("input", { type: "text", id: "url", value: serverUrl })
|
|
3667
|
+
] }),
|
|
3668
|
+
/* @__PURE__ */ jsxs("div", { style: "display: grid; grid-template-columns: 1fr auto; gap: 8px;", children: [
|
|
3669
|
+
/* @__PURE__ */ jsx("button", { id: "connect-btn", class: "btn", children: "Connect" }),
|
|
3670
|
+
/* @__PURE__ */ jsx("button", { id: "clear-logs-btn", class: "btn secondary", title: "Clear Logs", children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ 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" }) }) })
|
|
3671
|
+
] }),
|
|
3672
|
+
/* @__PURE__ */ jsxs("div", { class: "status-indicator", children: [
|
|
3673
|
+
/* @__PURE__ */ jsx("div", { id: "status-dot", class: "dot" }),
|
|
3674
|
+
/* @__PURE__ */ jsx("span", { id: "connection-status", children: "Disconnected" })
|
|
3675
|
+
] })
|
|
3676
|
+
] }),
|
|
3677
|
+
/* @__PURE__ */ jsx("div", { class: "logs-container scroller", id: "logs", children: /* @__PURE__ */ jsx("div", { class: "log-shim", id: "log-shim" }) }),
|
|
3678
|
+
/* @__PURE__ */ jsxs("div", { class: "compose-area", children: [
|
|
3679
|
+
/* @__PURE__ */ jsxs("div", { class: "compose-header", children: [
|
|
3680
|
+
/* @__PURE__ */ jsx("span", { children: "Payload" }),
|
|
3681
|
+
/* @__PURE__ */ jsx("span", { id: "target-event", style: "color: var(--primary);", children: "--" })
|
|
3682
|
+
] }),
|
|
3683
|
+
/* @__PURE__ */ jsx("div", { id: "editor-container" }),
|
|
3684
|
+
/* @__PURE__ */ jsx("div", { class: "send-bar", children: /* @__PURE__ */ jsx("button", { id: "send-btn", class: "btn", children: "Send Message" }) })
|
|
3685
|
+
] })
|
|
3686
|
+
] });
|
|
3687
|
+
}
|
|
3688
|
+
function buildNavTree(spec) {
|
|
3689
|
+
if (!spec || !spec.channels) return { children: {} };
|
|
3690
|
+
const root = { children: {} };
|
|
3691
|
+
Object.keys(spec.channels).forEach((name) => {
|
|
3692
|
+
const ch = spec.channels[name];
|
|
3693
|
+
const op = ch.publish || ch.subscribe;
|
|
3694
|
+
const type = ch.publish ? "publish" : "subscribe";
|
|
3695
|
+
const tag = op.tags && op.tags.length > 0 ? op.tags[0].name : "General";
|
|
3696
|
+
if (!root.children[tag]) root.children[tag] = { children: {} };
|
|
3697
|
+
const parts = name.split(/[\.\/]/);
|
|
3698
|
+
let current = root.children[tag];
|
|
3699
|
+
parts.forEach((part, i) => {
|
|
3700
|
+
if (!current.children[part]) current.children[part] = { children: {} };
|
|
3701
|
+
current = current.children[part];
|
|
3702
|
+
if (i === parts.length - 1) {
|
|
3703
|
+
current.isLeaf = true;
|
|
3704
|
+
current.data = { name, op, type };
|
|
3705
|
+
}
|
|
3706
|
+
});
|
|
3707
|
+
});
|
|
3708
|
+
return root;
|
|
3709
|
+
}
|
|
3710
|
+
async function getAstRoutes(applications) {
|
|
3711
|
+
const astRoutes = [];
|
|
3712
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
3713
|
+
if (seen.has(app.name)) return [];
|
|
3714
|
+
const newSeen = new Set(seen);
|
|
3715
|
+
newSeen.add(app.name);
|
|
3716
|
+
const expanded = [];
|
|
3717
|
+
for (const route of app.routes) {
|
|
3718
|
+
expanded.push({
|
|
3719
|
+
...route,
|
|
3720
|
+
// For events, path is the event name
|
|
3721
|
+
path: route.path.startsWith("/") ? route.path.slice(1) : route.path
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
if (app.mounted) {
|
|
3725
|
+
for (const mount of app.mounted) {
|
|
3726
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
3727
|
+
if (targetApp) {
|
|
3728
|
+
expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
return expanded;
|
|
3733
|
+
};
|
|
3734
|
+
applications.forEach((app) => {
|
|
3735
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
3736
|
+
});
|
|
3737
|
+
return astRoutes;
|
|
3738
|
+
}
|
|
3739
|
+
async function generateAsyncApi(rootRouter, options = {}) {
|
|
3740
|
+
const channels = {};
|
|
3741
|
+
let astRoutes = [];
|
|
3742
|
+
try {
|
|
3743
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
|
|
3744
|
+
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
3745
|
+
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
3746
|
+
const { applications } = await analyzer.analyze();
|
|
3747
|
+
astRoutes = await getAstRoutes(applications);
|
|
3748
|
+
} catch (e) {
|
|
3749
|
+
}
|
|
3750
|
+
const matchedAstRoutes = /* @__PURE__ */ new Set();
|
|
3751
|
+
const collect = async (router, prefix = "") => {
|
|
3752
|
+
const eventHandlers = router.getEventHandlers();
|
|
3753
|
+
let routerTag = "Other";
|
|
3754
|
+
if (router[$isApplication]) {
|
|
3755
|
+
routerTag = "Application";
|
|
3756
|
+
} else if (router.constructor.name && router.constructor.name !== "ShokupanRouter") {
|
|
3757
|
+
routerTag = router.constructor.name;
|
|
3758
|
+
} else {
|
|
3759
|
+
routerTag = router[$mountPath] || "Router";
|
|
3760
|
+
}
|
|
3761
|
+
if (eventHandlers) {
|
|
3762
|
+
for (const [eventName, handlers] of eventHandlers.entries()) {
|
|
3763
|
+
for (const handler of handlers) {
|
|
3764
|
+
const userSpec = handler.spec;
|
|
3765
|
+
let tags = userSpec?.tags;
|
|
3766
|
+
if (!tags && routerTag) {
|
|
3767
|
+
tags = [{ name: routerTag }];
|
|
3768
|
+
}
|
|
3769
|
+
let astMatch = astRoutes.find(
|
|
3770
|
+
(r) => (r.method === "EVENT" || r.method === "ON") && r.path === eventName
|
|
3771
|
+
);
|
|
3772
|
+
if (!astMatch) {
|
|
3773
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3774
|
+
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
3775
|
+
const normalize = (s) => stripComments(s).replace(/\s+/g, "");
|
|
3776
|
+
const runtimeHandlerSrc = normalize(runtimeSource);
|
|
3777
|
+
const eventRoutes = astRoutes.filter((r) => r.method === "EVENT" || r.method === "ON");
|
|
3778
|
+
astMatch = eventRoutes.find((r) => {
|
|
3779
|
+
const astHandlerSrc = normalize(r.handlerSource || r.handlerName || "");
|
|
3780
|
+
if (!astHandlerSrc || astHandlerSrc.length < 5) return false;
|
|
3781
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(normalize(r.handlerSource).substring(0, 50));
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
if (astMatch) matchedAstRoutes.add(astMatch);
|
|
3785
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3786
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3787
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3788
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3789
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3790
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3791
|
+
} : void 0;
|
|
3792
|
+
if (!channels[eventName]) {
|
|
3793
|
+
channels[eventName] = {
|
|
3794
|
+
publish: {
|
|
3795
|
+
operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
|
|
3796
|
+
tags,
|
|
3797
|
+
message: {
|
|
3798
|
+
payload: { type: "object" },
|
|
3799
|
+
...userSpec?.message ? userSpec.message : {}
|
|
3800
|
+
},
|
|
3801
|
+
...userSpec?.type === "publish" ? userSpec : {},
|
|
3802
|
+
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
3803
|
+
"x-shokupan-source": sourceInfo
|
|
3804
|
+
// Simplified
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
|
|
3808
|
+
if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
|
|
3809
|
+
} else {
|
|
3810
|
+
if (sourceInfo) {
|
|
3811
|
+
if (!channels[eventName].publish["x-source-info"]) {
|
|
3812
|
+
channels[eventName].publish["x-source-info"] = [];
|
|
3813
|
+
}
|
|
3814
|
+
const exists = channels[eventName].publish["x-source-info"].some(
|
|
3815
|
+
(s) => s.file === sourceInfo.file && s.line === sourceInfo.line
|
|
3816
|
+
);
|
|
3817
|
+
if (!exists) {
|
|
3818
|
+
channels[eventName].publish["x-source-info"].push(sourceInfo);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
let emits = astMatch?.emits || [];
|
|
3823
|
+
for (const emit of emits) {
|
|
3824
|
+
if (emit.event === "__DYNAMIC_EMIT__") {
|
|
3825
|
+
const warningKey = `${eventName}/Dynamic Emit`;
|
|
3826
|
+
channels[warningKey] = {
|
|
3827
|
+
subscribe: {
|
|
3828
|
+
operationId: `dynamicEmitWarning${eventName}`,
|
|
3829
|
+
summary: "Dynamic Emit Detected",
|
|
3830
|
+
description: "This handler emits an event with a dynamic name that could not be determined statically.",
|
|
3831
|
+
tags,
|
|
3832
|
+
"x-warning": true,
|
|
3833
|
+
"x-source-info": {
|
|
3834
|
+
file: astMatch?.sourceContext?.file,
|
|
3835
|
+
line: emit.location?.startLine,
|
|
3836
|
+
startLine: emit.location?.startLine,
|
|
3837
|
+
endLine: emit.location?.endLine,
|
|
3838
|
+
highlightLines: emit.location ? [emit.location.startLine, emit.location.endLine] : void 0
|
|
3839
|
+
},
|
|
3840
|
+
"x-shokupan-source": {
|
|
3841
|
+
file: astMatch?.sourceContext?.file,
|
|
3842
|
+
line: emit.location?.startLine
|
|
3843
|
+
},
|
|
3844
|
+
message: { payload: { type: "object" } }
|
|
3845
|
+
}
|
|
3846
|
+
};
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
const emitStart = emit.location?.startLine;
|
|
3850
|
+
const emitEnd = emit.location?.endLine;
|
|
3851
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3852
|
+
file: sourceInfo.file,
|
|
3853
|
+
line: emitStart,
|
|
3854
|
+
startLine: emitStart,
|
|
3855
|
+
endLine: emitEnd,
|
|
3856
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3857
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3858
|
+
} : void 0;
|
|
3859
|
+
if (!channels[emit.event]) {
|
|
3860
|
+
channels[emit.event] = {
|
|
3861
|
+
subscribe: {
|
|
3862
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3863
|
+
tags,
|
|
3864
|
+
message: {
|
|
3865
|
+
payload: emit.payload || { type: "object" }
|
|
3866
|
+
},
|
|
3867
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3868
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3869
|
+
file: sourceInfo.file,
|
|
3870
|
+
line: emitStart
|
|
3871
|
+
} : void 0
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
} else {
|
|
3875
|
+
if (newSourceInfo) {
|
|
3876
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3877
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3878
|
+
}
|
|
3879
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3880
|
+
const exists = existing.some(
|
|
3881
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3882
|
+
);
|
|
3883
|
+
if (!exists) {
|
|
3884
|
+
existing.push(newSourceInfo);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
const httpRoutes = router[$routes];
|
|
3893
|
+
if (httpRoutes) {
|
|
3894
|
+
for (const route of httpRoutes) {
|
|
3895
|
+
const handler = route.handler;
|
|
3896
|
+
let tags = route.handlerSpec?.tags;
|
|
3897
|
+
if (!tags && routerTag) {
|
|
3898
|
+
tags = [{ name: routerTag }];
|
|
3899
|
+
}
|
|
3900
|
+
const methodUpper = route.method.toUpperCase();
|
|
3901
|
+
let astMatch = astRoutes.find(
|
|
3902
|
+
(r) => r.method === methodUpper && (r.path === route.path || r.path === "/" + route.path)
|
|
3903
|
+
);
|
|
3904
|
+
if (!astMatch) {
|
|
3905
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3906
|
+
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
3907
|
+
const sameMethodRoutes = astRoutes.filter((r) => r.method === methodUpper);
|
|
3908
|
+
astMatch = sameMethodRoutes.find((r) => {
|
|
3909
|
+
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
3910
|
+
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
3911
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3915
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3916
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3917
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3918
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3919
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3920
|
+
} : void 0;
|
|
3921
|
+
let emits = astMatch?.emits || [];
|
|
3922
|
+
for (const emit of emits) {
|
|
3923
|
+
const emitStart = emit.location?.startLine;
|
|
3924
|
+
const emitEnd = emit.location?.endLine;
|
|
3925
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3926
|
+
file: sourceInfo.file,
|
|
3927
|
+
line: emitStart,
|
|
3928
|
+
startLine: emitStart,
|
|
3929
|
+
endLine: emitEnd,
|
|
3930
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3931
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3932
|
+
} : void 0;
|
|
3933
|
+
if (!channels[emit.event]) {
|
|
3934
|
+
channels[emit.event] = {
|
|
3935
|
+
subscribe: {
|
|
3936
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3937
|
+
tags,
|
|
3938
|
+
message: {
|
|
3939
|
+
payload: emit.payload || { type: "object" }
|
|
3940
|
+
},
|
|
3941
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3942
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3943
|
+
file: sourceInfo.file,
|
|
3944
|
+
line: emitStart
|
|
3945
|
+
} : void 0
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3948
|
+
} else {
|
|
3949
|
+
if (newSourceInfo) {
|
|
3950
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3951
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3952
|
+
}
|
|
3953
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3954
|
+
const exists = existing.some(
|
|
3955
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3956
|
+
);
|
|
3957
|
+
if (!exists) {
|
|
3958
|
+
existing.push(newSourceInfo);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
const childRouters = router[$childRouters];
|
|
3966
|
+
for (const child of childRouters) {
|
|
3967
|
+
await collect(child);
|
|
3968
|
+
}
|
|
3969
|
+
};
|
|
3970
|
+
await collect(rootRouter);
|
|
3971
|
+
const dynamicEvents = astRoutes.filter((r) => r.path === "__DYNAMIC_EVENT__" && !matchedAstRoutes.has(r));
|
|
3972
|
+
dynamicEvents.forEach((r, i) => {
|
|
3973
|
+
let prefix = "Anonymous";
|
|
3974
|
+
if (r.handlerName && !r.handlerName.includes("=>") && !r.handlerName.includes("{")) {
|
|
3975
|
+
const parts = r.handlerName.split(".");
|
|
3976
|
+
if (parts.length > 0) prefix = parts[0];
|
|
3977
|
+
}
|
|
3978
|
+
const key = `${prefix}.Dynamic Event ${i + 1}`;
|
|
3979
|
+
channels[key] = {
|
|
3980
|
+
publish: {
|
|
3981
|
+
operationId: `dynamicEventWarning${i}`,
|
|
3982
|
+
summary: "Dynamic Event Detected",
|
|
3983
|
+
description: `A dynamic event listener was detected in your source code but the event name could not be determined statically.`,
|
|
3984
|
+
tags: [{ name: "Warnings" }],
|
|
3985
|
+
"x-warning": true,
|
|
3986
|
+
"x-source-info": {
|
|
3987
|
+
file: r.sourceContext?.file,
|
|
3988
|
+
line: r.sourceContext?.startLine,
|
|
3989
|
+
startLine: r.sourceContext?.startLine,
|
|
3990
|
+
endLine: r.sourceContext?.endLine,
|
|
3991
|
+
highlightLines: r.sourceContext ? [r.sourceContext.startLine, r.sourceContext.endLine] : void 0
|
|
3992
|
+
},
|
|
3993
|
+
"x-shokupan-source": {
|
|
3994
|
+
file: r.sourceContext?.file,
|
|
3995
|
+
line: r.sourceContext?.startLine
|
|
3996
|
+
},
|
|
3997
|
+
message: { payload: { type: "object" } }
|
|
3998
|
+
}
|
|
3999
|
+
};
|
|
4000
|
+
});
|
|
4001
|
+
return {
|
|
4002
|
+
asyncapi: "3.0.0",
|
|
4003
|
+
info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
|
|
4004
|
+
channels
|
|
4005
|
+
};
|
|
4006
|
+
}
|
|
4007
|
+
const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
4008
|
+
__proto__: null,
|
|
4009
|
+
generateAsyncApi
|
|
4010
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
4011
|
+
class AsyncApiPlugin extends ShokupanRouter {
|
|
4012
|
+
constructor(pluginOptions = {}) {
|
|
4013
|
+
super({ renderer: renderToString });
|
|
4014
|
+
this.pluginOptions = pluginOptions;
|
|
4015
|
+
this.pluginOptions.path ??= "/asyncapi";
|
|
4016
|
+
this.init();
|
|
4017
|
+
}
|
|
4018
|
+
static getBasePath() {
|
|
4019
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
4020
|
+
if (dir.endsWith("dist")) {
|
|
4021
|
+
return dir + "/plugins/application/asyncapi";
|
|
4022
|
+
}
|
|
4023
|
+
return dir;
|
|
4024
|
+
}
|
|
4025
|
+
onInit(app, options) {
|
|
4026
|
+
const path = this.pluginOptions.path || options?.path || "/asyncapi";
|
|
4027
|
+
app.mount(path, this);
|
|
4028
|
+
if (app.applicationConfig.enableAsyncApiGen !== true) {
|
|
4029
|
+
console.warn("AsyncApiPlugin: enableAsyncApiGen is disabled. AsyncApiPlugin will not generate spec.");
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
init() {
|
|
4033
|
+
const serveFile = async (ctx, file, type) => {
|
|
4034
|
+
const content = await readFile(join$1(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
|
|
4035
|
+
ctx.set("Content-Type", type);
|
|
4036
|
+
return ctx.send(content);
|
|
4037
|
+
};
|
|
4038
|
+
this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
|
|
4039
|
+
this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
|
|
4040
|
+
this.get("/asyncapi-client.mjs", (ctx) => serveFile(ctx, "asyncapi-client.mjs", "application/javascript"));
|
|
4041
|
+
this.get("/", async (ctx) => {
|
|
4042
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4043
|
+
if (!spec) {
|
|
4044
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4045
|
+
}
|
|
4046
|
+
if (this.pluginOptions.spec) {
|
|
4047
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4048
|
+
}
|
|
4049
|
+
const serverUrl = `${ctx.hostname}:${ctx.app?.applicationConfig.port}`;
|
|
4050
|
+
const base = this.pluginOptions.path;
|
|
4051
|
+
const disableSourceView = this.pluginOptions.disableSourceView;
|
|
4052
|
+
const navTree = buildNavTree(spec);
|
|
4053
|
+
return ctx.jsx(AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }));
|
|
4054
|
+
});
|
|
4055
|
+
this.get("/json", async (ctx) => {
|
|
4056
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4057
|
+
if (!spec) {
|
|
4058
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4059
|
+
}
|
|
4060
|
+
if (this.pluginOptions.spec) {
|
|
4061
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4062
|
+
}
|
|
4063
|
+
return ctx.json(spec);
|
|
4064
|
+
});
|
|
4065
|
+
this.get("/_code", async (ctx) => {
|
|
4066
|
+
const file = ctx.query["file"];
|
|
4067
|
+
if (!file || typeof file !== "string") {
|
|
4068
|
+
return ctx.text("Missing file parameter", 400);
|
|
4069
|
+
}
|
|
4070
|
+
try {
|
|
4071
|
+
const content = await readFile(file, "utf8");
|
|
4072
|
+
return ctx.text(content);
|
|
4073
|
+
} catch (e) {
|
|
4074
|
+
return ctx.text("File not found: " + e.message, 404);
|
|
4075
|
+
}
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
2954
4079
|
class AuthPlugin extends ShokupanRouter {
|
|
2955
4080
|
constructor(authConfig) {
|
|
2956
4081
|
super();
|
|
2957
4082
|
this.authConfig = authConfig;
|
|
2958
4083
|
this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
|
|
2959
|
-
this.init();
|
|
2960
4084
|
}
|
|
2961
4085
|
secret;
|
|
2962
|
-
|
|
4086
|
+
arctic;
|
|
4087
|
+
jose;
|
|
4088
|
+
async onInit(app, options) {
|
|
4089
|
+
this.arctic = await import("arctic");
|
|
4090
|
+
this.jose = await import("jose");
|
|
4091
|
+
this.init();
|
|
2963
4092
|
if (options?.path) {
|
|
2964
4093
|
app.mount(options.path, this);
|
|
2965
4094
|
} else {
|
|
@@ -2967,6 +4096,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2967
4096
|
}
|
|
2968
4097
|
}
|
|
2969
4098
|
getProviderInstance(name, p) {
|
|
4099
|
+
const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
2970
4100
|
switch (name) {
|
|
2971
4101
|
case "github":
|
|
2972
4102
|
return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
|
|
@@ -2994,7 +4124,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2994
4124
|
}
|
|
2995
4125
|
async createSession(user, ctx) {
|
|
2996
4126
|
const alg = "HS256";
|
|
2997
|
-
const jwt = await new jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
4127
|
+
const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
2998
4128
|
const opts = this.authConfig.cookieOptions || {};
|
|
2999
4129
|
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
3000
4130
|
if (opts.secure) cookie += "; Secure";
|
|
@@ -3004,6 +4134,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3004
4134
|
return jwt;
|
|
3005
4135
|
}
|
|
3006
4136
|
init() {
|
|
4137
|
+
const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3007
4138
|
const providerEntries = Object.entries(this.authConfig.providers);
|
|
3008
4139
|
for (let i = 0; i < providerEntries.length; i++) {
|
|
3009
4140
|
const [providerName, providerConfig] = providerEntries[i];
|
|
@@ -3135,7 +4266,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3135
4266
|
};
|
|
3136
4267
|
} else if (provider === "apple") {
|
|
3137
4268
|
if (idToken) {
|
|
3138
|
-
const payload = jose.decodeJwt(idToken);
|
|
4269
|
+
const payload = this.jose.decodeJwt(idToken);
|
|
3139
4270
|
user = {
|
|
3140
4271
|
id: payload.sub,
|
|
3141
4272
|
email: payload["email"],
|
|
@@ -3166,6 +4297,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3166
4297
|
*/
|
|
3167
4298
|
getMiddleware() {
|
|
3168
4299
|
return async (ctx, next) => {
|
|
4300
|
+
if (!this.jose) {
|
|
4301
|
+
this.jose = await import("jose");
|
|
4302
|
+
}
|
|
3169
4303
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
3170
4304
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
3171
4305
|
if (!token) {
|
|
@@ -3174,7 +4308,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3174
4308
|
}
|
|
3175
4309
|
if (token) {
|
|
3176
4310
|
try {
|
|
3177
|
-
const { payload } = await jose.jwtVerify(token, this.secret);
|
|
4311
|
+
const { payload } = await this.jose.jwtVerify(token, this.secret);
|
|
3178
4312
|
ctx.user = payload;
|
|
3179
4313
|
} catch {
|
|
3180
4314
|
}
|
|
@@ -3291,32 +4425,956 @@ class ClusterPlugin {
|
|
|
3291
4425
|
}
|
|
3292
4426
|
}
|
|
3293
4427
|
}
|
|
3294
|
-
|
|
4428
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
|
|
4429
|
+
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
4430
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
4431
|
+
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
4432
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
4433
|
+
/* @__PURE__ */ jsx("title", { children: "Shokupan Debug Dashboard" }),
|
|
4434
|
+
/* @__PURE__ */ jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
|
|
4435
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
|
|
4436
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
4437
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/styles.css` }),
|
|
4438
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/reactflow.css` }),
|
|
4439
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
|
|
4440
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
|
|
4441
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
|
|
4442
|
+
/* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
|
|
4443
|
+
] }),
|
|
4444
|
+
/* @__PURE__ */ jsxs("body", { children: [
|
|
4445
|
+
/* @__PURE__ */ jsxs("div", { class: "container", children: [
|
|
4446
|
+
/* @__PURE__ */ jsxs("header", { children: [
|
|
4447
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4448
|
+
/* @__PURE__ */ jsx("h1", { children: "Dashboard" }),
|
|
4449
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary)", children: [
|
|
4450
|
+
"Uptime: ",
|
|
4451
|
+
/* @__PURE__ */ jsx("span", { id: "uptime", children: uptime })
|
|
4452
|
+
] })
|
|
4453
|
+
] }),
|
|
4454
|
+
/* @__PURE__ */ jsxs("div", { class: "tabs", children: [
|
|
4455
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
|
|
4456
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
|
|
4457
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
|
|
4458
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
|
|
4459
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
|
|
4460
|
+
integrations.scalar && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
|
|
4461
|
+
integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
|
|
4462
|
+
] })
|
|
4463
|
+
] }),
|
|
4464
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
|
|
4465
|
+
/* @__PURE__ */ jsx(MetricsGrid, { metrics }),
|
|
4466
|
+
/* @__PURE__ */ jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
|
|
4467
|
+
/* @__PURE__ */ jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ 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: [
|
|
4468
|
+
/* @__PURE__ */ jsx("option", { value: "1m", children: "1 Minute" }),
|
|
4469
|
+
/* @__PURE__ */ jsx("option", { value: "5m", children: "5 Minutes" }),
|
|
4470
|
+
/* @__PURE__ */ jsx("option", { value: "30m", children: "30 Minutes" }),
|
|
4471
|
+
/* @__PURE__ */ jsx("option", { value: "1h", children: "1 Hour" }),
|
|
4472
|
+
/* @__PURE__ */ jsx("option", { value: "2h", children: "2 Hours" }),
|
|
4473
|
+
/* @__PURE__ */ jsx("option", { value: "6h", children: "6 Hours" }),
|
|
4474
|
+
/* @__PURE__ */ jsx("option", { value: "12h", children: "12 Hours" }),
|
|
4475
|
+
/* @__PURE__ */ jsx("option", { value: "1d", children: "1 Day" }),
|
|
4476
|
+
/* @__PURE__ */ jsx("option", { value: "3d", children: "3 Days" }),
|
|
4477
|
+
/* @__PURE__ */ jsx("option", { value: "7d", children: "7 Days" }),
|
|
4478
|
+
/* @__PURE__ */ jsx("option", { value: "30d", children: "30 Days" })
|
|
4479
|
+
] }) }),
|
|
4480
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
4481
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
|
|
4482
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
|
|
4483
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
|
|
4484
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
|
|
4485
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
|
|
4486
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
|
|
4487
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
|
|
4488
|
+
] }),
|
|
4489
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
|
|
4490
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
4491
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
4492
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
4493
|
+
/* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
4494
|
+
/* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
4495
|
+
] }),
|
|
4496
|
+
/* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4497
|
+
] })
|
|
4498
|
+
] }),
|
|
4499
|
+
/* @__PURE__ */ jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
|
|
4500
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Component Registry" }),
|
|
4501
|
+
/* @__PURE__ */ jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
|
|
4502
|
+
] }) }),
|
|
4503
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-graph", class: "tab-content", children: [
|
|
4504
|
+
/* @__PURE__ */ jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ 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);" }) }) }),
|
|
4505
|
+
/* @__PURE__ */ jsx("div", { id: "cy" })
|
|
4506
|
+
] }),
|
|
4507
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-requests", class: "tab-content", children: [
|
|
4508
|
+
/* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4509
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
|
|
4510
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ 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" }) })
|
|
4511
|
+
] }),
|
|
4512
|
+
/* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
|
|
4513
|
+
/* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
|
|
4514
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Request Details" }),
|
|
4515
|
+
/* @__PURE__ */ jsx("div", { id: "request-details-content" }),
|
|
4516
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
4517
|
+
/* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
|
|
4518
|
+
] })
|
|
4519
|
+
] }),
|
|
4520
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-failures", class: "tab-content", children: [
|
|
4521
|
+
/* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4522
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
|
|
4523
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4524
|
+
/* @__PURE__ */ 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" }),
|
|
4525
|
+
/* @__PURE__ */ 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" })
|
|
4526
|
+
] })
|
|
4527
|
+
] }),
|
|
4528
|
+
/* @__PURE__ */ jsx("div", { id: "failures-table-container" })
|
|
4529
|
+
] }),
|
|
4530
|
+
integrations.scalar && /* @__PURE__ */ jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
4531
|
+
integrations.asyncapi && /* @__PURE__ */ jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
|
|
4532
|
+
] }),
|
|
4533
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
4534
|
+
__html: `
|
|
4535
|
+
// Injected function from server config
|
|
4536
|
+
const getRequestHeaders = ${getRequestHeadersSource};
|
|
4537
|
+
`
|
|
4538
|
+
} }),
|
|
4539
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/poll.js` }),
|
|
4540
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
|
|
4541
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
|
|
4542
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
|
|
4543
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/registry.js` }),
|
|
4544
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/failures.js` }),
|
|
4545
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/requests.js` }),
|
|
4546
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/tabs.js` })
|
|
4547
|
+
] })
|
|
4548
|
+
] });
|
|
4549
|
+
}
|
|
4550
|
+
function MetricsGrid({ metrics }) {
|
|
4551
|
+
const total = metrics.totalRequests;
|
|
4552
|
+
const active = metrics.activeRequests;
|
|
4553
|
+
const finished = total - active;
|
|
4554
|
+
const successRate = finished ? Math.round(metrics.successfulRequests / finished * 100) : 100;
|
|
4555
|
+
const failRate = finished ? Math.round(metrics.failedRequests / finished * 100) : 0;
|
|
4556
|
+
return /* @__PURE__ */ jsxs("div", { class: "metrics-grid", children: [
|
|
4557
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4558
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Total Requests" }),
|
|
4559
|
+
/* @__PURE__ */ jsx("div", { class: "card-value", id: "total-requests", children: metrics.totalRequests })
|
|
4560
|
+
] }),
|
|
4561
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4562
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Active Requests" }),
|
|
4563
|
+
/* @__PURE__ */ jsx("div", { class: "card-value", style: "color: var(--accent)", id: "active-requests", children: metrics.activeRequests })
|
|
4564
|
+
] }),
|
|
4565
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4566
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Success Rate" }),
|
|
4567
|
+
/* @__PURE__ */ jsx("div", { class: "card-value text-success", children: /* @__PURE__ */ jsxs("span", { id: "success-rate", children: [
|
|
4568
|
+
successRate,
|
|
4569
|
+
"%"
|
|
4570
|
+
] }) }),
|
|
4571
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4572
|
+
/* @__PURE__ */ jsx("span", { id: "successful-requests", children: metrics.successfulRequests }),
|
|
4573
|
+
" successful"
|
|
4574
|
+
] })
|
|
4575
|
+
] }),
|
|
4576
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4577
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Fail Rate" }),
|
|
4578
|
+
/* @__PURE__ */ jsx("div", { class: "card-value text-error", children: /* @__PURE__ */ jsxs("span", { id: "fail-rate", children: [
|
|
4579
|
+
failRate,
|
|
4580
|
+
"%"
|
|
4581
|
+
] }) }),
|
|
4582
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4583
|
+
/* @__PURE__ */ jsx("span", { id: "failed-requests", children: metrics.failedRequests }),
|
|
4584
|
+
" failed"
|
|
4585
|
+
] })
|
|
4586
|
+
] }),
|
|
4587
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4588
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Avg Latency" }),
|
|
4589
|
+
/* @__PURE__ */ jsxs("div", { class: "card-value", children: [
|
|
4590
|
+
/* @__PURE__ */ jsx("span", { id: "avg-latency", children: metrics.averageTotalTime_ms.toFixed(2) }),
|
|
4591
|
+
" ",
|
|
4592
|
+
/* @__PURE__ */ jsx("span", { style: "font-size: 1rem; color: var(--text-secondary)", children: "ms" })
|
|
4593
|
+
] })
|
|
4594
|
+
] })
|
|
4595
|
+
] });
|
|
4596
|
+
}
|
|
4597
|
+
function ChartCard({ title, id }) {
|
|
4598
|
+
return /* @__PURE__ */ jsxs("div", { class: "card", style: "height: 300px;", children: [
|
|
4599
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
|
|
4600
|
+
/* @__PURE__ */ jsx("div", { class: "card-chart", children: /* @__PURE__ */ jsx("canvas", { id }) })
|
|
4601
|
+
] });
|
|
4602
|
+
}
|
|
4603
|
+
function Card({ title, contentId }) {
|
|
4604
|
+
return /* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4605
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
|
|
4606
|
+
/* @__PURE__ */ jsx("div", { id: contentId })
|
|
4607
|
+
] });
|
|
4608
|
+
}
|
|
4609
|
+
const INTERVALS = [
|
|
4610
|
+
{ label: "10s", ms: 10 * 1e3 },
|
|
4611
|
+
{ label: "1m", ms: 60 * 1e3 },
|
|
4612
|
+
{ label: "5m", ms: 5 * 60 * 1e3 },
|
|
4613
|
+
{ label: "1h", ms: 60 * 60 * 1e3 },
|
|
4614
|
+
{ label: "2h", ms: 2 * 60 * 60 * 1e3 },
|
|
4615
|
+
{ label: "6h", ms: 6 * 60 * 60 * 1e3 },
|
|
4616
|
+
{ label: "12h", ms: 12 * 60 * 60 * 1e3 },
|
|
4617
|
+
{ label: "1d", ms: 24 * 60 * 60 * 1e3 },
|
|
4618
|
+
{ label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
|
|
4619
|
+
{ label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
|
|
4620
|
+
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
4621
|
+
];
|
|
4622
|
+
class MetricsCollector {
|
|
4623
|
+
constructor(db) {
|
|
4624
|
+
this.db = db;
|
|
4625
|
+
this.eventLoopHistogram.enable();
|
|
4626
|
+
const now = Date.now();
|
|
4627
|
+
INTERVALS.forEach((int) => {
|
|
4628
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
4629
|
+
this.pendingDetails[int.label] = [];
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
currentIntervalStart = {};
|
|
4633
|
+
pendingDetails = {};
|
|
4634
|
+
eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
|
|
4635
|
+
timer = null;
|
|
4636
|
+
recordRequest(duration, isError) {
|
|
4637
|
+
INTERVALS.forEach((int) => {
|
|
4638
|
+
this.pendingDetails[int.label].push({ duration, isError });
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
alignTimestamp(ts, intervalMs) {
|
|
4642
|
+
return Math.floor(ts / intervalMs) * intervalMs;
|
|
4643
|
+
}
|
|
4644
|
+
async collect() {
|
|
4645
|
+
try {
|
|
4646
|
+
const now = Date.now();
|
|
4647
|
+
for (const int of INTERVALS) {
|
|
4648
|
+
const start = this.currentIntervalStart[int.label];
|
|
4649
|
+
if (now >= start + int.ms) {
|
|
4650
|
+
await this.flushInterval(int.label, start, int.ms);
|
|
4651
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
} catch (error) {
|
|
4655
|
+
console.error("[MetricsCollector] Error in collect():", error);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
async flushInterval(label, timestamp, durationMs) {
|
|
4659
|
+
const reqs = this.pendingDetails[label];
|
|
4660
|
+
this.pendingDetails[label] = [];
|
|
4661
|
+
if (reqs.length === 0) {
|
|
4662
|
+
return;
|
|
4663
|
+
}
|
|
4664
|
+
const totalReqs = reqs.length;
|
|
4665
|
+
const errorReqs = reqs.filter((r) => r.isError).length;
|
|
4666
|
+
const successReqs = totalReqs - errorReqs;
|
|
4667
|
+
const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
|
|
4668
|
+
const rps = totalReqs / (durationMs / 1e3);
|
|
4669
|
+
const sum = duratons.reduce((a, b) => a + b, 0);
|
|
4670
|
+
const avg = totalReqs > 0 ? sum / totalReqs : 0;
|
|
4671
|
+
const getP = (p) => {
|
|
4672
|
+
if (duratons.length === 0) return 0;
|
|
4673
|
+
const idx = Math.floor(duratons.length * p);
|
|
4674
|
+
return duratons[idx];
|
|
4675
|
+
};
|
|
4676
|
+
const metric = {
|
|
4677
|
+
timestamp,
|
|
4678
|
+
interval: label,
|
|
4679
|
+
cpu: os.loadavg()[0],
|
|
4680
|
+
// Using load avg for simplicity as per requirements (Load)
|
|
4681
|
+
load: os.loadavg(),
|
|
4682
|
+
memory: {
|
|
4683
|
+
used: process.memoryUsage().rss,
|
|
4684
|
+
total: os.totalmem(),
|
|
4685
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
4686
|
+
heapTotal: process.memoryUsage().heapTotal
|
|
4687
|
+
},
|
|
4688
|
+
eventLoopLatency: {
|
|
4689
|
+
min: this.eventLoopHistogram.min / 1e6,
|
|
4690
|
+
max: this.eventLoopHistogram.max / 1e6,
|
|
4691
|
+
mean: this.eventLoopHistogram.mean / 1e6,
|
|
4692
|
+
p50: this.eventLoopHistogram.percentile(50) / 1e6,
|
|
4693
|
+
p95: this.eventLoopHistogram.percentile(95) / 1e6,
|
|
4694
|
+
p99: this.eventLoopHistogram.percentile(99) / 1e6
|
|
4695
|
+
},
|
|
4696
|
+
requests: {
|
|
4697
|
+
total: totalReqs,
|
|
4698
|
+
rps,
|
|
4699
|
+
success: successReqs,
|
|
4700
|
+
error: errorReqs
|
|
4701
|
+
},
|
|
4702
|
+
responseTime: {
|
|
4703
|
+
min: duratons[0] || 0,
|
|
4704
|
+
max: duratons[duratons.length - 1] || 0,
|
|
4705
|
+
avg,
|
|
4706
|
+
p50: getP(0.5),
|
|
4707
|
+
p95: getP(0.95),
|
|
4708
|
+
p99: getP(0.99)
|
|
4709
|
+
}
|
|
4710
|
+
};
|
|
4711
|
+
try {
|
|
4712
|
+
const recordId = new RecordId("metrics", timestamp);
|
|
4713
|
+
await this.db.upsert(recordId, metric);
|
|
4714
|
+
const test = await this.db.select(recordId);
|
|
4715
|
+
const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
4716
|
+
} catch (e) {
|
|
4717
|
+
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
// Cleanup if needed
|
|
4721
|
+
stop() {
|
|
4722
|
+
if (this.timer) clearInterval(this.timer);
|
|
4723
|
+
this.eventLoopHistogram.disable();
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
class Collector {
|
|
4727
|
+
constructor(dashboard) {
|
|
4728
|
+
this.dashboard = dashboard;
|
|
4729
|
+
}
|
|
4730
|
+
currentNode;
|
|
4731
|
+
trackStep(id, type, duration, status, error) {
|
|
4732
|
+
if (!id) return;
|
|
4733
|
+
this.dashboard.recordNodeMetric(id, type, duration, status === "error");
|
|
4734
|
+
}
|
|
4735
|
+
trackEdge(fromId, toId) {
|
|
4736
|
+
if (!fromId || !toId) return;
|
|
4737
|
+
this.dashboard.recordEdgeMetric(fromId, toId);
|
|
4738
|
+
}
|
|
4739
|
+
setNode(id) {
|
|
4740
|
+
this.currentNode = id;
|
|
4741
|
+
}
|
|
4742
|
+
getCurrentNode() {
|
|
4743
|
+
return this.currentNode;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
class Dashboard {
|
|
4747
|
+
constructor(dashboardConfig = {}) {
|
|
4748
|
+
this.dashboardConfig = dashboardConfig;
|
|
4749
|
+
}
|
|
4750
|
+
[$appRoot];
|
|
4751
|
+
router = new ShokupanRouter({ renderer: renderToString });
|
|
4752
|
+
metrics = {
|
|
4753
|
+
totalRequests: 0,
|
|
4754
|
+
successfulRequests: 0,
|
|
4755
|
+
failedRequests: 0,
|
|
4756
|
+
activeRequests: 0,
|
|
4757
|
+
averageTotalTime_ms: 0,
|
|
4758
|
+
recentTimings: [],
|
|
4759
|
+
logs: [],
|
|
4760
|
+
rateLimitedCounts: {},
|
|
4761
|
+
nodeMetrics: {},
|
|
4762
|
+
edgeMetrics: {}
|
|
4763
|
+
};
|
|
4764
|
+
startTime = Date.now();
|
|
4765
|
+
instrumented = false;
|
|
4766
|
+
metricsCollector;
|
|
4767
|
+
get db() {
|
|
4768
|
+
return this[$appRoot].db;
|
|
4769
|
+
}
|
|
4770
|
+
// ShokupanPlugin interface implementation
|
|
4771
|
+
onInit(app, options) {
|
|
4772
|
+
this[$appRoot] = app;
|
|
4773
|
+
this.metricsCollector = new MetricsCollector(this.db);
|
|
4774
|
+
const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
4775
|
+
const hooks = this.getHooks();
|
|
4776
|
+
if (!app.middleware) {
|
|
4777
|
+
app.middleware = [];
|
|
4778
|
+
}
|
|
4779
|
+
const hooksMiddleware = async (ctx, next) => {
|
|
4780
|
+
if (hooks.onRequestStart) {
|
|
4781
|
+
await hooks.onRequestStart(ctx);
|
|
4782
|
+
}
|
|
4783
|
+
await next();
|
|
4784
|
+
if (hooks.onResponseEnd) {
|
|
4785
|
+
const effectiveResponse = ctx._finalResponse || ctx.response || {};
|
|
4786
|
+
await hooks.onResponseEnd(ctx, effectiveResponse);
|
|
4787
|
+
}
|
|
4788
|
+
};
|
|
4789
|
+
app.use(hooksMiddleware);
|
|
4790
|
+
app.mount(mountPath, this.router);
|
|
4791
|
+
this.setupRoutes();
|
|
4792
|
+
}
|
|
4793
|
+
detectIntegrations() {
|
|
4794
|
+
const integrations = {};
|
|
4795
|
+
const routers = this[$appRoot]?.[$childRouters] || [];
|
|
4796
|
+
const checkConfig = (key) => {
|
|
4797
|
+
const conf = this.dashboardConfig.integrations?.[key];
|
|
4798
|
+
if (conf === false) return { enabled: false };
|
|
4799
|
+
if (typeof conf === "object" && conf.path) return { enabled: true, path: conf.path };
|
|
4800
|
+
return { enabled: true };
|
|
4801
|
+
};
|
|
4802
|
+
const scalarConf = checkConfig("scalar");
|
|
4803
|
+
if (scalarConf.enabled) {
|
|
4804
|
+
if (scalarConf.path) {
|
|
4805
|
+
integrations["scalar"] = scalarConf.path;
|
|
4806
|
+
} else {
|
|
4807
|
+
const plugin = routers.find((r) => r.constructor.name === "ScalarPlugin");
|
|
4808
|
+
if (plugin) {
|
|
4809
|
+
integrations["scalar"] = plugin[$mountPath];
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
const asyncApiConf = checkConfig("asyncapi");
|
|
4814
|
+
if (asyncApiConf.enabled) {
|
|
4815
|
+
if (asyncApiConf.path) {
|
|
4816
|
+
integrations["asyncapi"] = asyncApiConf.path;
|
|
4817
|
+
} else {
|
|
4818
|
+
const plugin = routers.find((r) => r.constructor.name === "AsyncApiPlugin");
|
|
4819
|
+
if (plugin) {
|
|
4820
|
+
integrations["asyncapi"] = plugin[$mountPath];
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
return integrations;
|
|
4825
|
+
}
|
|
4826
|
+
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
4827
|
+
static getBasePath() {
|
|
4828
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
4829
|
+
if (dir.endsWith("dist")) {
|
|
4830
|
+
return dir + "/plugins/application/dashboard";
|
|
4831
|
+
}
|
|
4832
|
+
return dir;
|
|
4833
|
+
}
|
|
4834
|
+
setupRoutes() {
|
|
4835
|
+
this.router.get("/metrics", async (ctx) => {
|
|
4836
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
4837
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
4838
|
+
const interval = ctx.query["interval"];
|
|
4839
|
+
if (interval) {
|
|
4840
|
+
const intervalMap = {
|
|
4841
|
+
"10s": 10 * 1e3,
|
|
4842
|
+
"1m": 60 * 1e3,
|
|
4843
|
+
"5m": 5 * 60 * 1e3,
|
|
4844
|
+
"30m": 30 * 60 * 1e3,
|
|
4845
|
+
"1h": 60 * 60 * 1e3,
|
|
4846
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
4847
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
4848
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
4849
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
4850
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
4851
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
4852
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
4853
|
+
};
|
|
4854
|
+
const ms = intervalMap[interval] || 60 * 1e3;
|
|
4855
|
+
const startTime = Date.now() - ms;
|
|
4856
|
+
let stats;
|
|
4857
|
+
try {
|
|
4858
|
+
stats = await this.db.query(`
|
|
4859
|
+
SELECT
|
|
4860
|
+
count() as total,
|
|
4861
|
+
count(IF status < 400 THEN 1 END) as success,
|
|
4862
|
+
count(IF status >= 400 THEN 1 END) as failed,
|
|
4863
|
+
math::mean(duration) as avg_latency
|
|
4864
|
+
FROM requests
|
|
4865
|
+
WHERE timestamp >= $start
|
|
4866
|
+
GROUP ALL
|
|
4867
|
+
`, { start: startTime });
|
|
4868
|
+
} catch (error) {
|
|
4869
|
+
console.error("[Dashboard] Query failed at plugin.ts:180-191", {
|
|
4870
|
+
error,
|
|
4871
|
+
interval,
|
|
4872
|
+
startTime,
|
|
4873
|
+
query: "metrics interval stats",
|
|
4874
|
+
stack: new Error().stack
|
|
4875
|
+
});
|
|
4876
|
+
throw error;
|
|
4877
|
+
}
|
|
4878
|
+
const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
|
|
4879
|
+
return ctx.json({
|
|
4880
|
+
metrics: {
|
|
4881
|
+
totalRequests: s.total || 0,
|
|
4882
|
+
successfulRequests: s.success || 0,
|
|
4883
|
+
failedRequests: s.failed || 0,
|
|
4884
|
+
activeRequests: this.metrics.activeRequests,
|
|
4885
|
+
averageTotalTime_ms: s.avg_latency || 0,
|
|
4886
|
+
recentTimings: this.metrics.recentTimings,
|
|
4887
|
+
logs: [],
|
|
4888
|
+
rateLimitedCounts: this.metrics.rateLimitedCounts,
|
|
4889
|
+
nodeMetrics: this.metrics.nodeMetrics,
|
|
4890
|
+
edgeMetrics: this.metrics.edgeMetrics
|
|
4891
|
+
},
|
|
4892
|
+
uptime
|
|
4893
|
+
});
|
|
4894
|
+
}
|
|
4895
|
+
return ctx.json({
|
|
4896
|
+
metrics: this.metrics,
|
|
4897
|
+
uptime
|
|
4898
|
+
});
|
|
4899
|
+
});
|
|
4900
|
+
this.router.get("/metrics/history", async (ctx) => {
|
|
4901
|
+
const interval = ctx.query["interval"] || "1m";
|
|
4902
|
+
const intervalMap = {
|
|
4903
|
+
"10s": 10 * 1e3,
|
|
4904
|
+
"1m": 60 * 1e3,
|
|
4905
|
+
"5m": 5 * 60 * 1e3,
|
|
4906
|
+
"30m": 30 * 60 * 1e3,
|
|
4907
|
+
"1h": 60 * 60 * 1e3,
|
|
4908
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
4909
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
4910
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
4911
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
4912
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
4913
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
4914
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
4915
|
+
};
|
|
4916
|
+
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
4917
|
+
const startTime = Date.now() - periodMs * 3;
|
|
4918
|
+
const endTime = Date.now();
|
|
4919
|
+
const result = await this.db.query(
|
|
4920
|
+
"SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
4921
|
+
{ start: startTime, end: endTime, interval }
|
|
4922
|
+
);
|
|
4923
|
+
return ctx.json({
|
|
4924
|
+
metrics: result[0] || []
|
|
4925
|
+
});
|
|
4926
|
+
});
|
|
4927
|
+
const getIntervalStartTime = (interval) => {
|
|
4928
|
+
if (!interval) return 0;
|
|
4929
|
+
const intervalMap = {
|
|
4930
|
+
"10s": 10 * 1e3,
|
|
4931
|
+
"1m": 60 * 1e3,
|
|
4932
|
+
"5m": 5 * 60 * 1e3,
|
|
4933
|
+
"30m": 30 * 60 * 1e3,
|
|
4934
|
+
"1h": 60 * 60 * 1e3,
|
|
4935
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
4936
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
4937
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
4938
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
4939
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
4940
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
4941
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
4942
|
+
};
|
|
4943
|
+
const ms = intervalMap[interval] || 0;
|
|
4944
|
+
return ms ? Date.now() - ms : 0;
|
|
4945
|
+
};
|
|
4946
|
+
this.router.get("/requests/top", async (ctx) => {
|
|
4947
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
4948
|
+
const result = await this.db.query(
|
|
4949
|
+
"SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
4950
|
+
{ start: startTime }
|
|
4951
|
+
);
|
|
4952
|
+
return ctx.json({ top: result[0] || [] });
|
|
4953
|
+
});
|
|
4954
|
+
this.router.get("/errors/top", async (ctx) => {
|
|
4955
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
4956
|
+
const result = await this.db.query(
|
|
4957
|
+
"SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
4958
|
+
{ start: startTime }
|
|
4959
|
+
);
|
|
4960
|
+
return ctx.json({ top: result[0] || [] });
|
|
4961
|
+
});
|
|
4962
|
+
this.router.get("/requests/failing", async (ctx) => {
|
|
4963
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
4964
|
+
const result = await this.db.query(
|
|
4965
|
+
"SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
4966
|
+
{ start: startTime }
|
|
4967
|
+
);
|
|
4968
|
+
return ctx.json({ top: result[0] || [] });
|
|
4969
|
+
});
|
|
4970
|
+
this.router.get("/requests/slowest", async (ctx) => {
|
|
4971
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
4972
|
+
const result = await this.db.query(
|
|
4973
|
+
"SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
4974
|
+
{ start: startTime }
|
|
4975
|
+
);
|
|
4976
|
+
return ctx.json({ slowest: result[0] || [] });
|
|
4977
|
+
});
|
|
4978
|
+
this.router.get("/registry", (ctx) => {
|
|
4979
|
+
const app = this[$appRoot];
|
|
4980
|
+
if (!this.instrumented && app) {
|
|
4981
|
+
this.instrumentApp(app);
|
|
4982
|
+
}
|
|
4983
|
+
const registry = app?.getComponentRegistry?.();
|
|
4984
|
+
if (registry) {
|
|
4985
|
+
this.assignIdsToRegistry(registry, "root");
|
|
4986
|
+
}
|
|
4987
|
+
return ctx.json({ registry: registry || {} });
|
|
4988
|
+
});
|
|
4989
|
+
this.router.get("/requests", async (ctx) => {
|
|
4990
|
+
const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
|
|
4991
|
+
return ctx.json({ requests: result[0] || [] });
|
|
4992
|
+
});
|
|
4993
|
+
this.router.get("/requests/:id", async (ctx) => {
|
|
4994
|
+
const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
|
|
4995
|
+
return ctx.json({ request: result[0]?.[0] });
|
|
4996
|
+
});
|
|
4997
|
+
this.router.get("/failures", async (ctx) => {
|
|
4998
|
+
const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
|
|
4999
|
+
return ctx.json({ failures: result[0] });
|
|
5000
|
+
});
|
|
5001
|
+
this.router.post("/replay", async (ctx) => {
|
|
5002
|
+
const body = await ctx.body();
|
|
5003
|
+
const app = this[$appRoot];
|
|
5004
|
+
if (!app) return unknownError(ctx);
|
|
5005
|
+
try {
|
|
5006
|
+
const result = await app.processRequest({
|
|
5007
|
+
method: body.method,
|
|
5008
|
+
path: body.url,
|
|
5009
|
+
// or path
|
|
5010
|
+
headers: body.headers,
|
|
5011
|
+
body: body.body
|
|
5012
|
+
});
|
|
5013
|
+
return ctx.json({
|
|
5014
|
+
status: result.status,
|
|
5015
|
+
headers: result.headers,
|
|
5016
|
+
data: result.data
|
|
5017
|
+
});
|
|
5018
|
+
} catch (e) {
|
|
5019
|
+
return ctx.json({ error: String(e) }, 500);
|
|
5020
|
+
}
|
|
5021
|
+
});
|
|
5022
|
+
this.router.get("/**", async (ctx) => {
|
|
5023
|
+
const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
|
|
5024
|
+
let relativePath = ctx.path;
|
|
5025
|
+
if (relativePath.startsWith(mountPath)) {
|
|
5026
|
+
relativePath = relativePath.slice(mountPath.length);
|
|
5027
|
+
}
|
|
5028
|
+
if (relativePath.startsWith("/")) {
|
|
5029
|
+
relativePath = relativePath.slice(1);
|
|
5030
|
+
}
|
|
5031
|
+
const path = relativePath;
|
|
5032
|
+
const staticFiles = [
|
|
5033
|
+
"charts.js",
|
|
5034
|
+
"failures.js",
|
|
5035
|
+
"graph.mjs",
|
|
5036
|
+
"poll.js",
|
|
5037
|
+
"reactflow.css",
|
|
5038
|
+
"registry.css",
|
|
5039
|
+
"registry.js",
|
|
5040
|
+
"requests.js",
|
|
5041
|
+
"styles.css",
|
|
5042
|
+
"tables.js",
|
|
5043
|
+
"tabs.js",
|
|
5044
|
+
"tabulator.css",
|
|
5045
|
+
"theme.css"
|
|
5046
|
+
];
|
|
5047
|
+
if (staticFiles.includes(path)) {
|
|
5048
|
+
const content = await readFile(join$1(Dashboard.getBasePath(), "static", path), "utf-8");
|
|
5049
|
+
if (path.endsWith(".css")) ctx.set("Content-Type", "text/css");
|
|
5050
|
+
else if (path.endsWith(".js") || path.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
|
|
5051
|
+
return ctx.send(content);
|
|
5052
|
+
}
|
|
5053
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
5054
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
5055
|
+
this.getLinkPattern();
|
|
5056
|
+
const integrations = this.detectIntegrations();
|
|
5057
|
+
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
5058
|
+
const html = renderToString(DashboardApp({
|
|
5059
|
+
metrics: this.metrics,
|
|
5060
|
+
uptime,
|
|
5061
|
+
rootPath: process.cwd(),
|
|
5062
|
+
integrations,
|
|
5063
|
+
base: mountPath,
|
|
5064
|
+
getRequestHeadersSource
|
|
5065
|
+
}));
|
|
5066
|
+
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
instrumentApp(app) {
|
|
5070
|
+
if (!app.getComponentRegistry) return;
|
|
5071
|
+
const registry = app.getComponentRegistry();
|
|
5072
|
+
this.assignIdsToRegistry(registry, "root");
|
|
5073
|
+
this.instrumented = true;
|
|
5074
|
+
}
|
|
5075
|
+
// Traverses registry, generates IDs, and attaches them to the actual function objects
|
|
5076
|
+
assignIdsToRegistry(node, parentId) {
|
|
5077
|
+
if (!node) return;
|
|
5078
|
+
const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
5079
|
+
node.middleware?.forEach((mw, idx) => {
|
|
5080
|
+
const id = makeId("mw", parentId, idx, mw.name);
|
|
5081
|
+
mw.id = id;
|
|
5082
|
+
if (mw._fn) mw._fn._debugId = id;
|
|
5083
|
+
});
|
|
5084
|
+
node.controllers?.forEach((ctrl, idx) => {
|
|
5085
|
+
const id = makeId("ctrl", parentId, idx, ctrl.name);
|
|
5086
|
+
ctrl.id = id;
|
|
5087
|
+
});
|
|
5088
|
+
node.routes?.forEach((r, idx) => {
|
|
5089
|
+
const id = makeId("route", parentId, idx, r.handlerName || "handler");
|
|
5090
|
+
r.id = id;
|
|
5091
|
+
if (r._fn) r._fn._debugId = id;
|
|
5092
|
+
});
|
|
5093
|
+
node.routers?.forEach((r, idx) => {
|
|
5094
|
+
const id = makeId("router", parentId, idx, r.path);
|
|
5095
|
+
r.id = id;
|
|
5096
|
+
this.assignIdsToRegistry(r.children, id);
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
recordNodeMetric(id, type, duration, isError) {
|
|
5100
|
+
if (!this.metrics.nodeMetrics[id]) {
|
|
5101
|
+
this.metrics.nodeMetrics[id] = {
|
|
5102
|
+
id,
|
|
5103
|
+
type,
|
|
5104
|
+
requests: 0,
|
|
5105
|
+
totalTime: 0,
|
|
5106
|
+
failures: 0,
|
|
5107
|
+
name: id
|
|
5108
|
+
// simplify
|
|
5109
|
+
};
|
|
5110
|
+
}
|
|
5111
|
+
const m = this.metrics.nodeMetrics[id];
|
|
5112
|
+
m.requests++;
|
|
5113
|
+
m.totalTime += duration;
|
|
5114
|
+
if (isError) m.failures++;
|
|
5115
|
+
}
|
|
5116
|
+
recordEdgeMetric(from, to) {
|
|
5117
|
+
const key = `${from}|${to}`;
|
|
5118
|
+
this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
|
|
5119
|
+
}
|
|
5120
|
+
getLinkPattern() {
|
|
5121
|
+
const term = process.env["TERM_PROGRAM"] || "";
|
|
5122
|
+
if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
|
|
5123
|
+
return "vscode://file/{{absolute}}:{{line}}";
|
|
5124
|
+
}
|
|
5125
|
+
return "file:///{{absolute}}:{{line}}";
|
|
5126
|
+
}
|
|
5127
|
+
getHooks() {
|
|
5128
|
+
return {
|
|
5129
|
+
onRequestStart: (ctx) => {
|
|
5130
|
+
const app = this[$appRoot];
|
|
5131
|
+
if (!this.instrumented && app) {
|
|
5132
|
+
this.instrumentApp(app);
|
|
5133
|
+
}
|
|
5134
|
+
this.metrics.totalRequests++;
|
|
5135
|
+
this.metrics.activeRequests++;
|
|
5136
|
+
ctx._debugStartTime = performance.now();
|
|
5137
|
+
ctx[$debug] = new Collector(this);
|
|
5138
|
+
},
|
|
5139
|
+
onResponseEnd: async (ctx, response) => {
|
|
5140
|
+
this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
|
|
5141
|
+
const start = ctx._debugStartTime;
|
|
5142
|
+
let duration = 0;
|
|
5143
|
+
if (start) {
|
|
5144
|
+
duration = performance.now() - start;
|
|
5145
|
+
this.updateTiming(duration);
|
|
5146
|
+
}
|
|
5147
|
+
const isError = response.status >= 400;
|
|
5148
|
+
this.metricsCollector.recordRequest(duration, isError);
|
|
5149
|
+
if (response.status >= 400) {
|
|
5150
|
+
this.metrics.failedRequests++;
|
|
5151
|
+
if (response.status === 429) {
|
|
5152
|
+
const path = ctx.path;
|
|
5153
|
+
this.metrics.rateLimitedCounts[path] = (this.metrics.rateLimitedCounts[path] || 0) + 1;
|
|
5154
|
+
}
|
|
5155
|
+
try {
|
|
5156
|
+
const headers = {};
|
|
5157
|
+
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
5158
|
+
ctx.request.headers.forEach((v, k) => {
|
|
5159
|
+
headers[k] = v;
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
await this.db.upsert(new RecordId("failed_requests", ctx.requestId), {
|
|
5163
|
+
method: ctx.method,
|
|
5164
|
+
url: ctx.url.toString(),
|
|
5165
|
+
headers,
|
|
5166
|
+
status: response.status,
|
|
5167
|
+
timestamp: Date.now(),
|
|
5168
|
+
state: ctx.state
|
|
5169
|
+
// body?
|
|
5170
|
+
});
|
|
5171
|
+
} catch (e) {
|
|
5172
|
+
console.error("Failed to record failed request", e);
|
|
5173
|
+
}
|
|
5174
|
+
} else {
|
|
5175
|
+
this.metrics.successfulRequests++;
|
|
5176
|
+
}
|
|
5177
|
+
const logEntry = {
|
|
5178
|
+
method: ctx.method,
|
|
5179
|
+
url: ctx.url.toString(),
|
|
5180
|
+
status: response.status,
|
|
5181
|
+
duration,
|
|
5182
|
+
timestamp: Date.now(),
|
|
5183
|
+
handlerStack: ctx.handlerStack
|
|
5184
|
+
};
|
|
5185
|
+
this.metrics.logs.push(logEntry);
|
|
5186
|
+
try {
|
|
5187
|
+
await this.db.upsert(new RecordId("requests", ctx.requestId), logEntry);
|
|
5188
|
+
} catch (e) {
|
|
5189
|
+
console.error("Failed to record request log", e);
|
|
5190
|
+
}
|
|
5191
|
+
const retention = this.dashboardConfig.retentionMs ?? 72e5;
|
|
5192
|
+
const cutoff = Date.now() - retention;
|
|
5193
|
+
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
5194
|
+
this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
};
|
|
5198
|
+
}
|
|
5199
|
+
updateTiming(duration) {
|
|
5200
|
+
const alpha = 0.1;
|
|
5201
|
+
if (this.metrics.averageTotalTime_ms === 0) {
|
|
5202
|
+
this.metrics.averageTotalTime_ms = duration;
|
|
5203
|
+
} else {
|
|
5204
|
+
this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
|
|
5205
|
+
}
|
|
5206
|
+
this.metrics.recentTimings.push(duration);
|
|
5207
|
+
if (this.metrics.recentTimings.length > 50) {
|
|
5208
|
+
this.metrics.recentTimings.shift();
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
function unknownError(ctx) {
|
|
5213
|
+
return ctx.json({ error: "Unknown Error" }, 500);
|
|
5214
|
+
}
|
|
5215
|
+
class GraphQLApolloPlugin extends ShokupanRouter {
|
|
5216
|
+
// Use generic any or verify type
|
|
5217
|
+
constructor(pluginOptions) {
|
|
5218
|
+
super();
|
|
5219
|
+
this.pluginOptions = pluginOptions;
|
|
5220
|
+
this.pluginOptions.path ??= "/graphql";
|
|
5221
|
+
}
|
|
5222
|
+
apolloServer;
|
|
5223
|
+
async onInit(app, options) {
|
|
5224
|
+
const { ApolloServer, HeaderMap } = await import("@apollo/server");
|
|
5225
|
+
this.apolloServer = new ApolloServer({
|
|
5226
|
+
typeDefs: this.pluginOptions.typeDefs,
|
|
5227
|
+
resolvers: this.pluginOptions.resolvers,
|
|
5228
|
+
...this.pluginOptions.apolloConfig || {}
|
|
5229
|
+
});
|
|
5230
|
+
const path = options?.path || this.pluginOptions.path || "/graphql";
|
|
5231
|
+
app.mount(path, this);
|
|
5232
|
+
app.onStart(async () => {
|
|
5233
|
+
await this.apolloServer.start();
|
|
5234
|
+
});
|
|
5235
|
+
this.post("/", async (ctx) => {
|
|
5236
|
+
const body = await ctx.body();
|
|
5237
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5238
|
+
httpGraphQLRequest: {
|
|
5239
|
+
body,
|
|
5240
|
+
method: ctx.req.method,
|
|
5241
|
+
search: ctx.url.search,
|
|
5242
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5243
|
+
},
|
|
5244
|
+
// Pass the Shokupan Context as the GraphQL Context
|
|
5245
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5246
|
+
});
|
|
5247
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5248
|
+
ctx.set(key, value);
|
|
5249
|
+
}
|
|
5250
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5251
|
+
return ctx.send(httpGraphQLResponse.body.string, {
|
|
5252
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5253
|
+
});
|
|
5254
|
+
} else {
|
|
5255
|
+
let string = "";
|
|
5256
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5257
|
+
string += chunk;
|
|
5258
|
+
}
|
|
5259
|
+
return ctx.send(string, {
|
|
5260
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
5263
|
+
});
|
|
5264
|
+
this.get("/", async (ctx) => {
|
|
5265
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5266
|
+
httpGraphQLRequest: {
|
|
5267
|
+
body: Object.keys(ctx.query).length > 0 ? ctx.query : void 0,
|
|
5268
|
+
method: ctx.req.method,
|
|
5269
|
+
search: ctx.url.search,
|
|
5270
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5271
|
+
},
|
|
5272
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5273
|
+
});
|
|
5274
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5275
|
+
ctx.set(key, value);
|
|
5276
|
+
}
|
|
5277
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5278
|
+
return ctx.html(httpGraphQLResponse.body.string, httpGraphQLResponse.status ?? 200);
|
|
5279
|
+
} else {
|
|
5280
|
+
let string = "";
|
|
5281
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5282
|
+
string += chunk;
|
|
5283
|
+
}
|
|
5284
|
+
return ctx.html(string, httpGraphQLResponse.status ?? 200);
|
|
5285
|
+
}
|
|
5286
|
+
});
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
3295
5289
|
class ScalarPlugin extends ShokupanRouter {
|
|
3296
5290
|
constructor(pluginOptions = {}) {
|
|
3297
5291
|
pluginOptions.config ??= {};
|
|
3298
5292
|
super();
|
|
3299
5293
|
this.pluginOptions = pluginOptions;
|
|
3300
|
-
this.
|
|
5294
|
+
this.initRoutes();
|
|
5295
|
+
}
|
|
5296
|
+
eta;
|
|
5297
|
+
async onInit(app, options) {
|
|
5298
|
+
const { Eta: Eta2 } = await import("eta");
|
|
5299
|
+
this.eta = new Eta2();
|
|
5300
|
+
const path = options?.path || this.pluginOptions.path || "/reference";
|
|
5301
|
+
app.mount(path, this);
|
|
5302
|
+
this.onMount(app);
|
|
3301
5303
|
}
|
|
3302
|
-
|
|
3303
|
-
if (
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
app.mount(options.path ?? "/", this);
|
|
5304
|
+
async ensureEta() {
|
|
5305
|
+
if (!this.eta) {
|
|
5306
|
+
const { Eta: Eta2 } = await import("eta");
|
|
5307
|
+
this.eta = new Eta2();
|
|
3307
5308
|
}
|
|
3308
|
-
this.onMount(app);
|
|
3309
5309
|
}
|
|
3310
|
-
|
|
3311
|
-
|
|
5310
|
+
initRoutes() {
|
|
5311
|
+
const bootId = Date.now().toString();
|
|
5312
|
+
this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
|
|
5313
|
+
this.get("/", async (ctx) => {
|
|
5314
|
+
await this.ensureEta();
|
|
3312
5315
|
let path = ctx.url.toString();
|
|
3313
5316
|
if (!path.endsWith("/")) path += "/";
|
|
3314
|
-
|
|
3315
|
-
<
|
|
5317
|
+
const devScript = ctx.app?.applicationConfig.development ? `
|
|
5318
|
+
<script>
|
|
5319
|
+
(function() {
|
|
5320
|
+
const bootId = "${bootId}";
|
|
5321
|
+
let isDown = false;
|
|
5322
|
+
|
|
5323
|
+
setInterval(async () => {
|
|
5324
|
+
try {
|
|
5325
|
+
const res = await fetch('${path}_lifecycle');
|
|
5326
|
+
if (!res.ok) throw new Error('Down');
|
|
5327
|
+
const data = await res.json();
|
|
5328
|
+
if (data.boot !== bootId) {
|
|
5329
|
+
console.log('Server restarted, reloading...');
|
|
5330
|
+
window.location.reload();
|
|
5331
|
+
}
|
|
5332
|
+
else if (isDown) {
|
|
5333
|
+
isDown = false;
|
|
5334
|
+
}
|
|
5335
|
+
} catch (e) {
|
|
5336
|
+
isDown = true;
|
|
5337
|
+
console.log('Connection lost...');
|
|
5338
|
+
}
|
|
5339
|
+
}, 2000);
|
|
5340
|
+
})();
|
|
5341
|
+
<\/script>
|
|
5342
|
+
` : "";
|
|
5343
|
+
let themeCss = "";
|
|
5344
|
+
try {
|
|
5345
|
+
try {
|
|
5346
|
+
themeCss = readFileSync(join$1(process.cwd(), "src/theme.css"), "utf-8");
|
|
5347
|
+
} catch {
|
|
5348
|
+
}
|
|
5349
|
+
} catch (e) {
|
|
5350
|
+
}
|
|
5351
|
+
if (!this.eta) throw new Error("Eta not initialized");
|
|
5352
|
+
return ctx.html(this.eta.renderString(`<!doctype html>
|
|
5353
|
+
<html lang="en">
|
|
3316
5354
|
<head>
|
|
3317
5355
|
<title>API Reference</title>
|
|
3318
5356
|
<meta charset = "utf-8" />
|
|
3319
5357
|
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
5358
|
+
<style>
|
|
5359
|
+
${themeCss}
|
|
5360
|
+
|
|
5361
|
+
:root {
|
|
5362
|
+
--scalar-color-1: var(--primary);
|
|
5363
|
+
--scalar-color-2: var(--secondary);
|
|
5364
|
+
--scalar-color-3: var(--accent);
|
|
5365
|
+
--scalar-color-accent: var(--accent);
|
|
5366
|
+
|
|
5367
|
+
--scalar-background-1: var(--bg-primary);
|
|
5368
|
+
--scalar-background-2: var(--bg-secondary);
|
|
5369
|
+
--scalar-background-3: var(--bg-card);
|
|
5370
|
+
|
|
5371
|
+
--scalar-text-1: var(--text-primary);
|
|
5372
|
+
--scalar-text-2: var(--text-secondary);
|
|
5373
|
+
--scalar-text-3: var(--text-muted);
|
|
5374
|
+
|
|
5375
|
+
--scalar-border-color: var(--border-color);
|
|
5376
|
+
}
|
|
5377
|
+
</style>
|
|
3320
5378
|
</head>
|
|
3321
5379
|
|
|
3322
5380
|
<body>
|
|
@@ -3328,9 +5386,10 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3328
5386
|
}
|
|
3329
5387
|
])
|
|
3330
5388
|
<\/script>
|
|
5389
|
+
<%~ it.devScript %>
|
|
3331
5390
|
</body>
|
|
3332
5391
|
|
|
3333
|
-
</html>`, { path, config: this.pluginOptions }));
|
|
5392
|
+
</html>`, { path, config: this.pluginOptions, devScript }));
|
|
3334
5393
|
});
|
|
3335
5394
|
this.get("/openapi.json", async (ctx) => {
|
|
3336
5395
|
let spec;
|
|
@@ -3389,23 +5448,23 @@ function Compression(options = {}) {
|
|
|
3389
5448
|
return next();
|
|
3390
5449
|
}
|
|
3391
5450
|
let response = await next();
|
|
3392
|
-
if (!(response instanceof Response) && ctx
|
|
3393
|
-
response = ctx
|
|
5451
|
+
if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
|
|
5452
|
+
response = ctx[$finalResponse];
|
|
3394
5453
|
}
|
|
3395
5454
|
if (response instanceof Response) {
|
|
3396
5455
|
if (response.headers.has("Content-Encoding")) return response;
|
|
3397
5456
|
let body;
|
|
3398
5457
|
let bodySize;
|
|
3399
|
-
if (ctx
|
|
3400
|
-
if (typeof ctx
|
|
3401
|
-
const encoded = new TextEncoder().encode(ctx
|
|
5458
|
+
if (ctx[$rawBody] !== void 0) {
|
|
5459
|
+
if (typeof ctx[$rawBody] === "string") {
|
|
5460
|
+
const encoded = new TextEncoder().encode(ctx[$rawBody]);
|
|
3402
5461
|
body = encoded;
|
|
3403
5462
|
bodySize = encoded.byteLength;
|
|
3404
|
-
} else if (ctx
|
|
3405
|
-
body = ctx
|
|
3406
|
-
bodySize = ctx.
|
|
5463
|
+
} else if (ctx[$rawBody] instanceof Uint8Array) {
|
|
5464
|
+
body = ctx[$rawBody];
|
|
5465
|
+
bodySize = ctx[$rawBody].byteLength;
|
|
3407
5466
|
} else {
|
|
3408
|
-
body = ctx
|
|
5467
|
+
body = ctx[$rawBody];
|
|
3409
5468
|
bodySize = body.byteLength;
|
|
3410
5469
|
}
|
|
3411
5470
|
} else {
|
|
@@ -3533,7 +5592,7 @@ function Cors(options = {}) {
|
|
|
3533
5592
|
}
|
|
3534
5593
|
const response = await next();
|
|
3535
5594
|
if (response instanceof Response) {
|
|
3536
|
-
const headerEntries =
|
|
5595
|
+
const headerEntries = Object.entries(headers);
|
|
3537
5596
|
for (let i = 0; i < headerEntries.length; i++) {
|
|
3538
5597
|
const [key, value] = headerEntries[i];
|
|
3539
5598
|
response.headers.set(key, value);
|
|
@@ -4271,21 +6330,41 @@ function Session(options) {
|
|
|
4271
6330
|
}
|
|
4272
6331
|
export {
|
|
4273
6332
|
$appRoot,
|
|
6333
|
+
$bodyParseError,
|
|
6334
|
+
$bodyParsed,
|
|
6335
|
+
$bodyType,
|
|
6336
|
+
$cachedBody,
|
|
6337
|
+
$cachedHost,
|
|
6338
|
+
$cachedHostname,
|
|
6339
|
+
$cachedOrigin,
|
|
6340
|
+
$cachedProtocol,
|
|
6341
|
+
$cachedQuery,
|
|
4274
6342
|
$childControllers,
|
|
4275
6343
|
$childRouters,
|
|
4276
6344
|
$controllerPath,
|
|
6345
|
+
$debug,
|
|
4277
6346
|
$dispatch,
|
|
6347
|
+
$eventMethods,
|
|
6348
|
+
$finalResponse,
|
|
6349
|
+
$io,
|
|
4278
6350
|
$isApplication,
|
|
4279
6351
|
$isMounted,
|
|
4280
6352
|
$isRouter,
|
|
4281
6353
|
$middleware,
|
|
4282
6354
|
$mountPath,
|
|
4283
6355
|
$parent,
|
|
6356
|
+
$rawBody,
|
|
6357
|
+
$requestId,
|
|
4284
6358
|
$routeArgs,
|
|
6359
|
+
$routeMatched,
|
|
4285
6360
|
$routeMethods,
|
|
4286
6361
|
$routeSpec,
|
|
4287
6362
|
$routes,
|
|
6363
|
+
$socket,
|
|
6364
|
+
$url,
|
|
6365
|
+
$ws,
|
|
4288
6366
|
All,
|
|
6367
|
+
AsyncApiPlugin,
|
|
4289
6368
|
AuthPlugin,
|
|
4290
6369
|
Body,
|
|
4291
6370
|
ClusterPlugin,
|
|
@@ -4294,8 +6373,11 @@ export {
|
|
|
4294
6373
|
Controller,
|
|
4295
6374
|
Cors,
|
|
4296
6375
|
Ctx,
|
|
6376
|
+
Dashboard,
|
|
4297
6377
|
Delete,
|
|
6378
|
+
Event,
|
|
4298
6379
|
Get,
|
|
6380
|
+
GraphQLApolloPlugin,
|
|
4299
6381
|
HTTPMethods,
|
|
4300
6382
|
Head,
|
|
4301
6383
|
Headers$1 as Headers,
|