shokupan 0.4.5 → 0.6.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 +10 -9
- package/dist/analysis/openapi-analyzer.d.ts +0 -4
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +30 -8
- package/dist/index.cjs +692 -461
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +635 -426
- package/dist/index.js.map +1 -1
- package/dist/json-parser-B3dnQmCC.js +35 -0
- package/dist/json-parser-B3dnQmCC.js.map +1 -0
- package/dist/json-parser-COdZ0fqY.cjs +35 -0
- package/dist/json-parser-COdZ0fqY.cjs.map +1 -0
- package/dist/{openapi-analyzer-D9YB3IkV.cjs → openapi-analyzer-Bei1sVWp.cjs} +63 -49
- package/dist/openapi-analyzer-Bei1sVWp.cjs.map +1 -0
- package/dist/{openapi-analyzer-BtIaHIfe.js → openapi-analyzer-Ce_7JxZh.js} +63 -49
- package/dist/openapi-analyzer-Ce_7JxZh.js.map +1 -0
- package/dist/plugins/scalar.d.ts +1 -1
- package/dist/router.d.ts +33 -22
- package/dist/{server-adapter-BWrEJbKL.js → server-adapter-0xH174zz.js} +4 -2
- package/dist/server-adapter-0xH174zz.js.map +1 -0
- package/dist/{server-adapter-fVKP60e0.cjs → server-adapter-DFhwlK8e.cjs} +4 -2
- package/dist/server-adapter-DFhwlK8e.cjs.map +1 -0
- package/dist/shokupan.d.ts +4 -8
- package/dist/types.d.ts +32 -3
- package/dist/util/datastore.d.ts +6 -0
- package/dist/util/json-parser.d.ts +12 -0
- package/dist/util/plugin-deps.d.ts +25 -0
- package/package.json +74 -14
- package/dist/benchmarking/advanced-cases/elysia.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/express.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/fastify.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/hapi.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/hono.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/koa.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/nest.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/shokupan.d.ts +0 -1
- package/dist/benchmarking/advanced-data.d.ts +0 -33
- package/dist/benchmarking/advanced-runner.d.ts +0 -1
- package/dist/benchmarking/advanced-worker.d.ts +0 -0
- package/dist/benchmarking/cases/elysia.d.ts +0 -1
- package/dist/benchmarking/cases/express.d.ts +0 -1
- package/dist/benchmarking/cases/fastify.d.ts +0 -1
- package/dist/benchmarking/cases/hapi.d.ts +0 -1
- package/dist/benchmarking/cases/hono.d.ts +0 -1
- package/dist/benchmarking/cases/koa.d.ts +0 -1
- package/dist/benchmarking/cases/nest.d.ts +0 -1
- package/dist/benchmarking/cases/shokupan.d.ts +0 -1
- package/dist/benchmarking/data.d.ts +0 -15
- package/dist/benchmarking/quick_bench.d.ts +0 -1
- package/dist/benchmarking/runner.d.ts +0 -1
- package/dist/benchmarking/worker.d.ts +0 -0
- package/dist/buntest.d.ts +0 -1
- package/dist/openapi-analyzer-BtIaHIfe.js.map +0 -1
- package/dist/openapi-analyzer-D9YB3IkV.cjs.map +0 -1
- package/dist/server-adapter-BWrEJbKL.js.map +0 -1
- package/dist/server-adapter-fVKP60e0.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
2
24
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
25
|
const promises = require("node:fs/promises");
|
|
4
26
|
const eta$2 = require("eta");
|
|
5
27
|
const promises$1 = require("fs/promises");
|
|
6
28
|
const path = require("path");
|
|
7
29
|
const node_async_hooks = require("node:async_hooks");
|
|
8
|
-
const node = require("@surrealdb/node");
|
|
9
|
-
const surrealdb = require("surrealdb");
|
|
10
30
|
const api = require("@opentelemetry/api");
|
|
11
31
|
const os = require("node:os");
|
|
12
32
|
const arctic = require("arctic");
|
|
@@ -14,9 +34,7 @@ const jose = require("jose");
|
|
|
14
34
|
const zlib = require("node:zlib");
|
|
15
35
|
const Ajv = require("ajv");
|
|
16
36
|
const addFormats = require("ajv-formats");
|
|
17
|
-
const
|
|
18
|
-
const classValidator = require("class-validator");
|
|
19
|
-
const openapiAnalyzer = require("./openapi-analyzer-D9YB3IkV.cjs");
|
|
37
|
+
const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
|
|
20
38
|
const crypto = require("crypto");
|
|
21
39
|
const events = require("events");
|
|
22
40
|
function _interopNamespaceDefault(e) {
|
|
@@ -101,8 +119,73 @@ class ShokupanResponse {
|
|
|
101
119
|
return this._headers !== null;
|
|
102
120
|
}
|
|
103
121
|
}
|
|
122
|
+
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
123
|
+
100,
|
|
124
|
+
101,
|
|
125
|
+
102,
|
|
126
|
+
103,
|
|
127
|
+
200,
|
|
128
|
+
201,
|
|
129
|
+
202,
|
|
130
|
+
203,
|
|
131
|
+
204,
|
|
132
|
+
205,
|
|
133
|
+
206,
|
|
134
|
+
207,
|
|
135
|
+
208,
|
|
136
|
+
226,
|
|
137
|
+
300,
|
|
138
|
+
301,
|
|
139
|
+
302,
|
|
140
|
+
303,
|
|
141
|
+
304,
|
|
142
|
+
305,
|
|
143
|
+
306,
|
|
144
|
+
307,
|
|
145
|
+
308,
|
|
146
|
+
400,
|
|
147
|
+
401,
|
|
148
|
+
402,
|
|
149
|
+
403,
|
|
150
|
+
404,
|
|
151
|
+
405,
|
|
152
|
+
406,
|
|
153
|
+
407,
|
|
154
|
+
408,
|
|
155
|
+
409,
|
|
156
|
+
410,
|
|
157
|
+
411,
|
|
158
|
+
412,
|
|
159
|
+
413,
|
|
160
|
+
414,
|
|
161
|
+
415,
|
|
162
|
+
416,
|
|
163
|
+
417,
|
|
164
|
+
418,
|
|
165
|
+
421,
|
|
166
|
+
422,
|
|
167
|
+
423,
|
|
168
|
+
424,
|
|
169
|
+
425,
|
|
170
|
+
426,
|
|
171
|
+
428,
|
|
172
|
+
429,
|
|
173
|
+
431,
|
|
174
|
+
451,
|
|
175
|
+
500,
|
|
176
|
+
501,
|
|
177
|
+
502,
|
|
178
|
+
503,
|
|
179
|
+
504,
|
|
180
|
+
505,
|
|
181
|
+
506,
|
|
182
|
+
507,
|
|
183
|
+
508,
|
|
184
|
+
510,
|
|
185
|
+
511
|
|
186
|
+
]);
|
|
187
|
+
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
104
188
|
class ShokupanContext {
|
|
105
|
-
// Raw body for compression optimization
|
|
106
189
|
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
107
190
|
this.request = request;
|
|
108
191
|
this.server = server;
|
|
@@ -125,7 +208,6 @@ class ShokupanContext {
|
|
|
125
208
|
}
|
|
126
209
|
this.response = new ShokupanResponse();
|
|
127
210
|
}
|
|
128
|
-
_url;
|
|
129
211
|
params = {};
|
|
130
212
|
// Router assigns this, but default to empty object
|
|
131
213
|
state;
|
|
@@ -134,6 +216,19 @@ class ShokupanContext {
|
|
|
134
216
|
_debug;
|
|
135
217
|
_finalResponse;
|
|
136
218
|
_rawBody;
|
|
219
|
+
// Raw body for compression optimization
|
|
220
|
+
// Body caching to avoid double parsing
|
|
221
|
+
_url;
|
|
222
|
+
_cachedBody;
|
|
223
|
+
_bodyType;
|
|
224
|
+
_bodyParsed = false;
|
|
225
|
+
_bodyParseError;
|
|
226
|
+
// Cached URL properties to avoid repeated parsing
|
|
227
|
+
_cachedHostname;
|
|
228
|
+
_cachedProtocol;
|
|
229
|
+
_cachedHost;
|
|
230
|
+
_cachedOrigin;
|
|
231
|
+
_cachedQuery;
|
|
137
232
|
get url() {
|
|
138
233
|
if (!this._url) {
|
|
139
234
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -182,8 +277,11 @@ class ShokupanContext {
|
|
|
182
277
|
* Request query params
|
|
183
278
|
*/
|
|
184
279
|
get query() {
|
|
280
|
+
if (this._cachedQuery) return this._cachedQuery;
|
|
185
281
|
const q = {};
|
|
186
|
-
|
|
282
|
+
const entries = Object.entries(this.url.searchParams);
|
|
283
|
+
for (let i = 0; i < entries.length; i++) {
|
|
284
|
+
const [key, value] = entries[i];
|
|
187
285
|
if (q[key] === void 0) {
|
|
188
286
|
q[key] = value;
|
|
189
287
|
} else if (Array.isArray(q[key])) {
|
|
@@ -192,6 +290,7 @@ class ShokupanContext {
|
|
|
192
290
|
q[key] = [q[key], value];
|
|
193
291
|
}
|
|
194
292
|
}
|
|
293
|
+
this._cachedQuery = q;
|
|
195
294
|
return q;
|
|
196
295
|
}
|
|
197
296
|
/**
|
|
@@ -204,31 +303,31 @@ class ShokupanContext {
|
|
|
204
303
|
* Request hostname (e.g. "localhost")
|
|
205
304
|
*/
|
|
206
305
|
get hostname() {
|
|
207
|
-
return this.url.hostname;
|
|
306
|
+
return this._cachedHostname ??= this.url.hostname;
|
|
208
307
|
}
|
|
209
308
|
/**
|
|
210
309
|
* Request host (e.g. "localhost:3000")
|
|
211
310
|
*/
|
|
212
311
|
get host() {
|
|
213
|
-
return this.url.host;
|
|
312
|
+
return this._cachedHost ??= this.url.host;
|
|
214
313
|
}
|
|
215
314
|
/**
|
|
216
315
|
* Request protocol (e.g. "http:", "https:")
|
|
217
316
|
*/
|
|
218
317
|
get protocol() {
|
|
219
|
-
return this.url.protocol;
|
|
318
|
+
return this._cachedProtocol ??= this.url.protocol;
|
|
220
319
|
}
|
|
221
320
|
/**
|
|
222
321
|
* Whether request is secure (https)
|
|
223
322
|
*/
|
|
224
323
|
get secure() {
|
|
225
|
-
return this.
|
|
324
|
+
return this.protocol === "https:";
|
|
226
325
|
}
|
|
227
326
|
/**
|
|
228
327
|
* Request origin (e.g. "http://localhost:3000")
|
|
229
328
|
*/
|
|
230
329
|
get origin() {
|
|
231
|
-
return this.url.origin;
|
|
330
|
+
return this._cachedOrigin ??= this.url.origin;
|
|
232
331
|
}
|
|
233
332
|
/**
|
|
234
333
|
* Request headers
|
|
@@ -324,6 +423,91 @@ class ShokupanContext {
|
|
|
324
423
|
}
|
|
325
424
|
return h;
|
|
326
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Read request body with caching to avoid double parsing.
|
|
428
|
+
* The body is only parsed once and cached for subsequent reads.
|
|
429
|
+
*/
|
|
430
|
+
async body() {
|
|
431
|
+
if (this._bodyParseError) {
|
|
432
|
+
throw this._bodyParseError;
|
|
433
|
+
}
|
|
434
|
+
if (this._bodyParsed) {
|
|
435
|
+
return this._cachedBody;
|
|
436
|
+
}
|
|
437
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
438
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
439
|
+
const rawText = await this.readRawBody();
|
|
440
|
+
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
441
|
+
if (parserType === "native") {
|
|
442
|
+
this._cachedBody = JSON.parse(rawText);
|
|
443
|
+
} else {
|
|
444
|
+
const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
|
|
445
|
+
const parser = getJSONParser(parserType);
|
|
446
|
+
this._cachedBody = parser(rawText);
|
|
447
|
+
}
|
|
448
|
+
this._bodyType = "json";
|
|
449
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
450
|
+
this._cachedBody = await this.request.formData();
|
|
451
|
+
this._bodyType = "formData";
|
|
452
|
+
} else {
|
|
453
|
+
this._cachedBody = await this.readRawBody();
|
|
454
|
+
this._bodyType = "text";
|
|
455
|
+
}
|
|
456
|
+
this._bodyParsed = true;
|
|
457
|
+
return this._cachedBody;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Pre-parse the request body before handler execution.
|
|
461
|
+
* This improves performance and enables Node.js compatibility for large payloads.
|
|
462
|
+
* Errors are deferred until the body is actually accessed in the handler.
|
|
463
|
+
*/
|
|
464
|
+
async parseBody() {
|
|
465
|
+
if (this._bodyParsed) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
await this.body();
|
|
473
|
+
} catch (error) {
|
|
474
|
+
this._bodyParseError = error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Read raw body from ReadableStream efficiently.
|
|
479
|
+
* This is much faster than request.text() for large payloads.
|
|
480
|
+
* Also handles the case where body is already a string (e.g., in tests).
|
|
481
|
+
*/
|
|
482
|
+
async readRawBody() {
|
|
483
|
+
if (typeof this.request.body === "string") {
|
|
484
|
+
return this.request.body;
|
|
485
|
+
}
|
|
486
|
+
const reader = this.request.body?.getReader();
|
|
487
|
+
if (!reader) {
|
|
488
|
+
return "";
|
|
489
|
+
}
|
|
490
|
+
const chunks = [];
|
|
491
|
+
let totalSize = 0;
|
|
492
|
+
try {
|
|
493
|
+
while (true) {
|
|
494
|
+
const { done, value } = await reader.read();
|
|
495
|
+
if (done) break;
|
|
496
|
+
chunks.push(value);
|
|
497
|
+
totalSize += value.length;
|
|
498
|
+
}
|
|
499
|
+
} finally {
|
|
500
|
+
reader.releaseLock();
|
|
501
|
+
}
|
|
502
|
+
const result = new Uint8Array(totalSize);
|
|
503
|
+
let offset = 0;
|
|
504
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
505
|
+
const chunk = chunks[i];
|
|
506
|
+
result.set(chunk, offset);
|
|
507
|
+
offset += chunk.length;
|
|
508
|
+
}
|
|
509
|
+
return new TextDecoder().decode(result);
|
|
510
|
+
}
|
|
327
511
|
/**
|
|
328
512
|
* Send a response
|
|
329
513
|
* @param body Response body
|
|
@@ -332,31 +516,24 @@ class ShokupanContext {
|
|
|
332
516
|
*/
|
|
333
517
|
send(body, options) {
|
|
334
518
|
const headers = this.mergeHeaders(options?.headers);
|
|
335
|
-
const status = options?.status ?? this.response.status;
|
|
519
|
+
const status = options?.status ?? this.response.status ?? 200;
|
|
520
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
521
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
522
|
+
}
|
|
336
523
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
337
524
|
this._rawBody = body;
|
|
338
525
|
}
|
|
339
526
|
this._finalResponse = new Response(body, { status, headers });
|
|
340
527
|
return this._finalResponse;
|
|
341
528
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Read request body
|
|
344
|
-
*/
|
|
345
|
-
async body() {
|
|
346
|
-
const contentType = this.request.headers.get("content-type") || "";
|
|
347
|
-
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
348
|
-
return this.request.json();
|
|
349
|
-
}
|
|
350
|
-
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
351
|
-
return this.request.formData();
|
|
352
|
-
}
|
|
353
|
-
return this.request.text();
|
|
354
|
-
}
|
|
355
529
|
/**
|
|
356
530
|
* Respond with a JSON object
|
|
357
531
|
*/
|
|
358
532
|
json(data, status, headers) {
|
|
359
|
-
const finalStatus = status ?? this.response.status;
|
|
533
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
534
|
+
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
535
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
536
|
+
}
|
|
360
537
|
const jsonString = JSON.stringify(data);
|
|
361
538
|
this._rawBody = jsonString;
|
|
362
539
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
@@ -375,7 +552,10 @@ class ShokupanContext {
|
|
|
375
552
|
* Respond with a text string
|
|
376
553
|
*/
|
|
377
554
|
text(data, status, headers) {
|
|
378
|
-
const finalStatus = status ?? this.response.status;
|
|
555
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
556
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
557
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
558
|
+
}
|
|
379
559
|
this._rawBody = data;
|
|
380
560
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
381
561
|
this._finalResponse = new Response(data, {
|
|
@@ -393,7 +573,10 @@ class ShokupanContext {
|
|
|
393
573
|
* Respond with HTML content
|
|
394
574
|
*/
|
|
395
575
|
html(html, status, headers) {
|
|
396
|
-
const finalStatus = status ?? this.response.status;
|
|
576
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
577
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
578
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
579
|
+
}
|
|
397
580
|
const finalHeaders = this.mergeHeaders(headers);
|
|
398
581
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
399
582
|
this._rawBody = html;
|
|
@@ -404,6 +587,9 @@ class ShokupanContext {
|
|
|
404
587
|
* Respond with a redirect
|
|
405
588
|
*/
|
|
406
589
|
redirect(url, status = 302) {
|
|
590
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
591
|
+
throw new Error(`Invalid redirect status code: ${status}`);
|
|
592
|
+
}
|
|
407
593
|
const headers = this.mergeHeaders();
|
|
408
594
|
headers.set("Location", url);
|
|
409
595
|
this._finalResponse = new Response(null, { status, headers });
|
|
@@ -414,6 +600,9 @@ class ShokupanContext {
|
|
|
414
600
|
* DOES NOT CHAIN!
|
|
415
601
|
*/
|
|
416
602
|
status(status) {
|
|
603
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
604
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
605
|
+
}
|
|
417
606
|
const headers = this.mergeHeaders();
|
|
418
607
|
this._finalResponse = new Response(null, { status, headers });
|
|
419
608
|
return this._finalResponse;
|
|
@@ -424,6 +613,9 @@ class ShokupanContext {
|
|
|
424
613
|
async file(path2, fileOptions, responseOptions) {
|
|
425
614
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
426
615
|
const status = responseOptions?.status ?? this.response.status;
|
|
616
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
617
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
618
|
+
}
|
|
427
619
|
if (typeof Bun !== "undefined") {
|
|
428
620
|
this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
429
621
|
return this._finalResponse;
|
|
@@ -447,6 +639,10 @@ class ShokupanContext {
|
|
|
447
639
|
* @param headers HTTP Headers
|
|
448
640
|
*/
|
|
449
641
|
async jsx(element, args, status, headers) {
|
|
642
|
+
status ??= 200;
|
|
643
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
644
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
645
|
+
}
|
|
450
646
|
if (!this.renderer) {
|
|
451
647
|
throw new Error("No JSX renderer configured");
|
|
452
648
|
}
|
|
@@ -471,7 +667,9 @@ function RateLimitMiddleware(options = {}) {
|
|
|
471
667
|
const hits = /* @__PURE__ */ new Map();
|
|
472
668
|
const interval = setInterval(() => {
|
|
473
669
|
const now = Date.now();
|
|
474
|
-
|
|
670
|
+
const entries = Array.from(hits.entries());
|
|
671
|
+
for (let i = 0; i < entries.length; i++) {
|
|
672
|
+
const [key, record] = entries[i];
|
|
475
673
|
if (record.resetTime <= now) {
|
|
476
674
|
hits.delete(key);
|
|
477
675
|
}
|
|
@@ -724,7 +922,9 @@ function deepMerge(target, ...sources) {
|
|
|
724
922
|
if (!sources.length) return target;
|
|
725
923
|
const source = sources.shift();
|
|
726
924
|
if (isObject(target) && isObject(source)) {
|
|
727
|
-
|
|
925
|
+
const sourceKeys = Object.keys(source);
|
|
926
|
+
for (let i = 0; i < sourceKeys.length; i++) {
|
|
927
|
+
const key = sourceKeys[i];
|
|
728
928
|
if (isObject(source[key])) {
|
|
729
929
|
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
730
930
|
deepMerge(target[key], source[key]);
|
|
@@ -748,15 +948,17 @@ function deepMerge(target, ...sources) {
|
|
|
748
948
|
}
|
|
749
949
|
return deepMerge(target, ...sources);
|
|
750
950
|
}
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
951
|
+
const REGEX_PATTERNS = {
|
|
952
|
+
QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
|
|
953
|
+
QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
|
|
954
|
+
QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
|
|
955
|
+
QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
|
|
956
|
+
QUERY_GENERIC: /ctx\.query\.(\w+)/g,
|
|
957
|
+
PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
|
|
958
|
+
PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
|
|
959
|
+
HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
|
|
960
|
+
ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
|
|
961
|
+
};
|
|
760
962
|
function analyzeHandler(handler) {
|
|
761
963
|
const handlerSource = handler.toString();
|
|
762
964
|
const inferredSpec = {};
|
|
@@ -766,29 +968,20 @@ function analyzeHandler(handler) {
|
|
|
766
968
|
};
|
|
767
969
|
}
|
|
768
970
|
const queryParams = /* @__PURE__ */ new Map();
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (match[1] && !queryParams.has(match[1])) {
|
|
777
|
-
queryParams.set(match[1], { type: "number" });
|
|
778
|
-
}
|
|
779
|
-
}
|
|
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
|
-
}
|
|
785
|
-
}
|
|
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" });
|
|
971
|
+
const processMatches = (regex, type, format) => {
|
|
972
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
973
|
+
for (const match of matches) {
|
|
974
|
+
const name = match[1] || match[2];
|
|
975
|
+
if (name && !queryParams.has(name)) {
|
|
976
|
+
queryParams.set(name, { type, format });
|
|
977
|
+
}
|
|
790
978
|
}
|
|
791
|
-
}
|
|
979
|
+
};
|
|
980
|
+
processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
|
|
981
|
+
processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
|
|
982
|
+
processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
|
|
983
|
+
processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
|
|
984
|
+
processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
|
|
792
985
|
if (queryParams.size > 0) {
|
|
793
986
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
794
987
|
queryParams.forEach((schema, paramName) => {
|
|
@@ -800,12 +993,15 @@ function analyzeHandler(handler) {
|
|
|
800
993
|
});
|
|
801
994
|
}
|
|
802
995
|
const pathParams = /* @__PURE__ */ new Map();
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
996
|
+
const processPathMatches = (regex, type, format) => {
|
|
997
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
998
|
+
for (const match of matches) {
|
|
999
|
+
const name = match[1];
|
|
1000
|
+
if (name) pathParams.set(name, { type, format });
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
|
|
1004
|
+
processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
|
|
809
1005
|
if (pathParams.size > 0) {
|
|
810
1006
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
811
1007
|
pathParams.forEach((schema, paramName) => {
|
|
@@ -817,7 +1013,8 @@ function analyzeHandler(handler) {
|
|
|
817
1013
|
});
|
|
818
1014
|
});
|
|
819
1015
|
}
|
|
820
|
-
|
|
1016
|
+
const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
|
|
1017
|
+
for (const match of headerMatches) {
|
|
821
1018
|
if (match[1]) {
|
|
822
1019
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
823
1020
|
inferredSpec.parameters.push({
|
|
@@ -836,13 +1033,19 @@ function analyzeHandler(handler) {
|
|
|
836
1033
|
}
|
|
837
1034
|
if (handlerSource.includes("ctx.html(")) {
|
|
838
1035
|
responses["200"] = {
|
|
839
|
-
description: "Successful response",
|
|
1036
|
+
description: "Successful HTML response",
|
|
1037
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (handlerSource.includes("ctx.jsx(")) {
|
|
1041
|
+
responses["200"] = {
|
|
1042
|
+
description: "Successful HTML response (Rendered JSX)",
|
|
840
1043
|
content: { "text/html": { schema: { type: "string" } } }
|
|
841
1044
|
};
|
|
842
1045
|
}
|
|
843
1046
|
if (handlerSource.includes("ctx.text(")) {
|
|
844
1047
|
responses["200"] = {
|
|
845
|
-
description: "Successful response",
|
|
1048
|
+
description: "Successful text response",
|
|
846
1049
|
content: { "text/plain": { schema: { type: "string" } } }
|
|
847
1050
|
};
|
|
848
1051
|
}
|
|
@@ -853,7 +1056,18 @@ function analyzeHandler(handler) {
|
|
|
853
1056
|
};
|
|
854
1057
|
}
|
|
855
1058
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
856
|
-
|
|
1059
|
+
let hasSpecificRedirect = false;
|
|
1060
|
+
const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
|
|
1061
|
+
for (const match of redirectMatches) {
|
|
1062
|
+
const status = match[1];
|
|
1063
|
+
if (/^30[12378]$/.test(status)) {
|
|
1064
|
+
responses[status] = { description: `Redirect (${status})` };
|
|
1065
|
+
hasSpecificRedirect = true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (!hasSpecificRedirect) {
|
|
1069
|
+
responses["302"] = { description: "Redirect" };
|
|
1070
|
+
}
|
|
857
1071
|
}
|
|
858
1072
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
859
1073
|
responses["200"] = {
|
|
@@ -861,7 +1075,8 @@ function analyzeHandler(handler) {
|
|
|
861
1075
|
content: { "application/json": { schema: { type: "object" } } }
|
|
862
1076
|
};
|
|
863
1077
|
}
|
|
864
|
-
|
|
1078
|
+
const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
|
|
1079
|
+
for (const match of errorStatusMatches) {
|
|
865
1080
|
const statusCode = match[1];
|
|
866
1081
|
if (statusCode && statusCode !== "200") {
|
|
867
1082
|
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
@@ -872,6 +1087,52 @@ function analyzeHandler(handler) {
|
|
|
872
1087
|
}
|
|
873
1088
|
return { inferredSpec };
|
|
874
1089
|
}
|
|
1090
|
+
async function getAstRoutes(applications) {
|
|
1091
|
+
const astRoutes = [];
|
|
1092
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1093
|
+
if (seen.has(app.name)) return [];
|
|
1094
|
+
const newSeen = new Set(seen);
|
|
1095
|
+
newSeen.add(app.name);
|
|
1096
|
+
const expanded = [];
|
|
1097
|
+
for (const route of app.routes) {
|
|
1098
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1099
|
+
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1100
|
+
let joined = cleanPrefix + cleanPath;
|
|
1101
|
+
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1102
|
+
joined = joined.slice(0, -1);
|
|
1103
|
+
}
|
|
1104
|
+
expanded.push({
|
|
1105
|
+
...route,
|
|
1106
|
+
path: joined || "/"
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (app.mounted) {
|
|
1110
|
+
for (const mount of app.mounted) {
|
|
1111
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
1112
|
+
if (targetApp) {
|
|
1113
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1114
|
+
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1115
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return expanded;
|
|
1120
|
+
};
|
|
1121
|
+
applications.forEach((app) => {
|
|
1122
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
1123
|
+
});
|
|
1124
|
+
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
1125
|
+
for (const route of astRoutes) {
|
|
1126
|
+
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
1127
|
+
let score = 0;
|
|
1128
|
+
if (route.responseSchema) score += 10;
|
|
1129
|
+
if (route.handlerSource) score += 5;
|
|
1130
|
+
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
1131
|
+
dedupedRoutes.set(key, { route, score });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1135
|
+
}
|
|
875
1136
|
async function generateOpenApi(rootRouter, options = {}) {
|
|
876
1137
|
const paths = {};
|
|
877
1138
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
@@ -879,61 +1140,11 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
879
1140
|
const defaultTagName = options.defaultTag || "Application";
|
|
880
1141
|
let astRoutes = [];
|
|
881
1142
|
try {
|
|
882
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-
|
|
1143
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-Bei1sVWp.cjs"));
|
|
883
1144
|
const analyzer = new OpenAPIAnalyzer(process.cwd());
|
|
884
1145
|
const { applications } = await analyzer.analyze();
|
|
885
|
-
|
|
886
|
-
applications.forEach((app) => {
|
|
887
|
-
appMap.set(app.name, app);
|
|
888
|
-
if (app.name !== app.className) {
|
|
889
|
-
appMap.set(app.className, app);
|
|
890
|
-
}
|
|
891
|
-
});
|
|
892
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
893
|
-
if (seen.has(app.name)) return [];
|
|
894
|
-
const newSeen = new Set(seen);
|
|
895
|
-
newSeen.add(app.name);
|
|
896
|
-
const expanded = [];
|
|
897
|
-
for (const route of app.routes) {
|
|
898
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
899
|
-
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
900
|
-
let joined = cleanPrefix + cleanPath;
|
|
901
|
-
if (joined.length > 1 && joined.endsWith("/")) {
|
|
902
|
-
joined = joined.slice(0, -1);
|
|
903
|
-
}
|
|
904
|
-
expanded.push({
|
|
905
|
-
...route,
|
|
906
|
-
path: joined || "/"
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
if (app.mounted) {
|
|
910
|
-
for (const mount of app.mounted) {
|
|
911
|
-
const targetApp = appMap.get(mount.target);
|
|
912
|
-
if (targetApp) {
|
|
913
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
914
|
-
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
915
|
-
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return expanded;
|
|
920
|
-
};
|
|
921
|
-
applications.forEach((app) => {
|
|
922
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
923
|
-
});
|
|
924
|
-
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
925
|
-
for (const route of astRoutes) {
|
|
926
|
-
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
927
|
-
let score = 0;
|
|
928
|
-
if (route.responseSchema) score += 10;
|
|
929
|
-
if (route.handlerSource) score += 5;
|
|
930
|
-
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
931
|
-
dedupedRoutes.set(key, { route, score });
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1146
|
+
astRoutes = await getAstRoutes(applications);
|
|
935
1147
|
} catch (e) {
|
|
936
|
-
console.warn("OpenAPI AST analysis failed or skipped:", e);
|
|
937
1148
|
}
|
|
938
1149
|
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
939
1150
|
let group = currentGroup;
|
|
@@ -990,32 +1201,14 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
990
1201
|
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
991
1202
|
);
|
|
992
1203
|
if (!astMatch) {
|
|
993
|
-
|
|
994
|
-
if (route.handler.originalHandler) {
|
|
995
|
-
runtimeSource = route.handler.originalHandler.toString();
|
|
996
|
-
}
|
|
1204
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
997
1205
|
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
998
1206
|
const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
|
|
999
1207
|
astMatch = sameMethodRoutes.find((r) => {
|
|
1000
1208
|
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
1001
1209
|
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
1002
|
-
|
|
1003
|
-
return match;
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
const potentialMatches = astRoutes.filter(
|
|
1007
|
-
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
1008
|
-
);
|
|
1009
|
-
if (potentialMatches.length > 1) {
|
|
1010
|
-
const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
|
|
1011
|
-
const preciseMatch = potentialMatches.find((r) => {
|
|
1012
|
-
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
1013
|
-
const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
1014
|
-
return match;
|
|
1210
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
1015
1211
|
});
|
|
1016
|
-
if (preciseMatch) {
|
|
1017
|
-
astMatch = preciseMatch;
|
|
1018
|
-
}
|
|
1019
1212
|
}
|
|
1020
1213
|
if (astMatch) {
|
|
1021
1214
|
if (astMatch.summary) operation.summary = astMatch.summary;
|
|
@@ -1024,25 +1217,19 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1024
1217
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1025
1218
|
if (astMatch.requestTypes?.body) {
|
|
1026
1219
|
operation.requestBody = {
|
|
1027
|
-
content: {
|
|
1028
|
-
"application/json": { schema: astMatch.requestTypes.body }
|
|
1029
|
-
}
|
|
1220
|
+
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
1030
1221
|
};
|
|
1031
1222
|
}
|
|
1032
1223
|
if (astMatch.responseSchema) {
|
|
1033
1224
|
operation.responses["200"] = {
|
|
1034
1225
|
description: "Successful response",
|
|
1035
|
-
content: {
|
|
1036
|
-
"application/json": { schema: astMatch.responseSchema }
|
|
1037
|
-
}
|
|
1226
|
+
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1038
1227
|
};
|
|
1039
1228
|
} else if (astMatch.responseType) {
|
|
1040
1229
|
const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
|
|
1041
1230
|
operation.responses["200"] = {
|
|
1042
1231
|
description: "Successful response",
|
|
1043
|
-
content: {
|
|
1044
|
-
[contentType]: { schema: { type: astMatch.responseType } }
|
|
1045
|
-
}
|
|
1232
|
+
content: { [contentType]: { schema: { type: astMatch.responseType } } }
|
|
1046
1233
|
};
|
|
1047
1234
|
}
|
|
1048
1235
|
const params = [];
|
|
@@ -1093,15 +1280,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1093
1280
|
deepMerge(operation, inferredSpec);
|
|
1094
1281
|
}
|
|
1095
1282
|
if (route.handlerSpec) {
|
|
1096
|
-
|
|
1097
|
-
if (spec.summary) operation.summary = spec.summary;
|
|
1098
|
-
if (spec.description) operation.description = spec.description;
|
|
1099
|
-
if (spec.operationId) operation.operationId = spec.operationId;
|
|
1100
|
-
if (spec.tags) operation.tags = spec.tags;
|
|
1101
|
-
if (spec.security) operation.security = spec.security;
|
|
1102
|
-
if (spec.responses) {
|
|
1103
|
-
operation.responses = { ...operation.responses, ...spec.responses };
|
|
1104
|
-
}
|
|
1283
|
+
deepMerge(operation, route.handlerSpec);
|
|
1105
1284
|
}
|
|
1106
1285
|
if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
|
|
1107
1286
|
if (operation.tags) {
|
|
@@ -1120,11 +1299,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1120
1299
|
paths[fullPath][methodLower] = operation;
|
|
1121
1300
|
}
|
|
1122
1301
|
}
|
|
1123
|
-
|
|
1302
|
+
const controllers = router[$childControllers];
|
|
1303
|
+
for (const controller of controllers) {
|
|
1124
1304
|
const controllerName = controller.constructor.name || "UnknownController";
|
|
1125
1305
|
tagGroups.get(group)?.add(controllerName);
|
|
1126
1306
|
}
|
|
1127
|
-
|
|
1307
|
+
const childRouters = router[$childRouters];
|
|
1308
|
+
for (const child of childRouters) {
|
|
1128
1309
|
const mountPath = child[$mountPath];
|
|
1129
1310
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1130
1311
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
@@ -1134,7 +1315,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1134
1315
|
};
|
|
1135
1316
|
collect(rootRouter);
|
|
1136
1317
|
const xTagGroups = [];
|
|
1137
|
-
for (const [name, tags] of tagGroups) {
|
|
1318
|
+
for (const [name, tags] of tagGroups.entries()) {
|
|
1138
1319
|
xTagGroups.push({ name, tags: Array.from(tags).sort() });
|
|
1139
1320
|
}
|
|
1140
1321
|
return {
|
|
@@ -1169,7 +1350,8 @@ function serveStatic(config, prefix) {
|
|
|
1169
1350
|
if (res) return res;
|
|
1170
1351
|
}
|
|
1171
1352
|
if (config.exclude) {
|
|
1172
|
-
for (
|
|
1353
|
+
for (let i = 0; i < config.exclude.length; i++) {
|
|
1354
|
+
const pattern = config.exclude[i];
|
|
1173
1355
|
if (pattern instanceof RegExp) {
|
|
1174
1356
|
if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
|
|
1175
1357
|
} else if (typeof pattern === "string") {
|
|
@@ -1188,7 +1370,8 @@ function serveStatic(config, prefix) {
|
|
|
1188
1370
|
stats = await promises$1.stat(requestPath);
|
|
1189
1371
|
} catch (e) {
|
|
1190
1372
|
if (config.extensions) {
|
|
1191
|
-
for (
|
|
1373
|
+
for (let i = 0; i < config.extensions.length; i++) {
|
|
1374
|
+
const ext = config.extensions[i];
|
|
1192
1375
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1193
1376
|
try {
|
|
1194
1377
|
const s = await promises$1.stat(p);
|
|
@@ -1217,7 +1400,8 @@ function serveStatic(config, prefix) {
|
|
|
1217
1400
|
indexes = [config.index];
|
|
1218
1401
|
}
|
|
1219
1402
|
let foundIndex = false;
|
|
1220
|
-
for (
|
|
1403
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1404
|
+
const idx = indexes[i];
|
|
1221
1405
|
const idxPath = path.join(finalPath, idx);
|
|
1222
1406
|
try {
|
|
1223
1407
|
const idxStats = await promises$1.stat(idxPath);
|
|
@@ -1297,38 +1481,39 @@ class RouterTrie {
|
|
|
1297
1481
|
};
|
|
1298
1482
|
}
|
|
1299
1483
|
insert(method, path2, handler) {
|
|
1300
|
-
let
|
|
1484
|
+
let node = this.root;
|
|
1301
1485
|
const segments = this.splitPath(path2);
|
|
1302
|
-
for (
|
|
1486
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1487
|
+
const segment = segments[i];
|
|
1303
1488
|
if (segment === "**") {
|
|
1304
|
-
if (!
|
|
1305
|
-
|
|
1489
|
+
if (!node.recursiveChild) {
|
|
1490
|
+
node.recursiveChild = this.createNode();
|
|
1306
1491
|
}
|
|
1307
|
-
|
|
1492
|
+
node = node.recursiveChild;
|
|
1308
1493
|
} else if (segment === "*") {
|
|
1309
|
-
if (!
|
|
1310
|
-
|
|
1494
|
+
if (!node.wildcardChild) {
|
|
1495
|
+
node.wildcardChild = this.createNode();
|
|
1311
1496
|
}
|
|
1312
|
-
|
|
1497
|
+
node = node.wildcardChild;
|
|
1313
1498
|
} else if (segment.startsWith(":")) {
|
|
1314
1499
|
const paramName = segment.slice(1);
|
|
1315
|
-
if (!
|
|
1316
|
-
|
|
1317
|
-
|
|
1500
|
+
if (!node.paramChild) {
|
|
1501
|
+
node.paramChild = this.createNode();
|
|
1502
|
+
node.paramChild.paramName = paramName;
|
|
1318
1503
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1504
|
+
node = node.paramChild;
|
|
1505
|
+
node.paramName = paramName;
|
|
1321
1506
|
} else {
|
|
1322
|
-
if (!
|
|
1323
|
-
|
|
1507
|
+
if (!node.children[segment]) {
|
|
1508
|
+
node.children[segment] = this.createNode();
|
|
1324
1509
|
}
|
|
1325
|
-
|
|
1510
|
+
node = node.children[segment];
|
|
1326
1511
|
}
|
|
1327
1512
|
}
|
|
1328
|
-
if (!
|
|
1329
|
-
|
|
1513
|
+
if (!node.handlers) {
|
|
1514
|
+
node.handlers = {};
|
|
1330
1515
|
}
|
|
1331
|
-
|
|
1516
|
+
node.handlers[method] = handler;
|
|
1332
1517
|
}
|
|
1333
1518
|
search(method, path2) {
|
|
1334
1519
|
const segments = this.splitPath(path2);
|
|
@@ -1345,34 +1530,34 @@ class RouterTrie {
|
|
|
1345
1530
|
}
|
|
1346
1531
|
return null;
|
|
1347
1532
|
}
|
|
1348
|
-
findNode(
|
|
1533
|
+
findNode(node, segments, index, params) {
|
|
1349
1534
|
if (index === segments.length) {
|
|
1350
|
-
if (
|
|
1351
|
-
if (
|
|
1352
|
-
return
|
|
1535
|
+
if (node.handlers) return node;
|
|
1536
|
+
if (node.recursiveChild && node.recursiveChild.handlers) {
|
|
1537
|
+
return node.recursiveChild;
|
|
1353
1538
|
}
|
|
1354
1539
|
return null;
|
|
1355
1540
|
}
|
|
1356
1541
|
const segment = segments[index];
|
|
1357
|
-
const child =
|
|
1542
|
+
const child = node.children[segment];
|
|
1358
1543
|
if (child) {
|
|
1359
1544
|
const result = this.findNode(child, segments, index + 1, params);
|
|
1360
1545
|
if (result) return result;
|
|
1361
1546
|
}
|
|
1362
|
-
if (
|
|
1363
|
-
params[
|
|
1364
|
-
const result = this.findNode(
|
|
1547
|
+
if (node.paramChild) {
|
|
1548
|
+
params[node.paramChild.paramName] = segment;
|
|
1549
|
+
const result = this.findNode(node.paramChild, segments, index + 1, params);
|
|
1365
1550
|
if (result) return result;
|
|
1366
|
-
delete params[
|
|
1551
|
+
delete params[node.paramChild.paramName];
|
|
1367
1552
|
}
|
|
1368
|
-
if (
|
|
1369
|
-
const result = this.findNode(
|
|
1553
|
+
if (node.wildcardChild) {
|
|
1554
|
+
const result = this.findNode(node.wildcardChild, segments, index + 1, params);
|
|
1370
1555
|
if (result) return result;
|
|
1371
1556
|
}
|
|
1372
|
-
if (
|
|
1557
|
+
if (node.recursiveChild) {
|
|
1373
1558
|
const remaining = segments.length - index;
|
|
1374
1559
|
for (let k = 0; k <= remaining; k++) {
|
|
1375
|
-
const result = this.findNode(
|
|
1560
|
+
const result = this.findNode(node.recursiveChild, segments, index + k, params);
|
|
1376
1561
|
if (result) return result;
|
|
1377
1562
|
}
|
|
1378
1563
|
}
|
|
@@ -1386,40 +1571,68 @@ class RouterTrie {
|
|
|
1386
1571
|
}
|
|
1387
1572
|
}
|
|
1388
1573
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
return
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1574
|
+
let db;
|
|
1575
|
+
let dbPromise = null;
|
|
1576
|
+
let RecordId;
|
|
1577
|
+
async function ensureDb() {
|
|
1578
|
+
if (db) return db;
|
|
1579
|
+
if (dbPromise) return dbPromise;
|
|
1580
|
+
dbPromise = (async () => {
|
|
1581
|
+
try {
|
|
1582
|
+
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1583
|
+
const surreal = await import("surrealdb");
|
|
1584
|
+
const Surreal = surreal.Surreal;
|
|
1585
|
+
RecordId = surreal.RecordId;
|
|
1586
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1587
|
+
const _db = new Surreal({
|
|
1588
|
+
engines: createNodeEngines()
|
|
1589
|
+
});
|
|
1590
|
+
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1591
|
+
await _db.query(`
|
|
1592
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1593
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1594
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1595
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1596
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1597
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1598
|
+
`);
|
|
1599
|
+
db = _db;
|
|
1600
|
+
return db;
|
|
1601
|
+
} catch (e) {
|
|
1602
|
+
dbPromise = null;
|
|
1603
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1604
|
+
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1605
|
+
}
|
|
1606
|
+
throw e;
|
|
1607
|
+
}
|
|
1608
|
+
})();
|
|
1609
|
+
return dbPromise;
|
|
1610
|
+
}
|
|
1403
1611
|
const datastore = {
|
|
1404
|
-
get(store, key) {
|
|
1405
|
-
|
|
1612
|
+
async get(store, key) {
|
|
1613
|
+
await ensureDb();
|
|
1614
|
+
return db.select(new RecordId(store, key));
|
|
1406
1615
|
},
|
|
1407
|
-
set(store, key, value) {
|
|
1408
|
-
|
|
1616
|
+
async set(store, key, value) {
|
|
1617
|
+
await ensureDb();
|
|
1618
|
+
return db.create(new RecordId(store, key)).content(value);
|
|
1409
1619
|
},
|
|
1410
1620
|
async query(query, vars) {
|
|
1621
|
+
await ensureDb();
|
|
1411
1622
|
try {
|
|
1412
|
-
const r = await db.query(query, vars)
|
|
1413
|
-
return r;
|
|
1623
|
+
const r = await db.query(query, vars);
|
|
1624
|
+
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1414
1625
|
} catch (e) {
|
|
1415
1626
|
console.error("DS ERROR:", e);
|
|
1416
1627
|
throw e;
|
|
1417
1628
|
}
|
|
1418
1629
|
},
|
|
1419
|
-
ready
|
|
1630
|
+
get ready() {
|
|
1631
|
+
return ensureDb().then(() => void 0);
|
|
1632
|
+
}
|
|
1420
1633
|
};
|
|
1421
1634
|
process.on("exit", async () => {
|
|
1422
|
-
await db.close();
|
|
1635
|
+
if (db) await db.close();
|
|
1423
1636
|
});
|
|
1424
1637
|
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1425
1638
|
function traceHandler(fn, name) {
|
|
@@ -1492,6 +1705,8 @@ class ShokupanRouter {
|
|
|
1492
1705
|
[$parent] = null;
|
|
1493
1706
|
[$childRouters] = [];
|
|
1494
1707
|
[$childControllers] = [];
|
|
1708
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
1709
|
+
hooksInitialized = false;
|
|
1495
1710
|
middleware = [];
|
|
1496
1711
|
get rootConfig() {
|
|
1497
1712
|
return this[$appRoot]?.applicationConfig;
|
|
@@ -1509,7 +1724,8 @@ class ShokupanRouter {
|
|
|
1509
1724
|
getComponentRegistry() {
|
|
1510
1725
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
1511
1726
|
const localRoutes = [];
|
|
1512
|
-
for (
|
|
1727
|
+
for (let i = 0; i < this[$routes].length; i++) {
|
|
1728
|
+
const r = this[$routes][i];
|
|
1513
1729
|
const entry = {
|
|
1514
1730
|
type: "route",
|
|
1515
1731
|
path: r.path,
|
|
@@ -1646,7 +1862,8 @@ class ShokupanRouter {
|
|
|
1646
1862
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1647
1863
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1648
1864
|
let routesAttached = 0;
|
|
1649
|
-
for (
|
|
1865
|
+
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1866
|
+
const name = Array.from(methods)[i];
|
|
1650
1867
|
if (name === "constructor") continue;
|
|
1651
1868
|
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1652
1869
|
const originalHandler = instance[name];
|
|
@@ -1658,7 +1875,8 @@ class ShokupanRouter {
|
|
|
1658
1875
|
method = config.method;
|
|
1659
1876
|
subPath = config.path;
|
|
1660
1877
|
} else {
|
|
1661
|
-
for (
|
|
1878
|
+
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1879
|
+
const m = HTTPMethods[j];
|
|
1662
1880
|
if (name.toUpperCase().startsWith(m)) {
|
|
1663
1881
|
method = m;
|
|
1664
1882
|
const rest = name.slice(m.length);
|
|
@@ -1673,8 +1891,8 @@ class ShokupanRouter {
|
|
|
1673
1891
|
buffer = "";
|
|
1674
1892
|
}
|
|
1675
1893
|
};
|
|
1676
|
-
for (let
|
|
1677
|
-
const char = rest[
|
|
1894
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
1895
|
+
const char = rest[i2];
|
|
1678
1896
|
if (char === "$") {
|
|
1679
1897
|
flush();
|
|
1680
1898
|
subPath += "/:";
|
|
@@ -1712,7 +1930,8 @@ class ShokupanRouter {
|
|
|
1712
1930
|
if (routeArgs?.length > 0) {
|
|
1713
1931
|
args = [];
|
|
1714
1932
|
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
1715
|
-
for (
|
|
1933
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
1934
|
+
const arg = sortedArgs[k];
|
|
1716
1935
|
switch (arg.type) {
|
|
1717
1936
|
case RouteParamType.BODY:
|
|
1718
1937
|
try {
|
|
@@ -1742,7 +1961,9 @@ class ShokupanRouter {
|
|
|
1742
1961
|
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1743
1962
|
} else {
|
|
1744
1963
|
const query = {};
|
|
1745
|
-
|
|
1964
|
+
const keys = Object.keys(url.searchParams);
|
|
1965
|
+
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
1966
|
+
const key = keys[k2];
|
|
1746
1967
|
const vals = url.searchParams.getAll(key);
|
|
1747
1968
|
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1748
1969
|
}
|
|
@@ -1799,9 +2020,11 @@ class ShokupanRouter {
|
|
|
1799
2020
|
path: r.path,
|
|
1800
2021
|
handler: r.handler
|
|
1801
2022
|
}));
|
|
1802
|
-
for (
|
|
2023
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2024
|
+
const child = this[$childRouters][i];
|
|
1803
2025
|
const childRoutes = child.getRoutes();
|
|
1804
|
-
for (
|
|
2026
|
+
for (let j = 0; j < childRoutes.length; j++) {
|
|
2027
|
+
const route = childRoutes[j];
|
|
1805
2028
|
const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
|
|
1806
2029
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1807
2030
|
const fullPath = cleanPrefix + cleanPath || "/";
|
|
@@ -1815,12 +2038,12 @@ class ShokupanRouter {
|
|
|
1815
2038
|
return routes;
|
|
1816
2039
|
}
|
|
1817
2040
|
/**
|
|
1818
|
-
* Makes
|
|
1819
|
-
* This is useful for
|
|
2041
|
+
* Makes an internal request through this router's full routing pipeline.
|
|
2042
|
+
* This is useful for calling other routes internally and supports streaming responses.
|
|
1820
2043
|
* @param options The request options.
|
|
1821
|
-
* @returns The
|
|
2044
|
+
* @returns The raw Response object.
|
|
1822
2045
|
*/
|
|
1823
|
-
async
|
|
2046
|
+
async internalRequest(arg) {
|
|
1824
2047
|
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
1825
2048
|
const store = asyncContext.getStore();
|
|
1826
2049
|
store?.get("req");
|
|
@@ -1839,9 +2062,10 @@ class ShokupanRouter {
|
|
|
1839
2062
|
return this.root[$dispatch](req);
|
|
1840
2063
|
}
|
|
1841
2064
|
/**
|
|
1842
|
-
* Processes a request
|
|
2065
|
+
* Processes a request for testing purposes.
|
|
2066
|
+
* Returns a simplified { status, headers, data } object instead of a Response.
|
|
1843
2067
|
*/
|
|
1844
|
-
async
|
|
2068
|
+
async testRequest(options) {
|
|
1845
2069
|
let url = options.url || options.path || "/";
|
|
1846
2070
|
if (!url.startsWith("http")) {
|
|
1847
2071
|
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
|
|
@@ -1850,7 +2074,9 @@ class ShokupanRouter {
|
|
|
1850
2074
|
}
|
|
1851
2075
|
if (options.query) {
|
|
1852
2076
|
const u = new URL(url);
|
|
1853
|
-
|
|
2077
|
+
const entries = Object.entries(options.query);
|
|
2078
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2079
|
+
const [k, v] = entries[i];
|
|
1854
2080
|
u.searchParams.set(k, v);
|
|
1855
2081
|
}
|
|
1856
2082
|
url = u.toString();
|
|
@@ -1895,28 +2121,17 @@ class ShokupanRouter {
|
|
|
1895
2121
|
data: result
|
|
1896
2122
|
};
|
|
1897
2123
|
}
|
|
1898
|
-
|
|
1899
|
-
if (!this.
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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);
|
|
2124
|
+
wrapWithHooks(handler) {
|
|
2125
|
+
if (!this.hooksInitialized) {
|
|
2126
|
+
this.ensureHooksInitialized();
|
|
2127
|
+
}
|
|
2128
|
+
const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
|
|
2129
|
+
const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
|
|
2130
|
+
const hasError = this.hookCache.get("onError")?.length > 0;
|
|
1911
2131
|
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1912
2132
|
const originalHandler = handler;
|
|
1913
2133
|
const wrapped = async (ctx) => {
|
|
1914
|
-
|
|
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
|
-
}
|
|
2134
|
+
await this.runHooks("onRequestStart", ctx);
|
|
1920
2135
|
const debug = ctx._debug;
|
|
1921
2136
|
let debugId;
|
|
1922
2137
|
let previousNode;
|
|
@@ -1930,17 +2145,11 @@ class ShokupanRouter {
|
|
|
1930
2145
|
try {
|
|
1931
2146
|
const res = await originalHandler(ctx);
|
|
1932
2147
|
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1933
|
-
|
|
1934
|
-
const h = hookList[i];
|
|
1935
|
-
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1936
|
-
}
|
|
2148
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
1937
2149
|
return res;
|
|
1938
2150
|
} catch (err) {
|
|
1939
2151
|
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1940
|
-
|
|
1941
|
-
const h = hookList[i];
|
|
1942
|
-
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1943
|
-
}
|
|
2152
|
+
await this.runHooks("onError", ctx, err);
|
|
1944
2153
|
throw err;
|
|
1945
2154
|
} finally {
|
|
1946
2155
|
if (debug && previousNode) debug.setNode(previousNode);
|
|
@@ -1962,18 +2171,19 @@ class ShokupanRouter {
|
|
|
1962
2171
|
result = this.trie.search("GET", path2);
|
|
1963
2172
|
if (result) return result;
|
|
1964
2173
|
}
|
|
1965
|
-
for (
|
|
2174
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2175
|
+
const child = this[$childRouters][i];
|
|
1966
2176
|
const prefix = child[$mountPath];
|
|
1967
2177
|
if (path2 === prefix || path2.startsWith(prefix + "/")) {
|
|
1968
2178
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1969
2179
|
const match = child.find(method, subPath);
|
|
1970
|
-
if (match) return
|
|
2180
|
+
if (match) return match;
|
|
1971
2181
|
}
|
|
1972
2182
|
if (prefix.endsWith("/")) {
|
|
1973
2183
|
if (path2.startsWith(prefix)) {
|
|
1974
2184
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1975
2185
|
const match = child.find(method, subPath);
|
|
1976
|
-
if (match) return
|
|
2186
|
+
if (match) return match;
|
|
1977
2187
|
}
|
|
1978
2188
|
}
|
|
1979
2189
|
}
|
|
@@ -1995,17 +2205,23 @@ class ShokupanRouter {
|
|
|
1995
2205
|
/**
|
|
1996
2206
|
* Adds a route to the router.
|
|
1997
2207
|
*
|
|
1998
|
-
* @param
|
|
1999
|
-
* @param
|
|
2000
|
-
* @param
|
|
2001
|
-
* @param
|
|
2002
|
-
* @param
|
|
2208
|
+
* @param arg - Route configuration object
|
|
2209
|
+
* @param arg.method - HTTP method
|
|
2210
|
+
* @param arg.path - URL path
|
|
2211
|
+
* @param arg.spec - OpenAPI specification for the route
|
|
2212
|
+
* @param arg.handler - Route handler function
|
|
2213
|
+
* @param arg.regex - Custom regex for path matching
|
|
2214
|
+
* @param arg.group - Group for the route
|
|
2215
|
+
* @param arg.requestTimeout - Timeout for this route in milliseconds
|
|
2216
|
+
* @param arg.renderer - JSX renderer for the route
|
|
2217
|
+
* @param arg.controller - Controller for the route
|
|
2003
2218
|
*/
|
|
2004
2219
|
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2005
2220
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
|
|
2006
2221
|
if (this.currentGuards.length > 0) {
|
|
2007
2222
|
spec = spec || {};
|
|
2008
|
-
for (
|
|
2223
|
+
for (let i = 0; i < this.currentGuards.length; i++) {
|
|
2224
|
+
const guard = this.currentGuards[i];
|
|
2009
2225
|
if (guard.spec) {
|
|
2010
2226
|
if (guard.spec.responses) {
|
|
2011
2227
|
spec.responses = spec.responses || {};
|
|
@@ -2034,7 +2250,8 @@ class ShokupanRouter {
|
|
|
2034
2250
|
if (routeGuards.length > 0) {
|
|
2035
2251
|
const innerHandler = wrappedHandler;
|
|
2036
2252
|
wrappedHandler = async (ctx) => {
|
|
2037
|
-
for (
|
|
2253
|
+
for (let i = 0; i < routeGuards.length; i++) {
|
|
2254
|
+
const guard = routeGuards[i];
|
|
2038
2255
|
let guardPassed = false;
|
|
2039
2256
|
let nextCalled = false;
|
|
2040
2257
|
const next = () => {
|
|
@@ -2092,41 +2309,43 @@ class ShokupanRouter {
|
|
|
2092
2309
|
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2093
2310
|
const duration = performance.now() - startTime;
|
|
2094
2311
|
const config = ctx.app.applicationConfig;
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2312
|
+
Promise.resolve().then(async () => {
|
|
2313
|
+
try {
|
|
2314
|
+
const timestamp = Date.now();
|
|
2315
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2316
|
+
await datastore.set("middleware_tracking", key, {
|
|
2317
|
+
name: handler.name || "anonymous",
|
|
2318
|
+
path: ctx.path,
|
|
2319
|
+
timestamp,
|
|
2320
|
+
duration,
|
|
2321
|
+
file,
|
|
2322
|
+
line,
|
|
2323
|
+
error: error ? String(error) : void 0,
|
|
2324
|
+
metadata: {
|
|
2325
|
+
isBuiltin: handler.isBuiltin,
|
|
2326
|
+
pluginName: handler.pluginName
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2330
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2331
|
+
const cutoff = Date.now() - ttl;
|
|
2332
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2333
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2334
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2335
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2336
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2109
2337
|
}
|
|
2110
|
-
})
|
|
2111
|
-
|
|
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}`);
|
|
2338
|
+
} catch (datastoreError) {
|
|
2339
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2119
2340
|
}
|
|
2120
|
-
}
|
|
2121
|
-
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2122
|
-
}
|
|
2341
|
+
});
|
|
2123
2342
|
}
|
|
2124
2343
|
}
|
|
2125
2344
|
};
|
|
2126
2345
|
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2127
2346
|
let bakedHandler = wrappedHandler;
|
|
2128
2347
|
if (this.config?.hooks) {
|
|
2129
|
-
bakedHandler = this.wrapWithHooks(wrappedHandler
|
|
2348
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler);
|
|
2130
2349
|
}
|
|
2131
2350
|
this[$routes].push({
|
|
2132
2351
|
method,
|
|
@@ -2283,6 +2502,67 @@ class ShokupanRouter {
|
|
|
2283
2502
|
generateApiSpec(options = {}) {
|
|
2284
2503
|
return generateOpenApi(this, options);
|
|
2285
2504
|
}
|
|
2505
|
+
ensureHooksInitialized() {
|
|
2506
|
+
const hooks = this.config?.hooks;
|
|
2507
|
+
if (hooks) {
|
|
2508
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2509
|
+
const hookTypes = [
|
|
2510
|
+
"onRequestStart",
|
|
2511
|
+
"onRequestEnd",
|
|
2512
|
+
"onResponseStart",
|
|
2513
|
+
"onResponseEnd",
|
|
2514
|
+
"onError",
|
|
2515
|
+
"beforeValidate",
|
|
2516
|
+
"afterValidate",
|
|
2517
|
+
"onRequestTimeout",
|
|
2518
|
+
"onReadTimeout",
|
|
2519
|
+
"onWriteTimeout"
|
|
2520
|
+
];
|
|
2521
|
+
for (let i = 0; i < hookTypes.length; i++) {
|
|
2522
|
+
const type = hookTypes[i];
|
|
2523
|
+
const fns = [];
|
|
2524
|
+
for (let j = 0; j < hookList.length; j++) {
|
|
2525
|
+
const h = hookList[j];
|
|
2526
|
+
if (h[type]) fns.push(h[type]);
|
|
2527
|
+
}
|
|
2528
|
+
if (fns.length > 0) {
|
|
2529
|
+
this.hookCache.set(type, fns);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
this.hooksInitialized = true;
|
|
2534
|
+
}
|
|
2535
|
+
async runHooks(name, ...args) {
|
|
2536
|
+
if (!this.hooksInitialized) {
|
|
2537
|
+
this.ensureHooksInitialized();
|
|
2538
|
+
}
|
|
2539
|
+
const fns = this.hookCache.get(name);
|
|
2540
|
+
if (!fns) return;
|
|
2541
|
+
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2542
|
+
const debug = ctx?._debug;
|
|
2543
|
+
if (debug) {
|
|
2544
|
+
await Promise.all(fns.map(async (fn, index) => {
|
|
2545
|
+
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2546
|
+
const previousNode = debug.getCurrentNode();
|
|
2547
|
+
debug.trackEdge(previousNode, hookId);
|
|
2548
|
+
debug.setNode(hookId);
|
|
2549
|
+
const start = performance.now();
|
|
2550
|
+
try {
|
|
2551
|
+
await fn(...args);
|
|
2552
|
+
const duration = performance.now() - start;
|
|
2553
|
+
debug.trackStep(hookId, "hook", duration, "success");
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
const duration = performance.now() - start;
|
|
2556
|
+
debug.trackStep(hookId, "hook", duration, "error", error);
|
|
2557
|
+
throw error;
|
|
2558
|
+
} finally {
|
|
2559
|
+
if (previousNode) debug.setNode(previousNode);
|
|
2560
|
+
}
|
|
2561
|
+
}));
|
|
2562
|
+
} else {
|
|
2563
|
+
await Promise.all(fns.map((fn) => fn(...args)));
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2286
2566
|
}
|
|
2287
2567
|
class SystemCpuMonitor {
|
|
2288
2568
|
constructor(intervalMs = 1e3) {
|
|
@@ -2340,15 +2620,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2340
2620
|
openApiSpec;
|
|
2341
2621
|
composedMiddleware;
|
|
2342
2622
|
cpuMonitor;
|
|
2343
|
-
hookCache = /* @__PURE__ */ new Map();
|
|
2344
|
-
hooksInitialized = false;
|
|
2345
2623
|
get logger() {
|
|
2346
2624
|
return this.applicationConfig.logger;
|
|
2347
2625
|
}
|
|
2348
2626
|
constructor(applicationConfig = {}) {
|
|
2349
2627
|
const config = Object.assign({}, defaults, applicationConfig);
|
|
2350
2628
|
const { hooks, ...routerConfig } = config;
|
|
2351
|
-
super(routerConfig);
|
|
2629
|
+
super({ ...routerConfig, hooks });
|
|
2352
2630
|
this[$isApplication] = true;
|
|
2353
2631
|
this[$appRoot] = this;
|
|
2354
2632
|
this.applicationConfig = config;
|
|
@@ -2363,7 +2641,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2363
2641
|
* Adds middleware to the application.
|
|
2364
2642
|
*/
|
|
2365
2643
|
use(middleware) {
|
|
2366
|
-
let trackedMiddleware = middleware;
|
|
2367
2644
|
const { file, line } = getCallerInfo();
|
|
2368
2645
|
if (!middleware.metadata) {
|
|
2369
2646
|
middleware.metadata = {
|
|
@@ -2374,32 +2651,36 @@ class Shokupan extends ShokupanRouter {
|
|
|
2374
2651
|
pluginName: middleware.pluginName
|
|
2375
2652
|
};
|
|
2376
2653
|
}
|
|
2377
|
-
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2654
|
+
if (this.applicationConfig.enableMiddlewareTracking) {
|
|
2655
|
+
const trackedMiddleware = async (ctx, next) => {
|
|
2656
|
+
const c = ctx;
|
|
2657
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2658
|
+
const metadata = middleware.metadata || {};
|
|
2659
|
+
const start = performance.now();
|
|
2660
|
+
const item = {
|
|
2661
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2662
|
+
file: metadata.file || file,
|
|
2663
|
+
line: metadata.line || line,
|
|
2664
|
+
isBuiltin: metadata.isBuiltin,
|
|
2665
|
+
startTime: start,
|
|
2666
|
+
duration: -1
|
|
2667
|
+
};
|
|
2668
|
+
c.handlerStack.push(item);
|
|
2669
|
+
try {
|
|
2670
|
+
return await middleware(ctx, next);
|
|
2671
|
+
} finally {
|
|
2672
|
+
item.duration = performance.now() - start;
|
|
2673
|
+
}
|
|
2395
2674
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2675
|
+
return middleware(ctx, next);
|
|
2676
|
+
};
|
|
2677
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2678
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2679
|
+
trackedMiddleware.order = this.middleware.length;
|
|
2680
|
+
this.middleware.push(trackedMiddleware);
|
|
2681
|
+
} else {
|
|
2682
|
+
this.middleware.push(middleware);
|
|
2683
|
+
}
|
|
2403
2684
|
return this;
|
|
2404
2685
|
}
|
|
2405
2686
|
startupHooks = [];
|
|
@@ -2430,17 +2711,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2430
2711
|
if (finalPort < 0 || finalPort > 65535) {
|
|
2431
2712
|
throw new Error("Invalid port number");
|
|
2432
2713
|
}
|
|
2433
|
-
|
|
2434
|
-
await hook();
|
|
2435
|
-
}
|
|
2714
|
+
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2436
2715
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2437
2716
|
this.openApiSpec = await generateOpenApi(this);
|
|
2438
|
-
|
|
2439
|
-
await hook(this.openApiSpec);
|
|
2440
|
-
}
|
|
2717
|
+
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2441
2718
|
}
|
|
2442
2719
|
if (port === 0 && process.platform === "linux") ;
|
|
2443
|
-
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2720
|
+
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2444
2721
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2445
2722
|
this.cpuMonitor.start();
|
|
2446
2723
|
}
|
|
@@ -2468,11 +2745,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
2468
2745
|
};
|
|
2469
2746
|
let factory = this.applicationConfig.serverFactory;
|
|
2470
2747
|
if (!factory && typeof Bun === "undefined") {
|
|
2471
|
-
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-
|
|
2748
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
|
|
2472
2749
|
factory = createHttpServer();
|
|
2473
2750
|
}
|
|
2474
2751
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
2475
|
-
console.log(`Shokupan server listening on http://${
|
|
2752
|
+
console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
|
|
2476
2753
|
return server;
|
|
2477
2754
|
}
|
|
2478
2755
|
[$dispatch](req) {
|
|
@@ -2481,7 +2758,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2481
2758
|
/**
|
|
2482
2759
|
* Processes a request by wrapping the standard fetch method.
|
|
2483
2760
|
*/
|
|
2484
|
-
async
|
|
2761
|
+
async testRequest(options) {
|
|
2485
2762
|
let url = options.url || options.path || "/";
|
|
2486
2763
|
if (!url.startsWith("http")) {
|
|
2487
2764
|
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
@@ -2490,7 +2767,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
2490
2767
|
}
|
|
2491
2768
|
if (options.query) {
|
|
2492
2769
|
const u = new URL(url);
|
|
2493
|
-
|
|
2770
|
+
const entries = Object.entries(options.query);
|
|
2771
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2772
|
+
const [k, v] = entries[i];
|
|
2494
2773
|
u.searchParams.set(k, v);
|
|
2495
2774
|
}
|
|
2496
2775
|
url = u.toString();
|
|
@@ -2559,18 +2838,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2559
2838
|
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2560
2839
|
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2561
2840
|
const res = ctx.text(msg, 429);
|
|
2562
|
-
await this.
|
|
2841
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2563
2842
|
return res;
|
|
2564
2843
|
}
|
|
2565
2844
|
try {
|
|
2566
|
-
|
|
2567
|
-
await this.executeHook("onRequestStart", ctx);
|
|
2568
|
-
}
|
|
2845
|
+
await this.runHooks("onRequestStart", ctx);
|
|
2569
2846
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2570
2847
|
const result = await fn(ctx, async () => {
|
|
2848
|
+
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2571
2849
|
const match = this.find(req.method, ctx.path);
|
|
2572
2850
|
if (match) {
|
|
2573
2851
|
ctx.params = match.params;
|
|
2852
|
+
await bodyParsing;
|
|
2574
2853
|
return match.handler(ctx);
|
|
2575
2854
|
}
|
|
2576
2855
|
return null;
|
|
@@ -2593,12 +2872,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2593
2872
|
} else {
|
|
2594
2873
|
response = ctx.text(String(result));
|
|
2595
2874
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
}
|
|
2599
|
-
if (this.hasHook("onResponseStart")) {
|
|
2600
|
-
await this.executeHook("onResponseStart", ctx, response);
|
|
2601
|
-
}
|
|
2875
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
2876
|
+
await this.runHooks("onResponseStart", ctx, response);
|
|
2602
2877
|
return response;
|
|
2603
2878
|
} catch (err) {
|
|
2604
2879
|
console.error(err);
|
|
@@ -2607,9 +2882,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2607
2882
|
const status = err.status || err.statusCode || 500;
|
|
2608
2883
|
const body = { error: err.message || "Internal Server Error" };
|
|
2609
2884
|
if (err.errors) body.errors = err.errors;
|
|
2610
|
-
|
|
2611
|
-
await this.executeHook("onError", err, ctx);
|
|
2612
|
-
}
|
|
2885
|
+
await this.runHooks("onError", ctx, err);
|
|
2613
2886
|
return ctx.json(body, status);
|
|
2614
2887
|
}
|
|
2615
2888
|
};
|
|
@@ -2620,9 +2893,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2620
2893
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2621
2894
|
timeoutId = setTimeout(async () => {
|
|
2622
2895
|
controller.abort();
|
|
2623
|
-
|
|
2624
|
-
await this.executeHook("onRequestTimeout", ctx);
|
|
2625
|
-
}
|
|
2896
|
+
await this.runHooks("onRequestTimeout", ctx);
|
|
2626
2897
|
reject(new Error("Request Timeout"));
|
|
2627
2898
|
}, timeoutMs);
|
|
2628
2899
|
});
|
|
@@ -2635,56 +2906,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
2635
2906
|
console.error("Unexpected error in request execution:", err);
|
|
2636
2907
|
return ctx.text("Internal Server Error", 500);
|
|
2637
2908
|
}).then(async (res) => {
|
|
2638
|
-
|
|
2639
|
-
await this.executeHook("onResponseEnd", ctx, res);
|
|
2640
|
-
}
|
|
2909
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2641
2910
|
return res;
|
|
2642
2911
|
});
|
|
2643
2912
|
}
|
|
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
|
-
}
|
|
2688
2913
|
}
|
|
2689
2914
|
class AuthPlugin extends ShokupanRouter {
|
|
2690
2915
|
constructor(authConfig) {
|
|
@@ -2732,7 +2957,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2732
2957
|
return jwt;
|
|
2733
2958
|
}
|
|
2734
2959
|
init() {
|
|
2735
|
-
|
|
2960
|
+
const providerEntries = Object.entries(this.authConfig.providers);
|
|
2961
|
+
for (let i = 0; i < providerEntries.length; i++) {
|
|
2962
|
+
const [providerName, providerConfig] = providerEntries[i];
|
|
2736
2963
|
if (!providerConfig) continue;
|
|
2737
2964
|
const provider = this.getProviderInstance(providerName, providerConfig);
|
|
2738
2965
|
if (!provider) {
|
|
@@ -3060,7 +3287,9 @@ function Cors(options = {}) {
|
|
|
3060
3287
|
}
|
|
3061
3288
|
const response = await next();
|
|
3062
3289
|
if (response instanceof Response) {
|
|
3063
|
-
|
|
3290
|
+
const headerEntries = Array.from(headers.entries());
|
|
3291
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3292
|
+
const [key, value] = headerEntries[i];
|
|
3064
3293
|
response.headers.set(key, value);
|
|
3065
3294
|
}
|
|
3066
3295
|
}
|
|
@@ -3130,6 +3359,8 @@ function useExpress(expressMiddleware) {
|
|
|
3130
3359
|
});
|
|
3131
3360
|
};
|
|
3132
3361
|
}
|
|
3362
|
+
let plainToInstance;
|
|
3363
|
+
let validateOrReject;
|
|
3133
3364
|
class ValidationError extends Error {
|
|
3134
3365
|
constructor(errors) {
|
|
3135
3366
|
super("Validation Error");
|
|
@@ -3194,9 +3425,21 @@ function isClass(schema) {
|
|
|
3194
3425
|
}
|
|
3195
3426
|
}
|
|
3196
3427
|
async function validateClassValidator(schema, data) {
|
|
3197
|
-
|
|
3428
|
+
if (!plainToInstance || !validateOrReject) {
|
|
3429
|
+
try {
|
|
3430
|
+
const ct = await import("class-transformer");
|
|
3431
|
+
const cv = await import("class-validator");
|
|
3432
|
+
plainToInstance = ct.plainToInstance;
|
|
3433
|
+
validateOrReject = cv.validateOrReject;
|
|
3434
|
+
} catch (e) {
|
|
3435
|
+
throw new Error(
|
|
3436
|
+
"class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
const object = plainToInstance(schema, data);
|
|
3198
3441
|
try {
|
|
3199
|
-
await
|
|
3442
|
+
await validateOrReject(object);
|
|
3200
3443
|
return object;
|
|
3201
3444
|
} catch (errors) {
|
|
3202
3445
|
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
@@ -3208,30 +3451,8 @@ async function validateClassValidator(schema, data) {
|
|
|
3208
3451
|
}
|
|
3209
3452
|
}
|
|
3210
3453
|
const safelyGetBody = async (ctx) => {
|
|
3211
|
-
const req = ctx.req;
|
|
3212
|
-
if (req._bodyParsed) {
|
|
3213
|
-
return req._bodyValue;
|
|
3214
|
-
}
|
|
3215
3454
|
try {
|
|
3216
|
-
|
|
3217
|
-
if (typeof req.json === "function") {
|
|
3218
|
-
data = await req.json();
|
|
3219
|
-
} else {
|
|
3220
|
-
data = req.body;
|
|
3221
|
-
if (typeof data === "string") {
|
|
3222
|
-
try {
|
|
3223
|
-
data = JSON.parse(data);
|
|
3224
|
-
} catch {
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
req._bodyParsed = true;
|
|
3229
|
-
req._bodyValue = data;
|
|
3230
|
-
Object.defineProperty(req, "json", {
|
|
3231
|
-
value: async () => req._bodyValue,
|
|
3232
|
-
configurable: true
|
|
3233
|
-
});
|
|
3234
|
-
return data;
|
|
3455
|
+
return await ctx.body();
|
|
3235
3456
|
} catch (e) {
|
|
3236
3457
|
return {};
|
|
3237
3458
|
}
|
|
@@ -3278,9 +3499,7 @@ function validate(config) {
|
|
|
3278
3499
|
body = await safelyGetBody(ctx);
|
|
3279
3500
|
dataToValidate.body = body;
|
|
3280
3501
|
}
|
|
3281
|
-
|
|
3282
|
-
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
3283
|
-
}
|
|
3502
|
+
await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
|
|
3284
3503
|
if (validators.params) {
|
|
3285
3504
|
ctx.params = await validators.params(ctx.params);
|
|
3286
3505
|
}
|
|
@@ -3296,21 +3515,20 @@ function validate(config) {
|
|
|
3296
3515
|
if (validators.body) {
|
|
3297
3516
|
const b = body ?? await safelyGetBody(ctx);
|
|
3298
3517
|
validBody = await validators.body(b);
|
|
3518
|
+
ctx._cachedBody = validBody;
|
|
3299
3519
|
const req = ctx.req;
|
|
3300
|
-
req._bodyValue = validBody;
|
|
3301
3520
|
Object.defineProperty(req, "json", {
|
|
3302
3521
|
value: async () => validBody,
|
|
3522
|
+
writable: true,
|
|
3303
3523
|
configurable: true
|
|
3304
3524
|
});
|
|
3305
3525
|
ctx.body = validBody;
|
|
3306
3526
|
}
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
3313
|
-
}
|
|
3527
|
+
const validatedData = { ...dataToValidate };
|
|
3528
|
+
if (config.params) validatedData.params = ctx.params;
|
|
3529
|
+
if (config.query) validatedData.query = validQuery;
|
|
3530
|
+
if (config.body) validatedData.body = validBody;
|
|
3531
|
+
await ctx.app.runHooks("afterValidate", ctx, validatedData);
|
|
3314
3532
|
return next();
|
|
3315
3533
|
};
|
|
3316
3534
|
}
|
|
@@ -3333,12 +3551,14 @@ function openApiValidator() {
|
|
|
3333
3551
|
if (cache.validators.has(ctx.path)) {
|
|
3334
3552
|
matchPath = ctx.path;
|
|
3335
3553
|
} else {
|
|
3336
|
-
|
|
3554
|
+
const pathEntries = Array.from(cache.paths.entries());
|
|
3555
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3556
|
+
const [path2, { regex, paramNames }] = pathEntries[i];
|
|
3337
3557
|
const match = regex.exec(ctx.path);
|
|
3338
3558
|
if (match) {
|
|
3339
3559
|
matchPath = path2;
|
|
3340
|
-
paramNames.forEach((name,
|
|
3341
|
-
matchParams[name] = match[
|
|
3560
|
+
paramNames.forEach((name, i2) => {
|
|
3561
|
+
matchParams[name] = match[i2 + 1];
|
|
3342
3562
|
});
|
|
3343
3563
|
break;
|
|
3344
3564
|
}
|
|
@@ -3395,7 +3615,9 @@ function openApiValidator() {
|
|
|
3395
3615
|
function compileValidators(spec) {
|
|
3396
3616
|
const validators = /* @__PURE__ */ new Map();
|
|
3397
3617
|
const paths = /* @__PURE__ */ new Map();
|
|
3398
|
-
|
|
3618
|
+
const pathEntries = Object.entries(spec.paths || {});
|
|
3619
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3620
|
+
const [path2, pathItem] = pathEntries[i];
|
|
3399
3621
|
if (path2.includes("{")) {
|
|
3400
3622
|
const paramNames = [];
|
|
3401
3623
|
const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
|
|
@@ -3408,7 +3630,9 @@ function compileValidators(spec) {
|
|
|
3408
3630
|
});
|
|
3409
3631
|
}
|
|
3410
3632
|
const pathValidators = {};
|
|
3411
|
-
|
|
3633
|
+
const methodEntries = Object.entries(pathItem);
|
|
3634
|
+
for (let k = 0; k < methodEntries.length; k++) {
|
|
3635
|
+
const [method, operation] = methodEntries[k];
|
|
3412
3636
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
3413
3637
|
const oper = operation;
|
|
3414
3638
|
const opValidators = {};
|
|
@@ -3422,7 +3646,8 @@ function compileValidators(spec) {
|
|
|
3422
3646
|
const queryRequired = [];
|
|
3423
3647
|
const pathRequired = [];
|
|
3424
3648
|
const headerRequired = [];
|
|
3425
|
-
for (
|
|
3649
|
+
for (let j = 0; j < parameters.length; j++) {
|
|
3650
|
+
const param = parameters[j];
|
|
3426
3651
|
if (param.in === "query") {
|
|
3427
3652
|
queryProps[param.name] = param.schema || {};
|
|
3428
3653
|
if (param.required) queryRequired.push(param.name);
|
|
@@ -3493,8 +3718,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3493
3718
|
|
|
3494
3719
|
<body>
|
|
3495
3720
|
<div id="app"></div>
|
|
3496
|
-
|
|
3497
|
-
<script src="<%= it.path %>scalar.js"><\/script>
|
|
3721
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
|
|
3498
3722
|
<script>
|
|
3499
3723
|
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
3500
3724
|
url: "<%= it.path %>openapi.json",
|
|
@@ -3505,9 +3729,6 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3505
3729
|
|
|
3506
3730
|
</html>`, { path: path2, config: this.pluginOptions }));
|
|
3507
3731
|
});
|
|
3508
|
-
this.get("/scalar.js", (ctx) => {
|
|
3509
|
-
return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
|
|
3510
|
-
});
|
|
3511
3732
|
this.get("/openapi.json", async (ctx) => {
|
|
3512
3733
|
let spec;
|
|
3513
3734
|
if (this.root.openApiSpec) {
|
|
@@ -3587,14 +3808,18 @@ function SecurityHeaders(options = {}) {
|
|
|
3587
3808
|
if (opt === void 0 || opt === true) {
|
|
3588
3809
|
set("Content-Security-Policy", "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests");
|
|
3589
3810
|
} else if (typeof opt === "object") {
|
|
3590
|
-
|
|
3811
|
+
const optEntries = Object.entries(opt);
|
|
3812
|
+
for (let i = 0; i < optEntries.length; i++) {
|
|
3813
|
+
const [key, val] = optEntries[i];
|
|
3591
3814
|
}
|
|
3592
3815
|
}
|
|
3593
3816
|
}
|
|
3594
3817
|
if (options.hidePoweredBy !== false) ;
|
|
3595
3818
|
const response = await next();
|
|
3596
3819
|
if (response instanceof Response) {
|
|
3597
|
-
|
|
3820
|
+
const headerEntries = Object.entries(headers);
|
|
3821
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3822
|
+
const [k, v] = headerEntries[i];
|
|
3598
3823
|
response.headers.set(k, v);
|
|
3599
3824
|
}
|
|
3600
3825
|
return response;
|
|
@@ -3680,7 +3905,9 @@ class MemoryStore extends events.EventEmitter {
|
|
|
3680
3905
|
}
|
|
3681
3906
|
all(cb) {
|
|
3682
3907
|
const result = {};
|
|
3683
|
-
|
|
3908
|
+
const sessionKeys = Object.keys(this.sessions);
|
|
3909
|
+
for (let i = 0; i < sessionKeys.length; i++) {
|
|
3910
|
+
const sid = sessionKeys[i];
|
|
3684
3911
|
try {
|
|
3685
3912
|
result[sid] = JSON.parse(this.sessions[sid]);
|
|
3686
3913
|
} catch {
|
|
@@ -3766,7 +3993,9 @@ function Session(options) {
|
|
|
3766
3993
|
sessObj.regenerate = (cb) => {
|
|
3767
3994
|
store.destroy(sessObj.id, (err) => {
|
|
3768
3995
|
sessionID = generateId(ctx);
|
|
3769
|
-
|
|
3996
|
+
const keys = Object.keys(sessObj);
|
|
3997
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3998
|
+
const key = keys[i];
|
|
3770
3999
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3771
4000
|
delete sessObj[key];
|
|
3772
4001
|
}
|
|
@@ -3781,7 +4010,9 @@ function Session(options) {
|
|
|
3781
4010
|
store.get(sessObj.id, (err, sess2) => {
|
|
3782
4011
|
if (err) return cb(err);
|
|
3783
4012
|
if (!sess2) return cb(new Error("Session not found"));
|
|
3784
|
-
|
|
4013
|
+
const keys = Object.keys(sessObj);
|
|
4014
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4015
|
+
const key = keys[i];
|
|
3785
4016
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3786
4017
|
delete sessObj[key];
|
|
3787
4018
|
}
|