shokupan 0.2.0 → 0.4.4
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 +197 -24
- package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
- package/dist/benchmarking/advanced-data.d.ts +33 -0
- package/dist/benchmarking/advanced-runner.d.ts +1 -0
- package/dist/benchmarking/advanced-worker.d.ts +0 -0
- package/dist/benchmarking/cases/elysia.d.ts +1 -0
- package/dist/benchmarking/cases/express.d.ts +1 -0
- package/dist/benchmarking/cases/fastify.d.ts +1 -0
- package/dist/benchmarking/cases/hapi.d.ts +1 -0
- package/dist/benchmarking/cases/hono.d.ts +1 -0
- package/dist/benchmarking/cases/koa.d.ts +1 -0
- package/dist/benchmarking/cases/nest.d.ts +1 -0
- package/dist/benchmarking/cases/shokupan.d.ts +1 -0
- package/dist/benchmarking/data.d.ts +15 -0
- package/dist/benchmarking/quick_bench.d.ts +1 -0
- package/dist/benchmarking/runner.d.ts +1 -0
- package/dist/benchmarking/worker.d.ts +0 -0
- package/dist/buntest.d.ts +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +17 -7
- package/dist/decorators.d.ts +47 -0
- package/dist/index.cjs +939 -385
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +931 -379
- package/dist/index.js.map +1 -1
- package/dist/{openapi-analyzer-BTExMLX4.js → openapi-analyzer-BtIaHIfe.js} +11 -6
- package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
- package/dist/{openapi-analyzer-BN0wFCML.cjs → openapi-analyzer-D9YB3IkV.cjs} +11 -6
- package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
- package/dist/plugins/auth.d.ts +1 -1
- package/dist/plugins/debugview/plugin.d.ts +7 -12
- package/dist/plugins/failed-request-recorder.d.ts +14 -0
- package/dist/plugins/idempotency/plugin.d.ts +14 -0
- package/dist/plugins/openapi-validator.d.ts +28 -0
- package/dist/plugins/rate-limit.d.ts +3 -1
- package/dist/plugins/serve-static.d.ts +2 -3
- package/dist/router/trie.d.ts +14 -0
- package/dist/router.d.ts +60 -4
- package/dist/{server-adapter-CnQFr4P7.js → server-adapter-BWrEJbKL.js} +5 -5
- package/dist/server-adapter-BWrEJbKL.js.map +1 -0
- package/dist/{server-adapter-BD6oKEto.cjs → server-adapter-fVKP60e0.cjs} +5 -5
- package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
- package/dist/shokupan.d.ts +14 -3
- package/dist/types.d.ts +100 -4
- package/dist/util/cpu-monitor.d.ts +11 -0
- package/dist/util/stack.d.ts +8 -0
- package/package.json +12 -5
- package/dist/openapi-analyzer-BN0wFCML.cjs.map +0 -1
- package/dist/openapi-analyzer-BTExMLX4.js.map +0 -1
- package/dist/server-adapter-BD6oKEto.cjs.map +0 -1
- package/dist/server-adapter-CnQFr4P7.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
3
4
|
const eta$2 = require("eta");
|
|
4
|
-
const promises = require("fs/promises");
|
|
5
|
+
const promises$1 = require("fs/promises");
|
|
5
6
|
const path = require("path");
|
|
6
7
|
const node_async_hooks = require("node:async_hooks");
|
|
8
|
+
const node = require("@surrealdb/node");
|
|
9
|
+
const surrealdb = require("surrealdb");
|
|
7
10
|
const api = require("@opentelemetry/api");
|
|
11
|
+
const os = require("node:os");
|
|
8
12
|
const arctic = require("arctic");
|
|
9
13
|
const jose = require("jose");
|
|
14
|
+
const zlib = require("node:zlib");
|
|
10
15
|
const Ajv = require("ajv");
|
|
11
16
|
const addFormats = require("ajv-formats");
|
|
12
17
|
const classTransformer = require("class-transformer");
|
|
13
18
|
const classValidator = require("class-validator");
|
|
14
|
-
const openapiAnalyzer = require("./openapi-analyzer-
|
|
19
|
+
const openapiAnalyzer = require("./openapi-analyzer-D9YB3IkV.cjs");
|
|
15
20
|
const crypto = require("crypto");
|
|
16
21
|
const events = require("events");
|
|
17
22
|
function _interopNamespaceDefault(e) {
|
|
@@ -30,7 +35,9 @@ function _interopNamespaceDefault(e) {
|
|
|
30
35
|
n.default = e;
|
|
31
36
|
return Object.freeze(n);
|
|
32
37
|
}
|
|
38
|
+
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
33
39
|
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
40
|
+
const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
|
|
34
41
|
class ShokupanResponse {
|
|
35
42
|
_headers = null;
|
|
36
43
|
_status = 200;
|
|
@@ -95,10 +102,12 @@ class ShokupanResponse {
|
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
class ShokupanContext {
|
|
98
|
-
|
|
105
|
+
// Raw body for compression optimization
|
|
106
|
+
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
99
107
|
this.request = request;
|
|
100
108
|
this.server = server;
|
|
101
109
|
this.app = app;
|
|
110
|
+
this.signal = signal;
|
|
102
111
|
this.state = state || {};
|
|
103
112
|
if (enableMiddlewareTracking) {
|
|
104
113
|
const self = this;
|
|
@@ -122,7 +131,9 @@ class ShokupanContext {
|
|
|
122
131
|
state;
|
|
123
132
|
handlerStack = [];
|
|
124
133
|
response;
|
|
134
|
+
_debug;
|
|
125
135
|
_finalResponse;
|
|
136
|
+
_rawBody;
|
|
126
137
|
get url() {
|
|
127
138
|
if (!this._url) {
|
|
128
139
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -171,7 +182,17 @@ class ShokupanContext {
|
|
|
171
182
|
* Request query params
|
|
172
183
|
*/
|
|
173
184
|
get query() {
|
|
174
|
-
|
|
185
|
+
const q = {};
|
|
186
|
+
for (const [key, value] of this.url.searchParams) {
|
|
187
|
+
if (q[key] === void 0) {
|
|
188
|
+
q[key] = value;
|
|
189
|
+
} else if (Array.isArray(q[key])) {
|
|
190
|
+
q[key].push(value);
|
|
191
|
+
} else {
|
|
192
|
+
q[key] = [q[key], value];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return q;
|
|
175
196
|
}
|
|
176
197
|
/**
|
|
177
198
|
* Client IP address
|
|
@@ -246,17 +267,36 @@ class ShokupanContext {
|
|
|
246
267
|
setCookie(name, value, options = {}) {
|
|
247
268
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
248
269
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
270
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
271
|
+
if (options.path) cookie += `; Path=${options.path || "/"}`;
|
|
249
272
|
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
250
273
|
if (options.httpOnly) cookie += `; HttpOnly`;
|
|
251
274
|
if (options.secure) cookie += `; Secure`;
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
let sameSite = options.sameSite;
|
|
276
|
+
if (sameSite === true) sameSite = "Strict";
|
|
277
|
+
if (sameSite === void 0 || sameSite === false) ;
|
|
278
|
+
else {
|
|
279
|
+
const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
|
|
280
|
+
switch (stringSameSite) {
|
|
281
|
+
case "lax":
|
|
282
|
+
cookie += "; SameSite=Lax";
|
|
283
|
+
break;
|
|
284
|
+
case "strict":
|
|
285
|
+
cookie += "; SameSite=Strict";
|
|
286
|
+
break;
|
|
287
|
+
case "none":
|
|
288
|
+
cookie += "; SameSite=None";
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
cookie += "; SameSite=Lax";
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
257
294
|
}
|
|
258
295
|
if (options.priority) {
|
|
259
|
-
|
|
296
|
+
const p = options.priority.toLowerCase();
|
|
297
|
+
if (p === "low") cookie += "; Priority=Low";
|
|
298
|
+
else if (p === "medium") cookie += "; Priority=Medium";
|
|
299
|
+
else if (p === "high") cookie += "; Priority=High";
|
|
260
300
|
}
|
|
261
301
|
this.response.append("Set-Cookie", cookie);
|
|
262
302
|
return this;
|
|
@@ -293,6 +333,9 @@ class ShokupanContext {
|
|
|
293
333
|
send(body, options) {
|
|
294
334
|
const headers = this.mergeHeaders(options?.headers);
|
|
295
335
|
const status = options?.status ?? this.response.status;
|
|
336
|
+
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
337
|
+
this._rawBody = body;
|
|
338
|
+
}
|
|
296
339
|
this._finalResponse = new Response(body, { status, headers });
|
|
297
340
|
return this._finalResponse;
|
|
298
341
|
}
|
|
@@ -300,11 +343,11 @@ class ShokupanContext {
|
|
|
300
343
|
* Read request body
|
|
301
344
|
*/
|
|
302
345
|
async body() {
|
|
303
|
-
const contentType = this.request.headers.get("content-type");
|
|
304
|
-
if (contentType
|
|
346
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
347
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
305
348
|
return this.request.json();
|
|
306
349
|
}
|
|
307
|
-
if (contentType
|
|
350
|
+
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
308
351
|
return this.request.formData();
|
|
309
352
|
}
|
|
310
353
|
return this.request.text();
|
|
@@ -315,6 +358,7 @@ class ShokupanContext {
|
|
|
315
358
|
json(data, status, headers) {
|
|
316
359
|
const finalStatus = status ?? this.response.status;
|
|
317
360
|
const jsonString = JSON.stringify(data);
|
|
361
|
+
this._rawBody = jsonString;
|
|
318
362
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
319
363
|
this._finalResponse = new Response(jsonString, {
|
|
320
364
|
status: finalStatus,
|
|
@@ -332,15 +376,16 @@ class ShokupanContext {
|
|
|
332
376
|
*/
|
|
333
377
|
text(data, status, headers) {
|
|
334
378
|
const finalStatus = status ?? this.response.status;
|
|
379
|
+
this._rawBody = data;
|
|
335
380
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
336
381
|
this._finalResponse = new Response(data, {
|
|
337
382
|
status: finalStatus,
|
|
338
|
-
headers: { "content-type": "text/plain" }
|
|
383
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
339
384
|
});
|
|
340
385
|
return this._finalResponse;
|
|
341
386
|
}
|
|
342
387
|
const finalHeaders = this.mergeHeaders(headers);
|
|
343
|
-
finalHeaders.set("content-type", "text/plain");
|
|
388
|
+
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
344
389
|
this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
345
390
|
return this._finalResponse;
|
|
346
391
|
}
|
|
@@ -348,9 +393,10 @@ class ShokupanContext {
|
|
|
348
393
|
* Respond with HTML content
|
|
349
394
|
*/
|
|
350
395
|
html(html, status, headers) {
|
|
351
|
-
const finalHeaders = this.mergeHeaders(headers);
|
|
352
|
-
finalHeaders.set("content-type", "text/html");
|
|
353
396
|
const finalStatus = status ?? this.response.status;
|
|
397
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
398
|
+
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
399
|
+
this._rawBody = html;
|
|
354
400
|
this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
355
401
|
return this._finalResponse;
|
|
356
402
|
}
|
|
@@ -375,11 +421,20 @@ class ShokupanContext {
|
|
|
375
421
|
/**
|
|
376
422
|
* Respond with a file
|
|
377
423
|
*/
|
|
378
|
-
file(path2, fileOptions, responseOptions) {
|
|
424
|
+
async file(path2, fileOptions, responseOptions) {
|
|
379
425
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
380
426
|
const status = responseOptions?.status ?? this.response.status;
|
|
381
|
-
|
|
382
|
-
|
|
427
|
+
if (typeof Bun !== "undefined") {
|
|
428
|
+
this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
429
|
+
return this._finalResponse;
|
|
430
|
+
} else {
|
|
431
|
+
const fileBuffer = await promises.readFile(path2);
|
|
432
|
+
if (fileOptions?.type) {
|
|
433
|
+
headers.set("content-type", fileOptions.type);
|
|
434
|
+
}
|
|
435
|
+
this._finalResponse = new Response(fileBuffer, { status, headers });
|
|
436
|
+
return this._finalResponse;
|
|
437
|
+
}
|
|
383
438
|
}
|
|
384
439
|
/**
|
|
385
440
|
* JSX Rendering Function
|
|
@@ -399,6 +454,74 @@ class ShokupanContext {
|
|
|
399
454
|
return this.html(html, status, headers);
|
|
400
455
|
}
|
|
401
456
|
}
|
|
457
|
+
function RateLimitMiddleware(options = {}) {
|
|
458
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
459
|
+
const max = options.limit || options.max || 5;
|
|
460
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
461
|
+
const statusCode = options.statusCode || 429;
|
|
462
|
+
const headers = options.headers !== false;
|
|
463
|
+
const mode = options.mode || "user";
|
|
464
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
465
|
+
if (mode === "absolute") {
|
|
466
|
+
return "global";
|
|
467
|
+
}
|
|
468
|
+
return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
469
|
+
});
|
|
470
|
+
const skip = options.skip || (() => false);
|
|
471
|
+
const hits = /* @__PURE__ */ new Map();
|
|
472
|
+
const interval = setInterval(() => {
|
|
473
|
+
const now = Date.now();
|
|
474
|
+
for (const [key, record] of hits.entries()) {
|
|
475
|
+
if (record.resetTime <= now) {
|
|
476
|
+
hits.delete(key);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}, windowMs);
|
|
480
|
+
if (interval.unref) interval.unref();
|
|
481
|
+
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
482
|
+
if (skip(ctx)) return next();
|
|
483
|
+
const key = keyGenerator(ctx);
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
let record = hits.get(key);
|
|
486
|
+
if (!record || record.resetTime <= now) {
|
|
487
|
+
record = {
|
|
488
|
+
hits: 0,
|
|
489
|
+
resetTime: now + windowMs
|
|
490
|
+
};
|
|
491
|
+
hits.set(key, record);
|
|
492
|
+
}
|
|
493
|
+
record.hits++;
|
|
494
|
+
const remaining = Math.max(0, max - record.hits);
|
|
495
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
496
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
497
|
+
const setHeaders = (res) => {
|
|
498
|
+
if (!headers || !res || !res.headers) return;
|
|
499
|
+
try {
|
|
500
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
501
|
+
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
502
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
503
|
+
} catch (e) {
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
if (record.hits > max) {
|
|
507
|
+
typeof message === "object" ? JSON.stringify(message) : String(message);
|
|
508
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
509
|
+
if (headers) {
|
|
510
|
+
setHeaders(res);
|
|
511
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
512
|
+
}
|
|
513
|
+
return res;
|
|
514
|
+
}
|
|
515
|
+
const response = await next();
|
|
516
|
+
if (response instanceof Response && headers) {
|
|
517
|
+
setHeaders(response);
|
|
518
|
+
}
|
|
519
|
+
return response;
|
|
520
|
+
};
|
|
521
|
+
rateLimitMiddleware.isBuiltin = true;
|
|
522
|
+
rateLimitMiddleware.pluginName = "RateLimit";
|
|
523
|
+
return rateLimitMiddleware;
|
|
524
|
+
}
|
|
402
525
|
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
403
526
|
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
404
527
|
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
@@ -495,6 +618,9 @@ const Patch = createMethodDecorator("PATCH");
|
|
|
495
618
|
const Options = createMethodDecorator("OPTIONS");
|
|
496
619
|
const Head = createMethodDecorator("HEAD");
|
|
497
620
|
const All = createMethodDecorator("ALL");
|
|
621
|
+
function RateLimit(options) {
|
|
622
|
+
return Use(RateLimitMiddleware(options));
|
|
623
|
+
}
|
|
498
624
|
class Container {
|
|
499
625
|
static services = /* @__PURE__ */ new Map();
|
|
500
626
|
static register(target, instance) {
|
|
@@ -536,17 +662,31 @@ const compose = (middleware) => {
|
|
|
536
662
|
}
|
|
537
663
|
return function dispatch(context, next) {
|
|
538
664
|
let index = -1;
|
|
539
|
-
function runner(i) {
|
|
665
|
+
async function runner(i) {
|
|
540
666
|
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
541
667
|
index = i;
|
|
542
668
|
if (i >= middleware.length) {
|
|
543
669
|
return next ? next() : Promise.resolve();
|
|
544
670
|
}
|
|
545
671
|
const fn = middleware[i];
|
|
672
|
+
if (!context._debug) {
|
|
673
|
+
return fn(context, () => runner(i + 1));
|
|
674
|
+
}
|
|
675
|
+
const debug = context._debug;
|
|
676
|
+
const debugId = fn._debugId || fn.name || "anonymous";
|
|
677
|
+
const previousNode = debug.getCurrentNode();
|
|
678
|
+
debug.trackEdge(previousNode, debugId);
|
|
679
|
+
debug.setNode(debugId);
|
|
680
|
+
const start = performance.now();
|
|
546
681
|
try {
|
|
547
|
-
|
|
682
|
+
const res = await Promise.resolve(fn(context, () => runner(i + 1)));
|
|
683
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "success");
|
|
684
|
+
return res;
|
|
548
685
|
} catch (err) {
|
|
686
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
|
|
549
687
|
return Promise.reject(err);
|
|
688
|
+
} finally {
|
|
689
|
+
if (previousNode) debug.setNode(previousNode);
|
|
550
690
|
}
|
|
551
691
|
}
|
|
552
692
|
return runner(0);
|
|
@@ -608,6 +748,15 @@ function deepMerge(target, ...sources) {
|
|
|
608
748
|
}
|
|
609
749
|
return deepMerge(target, ...sources);
|
|
610
750
|
}
|
|
751
|
+
const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
|
|
752
|
+
const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
|
|
753
|
+
const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
|
|
754
|
+
const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
|
|
755
|
+
const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
|
|
756
|
+
const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
|
|
757
|
+
const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
|
|
758
|
+
const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
|
|
759
|
+
const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
|
|
611
760
|
function analyzeHandler(handler) {
|
|
612
761
|
const handlerSource = handler.toString();
|
|
613
762
|
const inferredSpec = {};
|
|
@@ -617,46 +766,28 @@ function analyzeHandler(handler) {
|
|
|
617
766
|
};
|
|
618
767
|
}
|
|
619
768
|
const queryParams = /* @__PURE__ */ new Map();
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
queryIntMatch.forEach((match) => {
|
|
623
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
624
|
-
if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
|
|
625
|
-
});
|
|
769
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
|
|
770
|
+
if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
|
|
626
771
|
}
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
queryFloatMatch.forEach((match) => {
|
|
630
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
631
|
-
if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
|
|
632
|
-
});
|
|
772
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
|
|
773
|
+
if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
|
|
633
774
|
}
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (paramName && !queryParams.has(paramName)) {
|
|
639
|
-
queryParams.set(paramName, { type: "number" });
|
|
640
|
-
}
|
|
641
|
-
});
|
|
775
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
|
|
776
|
+
if (match[1] && !queryParams.has(match[1])) {
|
|
777
|
+
queryParams.set(match[1], { type: "number" });
|
|
778
|
+
}
|
|
642
779
|
}
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
queryParams.set(paramName, { type: "boolean" });
|
|
649
|
-
}
|
|
650
|
-
});
|
|
780
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
781
|
+
const name = match[1] || match[2];
|
|
782
|
+
if (name && !queryParams.has(name)) {
|
|
783
|
+
queryParams.set(name, { type: "boolean" });
|
|
784
|
+
}
|
|
651
785
|
}
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
queryParams.set(paramName, { type: "string" });
|
|
658
|
-
}
|
|
659
|
-
});
|
|
786
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
787
|
+
const name = match[1];
|
|
788
|
+
if (name && !queryParams.has(name)) {
|
|
789
|
+
queryParams.set(name, { type: "string" });
|
|
790
|
+
}
|
|
660
791
|
}
|
|
661
792
|
if (queryParams.size > 0) {
|
|
662
793
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -669,19 +800,11 @@ function analyzeHandler(handler) {
|
|
|
669
800
|
});
|
|
670
801
|
}
|
|
671
802
|
const pathParams = /* @__PURE__ */ new Map();
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
paramIntMatch.forEach((match) => {
|
|
675
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
676
|
-
if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
|
|
677
|
-
});
|
|
803
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
|
|
804
|
+
if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
|
|
678
805
|
}
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
paramFloatMatch.forEach((match) => {
|
|
682
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
683
|
-
if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
|
|
684
|
-
});
|
|
806
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
|
|
807
|
+
if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
|
|
685
808
|
}
|
|
686
809
|
if (pathParams.size > 0) {
|
|
687
810
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -694,76 +817,55 @@ function analyzeHandler(handler) {
|
|
|
694
817
|
});
|
|
695
818
|
});
|
|
696
819
|
}
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
schema: { type: "string" }
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
});
|
|
820
|
+
for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
|
|
821
|
+
if (match[1]) {
|
|
822
|
+
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
823
|
+
inferredSpec.parameters.push({
|
|
824
|
+
name: match[1],
|
|
825
|
+
in: "header",
|
|
826
|
+
schema: { type: "string" }
|
|
827
|
+
});
|
|
828
|
+
}
|
|
710
829
|
}
|
|
711
830
|
const responses = {};
|
|
712
831
|
if (handlerSource.includes("ctx.json(")) {
|
|
713
832
|
responses["200"] = {
|
|
714
833
|
description: "Successful response",
|
|
715
|
-
content: {
|
|
716
|
-
"application/json": { schema: { type: "object" } }
|
|
717
|
-
}
|
|
834
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
718
835
|
};
|
|
719
836
|
}
|
|
720
837
|
if (handlerSource.includes("ctx.html(")) {
|
|
721
838
|
responses["200"] = {
|
|
722
839
|
description: "Successful response",
|
|
723
|
-
content: {
|
|
724
|
-
"text/html": { schema: { type: "string" } }
|
|
725
|
-
}
|
|
840
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
726
841
|
};
|
|
727
842
|
}
|
|
728
843
|
if (handlerSource.includes("ctx.text(")) {
|
|
729
844
|
responses["200"] = {
|
|
730
845
|
description: "Successful response",
|
|
731
|
-
content: {
|
|
732
|
-
"text/plain": { schema: { type: "string" } }
|
|
733
|
-
}
|
|
846
|
+
content: { "text/plain": { schema: { type: "string" } } }
|
|
734
847
|
};
|
|
735
848
|
}
|
|
736
849
|
if (handlerSource.includes("ctx.file(")) {
|
|
737
850
|
responses["200"] = {
|
|
738
851
|
description: "File download",
|
|
739
|
-
content: {
|
|
740
|
-
"application/octet-stream": { schema: { type: "string", format: "binary" } }
|
|
741
|
-
}
|
|
852
|
+
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
|
|
742
853
|
};
|
|
743
854
|
}
|
|
744
855
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
745
|
-
responses["302"] = {
|
|
746
|
-
description: "Redirect"
|
|
747
|
-
};
|
|
856
|
+
responses["302"] = { description: "Redirect" };
|
|
748
857
|
}
|
|
749
858
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
750
859
|
responses["200"] = {
|
|
751
860
|
description: "Successful response",
|
|
752
|
-
content: {
|
|
753
|
-
"application/json": { schema: { type: "object" } }
|
|
754
|
-
}
|
|
861
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
755
862
|
};
|
|
756
863
|
}
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
responses[statusCode] = {
|
|
763
|
-
description: `Error response (${statusCode})`
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
});
|
|
864
|
+
for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
|
|
865
|
+
const statusCode = match[1];
|
|
866
|
+
if (statusCode && statusCode !== "200") {
|
|
867
|
+
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
868
|
+
}
|
|
767
869
|
}
|
|
768
870
|
if (Object.keys(responses).length > 0) {
|
|
769
871
|
inferredSpec.responses = responses;
|
|
@@ -777,7 +879,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
777
879
|
const defaultTagName = options.defaultTag || "Application";
|
|
778
880
|
let astRoutes = [];
|
|
779
881
|
try {
|
|
780
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-
|
|
882
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-D9YB3IkV.cjs"));
|
|
781
883
|
const analyzer = new OpenAPIAnalyzer(process.cwd());
|
|
782
884
|
const { applications } = await analyzer.analyze();
|
|
783
885
|
const appMap = /* @__PURE__ */ new Map();
|
|
@@ -1047,10 +1149,10 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1047
1149
|
};
|
|
1048
1150
|
}
|
|
1049
1151
|
const eta$1 = new eta$2.Eta();
|
|
1050
|
-
function serveStatic(
|
|
1152
|
+
function serveStatic(config, prefix) {
|
|
1051
1153
|
const rootPath = path.resolve(config.root || ".");
|
|
1052
1154
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1053
|
-
|
|
1155
|
+
const serveStaticMiddleware = async (ctx) => {
|
|
1054
1156
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
1055
1157
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
1056
1158
|
if (relative.length === 0) relative = "/";
|
|
@@ -1083,13 +1185,13 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1083
1185
|
let finalPath = requestPath;
|
|
1084
1186
|
let stats;
|
|
1085
1187
|
try {
|
|
1086
|
-
stats = await promises.stat(requestPath);
|
|
1188
|
+
stats = await promises$1.stat(requestPath);
|
|
1087
1189
|
} catch (e) {
|
|
1088
1190
|
if (config.extensions) {
|
|
1089
1191
|
for (const ext of config.extensions) {
|
|
1090
1192
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1091
1193
|
try {
|
|
1092
|
-
const s = await promises.stat(p);
|
|
1194
|
+
const s = await promises$1.stat(p);
|
|
1093
1195
|
if (s.isFile()) {
|
|
1094
1196
|
finalPath = p;
|
|
1095
1197
|
stats = s;
|
|
@@ -1118,7 +1220,7 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1118
1220
|
for (const idx of indexes) {
|
|
1119
1221
|
const idxPath = path.join(finalPath, idx);
|
|
1120
1222
|
try {
|
|
1121
|
-
const idxStats = await promises.stat(idxPath);
|
|
1223
|
+
const idxStats = await promises$1.stat(idxPath);
|
|
1122
1224
|
if (idxStats.isFile()) {
|
|
1123
1225
|
finalPath = idxPath;
|
|
1124
1226
|
foundIndex = true;
|
|
@@ -1130,7 +1232,7 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1130
1232
|
if (!foundIndex) {
|
|
1131
1233
|
if (config.listDirectory) {
|
|
1132
1234
|
try {
|
|
1133
|
-
const files = await promises.readdir(requestPath);
|
|
1235
|
+
const files = await promises$1.readdir(requestPath);
|
|
1134
1236
|
const listing = eta$1.renderString(`
|
|
1135
1237
|
<!DOCTYPE html>
|
|
1136
1238
|
<html>
|
|
@@ -1167,16 +1269,158 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1167
1269
|
}
|
|
1168
1270
|
}
|
|
1169
1271
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1272
|
+
let response;
|
|
1273
|
+
if (typeof Bun !== "undefined") {
|
|
1274
|
+
response = new Response(Bun.file(finalPath));
|
|
1275
|
+
} else {
|
|
1276
|
+
const fileBuffer = await promises$1.readFile(finalPath);
|
|
1277
|
+
response = new Response(fileBuffer);
|
|
1278
|
+
}
|
|
1172
1279
|
if (config.hooks?.onResponse) {
|
|
1173
1280
|
const hooked = await config.hooks.onResponse(ctx, response);
|
|
1174
1281
|
if (hooked) response = hooked;
|
|
1175
1282
|
}
|
|
1176
1283
|
return response;
|
|
1177
1284
|
};
|
|
1285
|
+
serveStaticMiddleware.isBuiltin = true;
|
|
1286
|
+
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1287
|
+
return serveStaticMiddleware;
|
|
1288
|
+
}
|
|
1289
|
+
class RouterTrie {
|
|
1290
|
+
root;
|
|
1291
|
+
constructor() {
|
|
1292
|
+
this.root = this.createNode();
|
|
1293
|
+
}
|
|
1294
|
+
createNode() {
|
|
1295
|
+
return {
|
|
1296
|
+
children: {}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
insert(method, path2, handler) {
|
|
1300
|
+
let node2 = this.root;
|
|
1301
|
+
const segments = this.splitPath(path2);
|
|
1302
|
+
for (const segment of segments) {
|
|
1303
|
+
if (segment === "**") {
|
|
1304
|
+
if (!node2.recursiveChild) {
|
|
1305
|
+
node2.recursiveChild = this.createNode();
|
|
1306
|
+
}
|
|
1307
|
+
node2 = node2.recursiveChild;
|
|
1308
|
+
} else if (segment === "*") {
|
|
1309
|
+
if (!node2.wildcardChild) {
|
|
1310
|
+
node2.wildcardChild = this.createNode();
|
|
1311
|
+
}
|
|
1312
|
+
node2 = node2.wildcardChild;
|
|
1313
|
+
} else if (segment.startsWith(":")) {
|
|
1314
|
+
const paramName = segment.slice(1);
|
|
1315
|
+
if (!node2.paramChild) {
|
|
1316
|
+
node2.paramChild = this.createNode();
|
|
1317
|
+
node2.paramChild.paramName = paramName;
|
|
1318
|
+
}
|
|
1319
|
+
node2 = node2.paramChild;
|
|
1320
|
+
node2.paramName = paramName;
|
|
1321
|
+
} else {
|
|
1322
|
+
if (!node2.children[segment]) {
|
|
1323
|
+
node2.children[segment] = this.createNode();
|
|
1324
|
+
}
|
|
1325
|
+
node2 = node2.children[segment];
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (!node2.handlers) {
|
|
1329
|
+
node2.handlers = {};
|
|
1330
|
+
}
|
|
1331
|
+
node2.handlers[method] = handler;
|
|
1332
|
+
}
|
|
1333
|
+
search(method, path2) {
|
|
1334
|
+
const segments = this.splitPath(path2);
|
|
1335
|
+
const params = {};
|
|
1336
|
+
const match = this.findNode(this.root, segments, 0, params);
|
|
1337
|
+
if (match && match.handlers) {
|
|
1338
|
+
const handler = match.handlers[method] || match.handlers["ALL"];
|
|
1339
|
+
if (handler) {
|
|
1340
|
+
return { handler, params };
|
|
1341
|
+
}
|
|
1342
|
+
if (method === "HEAD" && match.handlers["GET"]) {
|
|
1343
|
+
return { handler: match.handlers["GET"], params };
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
findNode(node2, segments, index, params) {
|
|
1349
|
+
if (index === segments.length) {
|
|
1350
|
+
if (node2.handlers) return node2;
|
|
1351
|
+
if (node2.recursiveChild && node2.recursiveChild.handlers) {
|
|
1352
|
+
return node2.recursiveChild;
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
const segment = segments[index];
|
|
1357
|
+
const child = node2.children[segment];
|
|
1358
|
+
if (child) {
|
|
1359
|
+
const result = this.findNode(child, segments, index + 1, params);
|
|
1360
|
+
if (result) return result;
|
|
1361
|
+
}
|
|
1362
|
+
if (node2.paramChild) {
|
|
1363
|
+
params[node2.paramChild.paramName] = segment;
|
|
1364
|
+
const result = this.findNode(node2.paramChild, segments, index + 1, params);
|
|
1365
|
+
if (result) return result;
|
|
1366
|
+
delete params[node2.paramChild.paramName];
|
|
1367
|
+
}
|
|
1368
|
+
if (node2.wildcardChild) {
|
|
1369
|
+
const result = this.findNode(node2.wildcardChild, segments, index + 1, params);
|
|
1370
|
+
if (result) return result;
|
|
1371
|
+
}
|
|
1372
|
+
if (node2.recursiveChild) {
|
|
1373
|
+
const remaining = segments.length - index;
|
|
1374
|
+
for (let k = 0; k <= remaining; k++) {
|
|
1375
|
+
const result = this.findNode(node2.recursiveChild, segments, index + k, params);
|
|
1376
|
+
if (result) return result;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
splitPath(path2) {
|
|
1382
|
+
if (path2 === "/" || path2 === "") return [];
|
|
1383
|
+
const s = path2.startsWith("/") ? path2.slice(1) : path2;
|
|
1384
|
+
if (s === "") return [];
|
|
1385
|
+
return s.split("/");
|
|
1386
|
+
}
|
|
1178
1387
|
}
|
|
1179
1388
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1389
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1390
|
+
const db = new surrealdb.Surreal({
|
|
1391
|
+
engines: node.createNodeEngines()
|
|
1392
|
+
});
|
|
1393
|
+
const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
|
|
1394
|
+
return db.query(`
|
|
1395
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1396
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1397
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1398
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1399
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1400
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1401
|
+
`);
|
|
1402
|
+
});
|
|
1403
|
+
const datastore = {
|
|
1404
|
+
get(store, key) {
|
|
1405
|
+
return db.select(new surrealdb.RecordId(store, key));
|
|
1406
|
+
},
|
|
1407
|
+
set(store, key, value) {
|
|
1408
|
+
return db.create(new surrealdb.RecordId(store, key)).content(value);
|
|
1409
|
+
},
|
|
1410
|
+
async query(query, vars) {
|
|
1411
|
+
try {
|
|
1412
|
+
const r = await db.query(query, vars).collect();
|
|
1413
|
+
return r;
|
|
1414
|
+
} catch (e) {
|
|
1415
|
+
console.error("DS ERROR:", e);
|
|
1416
|
+
throw e;
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
ready
|
|
1420
|
+
};
|
|
1421
|
+
process.on("exit", async () => {
|
|
1422
|
+
await db.close();
|
|
1423
|
+
});
|
|
1180
1424
|
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1181
1425
|
function traceHandler(fn, name) {
|
|
1182
1426
|
return async function(...args) {
|
|
@@ -1200,6 +1444,35 @@ function traceHandler(fn, name) {
|
|
|
1200
1444
|
});
|
|
1201
1445
|
};
|
|
1202
1446
|
}
|
|
1447
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1448
|
+
let file = "unknown";
|
|
1449
|
+
let line = 0;
|
|
1450
|
+
try {
|
|
1451
|
+
const err = new Error();
|
|
1452
|
+
const stack = err.stack?.split("\n") || [];
|
|
1453
|
+
let found = 0;
|
|
1454
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1455
|
+
const l = stack[i];
|
|
1456
|
+
if (!l.includes(":")) continue;
|
|
1457
|
+
if (l.includes("node_modules")) continue;
|
|
1458
|
+
if (l.includes("bun:main")) continue;
|
|
1459
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1460
|
+
if (l.includes("src/router.ts")) continue;
|
|
1461
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1462
|
+
found++;
|
|
1463
|
+
if (found >= skipFrames) {
|
|
1464
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1465
|
+
if (match) {
|
|
1466
|
+
file = match[1];
|
|
1467
|
+
line = parseInt(match[2], 10);
|
|
1468
|
+
return { file, line };
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
} catch (e) {
|
|
1473
|
+
}
|
|
1474
|
+
return { file, line };
|
|
1475
|
+
}
|
|
1203
1476
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1204
1477
|
const ShokupanApplicationTree = {};
|
|
1205
1478
|
class ShokupanRouter {
|
|
@@ -1219,6 +1492,7 @@ class ShokupanRouter {
|
|
|
1219
1492
|
[$parent] = null;
|
|
1220
1493
|
[$childRouters] = [];
|
|
1221
1494
|
[$childControllers] = [];
|
|
1495
|
+
middleware = [];
|
|
1222
1496
|
get rootConfig() {
|
|
1223
1497
|
return this[$appRoot]?.applicationConfig;
|
|
1224
1498
|
}
|
|
@@ -1227,7 +1501,66 @@ class ShokupanRouter {
|
|
|
1227
1501
|
}
|
|
1228
1502
|
[$routes] = [];
|
|
1229
1503
|
// Public via Symbol for OpenAPI generator
|
|
1504
|
+
trie = new RouterTrie();
|
|
1505
|
+
metadata;
|
|
1506
|
+
// Metadata for the router itself
|
|
1230
1507
|
currentGuards = [];
|
|
1508
|
+
// Registry Accessor
|
|
1509
|
+
getComponentRegistry() {
|
|
1510
|
+
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
1511
|
+
const localRoutes = [];
|
|
1512
|
+
for (const r of this[$routes]) {
|
|
1513
|
+
const entry = {
|
|
1514
|
+
type: "route",
|
|
1515
|
+
path: r.path,
|
|
1516
|
+
method: r.method,
|
|
1517
|
+
metadata: r.metadata,
|
|
1518
|
+
handlerName: r.handler.name,
|
|
1519
|
+
tags: r.handlerSpec?.tags,
|
|
1520
|
+
order: r.order,
|
|
1521
|
+
_fn: r.handler
|
|
1522
|
+
};
|
|
1523
|
+
if (r.controller) {
|
|
1524
|
+
if (!controllerRoutesMap.has(r.controller)) {
|
|
1525
|
+
controllerRoutesMap.set(r.controller, []);
|
|
1526
|
+
}
|
|
1527
|
+
controllerRoutesMap.get(r.controller).push(entry);
|
|
1528
|
+
} else {
|
|
1529
|
+
localRoutes.push(entry);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const mw = this.middleware;
|
|
1533
|
+
const middleware = mw ? mw.map((m) => ({
|
|
1534
|
+
name: m.name || "middleware",
|
|
1535
|
+
metadata: m.metadata,
|
|
1536
|
+
order: m.order,
|
|
1537
|
+
_fn: m
|
|
1538
|
+
// Expose function for debugging instrumentation
|
|
1539
|
+
})) : [];
|
|
1540
|
+
const routers = this[$childRouters].map((r) => ({
|
|
1541
|
+
type: "router",
|
|
1542
|
+
path: r[$mountPath],
|
|
1543
|
+
metadata: r.metadata,
|
|
1544
|
+
children: r.getComponentRegistry()
|
|
1545
|
+
}));
|
|
1546
|
+
const controllers = this[$childControllers].map((c) => {
|
|
1547
|
+
const routes = controllerRoutesMap.get(c) || [];
|
|
1548
|
+
return {
|
|
1549
|
+
type: "controller",
|
|
1550
|
+
path: c[$mountPath] || "/",
|
|
1551
|
+
name: c.constructor.name,
|
|
1552
|
+
metadata: c.metadata,
|
|
1553
|
+
children: { routes }
|
|
1554
|
+
};
|
|
1555
|
+
});
|
|
1556
|
+
return {
|
|
1557
|
+
metadata: this.metadata,
|
|
1558
|
+
middleware,
|
|
1559
|
+
routes: localRoutes,
|
|
1560
|
+
routers,
|
|
1561
|
+
controllers
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1231
1564
|
isRouterInstance(target) {
|
|
1232
1565
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1233
1566
|
}
|
|
@@ -1253,6 +1586,14 @@ class ShokupanRouter {
|
|
|
1253
1586
|
throw new Error("Router is already mounted");
|
|
1254
1587
|
}
|
|
1255
1588
|
controller[$mountPath] = prefix;
|
|
1589
|
+
if (!controller.metadata) {
|
|
1590
|
+
const info = getCallerInfo();
|
|
1591
|
+
controller.metadata = {
|
|
1592
|
+
file: info.file,
|
|
1593
|
+
line: info.line,
|
|
1594
|
+
name: "MountedRouter"
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1256
1597
|
this[$childRouters].push(controller);
|
|
1257
1598
|
controller[$parent] = this;
|
|
1258
1599
|
const setRouterContext = (router) => {
|
|
@@ -1285,6 +1626,12 @@ class ShokupanRouter {
|
|
|
1285
1626
|
}
|
|
1286
1627
|
}
|
|
1287
1628
|
instance[$mountPath] = prefix;
|
|
1629
|
+
const info = getCallerInfo();
|
|
1630
|
+
instance.metadata = {
|
|
1631
|
+
file: info.file,
|
|
1632
|
+
line: info.line,
|
|
1633
|
+
name: instance.constructor.name
|
|
1634
|
+
};
|
|
1288
1635
|
this[$childControllers].push(instance);
|
|
1289
1636
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1290
1637
|
const proto = Object.getPrototypeOf(instance);
|
|
@@ -1299,7 +1646,7 @@ class ShokupanRouter {
|
|
|
1299
1646
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1300
1647
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1301
1648
|
let routesAttached = 0;
|
|
1302
|
-
for (const name of methods) {
|
|
1649
|
+
for (const name of Array.from(methods)) {
|
|
1303
1650
|
if (name === "constructor") continue;
|
|
1304
1651
|
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1305
1652
|
const originalHandler = instance[name];
|
|
@@ -1368,14 +1715,39 @@ class ShokupanRouter {
|
|
|
1368
1715
|
for (const arg of sortedArgs) {
|
|
1369
1716
|
switch (arg.type) {
|
|
1370
1717
|
case RouteParamType.BODY:
|
|
1371
|
-
|
|
1718
|
+
try {
|
|
1719
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1720
|
+
args[arg.index] = await ctx.req.json();
|
|
1721
|
+
} else {
|
|
1722
|
+
const text = await ctx.req.text();
|
|
1723
|
+
if (!text) {
|
|
1724
|
+
args[arg.index] = {};
|
|
1725
|
+
} else {
|
|
1726
|
+
args[arg.index] = JSON.parse(text);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
} catch (e) {
|
|
1730
|
+
const err = new Error("Invalid JSON body");
|
|
1731
|
+
err.status = 400;
|
|
1732
|
+
throw err;
|
|
1733
|
+
}
|
|
1372
1734
|
break;
|
|
1373
1735
|
case RouteParamType.PARAM:
|
|
1374
1736
|
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1375
1737
|
break;
|
|
1376
1738
|
case RouteParamType.QUERY: {
|
|
1377
1739
|
const url = new URL(ctx.req.url);
|
|
1378
|
-
|
|
1740
|
+
if (arg.name) {
|
|
1741
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
1742
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1743
|
+
} else {
|
|
1744
|
+
const query = {};
|
|
1745
|
+
for (const key of url.searchParams.keys()) {
|
|
1746
|
+
const vals = url.searchParams.getAll(key);
|
|
1747
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1748
|
+
}
|
|
1749
|
+
args[arg.index] = query;
|
|
1750
|
+
}
|
|
1379
1751
|
break;
|
|
1380
1752
|
}
|
|
1381
1753
|
case RouteParamType.HEADER:
|
|
@@ -1408,7 +1780,7 @@ class ShokupanRouter {
|
|
|
1408
1780
|
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
1409
1781
|
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
1410
1782
|
const spec = { tags: [tagName], ...userSpec };
|
|
1411
|
-
this.add({ method, path: normalizedPath, handler: finalHandler, spec });
|
|
1783
|
+
this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
|
|
1412
1784
|
}
|
|
1413
1785
|
}
|
|
1414
1786
|
if (routesAttached === 0) {
|
|
@@ -1523,30 +1895,59 @@ class ShokupanRouter {
|
|
|
1523
1895
|
data: result
|
|
1524
1896
|
};
|
|
1525
1897
|
}
|
|
1526
|
-
|
|
1898
|
+
applyRouterHooks(match) {
|
|
1527
1899
|
if (!this.config?.hooks) return match;
|
|
1528
1900
|
const hooks = this.config.hooks;
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1901
|
+
return {
|
|
1902
|
+
...match,
|
|
1903
|
+
handler: this.wrapWithHooks(match.handler, hooks)
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
wrapWithHooks(handler, hooks) {
|
|
1907
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1908
|
+
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1909
|
+
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1910
|
+
const hasError = hookList.some((h) => !!h.onError);
|
|
1911
|
+
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1912
|
+
const originalHandler = handler;
|
|
1913
|
+
const wrapped = async (ctx) => {
|
|
1914
|
+
if (hasStart) {
|
|
1915
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1916
|
+
const h = hookList[i];
|
|
1917
|
+
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
const debug = ctx._debug;
|
|
1921
|
+
let debugId;
|
|
1922
|
+
let previousNode;
|
|
1923
|
+
if (debug) {
|
|
1924
|
+
debugId = originalHandler._debugId || originalHandler.name || "handler";
|
|
1925
|
+
previousNode = debug.getCurrentNode();
|
|
1926
|
+
debug.trackEdge(previousNode, debugId);
|
|
1927
|
+
debug.setNode(debugId);
|
|
1928
|
+
}
|
|
1929
|
+
const start = performance.now();
|
|
1533
1930
|
try {
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1931
|
+
const res = await originalHandler(ctx);
|
|
1932
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1933
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1934
|
+
const h = hookList[i];
|
|
1935
|
+
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1936
|
+
}
|
|
1937
|
+
return res;
|
|
1537
1938
|
} catch (err) {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
console.error("Error in router onError hook:", e);
|
|
1543
|
-
}
|
|
1939
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1940
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1941
|
+
const h = hookList[i];
|
|
1942
|
+
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1544
1943
|
}
|
|
1545
1944
|
throw err;
|
|
1945
|
+
} finally {
|
|
1946
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1546
1947
|
}
|
|
1547
1948
|
};
|
|
1548
|
-
|
|
1549
|
-
return
|
|
1949
|
+
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
1950
|
+
return wrapped;
|
|
1550
1951
|
}
|
|
1551
1952
|
/**
|
|
1552
1953
|
* Find a route matching the given method and path.
|
|
@@ -1555,24 +1956,10 @@ class ShokupanRouter {
|
|
|
1555
1956
|
* @returns Route handler and parameters if found, otherwise null
|
|
1556
1957
|
*/
|
|
1557
1958
|
find(method, path2) {
|
|
1558
|
-
|
|
1559
|
-
for (const route of routes) {
|
|
1560
|
-
if (route.method !== "ALL" && route.method !== m) continue;
|
|
1561
|
-
const match = route.regex.exec(path2);
|
|
1562
|
-
if (match) {
|
|
1563
|
-
const params = {};
|
|
1564
|
-
route.keys.forEach((key, index) => {
|
|
1565
|
-
params[key] = match[index + 1];
|
|
1566
|
-
});
|
|
1567
|
-
return this.applyHooks({ handler: route.handler, params });
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return null;
|
|
1571
|
-
};
|
|
1572
|
-
let result = findInRoutes(this[$routes], method);
|
|
1959
|
+
let result = this.trie.search(method, path2);
|
|
1573
1960
|
if (result) return result;
|
|
1574
1961
|
if (method === "HEAD") {
|
|
1575
|
-
result =
|
|
1962
|
+
result = this.trie.search("GET", path2);
|
|
1576
1963
|
if (result) return result;
|
|
1577
1964
|
}
|
|
1578
1965
|
for (const child of this[$childRouters]) {
|
|
@@ -1580,13 +1967,13 @@ class ShokupanRouter {
|
|
|
1580
1967
|
if (path2 === prefix || path2.startsWith(prefix + "/")) {
|
|
1581
1968
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1582
1969
|
const match = child.find(method, subPath);
|
|
1583
|
-
if (match) return this.
|
|
1970
|
+
if (match) return this.applyRouterHooks(match);
|
|
1584
1971
|
}
|
|
1585
1972
|
if (prefix.endsWith("/")) {
|
|
1586
1973
|
if (path2.startsWith(prefix)) {
|
|
1587
1974
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1588
1975
|
const match = child.find(method, subPath);
|
|
1589
|
-
if (match) return this.
|
|
1976
|
+
if (match) return this.applyRouterHooks(match);
|
|
1590
1977
|
}
|
|
1591
1978
|
}
|
|
1592
1979
|
}
|
|
@@ -1597,7 +1984,7 @@ class ShokupanRouter {
|
|
|
1597
1984
|
const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1598
1985
|
keys.push(key);
|
|
1599
1986
|
return "([^/]+)";
|
|
1600
|
-
}).replace(
|
|
1987
|
+
}).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
|
|
1601
1988
|
return {
|
|
1602
1989
|
regex: new RegExp(`^${pattern}$`),
|
|
1603
1990
|
keys
|
|
@@ -1614,8 +2001,23 @@ class ShokupanRouter {
|
|
|
1614
2001
|
* @param handler - Route handler function
|
|
1615
2002
|
* @param requestTimeout - Timeout for this route in milliseconds
|
|
1616
2003
|
*/
|
|
1617
|
-
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer }) {
|
|
2004
|
+
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
1618
2005
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
|
|
2006
|
+
if (this.currentGuards.length > 0) {
|
|
2007
|
+
spec = spec || {};
|
|
2008
|
+
for (const guard of this.currentGuards) {
|
|
2009
|
+
if (guard.spec) {
|
|
2010
|
+
if (guard.spec.responses) {
|
|
2011
|
+
spec.responses = spec.responses || {};
|
|
2012
|
+
Object.assign(spec.responses, guard.spec.responses);
|
|
2013
|
+
}
|
|
2014
|
+
if (guard.spec.security) {
|
|
2015
|
+
spec.security = spec.security || [];
|
|
2016
|
+
spec.security.push(...guard.spec.security);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
1619
2021
|
let wrappedHandler = handler;
|
|
1620
2022
|
const routeGuards = [...this.currentGuards];
|
|
1621
2023
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
@@ -1666,47 +2068,85 @@ class ShokupanRouter {
|
|
|
1666
2068
|
return innerHandler(ctx);
|
|
1667
2069
|
};
|
|
1668
2070
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
try {
|
|
1672
|
-
const err = new Error();
|
|
1673
|
-
const stack = err.stack?.split("\n") || [];
|
|
1674
|
-
const callerLine = stack.find(
|
|
1675
|
-
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
1676
|
-
);
|
|
1677
|
-
if (callerLine) {
|
|
1678
|
-
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1679
|
-
if (match) {
|
|
1680
|
-
file = match[1];
|
|
1681
|
-
line = parseInt(match[2], 10);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
} catch (e) {
|
|
1685
|
-
}
|
|
1686
|
-
const trackedHandler = wrappedHandler;
|
|
2071
|
+
const { file, line } = getCallerInfo();
|
|
2072
|
+
const trackingHandler = wrappedHandler;
|
|
1687
2073
|
wrappedHandler = async (ctx) => {
|
|
1688
|
-
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1689
|
-
ctx
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2074
|
+
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2075
|
+
return trackingHandler(ctx);
|
|
2076
|
+
}
|
|
2077
|
+
const startTime = performance.now();
|
|
2078
|
+
let error = void 0;
|
|
2079
|
+
try {
|
|
2080
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2081
|
+
ctx.handlerStack.push({
|
|
2082
|
+
name: handler.name || "anonymous",
|
|
2083
|
+
file,
|
|
2084
|
+
line
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
return await trackingHandler(ctx);
|
|
2088
|
+
} catch (e) {
|
|
2089
|
+
error = e;
|
|
2090
|
+
throw e;
|
|
2091
|
+
} finally {
|
|
2092
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2093
|
+
const duration = performance.now() - startTime;
|
|
2094
|
+
const config = ctx.app.applicationConfig;
|
|
2095
|
+
try {
|
|
2096
|
+
const timestamp = Date.now();
|
|
2097
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2098
|
+
await datastore.set("middleware_tracking", key, {
|
|
2099
|
+
name: handler.name || "anonymous",
|
|
2100
|
+
path: ctx.path,
|
|
2101
|
+
timestamp,
|
|
2102
|
+
duration,
|
|
2103
|
+
file,
|
|
2104
|
+
line,
|
|
2105
|
+
error: error ? String(error) : void 0,
|
|
2106
|
+
metadata: {
|
|
2107
|
+
isBuiltin: handler.isBuiltin,
|
|
2108
|
+
pluginName: handler.pluginName
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2112
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2113
|
+
const cutoff = Date.now() - ttl;
|
|
2114
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2115
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2116
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2117
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2118
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2119
|
+
}
|
|
2120
|
+
} catch (datastoreError) {
|
|
2121
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
1694
2124
|
}
|
|
1695
|
-
return trackedHandler(ctx);
|
|
1696
2125
|
};
|
|
1697
|
-
wrappedHandler.originalHandler =
|
|
2126
|
+
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2127
|
+
let bakedHandler = wrappedHandler;
|
|
2128
|
+
if (this.config?.hooks) {
|
|
2129
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
|
|
2130
|
+
}
|
|
1698
2131
|
this[$routes].push({
|
|
1699
2132
|
method,
|
|
1700
2133
|
path: path2,
|
|
1701
|
-
regex,
|
|
1702
|
-
keys,
|
|
1703
|
-
handler
|
|
2134
|
+
regex: regex ?? new RegExp(""),
|
|
2135
|
+
keys: keys ?? [],
|
|
2136
|
+
handler,
|
|
2137
|
+
bakedHandler,
|
|
1704
2138
|
handlerSpec: spec,
|
|
1705
2139
|
group,
|
|
1706
|
-
|
|
1707
|
-
requestTimeout
|
|
1708
|
-
|
|
2140
|
+
hooks: this.config?.hooks,
|
|
2141
|
+
requestTimeout,
|
|
2142
|
+
renderer,
|
|
2143
|
+
metadata: {
|
|
2144
|
+
file,
|
|
2145
|
+
line
|
|
2146
|
+
},
|
|
2147
|
+
controller
|
|
1709
2148
|
});
|
|
2149
|
+
this.trie.insert(method, path2, bakedHandler);
|
|
1710
2150
|
return this;
|
|
1711
2151
|
}
|
|
1712
2152
|
get(path2, ...args) {
|
|
@@ -1780,10 +2220,10 @@ class ShokupanRouter {
|
|
|
1780
2220
|
const config = typeof options === "string" ? { root: options } : options;
|
|
1781
2221
|
const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
|
|
1782
2222
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1783
|
-
serveStatic(
|
|
2223
|
+
const handlerMiddleware = serveStatic(config, prefix);
|
|
1784
2224
|
const routeHandler = async (ctx) => {
|
|
1785
|
-
|
|
1786
|
-
|
|
2225
|
+
return handlerMiddleware(ctx, async () => {
|
|
2226
|
+
});
|
|
1787
2227
|
};
|
|
1788
2228
|
let groupName = "Static";
|
|
1789
2229
|
const segments = normalizedPrefix.split("/").filter(Boolean);
|
|
@@ -1844,6 +2284,49 @@ class ShokupanRouter {
|
|
|
1844
2284
|
return generateOpenApi(this, options);
|
|
1845
2285
|
}
|
|
1846
2286
|
}
|
|
2287
|
+
class SystemCpuMonitor {
|
|
2288
|
+
constructor(intervalMs = 1e3) {
|
|
2289
|
+
this.intervalMs = intervalMs;
|
|
2290
|
+
}
|
|
2291
|
+
interval = null;
|
|
2292
|
+
lastCpus = [];
|
|
2293
|
+
currentUsage = 0;
|
|
2294
|
+
start() {
|
|
2295
|
+
if (this.interval) return;
|
|
2296
|
+
this.lastCpus = os__namespace.cpus();
|
|
2297
|
+
this.interval = setInterval(() => this.update(), this.intervalMs);
|
|
2298
|
+
}
|
|
2299
|
+
stop() {
|
|
2300
|
+
if (this.interval) {
|
|
2301
|
+
clearInterval(this.interval);
|
|
2302
|
+
this.interval = null;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
getUsage() {
|
|
2306
|
+
return this.currentUsage;
|
|
2307
|
+
}
|
|
2308
|
+
update() {
|
|
2309
|
+
const cpus = os__namespace.cpus();
|
|
2310
|
+
let idle = 0;
|
|
2311
|
+
let total = 0;
|
|
2312
|
+
for (let i = 0; i < cpus.length; i++) {
|
|
2313
|
+
const cpu = cpus[i];
|
|
2314
|
+
const prev = this.lastCpus[i];
|
|
2315
|
+
let type;
|
|
2316
|
+
for (type in cpu.times) {
|
|
2317
|
+
const ticks = cpu.times[type];
|
|
2318
|
+
const prevTicks = prev.times[type];
|
|
2319
|
+
const diff = ticks - prevTicks;
|
|
2320
|
+
total += diff;
|
|
2321
|
+
if (type === "idle") {
|
|
2322
|
+
idle += diff;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
this.lastCpus = cpus;
|
|
2327
|
+
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
1847
2330
|
const defaults = {
|
|
1848
2331
|
port: 3e3,
|
|
1849
2332
|
hostname: "localhost",
|
|
@@ -1855,51 +2338,67 @@ api.trace.getTracer("shokupan.application");
|
|
|
1855
2338
|
class Shokupan extends ShokupanRouter {
|
|
1856
2339
|
applicationConfig = {};
|
|
1857
2340
|
openApiSpec;
|
|
1858
|
-
middleware = [];
|
|
1859
2341
|
composedMiddleware;
|
|
2342
|
+
cpuMonitor;
|
|
2343
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
2344
|
+
hooksInitialized = false;
|
|
1860
2345
|
get logger() {
|
|
1861
2346
|
return this.applicationConfig.logger;
|
|
1862
2347
|
}
|
|
1863
2348
|
constructor(applicationConfig = {}) {
|
|
1864
|
-
|
|
2349
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2350
|
+
const { hooks, ...routerConfig } = config;
|
|
2351
|
+
super(routerConfig);
|
|
1865
2352
|
this[$isApplication] = true;
|
|
1866
2353
|
this[$appRoot] = this;
|
|
1867
|
-
|
|
2354
|
+
this.applicationConfig = config;
|
|
2355
|
+
const { file, line } = getCallerInfo();
|
|
2356
|
+
this.metadata = {
|
|
2357
|
+
file,
|
|
2358
|
+
line,
|
|
2359
|
+
name: "ShokupanApplication"
|
|
2360
|
+
};
|
|
1868
2361
|
}
|
|
1869
2362
|
/**
|
|
1870
2363
|
* Adds middleware to the application.
|
|
1871
2364
|
*/
|
|
1872
2365
|
use(middleware) {
|
|
1873
2366
|
let trackedMiddleware = middleware;
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
if (callerLine) {
|
|
1884
|
-
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1885
|
-
if (match) {
|
|
1886
|
-
file = match[1];
|
|
1887
|
-
line = parseInt(match[2], 10);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
} catch (e) {
|
|
2367
|
+
const { file, line } = getCallerInfo();
|
|
2368
|
+
if (!middleware.metadata) {
|
|
2369
|
+
middleware.metadata = {
|
|
2370
|
+
file,
|
|
2371
|
+
line,
|
|
2372
|
+
name: middleware.name || "middleware",
|
|
2373
|
+
isBuiltin: middleware.isBuiltin,
|
|
2374
|
+
pluginName: middleware.pluginName
|
|
2375
|
+
};
|
|
1891
2376
|
}
|
|
1892
2377
|
trackedMiddleware = async (ctx, next) => {
|
|
1893
2378
|
const c = ctx;
|
|
1894
2379
|
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2380
|
+
const metadata = middleware.metadata || {};
|
|
2381
|
+
const start = performance.now();
|
|
2382
|
+
const item = {
|
|
2383
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2384
|
+
file: metadata.file || file,
|
|
2385
|
+
line: metadata.line || line,
|
|
2386
|
+
isBuiltin: metadata.isBuiltin,
|
|
2387
|
+
startTime: start,
|
|
2388
|
+
duration: -1
|
|
2389
|
+
};
|
|
2390
|
+
c.handlerStack.push(item);
|
|
2391
|
+
try {
|
|
2392
|
+
return await middleware(ctx, next);
|
|
2393
|
+
} finally {
|
|
2394
|
+
item.duration = performance.now() - start;
|
|
2395
|
+
}
|
|
1900
2396
|
}
|
|
1901
2397
|
return middleware(ctx, next);
|
|
1902
2398
|
};
|
|
2399
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2400
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2401
|
+
trackedMiddleware.order = this.middleware.length;
|
|
1903
2402
|
this.middleware.push(trackedMiddleware);
|
|
1904
2403
|
return this;
|
|
1905
2404
|
}
|
|
@@ -1911,6 +2410,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1911
2410
|
this.startupHooks.push(callback);
|
|
1912
2411
|
return this;
|
|
1913
2412
|
}
|
|
2413
|
+
specAvailableHooks = [];
|
|
2414
|
+
/**
|
|
2415
|
+
* Registers a callback to be executed when the OpenAPI spec is available.
|
|
2416
|
+
* This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
|
|
2417
|
+
*/
|
|
2418
|
+
onSpecAvailable(callback) {
|
|
2419
|
+
this.specAvailableHooks.push(callback);
|
|
2420
|
+
return this;
|
|
2421
|
+
}
|
|
1914
2422
|
/**
|
|
1915
2423
|
* Starts the application server.
|
|
1916
2424
|
*
|
|
@@ -1927,8 +2435,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1927
2435
|
}
|
|
1928
2436
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
1929
2437
|
this.openApiSpec = await generateOpenApi(this);
|
|
2438
|
+
for (const hook of this.specAvailableHooks) {
|
|
2439
|
+
await hook(this.openApiSpec);
|
|
2440
|
+
}
|
|
1930
2441
|
}
|
|
1931
2442
|
if (port === 0 && process.platform === "linux") ;
|
|
2443
|
+
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2444
|
+
this.cpuMonitor = new SystemCpuMonitor();
|
|
2445
|
+
this.cpuMonitor.start();
|
|
2446
|
+
}
|
|
1932
2447
|
const serveOptions = {
|
|
1933
2448
|
port: finalPort,
|
|
1934
2449
|
hostname: this.applicationConfig.hostname,
|
|
@@ -1953,7 +2468,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
1953
2468
|
};
|
|
1954
2469
|
let factory = this.applicationConfig.serverFactory;
|
|
1955
2470
|
if (!factory && typeof Bun === "undefined") {
|
|
1956
|
-
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-
|
|
2471
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
|
|
1957
2472
|
factory = createHttpServer();
|
|
1958
2473
|
}
|
|
1959
2474
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
@@ -2038,11 +2553,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2038
2553
|
}
|
|
2039
2554
|
async handleRequest(req, server) {
|
|
2040
2555
|
const request = req;
|
|
2041
|
-
const
|
|
2556
|
+
const controller = new AbortController();
|
|
2557
|
+
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
|
|
2042
2558
|
const handle = async () => {
|
|
2559
|
+
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2560
|
+
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2561
|
+
const res = ctx.text(msg, 429);
|
|
2562
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2563
|
+
return res;
|
|
2564
|
+
}
|
|
2043
2565
|
try {
|
|
2044
|
-
if (this.
|
|
2045
|
-
await this.
|
|
2566
|
+
if (this.hasHook("onRequestStart")) {
|
|
2567
|
+
await this.executeHook("onRequestStart", ctx);
|
|
2046
2568
|
}
|
|
2047
2569
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2048
2570
|
const result = await fn(ctx, async () => {
|
|
@@ -2058,23 +2580,24 @@ class Shokupan extends ShokupanRouter {
|
|
|
2058
2580
|
response = result;
|
|
2059
2581
|
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2060
2582
|
response = ctx._finalResponse;
|
|
2061
|
-
} else if ((result === null || result === void 0) && ctx.response.status === 404) {
|
|
2062
|
-
const span = asyncContext.getStore()?.get("span");
|
|
2063
|
-
if (span) span.setAttribute("http.status_code", 404);
|
|
2064
|
-
response = ctx.text("Not Found", 404);
|
|
2065
2583
|
} else if (result === null || result === void 0) {
|
|
2066
|
-
if (ctx._finalResponse
|
|
2067
|
-
|
|
2584
|
+
if (ctx._finalResponse instanceof Response) {
|
|
2585
|
+
response = ctx._finalResponse;
|
|
2586
|
+
} else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
|
|
2587
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2588
|
+
} else {
|
|
2589
|
+
response = ctx.text("Not Found", 404);
|
|
2590
|
+
}
|
|
2068
2591
|
} else if (typeof result === "object") {
|
|
2069
2592
|
response = ctx.json(result);
|
|
2070
2593
|
} else {
|
|
2071
2594
|
response = ctx.text(String(result));
|
|
2072
2595
|
}
|
|
2073
|
-
if (this.
|
|
2074
|
-
await this.
|
|
2596
|
+
if (this.hasHook("onRequestEnd")) {
|
|
2597
|
+
await this.executeHook("onRequestEnd", ctx);
|
|
2075
2598
|
}
|
|
2076
|
-
if (this.
|
|
2077
|
-
await this.
|
|
2599
|
+
if (this.hasHook("onResponseStart")) {
|
|
2600
|
+
await this.executeHook("onResponseStart", ctx, response);
|
|
2078
2601
|
}
|
|
2079
2602
|
return response;
|
|
2080
2603
|
} catch (err) {
|
|
@@ -2084,28 +2607,21 @@ class Shokupan extends ShokupanRouter {
|
|
|
2084
2607
|
const status = err.status || err.statusCode || 500;
|
|
2085
2608
|
const body = { error: err.message || "Internal Server Error" };
|
|
2086
2609
|
if (err.errors) body.errors = err.errors;
|
|
2087
|
-
if (this.
|
|
2088
|
-
|
|
2089
|
-
await this.applicationConfig.hooks.onError(err, ctx);
|
|
2090
|
-
} catch (hookErr) {
|
|
2091
|
-
console.error("Error in onError hook:", hookErr);
|
|
2092
|
-
}
|
|
2610
|
+
if (this.hasHook("onError")) {
|
|
2611
|
+
await this.executeHook("onError", err, ctx);
|
|
2093
2612
|
}
|
|
2094
2613
|
return ctx.json(body, status);
|
|
2095
2614
|
}
|
|
2096
2615
|
};
|
|
2097
2616
|
let executionPromise = handle();
|
|
2098
2617
|
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2099
|
-
if (timeoutMs && timeoutMs > 0
|
|
2618
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2100
2619
|
let timeoutId;
|
|
2101
2620
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2102
2621
|
timeoutId = setTimeout(async () => {
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
}
|
|
2107
|
-
} catch (e) {
|
|
2108
|
-
console.error("Error in onRequestTimeout hook:", e);
|
|
2622
|
+
controller.abort();
|
|
2623
|
+
if (this.hasHook("onRequestTimeout")) {
|
|
2624
|
+
await this.executeHook("onRequestTimeout", ctx);
|
|
2109
2625
|
}
|
|
2110
2626
|
reject(new Error("Request Timeout"));
|
|
2111
2627
|
}, timeoutMs);
|
|
@@ -2119,12 +2635,56 @@ class Shokupan extends ShokupanRouter {
|
|
|
2119
2635
|
console.error("Unexpected error in request execution:", err);
|
|
2120
2636
|
return ctx.text("Internal Server Error", 500);
|
|
2121
2637
|
}).then(async (res) => {
|
|
2122
|
-
if (this.
|
|
2123
|
-
await this.
|
|
2638
|
+
if (this.hasHook("onResponseEnd")) {
|
|
2639
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2124
2640
|
}
|
|
2125
2641
|
return res;
|
|
2126
2642
|
});
|
|
2127
2643
|
}
|
|
2644
|
+
ensureHooksInitialized() {
|
|
2645
|
+
const hooks = this.applicationConfig.hooks;
|
|
2646
|
+
if (hooks) {
|
|
2647
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2648
|
+
const hookTypes = [
|
|
2649
|
+
"onRequestStart",
|
|
2650
|
+
"onRequestEnd",
|
|
2651
|
+
"onResponseStart",
|
|
2652
|
+
"onResponseEnd",
|
|
2653
|
+
"onError",
|
|
2654
|
+
"beforeValidate",
|
|
2655
|
+
"afterValidate",
|
|
2656
|
+
"onRequestTimeout",
|
|
2657
|
+
"onReadTimeout",
|
|
2658
|
+
"onWriteTimeout"
|
|
2659
|
+
];
|
|
2660
|
+
for (const type of hookTypes) {
|
|
2661
|
+
const fns = [];
|
|
2662
|
+
for (const h of hookList) {
|
|
2663
|
+
if (h[type]) fns.push(h[type]);
|
|
2664
|
+
}
|
|
2665
|
+
if (fns.length > 0) {
|
|
2666
|
+
this.hookCache.set(type, fns);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
this.hooksInitialized = true;
|
|
2671
|
+
}
|
|
2672
|
+
async executeHook(name, ...args) {
|
|
2673
|
+
if (!this.hooksInitialized) {
|
|
2674
|
+
this.ensureHooksInitialized();
|
|
2675
|
+
}
|
|
2676
|
+
const fns = this.hookCache.get(name);
|
|
2677
|
+
if (!fns) return;
|
|
2678
|
+
for (const fn of fns) {
|
|
2679
|
+
await fn(...args);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
hasHook(name) {
|
|
2683
|
+
if (!this.hooksInitialized) {
|
|
2684
|
+
this.ensureHooksInitialized();
|
|
2685
|
+
}
|
|
2686
|
+
return this.hookCache.has(name);
|
|
2687
|
+
}
|
|
2128
2688
|
}
|
|
2129
2689
|
class AuthPlugin extends ShokupanRouter {
|
|
2130
2690
|
constructor(authConfig) {
|
|
@@ -2329,7 +2889,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2329
2889
|
/**
|
|
2330
2890
|
* Middleware to verify JWT
|
|
2331
2891
|
*/
|
|
2332
|
-
|
|
2892
|
+
getMiddleware() {
|
|
2333
2893
|
return async (ctx, next) => {
|
|
2334
2894
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
2335
2895
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
@@ -2350,12 +2910,16 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2350
2910
|
}
|
|
2351
2911
|
function Compression(options = {}) {
|
|
2352
2912
|
const threshold = options.threshold ?? 512;
|
|
2353
|
-
|
|
2913
|
+
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
2354
2914
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2355
2915
|
let method = null;
|
|
2356
2916
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2357
|
-
else if (acceptEncoding.includes("zstd"))
|
|
2358
|
-
|
|
2917
|
+
else if (acceptEncoding.includes("zstd")) {
|
|
2918
|
+
if (typeof Bun === "undefined") {
|
|
2919
|
+
throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
|
|
2920
|
+
}
|
|
2921
|
+
method = "zstd";
|
|
2922
|
+
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2359
2923
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2360
2924
|
if (!method) return next();
|
|
2361
2925
|
let response = await next();
|
|
@@ -2364,21 +2928,37 @@ function Compression(options = {}) {
|
|
|
2364
2928
|
}
|
|
2365
2929
|
if (response instanceof Response) {
|
|
2366
2930
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2367
|
-
|
|
2368
|
-
|
|
2931
|
+
let body;
|
|
2932
|
+
let bodySize;
|
|
2933
|
+
if (ctx._rawBody !== void 0) {
|
|
2934
|
+
if (typeof ctx._rawBody === "string") {
|
|
2935
|
+
const encoded = new TextEncoder().encode(ctx._rawBody);
|
|
2936
|
+
body = encoded;
|
|
2937
|
+
bodySize = encoded.byteLength;
|
|
2938
|
+
} else if (ctx._rawBody instanceof Uint8Array) {
|
|
2939
|
+
body = ctx._rawBody;
|
|
2940
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2941
|
+
} else {
|
|
2942
|
+
body = ctx._rawBody;
|
|
2943
|
+
bodySize = body.byteLength;
|
|
2944
|
+
}
|
|
2945
|
+
} else {
|
|
2946
|
+
body = await response.arrayBuffer();
|
|
2947
|
+
bodySize = body.byteLength;
|
|
2948
|
+
}
|
|
2949
|
+
if (bodySize < threshold) {
|
|
2369
2950
|
return new Response(body, {
|
|
2370
2951
|
status: response.status,
|
|
2371
2952
|
statusText: response.statusText,
|
|
2372
|
-
headers: response.headers
|
|
2953
|
+
headers: new Headers(response.headers)
|
|
2373
2954
|
});
|
|
2374
2955
|
}
|
|
2375
2956
|
let compressed;
|
|
2376
2957
|
switch (method) {
|
|
2377
2958
|
case "br":
|
|
2378
|
-
|
|
2379
|
-
compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
|
|
2959
|
+
compressed = await new Promise((res, rej) => zlib__namespace.brotliCompress(body, {
|
|
2380
2960
|
params: {
|
|
2381
|
-
[
|
|
2961
|
+
[zlib__namespace.constants.BROTLI_PARAM_QUALITY]: 4
|
|
2382
2962
|
}
|
|
2383
2963
|
}, (err, data) => {
|
|
2384
2964
|
if (err) return rej(err);
|
|
@@ -2386,13 +2966,19 @@ function Compression(options = {}) {
|
|
|
2386
2966
|
}));
|
|
2387
2967
|
break;
|
|
2388
2968
|
case "gzip":
|
|
2389
|
-
compressed =
|
|
2969
|
+
compressed = await new Promise((res, rej) => zlib__namespace.gzip(body, (err, data) => {
|
|
2970
|
+
if (err) return rej(err);
|
|
2971
|
+
res(data);
|
|
2972
|
+
}));
|
|
2390
2973
|
break;
|
|
2391
2974
|
case "zstd":
|
|
2392
2975
|
compressed = await Bun.zstdCompress(body);
|
|
2393
2976
|
break;
|
|
2394
2977
|
default:
|
|
2395
|
-
compressed =
|
|
2978
|
+
compressed = await new Promise((res, rej) => zlib__namespace.deflate(body, (err, data) => {
|
|
2979
|
+
if (err) return rej(err);
|
|
2980
|
+
res(data);
|
|
2981
|
+
}));
|
|
2396
2982
|
break;
|
|
2397
2983
|
}
|
|
2398
2984
|
const headers = new Headers(response.headers);
|
|
@@ -2406,6 +2992,9 @@ function Compression(options = {}) {
|
|
|
2406
2992
|
}
|
|
2407
2993
|
return response;
|
|
2408
2994
|
};
|
|
2995
|
+
compressionMiddleware.isBuiltin = true;
|
|
2996
|
+
compressionMiddleware.pluginName = "Compression";
|
|
2997
|
+
return compressionMiddleware;
|
|
2409
2998
|
}
|
|
2410
2999
|
function Cors(options = {}) {
|
|
2411
3000
|
const defaults2 = {
|
|
@@ -2415,7 +3004,7 @@ function Cors(options = {}) {
|
|
|
2415
3004
|
optionsSuccessStatus: 204
|
|
2416
3005
|
};
|
|
2417
3006
|
const opts = { ...defaults2, ...options };
|
|
2418
|
-
|
|
3007
|
+
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
2419
3008
|
const headers = new Headers();
|
|
2420
3009
|
const origin = ctx.headers.get("origin");
|
|
2421
3010
|
const set = (k, v) => headers.set(k, v);
|
|
@@ -2477,6 +3066,9 @@ function Cors(options = {}) {
|
|
|
2477
3066
|
}
|
|
2478
3067
|
return response;
|
|
2479
3068
|
};
|
|
3069
|
+
corsMiddleware.isBuiltin = true;
|
|
3070
|
+
corsMiddleware.pluginName = "Cors";
|
|
3071
|
+
return corsMiddleware;
|
|
2480
3072
|
}
|
|
2481
3073
|
function useExpress(expressMiddleware) {
|
|
2482
3074
|
return async (ctx, next) => {
|
|
@@ -2644,7 +3236,33 @@ const safelyGetBody = async (ctx) => {
|
|
|
2644
3236
|
return {};
|
|
2645
3237
|
}
|
|
2646
3238
|
};
|
|
3239
|
+
function getValidator(schema) {
|
|
3240
|
+
if (isZod(schema)) {
|
|
3241
|
+
return (data) => validateZod(schema, data);
|
|
3242
|
+
}
|
|
3243
|
+
if (isTypeBox(schema)) {
|
|
3244
|
+
return (data) => validateTypeBox(schema, data);
|
|
3245
|
+
}
|
|
3246
|
+
if (isAjv(schema)) {
|
|
3247
|
+
return (data) => validateAjv(schema, data);
|
|
3248
|
+
}
|
|
3249
|
+
if (isValibotWrapper(schema)) {
|
|
3250
|
+
return (data) => validateValibotWrapper(schema, data);
|
|
3251
|
+
}
|
|
3252
|
+
if (isClass(schema)) {
|
|
3253
|
+
return (data) => validateClassValidator(schema, data);
|
|
3254
|
+
}
|
|
3255
|
+
if (typeof schema === "function") {
|
|
3256
|
+
return schema;
|
|
3257
|
+
}
|
|
3258
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3259
|
+
}
|
|
2647
3260
|
function validate(config) {
|
|
3261
|
+
const validators = {};
|
|
3262
|
+
if (config.params) validators.params = getValidator(config.params);
|
|
3263
|
+
if (config.query) validators.query = getValidator(config.query);
|
|
3264
|
+
if (config.headers) validators.headers = getValidator(config.headers);
|
|
3265
|
+
if (config.body) validators.body = getValidator(config.body);
|
|
2648
3266
|
return async (ctx, next) => {
|
|
2649
3267
|
const dataToValidate = {};
|
|
2650
3268
|
if (config.params) dataToValidate.params = ctx.params;
|
|
@@ -2663,21 +3281,21 @@ function validate(config) {
|
|
|
2663
3281
|
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2664
3282
|
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2665
3283
|
}
|
|
2666
|
-
if (
|
|
2667
|
-
ctx.params = await
|
|
3284
|
+
if (validators.params) {
|
|
3285
|
+
ctx.params = await validators.params(ctx.params);
|
|
2668
3286
|
}
|
|
2669
3287
|
let validQuery;
|
|
2670
|
-
if (
|
|
2671
|
-
validQuery = await
|
|
3288
|
+
if (validators.query && queryObj) {
|
|
3289
|
+
validQuery = await validators.query(queryObj);
|
|
2672
3290
|
}
|
|
2673
|
-
if (
|
|
3291
|
+
if (validators.headers) {
|
|
2674
3292
|
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2675
|
-
await
|
|
3293
|
+
await validators.headers(headersObj);
|
|
2676
3294
|
}
|
|
2677
3295
|
let validBody;
|
|
2678
|
-
if (
|
|
3296
|
+
if (validators.body) {
|
|
2679
3297
|
const b = body ?? await safelyGetBody(ctx);
|
|
2680
|
-
validBody = await
|
|
3298
|
+
validBody = await validators.body(b);
|
|
2681
3299
|
const req = ctx.req;
|
|
2682
3300
|
req._bodyValue = validBody;
|
|
2683
3301
|
Object.defineProperty(req, "json", {
|
|
@@ -2696,36 +3314,6 @@ function validate(config) {
|
|
|
2696
3314
|
return next();
|
|
2697
3315
|
};
|
|
2698
3316
|
}
|
|
2699
|
-
async function runValidation(schema, data) {
|
|
2700
|
-
if (isZod(schema)) {
|
|
2701
|
-
return validateZod(schema, data);
|
|
2702
|
-
}
|
|
2703
|
-
if (isTypeBox(schema)) {
|
|
2704
|
-
return validateTypeBox(schema, data);
|
|
2705
|
-
}
|
|
2706
|
-
if (isAjv(schema)) {
|
|
2707
|
-
return validateAjv(schema, data);
|
|
2708
|
-
}
|
|
2709
|
-
if (isValibotWrapper(schema)) {
|
|
2710
|
-
return validateValibotWrapper(schema, data);
|
|
2711
|
-
}
|
|
2712
|
-
if (isClass(schema)) {
|
|
2713
|
-
return validateClassValidator(schema, data);
|
|
2714
|
-
}
|
|
2715
|
-
if (isTypeBox(schema)) {
|
|
2716
|
-
return validateTypeBox(schema, data);
|
|
2717
|
-
}
|
|
2718
|
-
if (isAjv(schema)) {
|
|
2719
|
-
return validateAjv(schema, data);
|
|
2720
|
-
}
|
|
2721
|
-
if (isValibotWrapper(schema)) {
|
|
2722
|
-
return validateValibotWrapper(schema, data);
|
|
2723
|
-
}
|
|
2724
|
-
if (typeof schema === "function") {
|
|
2725
|
-
return schema(data);
|
|
2726
|
-
}
|
|
2727
|
-
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
2728
|
-
}
|
|
2729
3317
|
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
2730
3318
|
addFormats(ajv);
|
|
2731
3319
|
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
@@ -2740,17 +3328,18 @@ function openApiValidator() {
|
|
|
2740
3328
|
cache = compileValidators(app.openApiSpec);
|
|
2741
3329
|
compiledValidators.set(app, cache);
|
|
2742
3330
|
}
|
|
2743
|
-
const method = ctx.req.method.toLowerCase();
|
|
2744
3331
|
let matchPath;
|
|
2745
|
-
|
|
3332
|
+
let matchParams = {};
|
|
3333
|
+
if (cache.validators.has(ctx.path)) {
|
|
2746
3334
|
matchPath = ctx.path;
|
|
2747
3335
|
} else {
|
|
2748
|
-
for (const
|
|
2749
|
-
const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2750
|
-
const regex = new RegExp(regexStr);
|
|
3336
|
+
for (const [path2, { regex, paramNames }] of cache.paths) {
|
|
2751
3337
|
const match = regex.exec(ctx.path);
|
|
2752
3338
|
if (match) {
|
|
2753
|
-
matchPath =
|
|
3339
|
+
matchPath = path2;
|
|
3340
|
+
paramNames.forEach((name, i) => {
|
|
3341
|
+
matchParams[name] = match[i + 1];
|
|
3342
|
+
});
|
|
2754
3343
|
break;
|
|
2755
3344
|
}
|
|
2756
3345
|
}
|
|
@@ -2758,7 +3347,8 @@ function openApiValidator() {
|
|
|
2758
3347
|
if (!matchPath) {
|
|
2759
3348
|
return next();
|
|
2760
3349
|
}
|
|
2761
|
-
const
|
|
3350
|
+
const method = ctx.req.method.toLowerCase();
|
|
3351
|
+
const validators = cache.validators.get(matchPath)?.[method];
|
|
2762
3352
|
if (!validators) {
|
|
2763
3353
|
return next();
|
|
2764
3354
|
}
|
|
@@ -2783,21 +3373,7 @@ function openApiValidator() {
|
|
|
2783
3373
|
}
|
|
2784
3374
|
}
|
|
2785
3375
|
if (validators.params) {
|
|
2786
|
-
|
|
2787
|
-
if (Object.keys(params).length === 0 && matchPath) {
|
|
2788
|
-
const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
|
|
2789
|
-
if (paramNames.length > 0) {
|
|
2790
|
-
const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2791
|
-
const regex = new RegExp(regexStr);
|
|
2792
|
-
const match = regex.exec(ctx.path);
|
|
2793
|
-
if (match) {
|
|
2794
|
-
params = {};
|
|
2795
|
-
paramNames.forEach((name, i) => {
|
|
2796
|
-
params[name] = match[i + 1];
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
3376
|
+
const params = { ...matchParams, ...ctx.params };
|
|
2801
3377
|
const valid = validators.params(params);
|
|
2802
3378
|
if (!valid && validators.params.errors) {
|
|
2803
3379
|
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
@@ -2817,15 +3393,27 @@ function openApiValidator() {
|
|
|
2817
3393
|
};
|
|
2818
3394
|
}
|
|
2819
3395
|
function compileValidators(spec) {
|
|
2820
|
-
const
|
|
3396
|
+
const validators = /* @__PURE__ */ new Map();
|
|
3397
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2821
3398
|
for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
|
|
3399
|
+
if (path2.includes("{")) {
|
|
3400
|
+
const paramNames = [];
|
|
3401
|
+
const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
|
|
3402
|
+
paramNames.push(name);
|
|
3403
|
+
return "([^/]+)";
|
|
3404
|
+
}) + "$";
|
|
3405
|
+
paths.set(path2, {
|
|
3406
|
+
regex: new RegExp(regexStr),
|
|
3407
|
+
paramNames
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
2822
3410
|
const pathValidators = {};
|
|
2823
3411
|
for (const [method, operation] of Object.entries(pathItem)) {
|
|
2824
3412
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
2825
3413
|
const oper = operation;
|
|
2826
|
-
const
|
|
3414
|
+
const opValidators = {};
|
|
2827
3415
|
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
2828
|
-
|
|
3416
|
+
opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
2829
3417
|
}
|
|
2830
3418
|
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
2831
3419
|
const queryProps = {};
|
|
@@ -2847,85 +3435,41 @@ function compileValidators(spec) {
|
|
|
2847
3435
|
}
|
|
2848
3436
|
}
|
|
2849
3437
|
if (Object.keys(queryProps).length > 0) {
|
|
2850
|
-
|
|
3438
|
+
opValidators.query = ajv.compile({
|
|
2851
3439
|
type: "object",
|
|
2852
3440
|
properties: queryProps,
|
|
2853
3441
|
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
2854
3442
|
});
|
|
2855
3443
|
}
|
|
2856
3444
|
if (Object.keys(pathProps).length > 0) {
|
|
2857
|
-
|
|
3445
|
+
opValidators.params = ajv.compile({
|
|
2858
3446
|
type: "object",
|
|
2859
3447
|
properties: pathProps,
|
|
2860
3448
|
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
2861
3449
|
});
|
|
2862
3450
|
}
|
|
2863
3451
|
if (Object.keys(headerProps).length > 0) {
|
|
2864
|
-
|
|
3452
|
+
opValidators.headers = ajv.compile({
|
|
2865
3453
|
type: "object",
|
|
2866
3454
|
properties: headerProps,
|
|
2867
3455
|
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
2868
3456
|
});
|
|
2869
3457
|
}
|
|
2870
|
-
pathValidators[method] =
|
|
3458
|
+
pathValidators[method] = opValidators;
|
|
2871
3459
|
}
|
|
2872
|
-
|
|
3460
|
+
validators.set(path2, pathValidators);
|
|
2873
3461
|
}
|
|
2874
|
-
return
|
|
3462
|
+
return { paths, validators };
|
|
2875
3463
|
}
|
|
2876
|
-
function
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3464
|
+
function precompileValidators(app, spec) {
|
|
3465
|
+
const cache = compileValidators(spec);
|
|
3466
|
+
compiledValidators.set(app, cache);
|
|
3467
|
+
}
|
|
3468
|
+
function enableOpenApiValidation(app) {
|
|
3469
|
+
app.use(openApiValidator());
|
|
3470
|
+
app.onSpecAvailable((spec) => {
|
|
3471
|
+
precompileValidators(app, spec);
|
|
2884
3472
|
});
|
|
2885
|
-
const skip = options.skip || (() => false);
|
|
2886
|
-
const hits = /* @__PURE__ */ new Map();
|
|
2887
|
-
const interval = setInterval(() => {
|
|
2888
|
-
const now = Date.now();
|
|
2889
|
-
for (const [key, record] of hits.entries()) {
|
|
2890
|
-
if (record.resetTime <= now) {
|
|
2891
|
-
hits.delete(key);
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
}, windowMs);
|
|
2895
|
-
interval.unref?.();
|
|
2896
|
-
return async (ctx, next) => {
|
|
2897
|
-
if (skip(ctx)) return next();
|
|
2898
|
-
const key = keyGenerator(ctx);
|
|
2899
|
-
const now = Date.now();
|
|
2900
|
-
let record = hits.get(key);
|
|
2901
|
-
if (!record || record.resetTime <= now) {
|
|
2902
|
-
record = {
|
|
2903
|
-
hits: 0,
|
|
2904
|
-
resetTime: now + windowMs
|
|
2905
|
-
};
|
|
2906
|
-
hits.set(key, record);
|
|
2907
|
-
}
|
|
2908
|
-
record.hits++;
|
|
2909
|
-
const remaining = Math.max(0, max - record.hits);
|
|
2910
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2911
|
-
if (record.hits > max) {
|
|
2912
|
-
if (headers) {
|
|
2913
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2914
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2915
|
-
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2916
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2917
|
-
return res;
|
|
2918
|
-
}
|
|
2919
|
-
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2920
|
-
}
|
|
2921
|
-
const response = await next();
|
|
2922
|
-
if (response instanceof Response && headers) {
|
|
2923
|
-
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2924
|
-
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2925
|
-
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2926
|
-
}
|
|
2927
|
-
return response;
|
|
2928
|
-
};
|
|
2929
3473
|
}
|
|
2930
3474
|
const eta = new eta$2.Eta();
|
|
2931
3475
|
class ScalarPlugin extends ShokupanRouter {
|
|
@@ -3002,7 +3546,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3002
3546
|
}
|
|
3003
3547
|
}
|
|
3004
3548
|
function SecurityHeaders(options = {}) {
|
|
3005
|
-
|
|
3549
|
+
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
3006
3550
|
const headers = {};
|
|
3007
3551
|
const set = (k, v) => headers[k] = v;
|
|
3008
3552
|
if (options.dnsPrefetchControl !== false) {
|
|
@@ -3056,6 +3600,9 @@ function SecurityHeaders(options = {}) {
|
|
|
3056
3600
|
}
|
|
3057
3601
|
return response;
|
|
3058
3602
|
};
|
|
3603
|
+
securityHeadersMiddleware.isBuiltin = true;
|
|
3604
|
+
securityHeadersMiddleware.pluginName = "SecurityHeaders";
|
|
3605
|
+
return securityHeadersMiddleware;
|
|
3059
3606
|
}
|
|
3060
3607
|
class Cookie {
|
|
3061
3608
|
maxAge;
|
|
@@ -3169,7 +3716,7 @@ function Session(options) {
|
|
|
3169
3716
|
const resave = options.resave === void 0 ? true : options.resave;
|
|
3170
3717
|
const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
|
|
3171
3718
|
const rolling = options.rolling || false;
|
|
3172
|
-
|
|
3719
|
+
const sessionMiddleware = async function SessionMiddleware(ctx, next) {
|
|
3173
3720
|
let reqSessionId = null;
|
|
3174
3721
|
const cookieHeader = ctx.req.headers.get("cookie");
|
|
3175
3722
|
const cookies = {};
|
|
@@ -3305,6 +3852,9 @@ function Session(options) {
|
|
|
3305
3852
|
}
|
|
3306
3853
|
return result;
|
|
3307
3854
|
};
|
|
3855
|
+
sessionMiddleware.isBuiltin = true;
|
|
3856
|
+
sessionMiddleware.pluginName = "Session";
|
|
3857
|
+
return sessionMiddleware;
|
|
3308
3858
|
}
|
|
3309
3859
|
exports.$appRoot = $appRoot;
|
|
3310
3860
|
exports.$childControllers = $childControllers;
|
|
@@ -3344,6 +3894,7 @@ exports.Post = Post;
|
|
|
3344
3894
|
exports.Put = Put;
|
|
3345
3895
|
exports.Query = Query;
|
|
3346
3896
|
exports.RateLimit = RateLimit;
|
|
3897
|
+
exports.RateLimitMiddleware = RateLimitMiddleware;
|
|
3347
3898
|
exports.Req = Req;
|
|
3348
3899
|
exports.RouteParamType = RouteParamType;
|
|
3349
3900
|
exports.RouterRegistry = RouterRegistry;
|
|
@@ -3359,8 +3910,11 @@ exports.ShokupanRouter = ShokupanRouter;
|
|
|
3359
3910
|
exports.Spec = Spec;
|
|
3360
3911
|
exports.Use = Use;
|
|
3361
3912
|
exports.ValidationError = ValidationError;
|
|
3913
|
+
exports.compileValidators = compileValidators;
|
|
3362
3914
|
exports.compose = compose;
|
|
3915
|
+
exports.enableOpenApiValidation = enableOpenApiValidation;
|
|
3363
3916
|
exports.openApiValidator = openApiValidator;
|
|
3917
|
+
exports.precompileValidators = precompileValidators;
|
|
3364
3918
|
exports.useExpress = useExpress;
|
|
3365
3919
|
exports.valibot = valibot;
|
|
3366
3920
|
exports.validate = validate;
|