@stacksjs/bun-router 0.0.16 → 0.0.17
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/dist/auth.d.ts +22 -8
- package/dist/chunk-1ahs68ys.js +18 -0
- package/dist/{chunk-0145g1me.js → chunk-cgptvjdf.js} +147 -75
- package/dist/chunk-g3ybefhg.js +1923 -0
- package/dist/cli.js +3 -2
- package/dist/container/container.d.ts +1 -0
- package/dist/container/contextual-binding.d.ts +0 -1
- package/dist/container/decorators.d.ts +2 -5
- package/dist/container/index.d.ts +16 -0
- package/dist/container/index.js +322 -0
- package/dist/file-serving/static-files.d.ts +16 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +1159 -1380
- package/dist/middleware/cors.d.ts +9 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +16 -1
- package/dist/middleware/rate_limit.d.ts +7 -1
- package/dist/middleware/request_tracer.d.ts +15 -9
- package/dist/middleware/response_cache.d.ts +2 -1
- package/dist/request/context.d.ts +12 -1
- package/dist/request/macros.d.ts +40 -2
- package/dist/response/macros.d.ts +3 -3
- package/dist/router/fluent-routing.d.ts +50 -13
- package/dist/router/handler-resolver.d.ts +8 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/route-compiler.d.ts +8 -10
- package/dist/router/route-trie.d.ts +7 -1
- package/dist/router/router.d.ts +13 -2
- package/dist/router/validation-integration.d.ts +10 -4
- package/dist/types/middleware-types.d.ts +1 -1
- package/dist/types/route-inference.d.ts +9 -2
- package/dist/types.d.ts +62 -9
- package/dist/utils.d.ts +1 -1
- package/package.json +11 -1
- package/src/auth.ts +32 -12
- package/src/container/container.ts +58 -12
- package/src/container/contextual-binding.ts +4 -1
- package/src/container/decorators.ts +16 -7
- package/src/container/index.ts +49 -0
- package/src/errors/circuit-breaker.ts +3 -2
- package/src/errors/graceful-degradation.ts +2 -0
- package/src/file-serving/static-files.ts +144 -7
- package/src/index.ts +53 -1
- package/src/middleware/cors.ts +76 -8
- package/src/middleware/csrf.ts +70 -22
- package/src/middleware/ddos_protection.ts +27 -3
- package/src/middleware/input_validation.ts +10 -6
- package/src/middleware/performance_alerting.ts +2 -0
- package/src/middleware/performance_monitor.ts +2 -0
- package/src/middleware/rate_limit.ts +7 -1
- package/src/middleware/request_signing.ts +2 -0
- package/src/middleware/request_tracer.ts +34 -17
- package/src/middleware/response_cache.ts +48 -13
- package/src/middleware/session.ts +3 -3
- package/src/observability/metrics.ts +1 -1
- package/src/request/context.ts +50 -3
- package/src/request/enhanced-request.ts +20 -1
- package/src/request/macros.ts +183 -49
- package/src/response/macros.ts +3 -3
- package/src/router/file-based-routing.ts +17 -10
- package/src/router/fluent-routing.ts +117 -48
- package/src/router/group-organization.ts +17 -1
- package/src/router/handler-resolver.ts +36 -0
- package/src/router/http-methods.ts +33 -3
- package/src/router/index.ts +6 -0
- package/src/router/middleware.ts +3 -0
- package/src/router/optimized-route-matching.ts +58 -9
- package/src/router/route-compiler.ts +62 -73
- package/src/router/route-matching.ts +19 -0
- package/src/router/route-trie.ts +34 -48
- package/src/router/router.ts +72 -60
- package/src/router/server.ts +361 -139
- package/src/router/validation-integration.ts +11 -5
- package/src/testing/auth-testing.ts +6 -4
- package/src/types/middleware-types.ts +7 -4
- package/src/types/route-inference.ts +29 -14
- package/src/types.ts +63 -7
- package/src/url.ts +7 -2
- package/src/utils.ts +9 -6
- package/src/validation/validator.ts +7 -4
- package/src/websocket/clustering.ts +8 -1
- package/dist/router/fluent-router.d.ts +0 -315
- package/src/router/fluent-router.ts +0 -927
package/dist/index.js
CHANGED
|
@@ -26,8 +26,6 @@ import {
|
|
|
26
26
|
Session,
|
|
27
27
|
SessionManager,
|
|
28
28
|
SignatureError,
|
|
29
|
-
__export,
|
|
30
|
-
__require,
|
|
31
29
|
apiKeyAuth,
|
|
32
30
|
auth,
|
|
33
31
|
basicAuth,
|
|
@@ -66,9 +64,50 @@ import {
|
|
|
66
64
|
signRequest,
|
|
67
65
|
singleFileUpload,
|
|
68
66
|
verifySignature
|
|
69
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-cgptvjdf.js";
|
|
68
|
+
import {
|
|
69
|
+
AuthenticationException,
|
|
70
|
+
AuthorizationException,
|
|
71
|
+
BindingBuilder,
|
|
72
|
+
BusinessLogicException,
|
|
73
|
+
CircuitBreakerOpenException,
|
|
74
|
+
CircularDependencyException,
|
|
75
|
+
ConfigurationException,
|
|
76
|
+
Container,
|
|
77
|
+
ContextualContainer,
|
|
78
|
+
DatabaseException,
|
|
79
|
+
DefaultServiceProviderManager,
|
|
80
|
+
EnvironmentManager,
|
|
81
|
+
ErrorFactory,
|
|
82
|
+
ExternalServiceException,
|
|
83
|
+
FileSystemException,
|
|
84
|
+
HandlerResolutionException,
|
|
85
|
+
InternalServerException,
|
|
86
|
+
MethodNotAllowedException,
|
|
87
|
+
MiddlewareConfigurationException,
|
|
88
|
+
ModelBindingException,
|
|
89
|
+
NotFoundException,
|
|
90
|
+
RateLimitException,
|
|
91
|
+
RouteConflictException,
|
|
92
|
+
RouteNotFoundException,
|
|
93
|
+
RouterErrorFactory,
|
|
94
|
+
RouterException,
|
|
95
|
+
ServiceUnavailableException,
|
|
96
|
+
TimeoutException,
|
|
97
|
+
ValidationException,
|
|
98
|
+
createContainer,
|
|
99
|
+
detectRouteConflicts,
|
|
100
|
+
findSimilarRoutes,
|
|
101
|
+
getContainer,
|
|
102
|
+
setContainer
|
|
103
|
+
} from "./chunk-g3ybefhg.js";
|
|
104
|
+
import {
|
|
105
|
+
__export,
|
|
106
|
+
__require
|
|
107
|
+
} from "./chunk-1ahs68ys.js";
|
|
70
108
|
|
|
71
109
|
// src/auth.ts
|
|
110
|
+
import { timingSafeEqual } from "crypto";
|
|
72
111
|
function base64UrlEncode(str) {
|
|
73
112
|
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
74
113
|
}
|
|
@@ -132,7 +171,9 @@ class JWT {
|
|
|
132
171
|
}
|
|
133
172
|
const data = `${encodedHeader}.${encodedPayload}`;
|
|
134
173
|
const expectedSignature = this.createSignature(data, this.secret, header.alg);
|
|
135
|
-
|
|
174
|
+
const sigBuf = Buffer.from(signature);
|
|
175
|
+
const expectedBuf = Buffer.from(expectedSignature);
|
|
176
|
+
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
136
177
|
throw new Error("Invalid signature");
|
|
137
178
|
}
|
|
138
179
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -386,381 +427,6 @@ __export(exports_errors, {
|
|
|
386
427
|
AuthenticationException: () => AuthenticationException
|
|
387
428
|
});
|
|
388
429
|
|
|
389
|
-
// src/errors/exceptions.ts
|
|
390
|
-
class RouterException extends Error {
|
|
391
|
-
code;
|
|
392
|
-
statusCode;
|
|
393
|
-
context;
|
|
394
|
-
retryable;
|
|
395
|
-
severity;
|
|
396
|
-
category;
|
|
397
|
-
timestamp;
|
|
398
|
-
cause;
|
|
399
|
-
constructor(details) {
|
|
400
|
-
super(details.message);
|
|
401
|
-
this.name = this.constructor.name;
|
|
402
|
-
this.code = details.code;
|
|
403
|
-
this.statusCode = details.statusCode || 500;
|
|
404
|
-
this.context = details.context || {};
|
|
405
|
-
this.retryable = details.retryable || false;
|
|
406
|
-
this.severity = details.severity || "medium";
|
|
407
|
-
this.category = details.category || "system";
|
|
408
|
-
this.timestamp = new Date;
|
|
409
|
-
this.cause = details.cause;
|
|
410
|
-
if (Error.captureStackTrace) {
|
|
411
|
-
Error.captureStackTrace(this, this.constructor);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
toJSON() {
|
|
415
|
-
return {
|
|
416
|
-
name: this.name,
|
|
417
|
-
code: this.code,
|
|
418
|
-
message: this.message,
|
|
419
|
-
statusCode: this.statusCode,
|
|
420
|
-
context: this.context,
|
|
421
|
-
retryable: this.retryable,
|
|
422
|
-
severity: this.severity,
|
|
423
|
-
category: this.category,
|
|
424
|
-
timestamp: this.timestamp.toISOString(),
|
|
425
|
-
stack: this.stack,
|
|
426
|
-
cause: this.cause ? {
|
|
427
|
-
name: this.cause.name,
|
|
428
|
-
message: this.cause.message,
|
|
429
|
-
stack: this.cause.stack
|
|
430
|
-
} : undefined
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
toResponse() {
|
|
434
|
-
const body = {
|
|
435
|
-
error: {
|
|
436
|
-
code: this.code,
|
|
437
|
-
message: this.message,
|
|
438
|
-
statusCode: this.statusCode,
|
|
439
|
-
timestamp: this.timestamp.toISOString(),
|
|
440
|
-
requestId: this.context.requestId
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
return new Response(JSON.stringify(body), {
|
|
444
|
-
status: this.statusCode,
|
|
445
|
-
headers: {
|
|
446
|
-
"Content-Type": "application/json",
|
|
447
|
-
"X-Error-Code": this.code,
|
|
448
|
-
"X-Request-ID": this.context.requestId || ""
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
class ValidationException extends RouterException {
|
|
455
|
-
fields;
|
|
456
|
-
constructor(message, fields = {}, context) {
|
|
457
|
-
super({
|
|
458
|
-
code: "VALIDATION_ERROR",
|
|
459
|
-
message,
|
|
460
|
-
statusCode: 400,
|
|
461
|
-
context,
|
|
462
|
-
retryable: false,
|
|
463
|
-
severity: "low",
|
|
464
|
-
category: "validation"
|
|
465
|
-
});
|
|
466
|
-
this.fields = fields;
|
|
467
|
-
}
|
|
468
|
-
toResponse() {
|
|
469
|
-
const body = {
|
|
470
|
-
error: {
|
|
471
|
-
code: this.code,
|
|
472
|
-
message: this.message,
|
|
473
|
-
statusCode: this.statusCode,
|
|
474
|
-
timestamp: this.timestamp.toISOString(),
|
|
475
|
-
requestId: this.context.requestId,
|
|
476
|
-
fields: this.fields
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
return new Response(JSON.stringify(body), {
|
|
480
|
-
status: this.statusCode,
|
|
481
|
-
headers: {
|
|
482
|
-
"Content-Type": "application/json",
|
|
483
|
-
"X-Error-Code": this.code,
|
|
484
|
-
"X-Request-ID": this.context.requestId || ""
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
class AuthenticationException extends RouterException {
|
|
491
|
-
constructor(message = "Authentication required", context) {
|
|
492
|
-
super({
|
|
493
|
-
code: "AUTHENTICATION_ERROR",
|
|
494
|
-
message,
|
|
495
|
-
statusCode: 401,
|
|
496
|
-
context,
|
|
497
|
-
retryable: false,
|
|
498
|
-
severity: "medium",
|
|
499
|
-
category: "authentication"
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
class AuthorizationException extends RouterException {
|
|
505
|
-
requiredPermissions;
|
|
506
|
-
constructor(message = "Access denied", requiredPermissions, context) {
|
|
507
|
-
super({
|
|
508
|
-
code: "AUTHORIZATION_ERROR",
|
|
509
|
-
message,
|
|
510
|
-
statusCode: 403,
|
|
511
|
-
context,
|
|
512
|
-
retryable: false,
|
|
513
|
-
severity: "medium",
|
|
514
|
-
category: "authorization"
|
|
515
|
-
});
|
|
516
|
-
this.requiredPermissions = requiredPermissions;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
class NotFoundException extends RouterException {
|
|
521
|
-
resource;
|
|
522
|
-
resourceId;
|
|
523
|
-
constructor(message = "Resource not found", resource, resourceId, context) {
|
|
524
|
-
super({
|
|
525
|
-
code: "NOT_FOUND",
|
|
526
|
-
message,
|
|
527
|
-
statusCode: 404,
|
|
528
|
-
context,
|
|
529
|
-
retryable: false,
|
|
530
|
-
severity: "low",
|
|
531
|
-
category: "business"
|
|
532
|
-
});
|
|
533
|
-
this.resource = resource;
|
|
534
|
-
this.resourceId = resourceId;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
class MethodNotAllowedException extends RouterException {
|
|
539
|
-
allowedMethods;
|
|
540
|
-
constructor(method, allowedMethods, context) {
|
|
541
|
-
super({
|
|
542
|
-
code: "METHOD_NOT_ALLOWED",
|
|
543
|
-
message: `Method ${method} not allowed. Allowed methods: ${allowedMethods.join(", ")}`,
|
|
544
|
-
statusCode: 405,
|
|
545
|
-
context,
|
|
546
|
-
retryable: false,
|
|
547
|
-
severity: "low",
|
|
548
|
-
category: "business"
|
|
549
|
-
});
|
|
550
|
-
this.allowedMethods = allowedMethods;
|
|
551
|
-
}
|
|
552
|
-
toResponse() {
|
|
553
|
-
const response = super.toResponse();
|
|
554
|
-
response.headers.set("Allow", this.allowedMethods.join(", "));
|
|
555
|
-
return response;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
class TimeoutException extends RouterException {
|
|
560
|
-
timeoutMs;
|
|
561
|
-
constructor(message = "Request timeout", timeoutMs, context) {
|
|
562
|
-
super({
|
|
563
|
-
code: "REQUEST_TIMEOUT",
|
|
564
|
-
message,
|
|
565
|
-
statusCode: 408,
|
|
566
|
-
context,
|
|
567
|
-
retryable: true,
|
|
568
|
-
severity: "medium",
|
|
569
|
-
category: "network"
|
|
570
|
-
});
|
|
571
|
-
this.timeoutMs = timeoutMs;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
class RateLimitException extends RouterException {
|
|
576
|
-
retryAfter;
|
|
577
|
-
limit;
|
|
578
|
-
remaining;
|
|
579
|
-
constructor(message = "Rate limit exceeded", retryAfter, limit, remaining = 0, context) {
|
|
580
|
-
super({
|
|
581
|
-
code: "RATE_LIMIT_EXCEEDED",
|
|
582
|
-
message,
|
|
583
|
-
statusCode: 429,
|
|
584
|
-
context,
|
|
585
|
-
retryable: true,
|
|
586
|
-
severity: "medium",
|
|
587
|
-
category: "network"
|
|
588
|
-
});
|
|
589
|
-
this.retryAfter = retryAfter;
|
|
590
|
-
this.limit = limit;
|
|
591
|
-
this.remaining = remaining;
|
|
592
|
-
}
|
|
593
|
-
toResponse() {
|
|
594
|
-
const response = super.toResponse();
|
|
595
|
-
response.headers.set("Retry-After", this.retryAfter.toString());
|
|
596
|
-
response.headers.set("X-RateLimit-Limit", this.limit.toString());
|
|
597
|
-
response.headers.set("X-RateLimit-Remaining", this.remaining.toString());
|
|
598
|
-
return response;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
class InternalServerException extends RouterException {
|
|
603
|
-
constructor(message = "Internal server error", cause, context) {
|
|
604
|
-
super({
|
|
605
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
606
|
-
message,
|
|
607
|
-
statusCode: 500,
|
|
608
|
-
context,
|
|
609
|
-
cause,
|
|
610
|
-
retryable: true,
|
|
611
|
-
severity: "high",
|
|
612
|
-
category: "system"
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
class ServiceUnavailableException extends RouterException {
|
|
618
|
-
retryAfter;
|
|
619
|
-
constructor(message = "Service unavailable", retryAfter, context) {
|
|
620
|
-
super({
|
|
621
|
-
code: "SERVICE_UNAVAILABLE",
|
|
622
|
-
message,
|
|
623
|
-
statusCode: 503,
|
|
624
|
-
context,
|
|
625
|
-
retryable: true,
|
|
626
|
-
severity: "high",
|
|
627
|
-
category: "system"
|
|
628
|
-
});
|
|
629
|
-
this.retryAfter = retryAfter;
|
|
630
|
-
}
|
|
631
|
-
toResponse() {
|
|
632
|
-
const response = super.toResponse();
|
|
633
|
-
if (this.retryAfter) {
|
|
634
|
-
response.headers.set("Retry-After", this.retryAfter.toString());
|
|
635
|
-
}
|
|
636
|
-
return response;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
class DatabaseException extends RouterException {
|
|
641
|
-
query;
|
|
642
|
-
operation;
|
|
643
|
-
constructor(message, operation, query, cause, context) {
|
|
644
|
-
super({
|
|
645
|
-
code: "DATABASE_ERROR",
|
|
646
|
-
message,
|
|
647
|
-
statusCode: 500,
|
|
648
|
-
context,
|
|
649
|
-
cause,
|
|
650
|
-
retryable: true,
|
|
651
|
-
severity: "high",
|
|
652
|
-
category: "database"
|
|
653
|
-
});
|
|
654
|
-
this.operation = operation;
|
|
655
|
-
this.query = query;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
class ExternalServiceException extends RouterException {
|
|
660
|
-
service;
|
|
661
|
-
endpoint;
|
|
662
|
-
constructor(message, service, endpoint, statusCode = 502, cause, context) {
|
|
663
|
-
super({
|
|
664
|
-
code: "EXTERNAL_SERVICE_ERROR",
|
|
665
|
-
message,
|
|
666
|
-
statusCode,
|
|
667
|
-
context,
|
|
668
|
-
cause,
|
|
669
|
-
retryable: true,
|
|
670
|
-
severity: "medium",
|
|
671
|
-
category: "external"
|
|
672
|
-
});
|
|
673
|
-
this.service = service;
|
|
674
|
-
this.endpoint = endpoint;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
class CircuitBreakerOpenException extends RouterException {
|
|
679
|
-
service;
|
|
680
|
-
nextAttemptAt;
|
|
681
|
-
constructor(service, nextAttemptAt, context) {
|
|
682
|
-
super({
|
|
683
|
-
code: "CIRCUIT_BREAKER_OPEN",
|
|
684
|
-
message: `Circuit breaker is open for service: ${service}. Next attempt at: ${nextAttemptAt.toISOString()}`,
|
|
685
|
-
statusCode: 503,
|
|
686
|
-
context,
|
|
687
|
-
retryable: true,
|
|
688
|
-
severity: "medium",
|
|
689
|
-
category: "external"
|
|
690
|
-
});
|
|
691
|
-
this.service = service;
|
|
692
|
-
this.nextAttemptAt = nextAttemptAt;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
class BusinessLogicException extends RouterException {
|
|
697
|
-
businessRule;
|
|
698
|
-
constructor(message, businessRule, context) {
|
|
699
|
-
super({
|
|
700
|
-
code: "BUSINESS_LOGIC_ERROR",
|
|
701
|
-
message,
|
|
702
|
-
statusCode: 422,
|
|
703
|
-
context,
|
|
704
|
-
retryable: false,
|
|
705
|
-
severity: "low",
|
|
706
|
-
category: "business"
|
|
707
|
-
});
|
|
708
|
-
this.businessRule = businessRule;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
class ConfigurationException extends RouterException {
|
|
713
|
-
configKey;
|
|
714
|
-
constructor(message, configKey, context) {
|
|
715
|
-
super({
|
|
716
|
-
code: "CONFIGURATION_ERROR",
|
|
717
|
-
message,
|
|
718
|
-
statusCode: 500,
|
|
719
|
-
context,
|
|
720
|
-
retryable: false,
|
|
721
|
-
severity: "critical",
|
|
722
|
-
category: "system"
|
|
723
|
-
});
|
|
724
|
-
this.configKey = configKey;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
class FileSystemException extends RouterException {
|
|
729
|
-
path;
|
|
730
|
-
operation;
|
|
731
|
-
constructor(message, path, operation, cause, context) {
|
|
732
|
-
super({
|
|
733
|
-
code: "FILESYSTEM_ERROR",
|
|
734
|
-
message,
|
|
735
|
-
statusCode: 500,
|
|
736
|
-
context,
|
|
737
|
-
cause,
|
|
738
|
-
retryable: false,
|
|
739
|
-
severity: "medium",
|
|
740
|
-
category: "system"
|
|
741
|
-
});
|
|
742
|
-
this.path = path;
|
|
743
|
-
this.operation = operation;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
var ErrorFactory = {
|
|
747
|
-
validation: (message, fields, context) => new ValidationException(message, fields, context),
|
|
748
|
-
authentication: (message, context) => new AuthenticationException(message, context),
|
|
749
|
-
authorization: (message, requiredPermissions, context) => new AuthorizationException(message, requiredPermissions, context),
|
|
750
|
-
notFound: (resource, resourceId, context) => new NotFoundException(undefined, resource, resourceId, context),
|
|
751
|
-
methodNotAllowed: (method, allowedMethods, context) => new MethodNotAllowedException(method, allowedMethods, context),
|
|
752
|
-
timeout: (timeoutMs, message, context) => new TimeoutException(message, timeoutMs, context),
|
|
753
|
-
rateLimit: (retryAfter, limit, remaining, context) => new RateLimitException(undefined, retryAfter, limit, remaining, context),
|
|
754
|
-
internalServer: (message, cause, context) => new InternalServerException(message, cause, context),
|
|
755
|
-
serviceUnavailable: (message, retryAfter, context) => new ServiceUnavailableException(message, retryAfter, context),
|
|
756
|
-
database: (message, operation, query, cause, context) => new DatabaseException(message, operation, query, cause, context),
|
|
757
|
-
externalService: (message, service, endpoint, statusCode, cause, context) => new ExternalServiceException(message, service, endpoint, statusCode, cause, context),
|
|
758
|
-
circuitBreakerOpen: (service, nextAttemptAt, context) => new CircuitBreakerOpenException(service, nextAttemptAt, context),
|
|
759
|
-
businessLogic: (message, businessRule, context) => new BusinessLogicException(message, businessRule, context),
|
|
760
|
-
configuration: (message, configKey, context) => new ConfigurationException(message, configKey, context),
|
|
761
|
-
fileSystem: (message, path, operation, cause, context) => new FileSystemException(message, path, operation, cause, context)
|
|
762
|
-
};
|
|
763
|
-
|
|
764
430
|
// src/errors/circuit-breaker.ts
|
|
765
431
|
class CircuitBreaker {
|
|
766
432
|
config;
|
|
@@ -794,7 +460,7 @@ class CircuitBreaker {
|
|
|
794
460
|
consecutiveFailures: 0,
|
|
795
461
|
consecutiveSuccesses: 0
|
|
796
462
|
};
|
|
797
|
-
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4);
|
|
463
|
+
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4).unref?.();
|
|
798
464
|
}
|
|
799
465
|
async execute(call) {
|
|
800
466
|
if (this.state === "OPEN") {
|
|
@@ -1705,6 +1371,7 @@ class ServiceHealthMonitor {
|
|
|
1705
1371
|
consecutiveFailures: 0
|
|
1706
1372
|
});
|
|
1707
1373
|
const interval = setInterval(() => this.performHealthCheck(serviceName, healthConfig), healthConfig.interval);
|
|
1374
|
+
interval.unref?.();
|
|
1708
1375
|
this.healthCheckIntervals.set(serviceName, interval);
|
|
1709
1376
|
}
|
|
1710
1377
|
}
|
|
@@ -2334,448 +2001,44 @@ function createAdvancedErrorHandler(config2 = {}) {
|
|
|
2334
2001
|
cleanup: () => handler.cleanup()
|
|
2335
2002
|
};
|
|
2336
2003
|
}
|
|
2337
|
-
// src/errors/router-errors.ts
|
|
2338
|
-
class RouteConflictException extends RouterException {
|
|
2339
|
-
conflictingRoutes;
|
|
2340
|
-
suggestion;
|
|
2341
|
-
constructor(conflictingRoutes, context) {
|
|
2342
|
-
const message = RouteConflictException.buildMessage(conflictingRoutes);
|
|
2343
|
-
const suggestion = RouteConflictException.buildSuggestion(conflictingRoutes);
|
|
2344
|
-
super({
|
|
2345
|
-
code: "ROUTE_CONFLICT",
|
|
2346
|
-
message,
|
|
2347
|
-
statusCode: 500,
|
|
2348
|
-
context,
|
|
2349
|
-
retryable: false,
|
|
2350
|
-
severity: "critical",
|
|
2351
|
-
category: "system"
|
|
2352
|
-
});
|
|
2353
|
-
this.conflictingRoutes = conflictingRoutes;
|
|
2354
|
-
this.suggestion = suggestion;
|
|
2355
|
-
}
|
|
2356
|
-
static buildMessage(routes) {
|
|
2357
|
-
const lines = [
|
|
2358
|
-
"Route conflict detected! Multiple routes match the same path pattern.",
|
|
2359
|
-
"",
|
|
2360
|
-
"Conflicting routes:"
|
|
2361
|
-
];
|
|
2362
|
-
for (const route of routes) {
|
|
2363
|
-
lines.push(` \u2022 ${route.method} ${route.path}`);
|
|
2364
|
-
if (route.name) {
|
|
2365
|
-
lines.push(` Name: "${route.name}"`);
|
|
2366
|
-
}
|
|
2367
|
-
if (route.definedAt) {
|
|
2368
|
-
lines.push(` Defined at: ${route.definedAt}`);
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
return lines.join(`
|
|
2372
|
-
`);
|
|
2373
|
-
}
|
|
2374
|
-
static buildSuggestion(routes) {
|
|
2375
|
-
const suggestions = [];
|
|
2376
|
-
const hasStatic = routes.some((r) => !r.path.includes("{"));
|
|
2377
|
-
const hasDynamic = routes.some((r) => r.path.includes("{"));
|
|
2378
|
-
if (hasStatic && hasDynamic) {
|
|
2379
|
-
suggestions.push("\u2022 Static routes should be defined before dynamic routes", "\u2022 Consider using route constraints (whereNumber, whereUuid) to differentiate");
|
|
2380
|
-
}
|
|
2381
|
-
const dynamicRoutes = routes.filter((r) => r.path.includes("{"));
|
|
2382
|
-
if (dynamicRoutes.length > 1) {
|
|
2383
|
-
suggestions.push("\u2022 Use different parameter names or add constraints:", ' router.get("/users/{id}").whereNumber("id")', ' router.get("/users/{slug}").whereAlpha("slug")');
|
|
2384
|
-
}
|
|
2385
|
-
const hasWildcard = routes.some((r) => r.path.includes("*"));
|
|
2386
|
-
if (hasWildcard) {
|
|
2387
|
-
suggestions.push("\u2022 Wildcard routes should be defined last", "\u2022 Consider using more specific path prefixes");
|
|
2388
|
-
}
|
|
2389
|
-
if (suggestions.length === 0) {
|
|
2390
|
-
suggestions.push("\u2022 Rename one of the conflicting routes", "\u2022 Use route groups with different prefixes", "\u2022 Add parameter constraints to differentiate routes");
|
|
2391
|
-
}
|
|
2392
|
-
return `How to fix:
|
|
2393
|
-
${suggestions.join(`
|
|
2394
|
-
`)}`;
|
|
2395
|
-
}
|
|
2396
|
-
toJSON() {
|
|
2397
|
-
return {
|
|
2398
|
-
...super.toJSON(),
|
|
2399
|
-
conflictingRoutes: this.conflictingRoutes,
|
|
2400
|
-
suggestion: this.suggestion
|
|
2401
|
-
};
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
class CircularDependencyException extends RouterException {
|
|
2406
|
-
dependencyChain;
|
|
2407
|
-
suggestion;
|
|
2408
|
-
constructor(dependencyChain, context) {
|
|
2409
|
-
const message = CircularDependencyException.buildMessage(dependencyChain);
|
|
2410
|
-
const suggestion = CircularDependencyException.buildSuggestion(dependencyChain);
|
|
2411
|
-
super({
|
|
2412
|
-
code: "CIRCULAR_DEPENDENCY",
|
|
2413
|
-
message,
|
|
2414
|
-
statusCode: 500,
|
|
2415
|
-
context,
|
|
2416
|
-
retryable: false,
|
|
2417
|
-
severity: "critical",
|
|
2418
|
-
category: "system"
|
|
2419
|
-
});
|
|
2420
|
-
this.dependencyChain = dependencyChain;
|
|
2421
|
-
this.suggestion = suggestion;
|
|
2422
|
-
}
|
|
2423
|
-
static buildMessage(chain) {
|
|
2424
|
-
const lines = [
|
|
2425
|
-
"Circular dependency detected in the dependency injection container!",
|
|
2426
|
-
"",
|
|
2427
|
-
"Dependency chain:",
|
|
2428
|
-
""
|
|
2429
|
-
];
|
|
2430
|
-
for (let i = 0;i < chain.length; i++) {
|
|
2431
|
-
const isLast = i === chain.length - 1;
|
|
2432
|
-
const prefix = i === 0 ? "\u250C\u2500\u25BA" : isLast ? "\u2514\u2500\u25BA" : "\u251C\u2500\u25BA";
|
|
2433
|
-
lines.push(` ${prefix} ${chain[i]}`);
|
|
2434
|
-
if (!isLast) {
|
|
2435
|
-
lines.push(" \u2502");
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
lines.push(" \u2502");
|
|
2439
|
-
lines.push(` \u2514\u2500\u2500 cycles back to: ${chain[0]}`);
|
|
2440
|
-
return lines.join(`
|
|
2441
|
-
`);
|
|
2442
|
-
}
|
|
2443
|
-
static buildSuggestion(chain) {
|
|
2444
|
-
return `How to fix:
|
|
2445
|
-
|
|
2446
|
-
1. Break the cycle by introducing an interface:
|
|
2447
|
-
\u2022 Create an interface for one of the dependencies
|
|
2448
|
-
\u2022 Use lazy injection: container.bind('${chain[0]}').lazy()
|
|
2449
|
-
|
|
2450
|
-
2. Restructure your dependencies:
|
|
2451
|
-
\u2022 Extract shared logic into a separate service
|
|
2452
|
-
\u2022 Use events/callbacks instead of direct dependencies
|
|
2453
|
-
|
|
2454
|
-
3. Use setter injection instead of constructor injection:
|
|
2455
|
-
\u2022 Remove the dependency from the constructor
|
|
2456
|
-
\u2022 Set it after instantiation using a setter method
|
|
2457
|
-
|
|
2458
|
-
4. Check if the dependency is actually needed:
|
|
2459
|
-
\u2022 Sometimes circular dependencies indicate a design issue
|
|
2460
|
-
\u2022 Consider if the classes have too many responsibilities
|
|
2461
|
-
|
|
2462
|
-
Dependency chain involved:
|
|
2463
|
-
${chain.map((dep, i) => ` ${i + 1}. ${dep}`).join(`
|
|
2464
|
-
`)}`;
|
|
2465
|
-
}
|
|
2466
|
-
toJSON() {
|
|
2467
|
-
return {
|
|
2468
|
-
...super.toJSON(),
|
|
2469
|
-
dependencyChain: this.dependencyChain,
|
|
2470
|
-
suggestion: this.suggestion
|
|
2471
|
-
};
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
class MiddlewareConfigurationException extends RouterException {
|
|
2476
|
-
middlewareName;
|
|
2477
|
-
configErrors;
|
|
2478
|
-
suggestion;
|
|
2479
|
-
constructor(middlewareName, configErrors, context) {
|
|
2480
|
-
const message = `Middleware "${middlewareName}" configuration error:
|
|
2481
|
-
${configErrors.map((e) => ` \u2022 ${e}`).join(`
|
|
2482
|
-
`)}`;
|
|
2483
|
-
super({
|
|
2484
|
-
code: "MIDDLEWARE_CONFIGURATION_ERROR",
|
|
2485
|
-
message,
|
|
2486
|
-
statusCode: 500,
|
|
2487
|
-
context,
|
|
2488
|
-
retryable: false,
|
|
2489
|
-
severity: "high",
|
|
2490
|
-
category: "system"
|
|
2491
|
-
});
|
|
2492
|
-
this.middlewareName = middlewareName;
|
|
2493
|
-
this.configErrors = configErrors;
|
|
2494
|
-
this.suggestion = this.buildSuggestion();
|
|
2495
|
-
}
|
|
2496
|
-
buildSuggestion() {
|
|
2497
|
-
return `How to fix:
|
|
2498
|
-
\u2022 Check the middleware documentation for required options
|
|
2499
|
-
\u2022 Ensure all required configuration values are provided
|
|
2500
|
-
\u2022 Verify configuration value types match expected types
|
|
2501
|
-
|
|
2502
|
-
Example configuration:
|
|
2503
|
-
router.use(new ${this.middlewareName}({
|
|
2504
|
-
// Add required options here
|
|
2505
|
-
}))`;
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
class RouteNotFoundException extends RouterException {
|
|
2510
|
-
requestedPath;
|
|
2511
|
-
requestedMethod;
|
|
2512
|
-
similarRoutes;
|
|
2513
|
-
suggestion;
|
|
2514
|
-
constructor(requestedMethod, requestedPath, similarRoutes = [], context) {
|
|
2515
|
-
const message = RouteNotFoundException.buildMessage(requestedMethod, requestedPath, similarRoutes);
|
|
2516
|
-
super({
|
|
2517
|
-
code: "ROUTE_NOT_FOUND",
|
|
2518
|
-
message,
|
|
2519
|
-
statusCode: 404,
|
|
2520
|
-
context,
|
|
2521
|
-
retryable: false,
|
|
2522
|
-
severity: "low",
|
|
2523
|
-
category: "business"
|
|
2524
|
-
});
|
|
2525
|
-
this.requestedPath = requestedPath;
|
|
2526
|
-
this.requestedMethod = requestedMethod;
|
|
2527
|
-
this.similarRoutes = similarRoutes;
|
|
2528
|
-
this.suggestion = this.buildSuggestion();
|
|
2529
|
-
}
|
|
2530
|
-
static buildMessage(method, path, similarRoutes) {
|
|
2531
|
-
const lines = [
|
|
2532
|
-
`Route not found: ${method} ${path}`
|
|
2533
|
-
];
|
|
2534
|
-
if (similarRoutes.length > 0) {
|
|
2535
|
-
lines.push("");
|
|
2536
|
-
lines.push("Did you mean one of these routes?");
|
|
2537
|
-
for (const route of similarRoutes.slice(0, 5)) {
|
|
2538
|
-
lines.push(` \u2022 ${route.method} ${route.path}`);
|
|
2539
|
-
if (route.similarity) {
|
|
2540
|
-
lines.push(` (${Math.round(route.similarity * 100)}% match)`);
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
return lines.join(`
|
|
2545
|
-
`);
|
|
2546
|
-
}
|
|
2547
|
-
buildSuggestion() {
|
|
2548
|
-
const suggestions = [
|
|
2549
|
-
`\u2022 Verify the HTTP method is correct (you used ${this.requestedMethod})`,
|
|
2550
|
-
"\u2022 Check for typos in the URL path",
|
|
2551
|
-
"\u2022 Ensure the route is registered before the server starts",
|
|
2552
|
-
"\u2022 Check if the route requires specific middleware (e.g., auth)"
|
|
2553
|
-
];
|
|
2554
|
-
if (this.requestedPath.includes("//")) {
|
|
2555
|
-
suggestions.push("\u2022 Remove duplicate slashes from the URL");
|
|
2556
|
-
}
|
|
2557
|
-
if (!this.requestedPath.startsWith("/")) {
|
|
2558
|
-
suggestions.push("\u2022 Ensure the path starts with a forward slash (/)");
|
|
2559
|
-
}
|
|
2560
|
-
return `How to fix:
|
|
2561
|
-
${suggestions.join(`
|
|
2562
|
-
`)}`;
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
class HandlerResolutionException extends RouterException {
|
|
2567
|
-
handlerRef;
|
|
2568
|
-
suggestion;
|
|
2569
|
-
constructor(handlerRef, reason, context) {
|
|
2570
|
-
const message = `Failed to resolve handler "${handlerRef}": ${reason}`;
|
|
2571
|
-
super({
|
|
2572
|
-
code: "HANDLER_RESOLUTION_ERROR",
|
|
2573
|
-
message,
|
|
2574
|
-
statusCode: 500,
|
|
2575
|
-
context,
|
|
2576
|
-
retryable: false,
|
|
2577
|
-
severity: "high",
|
|
2578
|
-
category: "system"
|
|
2579
|
-
});
|
|
2580
|
-
this.handlerRef = handlerRef;
|
|
2581
|
-
this.suggestion = this.buildSuggestion(handlerRef);
|
|
2582
|
-
}
|
|
2583
|
-
buildSuggestion(handlerRef) {
|
|
2584
|
-
const isControllerMethod = handlerRef.includes("@");
|
|
2585
|
-
const isActionPath = handlerRef.includes("/");
|
|
2586
|
-
if (isControllerMethod) {
|
|
2587
|
-
const [controller, method] = handlerRef.split("@");
|
|
2588
|
-
return `How to fix:
|
|
2589
|
-
\u2022 Ensure the controller "${controller}" is registered in the container
|
|
2590
|
-
\u2022 Verify the method "${method}" exists and is public
|
|
2591
|
-
\u2022 Check the controller file is imported
|
|
2592
|
-
|
|
2593
|
-
Example:
|
|
2594
|
-
// Register the controller
|
|
2595
|
-
container.singleton('${controller}', ${controller})
|
|
2596
|
-
|
|
2597
|
-
// Define the route
|
|
2598
|
-
router.get('/path', '${handlerRef}')`;
|
|
2599
|
-
}
|
|
2600
|
-
if (isActionPath) {
|
|
2601
|
-
return `How to fix:
|
|
2602
|
-
\u2022 Ensure the action class exists at the specified path
|
|
2603
|
-
\u2022 Verify the action has a "handle" method
|
|
2604
|
-
\u2022 Check the action file is properly exported
|
|
2605
|
-
|
|
2606
|
-
Example:
|
|
2607
|
-
// Action class at ${handlerRef}.ts
|
|
2608
|
-
export default class ${handlerRef.split("/").pop()} {
|
|
2609
|
-
handle(req: Request): Response {
|
|
2610
|
-
return new Response('OK')
|
|
2611
|
-
}
|
|
2612
|
-
}`;
|
|
2613
|
-
}
|
|
2614
|
-
return `How to fix:
|
|
2615
|
-
\u2022 Verify the handler reference is correct
|
|
2616
|
-
\u2022 Ensure the handler function/class is exported
|
|
2617
|
-
\u2022 Check for circular dependencies in the handler module`;
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
class ModelBindingException extends RouterException {
|
|
2622
|
-
modelName;
|
|
2623
|
-
paramName;
|
|
2624
|
-
paramValue;
|
|
2625
|
-
suggestion;
|
|
2626
|
-
constructor(modelName, paramName, paramValue, reason, context) {
|
|
2627
|
-
const message = `Failed to bind model "${modelName}" for parameter "${paramName}" with value "${paramValue}": ${reason}`;
|
|
2628
|
-
super({
|
|
2629
|
-
code: "MODEL_BINDING_ERROR",
|
|
2630
|
-
message,
|
|
2631
|
-
statusCode: 404,
|
|
2632
|
-
context,
|
|
2633
|
-
retryable: false,
|
|
2634
|
-
severity: "low",
|
|
2635
|
-
category: "business"
|
|
2636
|
-
});
|
|
2637
|
-
this.modelName = modelName;
|
|
2638
|
-
this.paramName = paramName;
|
|
2639
|
-
this.paramValue = paramValue;
|
|
2640
|
-
this.suggestion = this.buildSuggestion();
|
|
2641
|
-
}
|
|
2642
|
-
buildSuggestion() {
|
|
2643
|
-
return `How to fix:
|
|
2644
|
-
\u2022 Verify the ${this.paramName} "${this.paramValue}" exists in your data source
|
|
2645
|
-
\u2022 Check if the model resolver is correctly configured
|
|
2646
|
-
\u2022 Ensure the route parameter matches the model's identifier field
|
|
2647
|
-
|
|
2648
|
-
Example model binding:
|
|
2649
|
-
router.model('${this.paramName}', async (value) => {
|
|
2650
|
-
const ${this.modelName.toLowerCase()} = await db.${this.modelName.toLowerCase()}s.findUnique({
|
|
2651
|
-
where: { id: value }
|
|
2652
|
-
})
|
|
2653
|
-
if (!${this.modelName.toLowerCase()}) {
|
|
2654
|
-
throw new Error('${this.modelName} not found')
|
|
2655
|
-
}
|
|
2656
|
-
return ${this.modelName.toLowerCase()}
|
|
2657
|
-
})`;
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
function findSimilarRoutes(requestedPath, availableRoutes, maxResults = 5) {
|
|
2661
|
-
const results = [];
|
|
2662
|
-
for (const route of availableRoutes) {
|
|
2663
|
-
const similarity = calculateSimilarity(requestedPath, route.path);
|
|
2664
|
-
if (similarity > 0.3) {
|
|
2665
|
-
results.push({
|
|
2666
|
-
method: route.method,
|
|
2667
|
-
path: route.path,
|
|
2668
|
-
similarity
|
|
2669
|
-
});
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
return results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0)).slice(0, maxResults);
|
|
2673
|
-
}
|
|
2674
|
-
function calculateSimilarity(str1, str2) {
|
|
2675
|
-
const len1 = str1.length;
|
|
2676
|
-
const len2 = str2.length;
|
|
2677
|
-
if (len1 === 0)
|
|
2678
|
-
return len2 === 0 ? 1 : 0;
|
|
2679
|
-
if (len2 === 0)
|
|
2680
|
-
return 0;
|
|
2681
|
-
const matrix = [];
|
|
2682
|
-
for (let i = 0;i <= len1; i++) {
|
|
2683
|
-
matrix[i] = [i];
|
|
2684
|
-
}
|
|
2685
|
-
for (let j = 0;j <= len2; j++) {
|
|
2686
|
-
matrix[0][j] = j;
|
|
2687
|
-
}
|
|
2688
|
-
for (let i = 1;i <= len1; i++) {
|
|
2689
|
-
for (let j = 1;j <= len2; j++) {
|
|
2690
|
-
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
2691
|
-
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
const distance = matrix[len1][len2];
|
|
2695
|
-
const maxLen = Math.max(len1, len2);
|
|
2696
|
-
return 1 - distance / maxLen;
|
|
2697
|
-
}
|
|
2698
|
-
function detectRouteConflicts(routes) {
|
|
2699
|
-
const conflicts = [];
|
|
2700
|
-
const checked = new Set;
|
|
2701
|
-
for (let i = 0;i < routes.length; i++) {
|
|
2702
|
-
const route1 = routes[i];
|
|
2703
|
-
const key1 = `${route1.method}:${route1.path}`;
|
|
2704
|
-
if (checked.has(key1))
|
|
2705
|
-
continue;
|
|
2706
|
-
const conflicting = [];
|
|
2707
|
-
for (let j = i + 1;j < routes.length; j++) {
|
|
2708
|
-
const route2 = routes[j];
|
|
2709
|
-
if (route1.method === route2.method && routesConflict(route1.path, route2.path)) {
|
|
2710
|
-
if (conflicting.length === 0) {
|
|
2711
|
-
conflicting.push({
|
|
2712
|
-
method: route1.method,
|
|
2713
|
-
path: route1.path,
|
|
2714
|
-
name: route1.name
|
|
2715
|
-
});
|
|
2716
|
-
}
|
|
2717
|
-
conflicting.push({
|
|
2718
|
-
method: route2.method,
|
|
2719
|
-
path: route2.path,
|
|
2720
|
-
name: route2.name
|
|
2721
|
-
});
|
|
2722
|
-
checked.add(`${route2.method}:${route2.path}`);
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
if (conflicting.length > 0) {
|
|
2726
|
-
conflicts.push(conflicting);
|
|
2727
|
-
}
|
|
2728
|
-
checked.add(key1);
|
|
2729
|
-
}
|
|
2730
|
-
return conflicts;
|
|
2731
|
-
}
|
|
2732
|
-
function routesConflict(path1, path2) {
|
|
2733
|
-
if (path1 === path2)
|
|
2734
|
-
return true;
|
|
2735
|
-
const segments1 = path1.split("/").filter(Boolean);
|
|
2736
|
-
const segments2 = path2.split("/").filter(Boolean);
|
|
2737
|
-
if (segments1.length !== segments2.length) {
|
|
2738
|
-
const hasWildcard1 = path1.includes("*");
|
|
2739
|
-
const hasWildcard2 = path2.includes("*");
|
|
2740
|
-
if (!hasWildcard1 && !hasWildcard2)
|
|
2741
|
-
return false;
|
|
2742
|
-
}
|
|
2743
|
-
const maxLen = Math.max(segments1.length, segments2.length);
|
|
2744
|
-
for (let i = 0;i < maxLen; i++) {
|
|
2745
|
-
const seg1 = segments1[i] || "";
|
|
2746
|
-
const seg2 = segments2[i] || "";
|
|
2747
|
-
if (seg1 === "*" || seg2 === "*")
|
|
2748
|
-
return true;
|
|
2749
|
-
const isDynamic1 = seg1.startsWith("{") && seg1.endsWith("}");
|
|
2750
|
-
const isDynamic2 = seg2.startsWith("{") && seg2.endsWith("}");
|
|
2751
|
-
if (!isDynamic1 && !isDynamic2) {
|
|
2752
|
-
if (seg1 !== seg2)
|
|
2753
|
-
return false;
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
return true;
|
|
2757
|
-
}
|
|
2758
|
-
var RouterErrorFactory = {
|
|
2759
|
-
routeConflict: (routes, context) => new RouteConflictException(routes, context),
|
|
2760
|
-
circularDependency: (chain, context) => new CircularDependencyException(chain, context),
|
|
2761
|
-
middlewareConfig: (name, errors, context) => new MiddlewareConfigurationException(name, errors, context),
|
|
2762
|
-
routeNotFound: (method, path, similarRoutes, context) => new RouteNotFoundException(method, path, similarRoutes, context),
|
|
2763
|
-
handlerResolution: (ref, reason, context) => new HandlerResolutionException(ref, reason, context),
|
|
2764
|
-
modelBinding: (model, param, value, reason, context) => new ModelBindingException(model, param, value, reason, context)
|
|
2765
|
-
};
|
|
2766
2004
|
// src/request/context.ts
|
|
2767
2005
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2768
2006
|
var storage = new AsyncLocalStorage;
|
|
2007
|
+
var contextEnabled = false;
|
|
2008
|
+
var syncCurrent = null;
|
|
2009
|
+
function enableRequestContext() {
|
|
2010
|
+
contextEnabled = true;
|
|
2011
|
+
}
|
|
2769
2012
|
function runWithRequest(initial, fn) {
|
|
2013
|
+
if (!contextEnabled) {
|
|
2014
|
+
syncCurrent = initial;
|
|
2015
|
+
const result = fn();
|
|
2016
|
+
if (result instanceof Promise) {
|
|
2017
|
+
return result.finally(() => {
|
|
2018
|
+
syncCurrent = null;
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
syncCurrent = null;
|
|
2022
|
+
return result;
|
|
2023
|
+
}
|
|
2770
2024
|
return storage.run({ current: initial }, fn);
|
|
2771
2025
|
}
|
|
2772
2026
|
function setCurrentRequest(req) {
|
|
2773
2027
|
const store = storage.getStore();
|
|
2774
|
-
if (store)
|
|
2028
|
+
if (store) {
|
|
2775
2029
|
store.current = req;
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (syncCurrent !== null) {
|
|
2033
|
+
syncCurrent = req;
|
|
2034
|
+
}
|
|
2776
2035
|
}
|
|
2777
2036
|
function getCurrentRequest() {
|
|
2778
|
-
|
|
2037
|
+
if (!contextEnabled) {
|
|
2038
|
+
contextEnabled = true;
|
|
2039
|
+
return syncCurrent ?? undefined;
|
|
2040
|
+
}
|
|
2041
|
+
return storage.getStore()?.current ?? syncCurrent ?? undefined;
|
|
2779
2042
|
}
|
|
2780
2043
|
function request() {
|
|
2781
2044
|
const req = getCurrentRequest();
|
|
@@ -3942,13 +3205,20 @@ function registerFileBasedRouting(RouterClass) {
|
|
|
3942
3205
|
}
|
|
3943
3206
|
}
|
|
3944
3207
|
this._viewsDir = viewsDir;
|
|
3208
|
+
const existingGetPaths = new Set;
|
|
3209
|
+
for (const r of this.routes) {
|
|
3210
|
+
if (r.method === "GET") {
|
|
3211
|
+
existingGetPaths.add(r.path);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3945
3214
|
for (const route of routes) {
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
if (!existingRoute) {
|
|
3949
|
-
this.get(route.routePath, handler, "web");
|
|
3950
|
-
this._fileBasedRoutes.push(route);
|
|
3215
|
+
if (existingGetPaths.has(route.routePath)) {
|
|
3216
|
+
continue;
|
|
3951
3217
|
}
|
|
3218
|
+
const handler = createViewHandler(route.filePath, viewsDir, config2, this.config.queryPreservation);
|
|
3219
|
+
this.get(route.routePath, handler, "web");
|
|
3220
|
+
this._fileBasedRoutes.push(route);
|
|
3221
|
+
existingGetPaths.add(route.routePath);
|
|
3952
3222
|
}
|
|
3953
3223
|
this._fileRoutesInitialized = true;
|
|
3954
3224
|
},
|
|
@@ -5696,7 +4966,11 @@ function url(name, params = {}, options = {}) {
|
|
|
5696
4966
|
consumed.add(key);
|
|
5697
4967
|
}
|
|
5698
4968
|
}
|
|
5699
|
-
|
|
4969
|
+
if (resolved.includes("?}")) {
|
|
4970
|
+
resolved = resolved.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
4971
|
+
if (resolved.length > 1 && resolved.endsWith("/"))
|
|
4972
|
+
resolved = resolved.slice(0, -1);
|
|
4973
|
+
}
|
|
5700
4974
|
const missing = resolved.match(/\{([^}?]+)\}/g);
|
|
5701
4975
|
if (missing && missing.length > 0)
|
|
5702
4976
|
throw new Error(`Missing required params for route '${name}': ${missing.join(", ")}`);
|
|
@@ -5715,24 +4989,229 @@ function url(name, params = {}, options = {}) {
|
|
|
5715
4989
|
return resolved;
|
|
5716
4990
|
}
|
|
5717
4991
|
|
|
5718
|
-
// src/
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
path;
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
cacheConfig;
|
|
5725
|
-
throttleConfig;
|
|
5726
|
-
routeName;
|
|
5727
|
-
modelBindings = {};
|
|
5728
|
-
constructor(method, path, handler) {
|
|
5729
|
-
this.method = method;
|
|
5730
|
-
this.path = path;
|
|
5731
|
-
this.handler = handler;
|
|
4992
|
+
// src/utils.ts
|
|
4993
|
+
import { join as join3 } from "path";
|
|
4994
|
+
function normalizePath(path) {
|
|
4995
|
+
let normalizedPath = path.replace(/\/+/g, "/");
|
|
4996
|
+
if (!normalizedPath.startsWith("/")) {
|
|
4997
|
+
normalizedPath = `/${normalizedPath}`;
|
|
5732
4998
|
}
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
4999
|
+
if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
|
|
5000
|
+
normalizedPath = normalizedPath.slice(0, -1);
|
|
5001
|
+
}
|
|
5002
|
+
return normalizedPath;
|
|
5003
|
+
}
|
|
5004
|
+
function toActionPath(path) {
|
|
5005
|
+
return path.replace(/\//g, "_").toLowerCase();
|
|
5006
|
+
}
|
|
5007
|
+
function isActionClass(handler) {
|
|
5008
|
+
if (typeof handler !== "function" && typeof handler !== "object") {
|
|
5009
|
+
return false;
|
|
5010
|
+
}
|
|
5011
|
+
if (typeof handler === "function" && handler.prototype) {
|
|
5012
|
+
const prototype = handler.prototype;
|
|
5013
|
+
if (typeof prototype.handle === "function") {
|
|
5014
|
+
return true;
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
if (typeof handler === "object" && handler !== null && "handle" in handler && typeof handler.handle === "function") {
|
|
5018
|
+
return true;
|
|
5019
|
+
}
|
|
5020
|
+
return false;
|
|
5021
|
+
}
|
|
5022
|
+
function isRouteHandler(handler) {
|
|
5023
|
+
return typeof handler === "function" && !isActionClass(handler);
|
|
5024
|
+
}
|
|
5025
|
+
function extractParamNames(pattern) {
|
|
5026
|
+
const matches = pattern.match(/\{([^}]+)\}/g);
|
|
5027
|
+
return matches ? matches.map((m) => m.slice(1, -1).replace("?", "")) : [];
|
|
5028
|
+
}
|
|
5029
|
+
function createPathRegex(pattern) {
|
|
5030
|
+
const regexPattern = pattern.replace(/\{([^}]+)\}/g, "([^/]+)");
|
|
5031
|
+
return new RegExp(`^${regexPattern}$`);
|
|
5032
|
+
}
|
|
5033
|
+
var PARAM_SEGMENT_RE = /\{([^}:]+)(?::[^}]+)?(\?)?\}/;
|
|
5034
|
+
var OPTIONAL_SEGMENT_RE = /\{[^}]+\?\}/;
|
|
5035
|
+
var compiledPatternCache = new Map;
|
|
5036
|
+
var constraintRegexCache = new Map;
|
|
5037
|
+
function compilePattern(pattern) {
|
|
5038
|
+
const cached = compiledPatternCache.get(pattern);
|
|
5039
|
+
if (cached)
|
|
5040
|
+
return cached;
|
|
5041
|
+
const isWildcard = pattern.endsWith("/*");
|
|
5042
|
+
const normalizedPattern = normalizePath(pattern);
|
|
5043
|
+
let optionalCount = 0;
|
|
5044
|
+
const segments = normalizedPattern.split("/").filter(Boolean).map((seg) => {
|
|
5045
|
+
const isOptional = OPTIONAL_SEGMENT_RE.test(seg);
|
|
5046
|
+
if (isOptional)
|
|
5047
|
+
optionalCount++;
|
|
5048
|
+
const paramMatch = seg.match(PARAM_SEGMENT_RE);
|
|
5049
|
+
return {
|
|
5050
|
+
isParam: !!paramMatch,
|
|
5051
|
+
paramName: paramMatch ? paramMatch[1].replace("?", "") : "",
|
|
5052
|
+
isOptional,
|
|
5053
|
+
literal: seg
|
|
5054
|
+
};
|
|
5055
|
+
});
|
|
5056
|
+
const compiled = {
|
|
5057
|
+
isWildcard,
|
|
5058
|
+
wildcardBase: isWildcard ? pattern.slice(0, -2) : "",
|
|
5059
|
+
segments,
|
|
5060
|
+
optionalCount
|
|
5061
|
+
};
|
|
5062
|
+
compiledPatternCache.set(pattern, compiled);
|
|
5063
|
+
return compiled;
|
|
5064
|
+
}
|
|
5065
|
+
function getConstraintRegex(constraint) {
|
|
5066
|
+
const cached = constraintRegexCache.get(constraint);
|
|
5067
|
+
if (cached !== undefined)
|
|
5068
|
+
return cached;
|
|
5069
|
+
let regex;
|
|
5070
|
+
try {
|
|
5071
|
+
regex = new RegExp(`^${constraint}$`);
|
|
5072
|
+
} catch {
|
|
5073
|
+
regex = null;
|
|
5074
|
+
}
|
|
5075
|
+
constraintRegexCache.set(constraint, regex);
|
|
5076
|
+
return regex;
|
|
5077
|
+
}
|
|
5078
|
+
function matchPath(pattern, path, params, constraints) {
|
|
5079
|
+
const compiled = compilePattern(pattern);
|
|
5080
|
+
if (compiled.isWildcard && path.startsWith(compiled.wildcardBase)) {
|
|
5081
|
+
return true;
|
|
5082
|
+
}
|
|
5083
|
+
const patternSegments = compiled.segments;
|
|
5084
|
+
const pathSegments = normalizePath(path).split("/").filter(Boolean);
|
|
5085
|
+
if (pathSegments.length < patternSegments.length - compiled.optionalCount || pathSegments.length > patternSegments.length) {
|
|
5086
|
+
return false;
|
|
5087
|
+
}
|
|
5088
|
+
for (let i = 0;i < patternSegments.length; i++) {
|
|
5089
|
+
const seg = patternSegments[i];
|
|
5090
|
+
const pathSegment = pathSegments[i];
|
|
5091
|
+
if (pathSegment === undefined) {
|
|
5092
|
+
if (seg.isOptional) {
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
return false;
|
|
5096
|
+
}
|
|
5097
|
+
if (seg.isParam) {
|
|
5098
|
+
params[seg.paramName] = pathSegment;
|
|
5099
|
+
if (constraints && constraints[seg.paramName]) {
|
|
5100
|
+
const regex = getConstraintRegex(constraints[seg.paramName]);
|
|
5101
|
+
if (regex) {
|
|
5102
|
+
if (!regex.test(pathSegment)) {
|
|
5103
|
+
return false;
|
|
5104
|
+
}
|
|
5105
|
+
} else {
|
|
5106
|
+
console.warn(`Invalid constraint regex for parameter ${seg.paramName}: ${constraints[seg.paramName]}`);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
} else if (seg.literal !== pathSegment) {
|
|
5110
|
+
return false;
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
return true;
|
|
5114
|
+
}
|
|
5115
|
+
function joinPaths(...segments) {
|
|
5116
|
+
return normalizePath(segments.join("/"));
|
|
5117
|
+
}
|
|
5118
|
+
function validatePath(path) {
|
|
5119
|
+
if (!path.startsWith("/")) {
|
|
5120
|
+
return false;
|
|
5121
|
+
}
|
|
5122
|
+
const stack = [];
|
|
5123
|
+
for (const char of path) {
|
|
5124
|
+
if (char === "{") {
|
|
5125
|
+
stack.push(char);
|
|
5126
|
+
} else if (char === "}") {
|
|
5127
|
+
if (stack.length === 0 || stack.pop() !== "{") {
|
|
5128
|
+
return false;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
return stack.length === 0;
|
|
5133
|
+
}
|
|
5134
|
+
async function fileExists(path) {
|
|
5135
|
+
try {
|
|
5136
|
+
const file = Bun.file(path);
|
|
5137
|
+
return await file.exists();
|
|
5138
|
+
} catch {
|
|
5139
|
+
return false;
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
async function resolveViewPath(viewName, viewsPath, extensions) {
|
|
5143
|
+
if (extensions.some((ext) => viewName.endsWith(ext))) {
|
|
5144
|
+
const fullPath = normalizePath(join3(viewsPath, viewName));
|
|
5145
|
+
if (await fileExists(fullPath)) {
|
|
5146
|
+
return fullPath;
|
|
5147
|
+
}
|
|
5148
|
+
return null;
|
|
5149
|
+
}
|
|
5150
|
+
for (const ext of extensions) {
|
|
5151
|
+
const fullPath = normalizePath(join3(viewsPath, `${viewName}${ext}`));
|
|
5152
|
+
if (await fileExists(fullPath)) {
|
|
5153
|
+
return fullPath;
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
return null;
|
|
5157
|
+
}
|
|
5158
|
+
function processHtmlTemplate(template, data) {
|
|
5159
|
+
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
5160
|
+
const trimmedKey = key.trim();
|
|
5161
|
+
const value = getNestedValue(data, trimmedKey);
|
|
5162
|
+
return value !== undefined ? String(value) : match;
|
|
5163
|
+
});
|
|
5164
|
+
result = processConditionals(result, data);
|
|
5165
|
+
result = processLoops(result, data);
|
|
5166
|
+
return result;
|
|
5167
|
+
}
|
|
5168
|
+
function processConditionals(template, data) {
|
|
5169
|
+
return template.replace(/\{\{#if ([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (_match, condition, content, alternative = "") => {
|
|
5170
|
+
const conditionTrimmed = condition.trim();
|
|
5171
|
+
const value = getNestedValue(data, conditionTrimmed);
|
|
5172
|
+
return value ? content : alternative;
|
|
5173
|
+
});
|
|
5174
|
+
}
|
|
5175
|
+
function processLoops(template, data) {
|
|
5176
|
+
return template.replace(/\{\{#each ([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, arrayKey, content) => {
|
|
5177
|
+
const array = getNestedValue(data, arrayKey.trim());
|
|
5178
|
+
if (!Array.isArray(array))
|
|
5179
|
+
return "";
|
|
5180
|
+
return array.map((item) => {
|
|
5181
|
+
return content.replace(/\{\{@index\}\}/g, String(array.indexOf(item))).replace(/\{\{this\}\}/g, String(item)).replace(/\{\{this\.([^}]+)\}\}/g, (_matchStr, prop) => {
|
|
5182
|
+
const value = item[prop.trim()];
|
|
5183
|
+
return value !== undefined ? String(value) : "";
|
|
5184
|
+
});
|
|
5185
|
+
}).join("");
|
|
5186
|
+
});
|
|
5187
|
+
}
|
|
5188
|
+
function getNestedValue(obj, path) {
|
|
5189
|
+
return path.split(".").reduce((prev, curr) => {
|
|
5190
|
+
if (prev && typeof prev === "object" && curr in prev) {
|
|
5191
|
+
return prev[curr];
|
|
5192
|
+
}
|
|
5193
|
+
return;
|
|
5194
|
+
}, obj);
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
// src/router/fluent-routing.ts
|
|
5198
|
+
class FluentRouteBuilder {
|
|
5199
|
+
method;
|
|
5200
|
+
path;
|
|
5201
|
+
handler;
|
|
5202
|
+
middleware = [];
|
|
5203
|
+
cacheConfig;
|
|
5204
|
+
throttleConfig;
|
|
5205
|
+
routeName;
|
|
5206
|
+
modelBindings = {};
|
|
5207
|
+
constructor(method, path, handler) {
|
|
5208
|
+
this.method = method;
|
|
5209
|
+
this.path = path;
|
|
5210
|
+
this.handler = handler;
|
|
5211
|
+
}
|
|
5212
|
+
addMiddleware(middleware2) {
|
|
5213
|
+
if (Array.isArray(middleware2)) {
|
|
5214
|
+
this.middleware.push(...middleware2);
|
|
5736
5215
|
} else {
|
|
5737
5216
|
this.middleware.push(middleware2);
|
|
5738
5217
|
}
|
|
@@ -5801,14 +5280,13 @@ class FluentConditionalBuilder {
|
|
|
5801
5280
|
}
|
|
5802
5281
|
middleware(middlewareName) {
|
|
5803
5282
|
const [name, params] = middlewareName.split(":");
|
|
5804
|
-
const middlewareFactory = this.router.
|
|
5283
|
+
const middlewareFactory = this.router.resolveNamedMiddleware(name);
|
|
5805
5284
|
if (!middlewareFactory) {
|
|
5806
5285
|
throw new Error(`Unknown middleware: ${name}`);
|
|
5807
5286
|
}
|
|
5808
|
-
|
|
5809
|
-
this.router.conditionalMiddleware.push({
|
|
5287
|
+
this.router.addConditionalMiddleware({
|
|
5810
5288
|
condition: this.condition,
|
|
5811
|
-
middleware: [
|
|
5289
|
+
middleware: [middlewareFactory(params)]
|
|
5812
5290
|
});
|
|
5813
5291
|
return this;
|
|
5814
5292
|
}
|
|
@@ -5937,6 +5415,12 @@ class FluentRouter {
|
|
|
5937
5415
|
when(condition) {
|
|
5938
5416
|
return new FluentConditionalBuilder(this, condition);
|
|
5939
5417
|
}
|
|
5418
|
+
resolveNamedMiddleware(name) {
|
|
5419
|
+
return this.namedMiddleware.get(name);
|
|
5420
|
+
}
|
|
5421
|
+
addConditionalMiddleware(conditional) {
|
|
5422
|
+
this.conditionalMiddleware.push(conditional);
|
|
5423
|
+
}
|
|
5940
5424
|
middleware(middlewareName) {
|
|
5941
5425
|
const [name, params] = middlewareName.split(":");
|
|
5942
5426
|
const middlewareFactory = this.namedMiddleware.get(name);
|
|
@@ -6051,314 +5535,126 @@ class FluentRouter {
|
|
|
6051
5535
|
}
|
|
6052
5536
|
this.register(builder);
|
|
6053
5537
|
}
|
|
6054
|
-
}
|
|
6055
|
-
return this;
|
|
6056
|
-
}
|
|
6057
|
-
getRoutes() {
|
|
6058
|
-
return [...this.routes, ...this.subdomainRouter.getAllRoutes()];
|
|
6059
|
-
}
|
|
6060
|
-
async handle(request2) {
|
|
6061
|
-
const _url = new URL(request2.url);
|
|
6062
|
-
const url2 = new URL(request2.url);
|
|
6063
|
-
const domain = url2.hostname;
|
|
6064
|
-
const match = this.subdomainRouter.findDomainGroup(domain);
|
|
6065
|
-
if (match) {
|
|
6066
|
-
const enhancedReq = request2;
|
|
6067
|
-
enhancedReq.domainParams = match.parameters;
|
|
6068
|
-
for (const route of match.group.getRoutes()) {
|
|
6069
|
-
if (this.matchesRoute(route, request2)) {
|
|
6070
|
-
return await this.executeRoute(route, enhancedReq);
|
|
6071
|
-
}
|
|
6072
|
-
}
|
|
6073
|
-
}
|
|
6074
|
-
for (const route of this.routes) {
|
|
6075
|
-
if (this.matchesRoute(route, request2)) {
|
|
6076
|
-
return await this.executeRoute(route, request2);
|
|
6077
|
-
}
|
|
6078
|
-
}
|
|
6079
|
-
return null;
|
|
6080
|
-
}
|
|
6081
|
-
matchesRoute(route, request2) {
|
|
6082
|
-
if (route.method !== request2.method) {
|
|
6083
|
-
return false;
|
|
6084
|
-
}
|
|
6085
|
-
const url2 = new URL(request2.url);
|
|
6086
|
-
const routePattern = route.path.replace(/\{([^:}]+):?([^}]*)\}/g, "([^/]+)");
|
|
6087
|
-
const regex = new RegExp(`^${routePattern}$`);
|
|
6088
|
-
return regex.test(url2.pathname);
|
|
6089
|
-
}
|
|
6090
|
-
async executeRoute(route, request2) {
|
|
6091
|
-
const allMiddleware = [...this.globalMiddleware, ...route.middleware];
|
|
6092
|
-
let index = 0;
|
|
6093
|
-
const next = async () => {
|
|
6094
|
-
if (index < allMiddleware.length) {
|
|
6095
|
-
const middleware2 = allMiddleware[index++];
|
|
6096
|
-
return await middleware2(request2, next);
|
|
6097
|
-
}
|
|
6098
|
-
return await route.handler(request2);
|
|
6099
|
-
};
|
|
6100
|
-
return await next();
|
|
6101
|
-
}
|
|
6102
|
-
}
|
|
6103
|
-
var RouteFactory = {
|
|
6104
|
-
api: (path, handler, options = {}) => {
|
|
6105
|
-
const method = options.method || "GET";
|
|
6106
|
-
const builder = new FluentRouteBuilder(method, path, handler);
|
|
6107
|
-
if (options.cache) {
|
|
6108
|
-
builder.cached(options.cache);
|
|
6109
|
-
}
|
|
6110
|
-
if (options.throttle) {
|
|
6111
|
-
builder.throttle(options.throttle);
|
|
6112
|
-
}
|
|
6113
|
-
return builder;
|
|
6114
|
-
},
|
|
6115
|
-
protected: (path, handler, options = {}) => {
|
|
6116
|
-
const method = options.method || "GET";
|
|
6117
|
-
const builder = new FluentRouteBuilder(method, path, handler);
|
|
6118
|
-
builder.addMiddleware(async (req, next) => {
|
|
6119
|
-
if (!req.headers.get("Authorization")) {
|
|
6120
|
-
return new Response("Unauthorized", { status: 401 });
|
|
6121
|
-
}
|
|
6122
|
-
return await next();
|
|
6123
|
-
});
|
|
6124
|
-
return builder;
|
|
6125
|
-
},
|
|
6126
|
-
upload: (path, handler, _options = {}) => {
|
|
6127
|
-
const builder = new FluentRouteBuilder("POST", path, handler);
|
|
6128
|
-
builder.addMiddleware(async (_req, next) => {
|
|
6129
|
-
return await next();
|
|
6130
|
-
});
|
|
6131
|
-
return builder;
|
|
6132
|
-
}
|
|
6133
|
-
};
|
|
6134
|
-
var RouterUtils = {
|
|
6135
|
-
route: (name, params = {}, query = {}) => {
|
|
6136
|
-
let url2 = `/${name}`;
|
|
6137
|
-
for (const [key, value] of Object.entries(params)) {
|
|
6138
|
-
url2 = url2.replace(`{${key}}`, value);
|
|
6139
|
-
}
|
|
6140
|
-
const queryString = new URLSearchParams(query).toString();
|
|
6141
|
-
if (queryString) {
|
|
6142
|
-
url2 += `?${queryString}`;
|
|
6143
|
-
}
|
|
6144
|
-
return url2;
|
|
6145
|
-
},
|
|
6146
|
-
redirect: (url2, status = 302) => {
|
|
6147
|
-
return new Response(null, {
|
|
6148
|
-
status,
|
|
6149
|
-
headers: { Location: url2 }
|
|
6150
|
-
});
|
|
6151
|
-
},
|
|
6152
|
-
json: (data, status = 200) => {
|
|
6153
|
-
return new Response(JSON.stringify(data), {
|
|
6154
|
-
status,
|
|
6155
|
-
headers: { "Content-Type": "application/json" }
|
|
6156
|
-
});
|
|
6157
|
-
}
|
|
6158
|
-
};
|
|
6159
|
-
var router = new FluentRouter;
|
|
6160
|
-
|
|
6161
|
-
// src/utils.ts
|
|
6162
|
-
import { join as join3 } from "path";
|
|
6163
|
-
function normalizePath(path) {
|
|
6164
|
-
let normalizedPath = path.replace(/\/+/g, "/");
|
|
6165
|
-
if (!normalizedPath.startsWith("/")) {
|
|
6166
|
-
normalizedPath = `/${normalizedPath}`;
|
|
6167
|
-
}
|
|
6168
|
-
if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
|
|
6169
|
-
normalizedPath = normalizedPath.slice(0, -1);
|
|
6170
|
-
}
|
|
6171
|
-
return normalizedPath;
|
|
6172
|
-
}
|
|
6173
|
-
function toActionPath(path) {
|
|
6174
|
-
return path.replace(/\//g, "_").toLowerCase();
|
|
6175
|
-
}
|
|
6176
|
-
function isActionClass(handler) {
|
|
6177
|
-
if (typeof handler !== "function" && typeof handler !== "object") {
|
|
6178
|
-
return false;
|
|
6179
|
-
}
|
|
6180
|
-
if (typeof handler === "function" && handler.prototype) {
|
|
6181
|
-
const prototype = handler.prototype;
|
|
6182
|
-
if (typeof prototype.handle === "function") {
|
|
6183
|
-
return true;
|
|
6184
|
-
}
|
|
6185
|
-
}
|
|
6186
|
-
if (typeof handler === "object" && handler !== null && "handle" in handler && typeof handler.handle === "function") {
|
|
6187
|
-
return true;
|
|
6188
|
-
}
|
|
6189
|
-
return false;
|
|
6190
|
-
}
|
|
6191
|
-
function isRouteHandler(handler) {
|
|
6192
|
-
return typeof handler === "function" && !isActionClass(handler);
|
|
6193
|
-
}
|
|
6194
|
-
function extractParamNames(pattern) {
|
|
6195
|
-
const matches = pattern.match(/\{([^}]+)\}/g);
|
|
6196
|
-
return matches ? matches.map((m) => m.slice(1, -1).replace("?", "")) : [];
|
|
6197
|
-
}
|
|
6198
|
-
function createPathRegex(pattern) {
|
|
6199
|
-
const regexPattern = pattern.replace(/\{([^}]+)\}/g, "([^/]+)");
|
|
6200
|
-
return new RegExp(`^${regexPattern}$`);
|
|
6201
|
-
}
|
|
6202
|
-
var PARAM_SEGMENT_RE = /\{([^}:]+)(?::[^}]+)?(\?)?\}/;
|
|
6203
|
-
var OPTIONAL_SEGMENT_RE = /\{[^}]+\?\}/;
|
|
6204
|
-
var compiledPatternCache = new Map;
|
|
6205
|
-
var constraintRegexCache = new Map;
|
|
6206
|
-
function compilePattern(pattern) {
|
|
6207
|
-
const cached = compiledPatternCache.get(pattern);
|
|
6208
|
-
if (cached)
|
|
6209
|
-
return cached;
|
|
6210
|
-
const isWildcard = pattern.endsWith("/*");
|
|
6211
|
-
const normalizedPattern = normalizePath(pattern);
|
|
6212
|
-
let optionalCount = 0;
|
|
6213
|
-
const segments = normalizedPattern.split("/").filter(Boolean).map((seg) => {
|
|
6214
|
-
const isOptional = OPTIONAL_SEGMENT_RE.test(seg);
|
|
6215
|
-
if (isOptional)
|
|
6216
|
-
optionalCount++;
|
|
6217
|
-
const paramMatch = seg.match(PARAM_SEGMENT_RE);
|
|
6218
|
-
return {
|
|
6219
|
-
isParam: !!paramMatch,
|
|
6220
|
-
paramName: paramMatch ? paramMatch[1].replace("?", "") : "",
|
|
6221
|
-
isOptional,
|
|
6222
|
-
literal: seg
|
|
6223
|
-
};
|
|
6224
|
-
});
|
|
6225
|
-
const compiled = {
|
|
6226
|
-
isWildcard,
|
|
6227
|
-
wildcardBase: isWildcard ? pattern.slice(0, -2) : "",
|
|
6228
|
-
segments,
|
|
6229
|
-
optionalCount
|
|
6230
|
-
};
|
|
6231
|
-
compiledPatternCache.set(pattern, compiled);
|
|
6232
|
-
return compiled;
|
|
6233
|
-
}
|
|
6234
|
-
function getConstraintRegex(constraint) {
|
|
6235
|
-
const cached = constraintRegexCache.get(constraint);
|
|
6236
|
-
if (cached !== undefined)
|
|
6237
|
-
return cached;
|
|
6238
|
-
let regex;
|
|
6239
|
-
try {
|
|
6240
|
-
regex = new RegExp(`^${constraint}$`);
|
|
6241
|
-
} catch {
|
|
6242
|
-
regex = null;
|
|
6243
|
-
}
|
|
6244
|
-
constraintRegexCache.set(constraint, regex);
|
|
6245
|
-
return regex;
|
|
6246
|
-
}
|
|
6247
|
-
function matchPath(pattern, path, params, constraints) {
|
|
6248
|
-
const compiled = compilePattern(pattern);
|
|
6249
|
-
if (compiled.isWildcard && path.startsWith(compiled.wildcardBase)) {
|
|
6250
|
-
return true;
|
|
5538
|
+
}
|
|
5539
|
+
return this;
|
|
6251
5540
|
}
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
if (pathSegments.length < patternSegments.length - compiled.optionalCount || pathSegments.length > patternSegments.length) {
|
|
6255
|
-
return false;
|
|
5541
|
+
getRoutes() {
|
|
5542
|
+
return [...this.routes, ...this.subdomainRouter.getAllRoutes()];
|
|
6256
5543
|
}
|
|
6257
|
-
|
|
6258
|
-
const
|
|
6259
|
-
const
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
5544
|
+
async handle(request2) {
|
|
5545
|
+
const url2 = new URL(request2.url);
|
|
5546
|
+
const domain = url2.hostname;
|
|
5547
|
+
const match = this.subdomainRouter.findDomainGroup(domain);
|
|
5548
|
+
if (match) {
|
|
5549
|
+
const enhancedReq = request2;
|
|
5550
|
+
enhancedReq.domainParams = match.parameters;
|
|
5551
|
+
for (const route of match.group.getRoutes()) {
|
|
5552
|
+
const params = this.matchRouteParams(route, request2.method, url2.pathname);
|
|
5553
|
+
if (params) {
|
|
5554
|
+
return await this.executeRoute(route, enhancedReq, params);
|
|
5555
|
+
}
|
|
6263
5556
|
}
|
|
6264
|
-
return false;
|
|
6265
5557
|
}
|
|
6266
|
-
|
|
6267
|
-
params
|
|
6268
|
-
if (
|
|
6269
|
-
|
|
6270
|
-
if (regex) {
|
|
6271
|
-
if (!regex.test(pathSegment)) {
|
|
6272
|
-
return false;
|
|
6273
|
-
}
|
|
6274
|
-
} else {
|
|
6275
|
-
console.warn(`Invalid constraint regex for parameter ${seg.paramName}: ${constraints[seg.paramName]}`);
|
|
6276
|
-
}
|
|
5558
|
+
for (const route of this.routes) {
|
|
5559
|
+
const params = this.matchRouteParams(route, request2.method, url2.pathname);
|
|
5560
|
+
if (params) {
|
|
5561
|
+
return await this.executeRoute(route, request2, params);
|
|
6277
5562
|
}
|
|
6278
|
-
} else if (seg.literal !== pathSegment) {
|
|
6279
|
-
return false;
|
|
6280
5563
|
}
|
|
5564
|
+
return null;
|
|
6281
5565
|
}
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
}
|
|
6287
|
-
|
|
6288
|
-
if (!path.startsWith("/")) {
|
|
6289
|
-
return false;
|
|
5566
|
+
matchRouteParams(route, method, pathname) {
|
|
5567
|
+
if (route.method !== method) {
|
|
5568
|
+
return null;
|
|
5569
|
+
}
|
|
5570
|
+
const params = {};
|
|
5571
|
+
return matchPath(route.path, pathname, params) ? params : null;
|
|
6290
5572
|
}
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
return false;
|
|
5573
|
+
async executeRoute(route, request2, params) {
|
|
5574
|
+
request2.params = params;
|
|
5575
|
+
const allMiddleware = [...this.globalMiddleware];
|
|
5576
|
+
for (const conditional of this.conditionalMiddleware) {
|
|
5577
|
+
if (await conditional.condition(request2)) {
|
|
5578
|
+
allMiddleware.push(...conditional.middleware);
|
|
6298
5579
|
}
|
|
6299
5580
|
}
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
5581
|
+
allMiddleware.push(...route.middleware);
|
|
5582
|
+
let index = 0;
|
|
5583
|
+
const next = async () => {
|
|
5584
|
+
if (index < allMiddleware.length) {
|
|
5585
|
+
const middleware2 = allMiddleware[index++];
|
|
5586
|
+
const result = await middleware2(request2, next);
|
|
5587
|
+
return result ?? new Response(null);
|
|
5588
|
+
}
|
|
5589
|
+
return await route.handler(request2);
|
|
5590
|
+
};
|
|
5591
|
+
return await next();
|
|
6309
5592
|
}
|
|
6310
5593
|
}
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
const
|
|
6314
|
-
|
|
6315
|
-
|
|
5594
|
+
var RouteFactory = {
|
|
5595
|
+
api: (path, handler, options = {}) => {
|
|
5596
|
+
const method = options.method || "GET";
|
|
5597
|
+
const builder = new FluentRouteBuilder(method, path, handler);
|
|
5598
|
+
if (options.cache) {
|
|
5599
|
+
builder.cached(options.cache);
|
|
6316
5600
|
}
|
|
6317
|
-
|
|
5601
|
+
if (options.throttle) {
|
|
5602
|
+
builder.throttle(options.throttle);
|
|
5603
|
+
}
|
|
5604
|
+
return builder;
|
|
5605
|
+
},
|
|
5606
|
+
protected: (path, handler, options = {}) => {
|
|
5607
|
+
const method = options.method || "GET";
|
|
5608
|
+
const builder = new FluentRouteBuilder(method, path, handler);
|
|
5609
|
+
builder.addMiddleware(async (req, next) => {
|
|
5610
|
+
if (!req.headers.get("Authorization")) {
|
|
5611
|
+
return new Response("Unauthorized", { status: 401 });
|
|
5612
|
+
}
|
|
5613
|
+
return await next();
|
|
5614
|
+
});
|
|
5615
|
+
return builder;
|
|
5616
|
+
},
|
|
5617
|
+
upload: (path, handler, _options = {}) => {
|
|
5618
|
+
const builder = new FluentRouteBuilder("POST", path, handler);
|
|
5619
|
+
builder.addMiddleware(async (_req, next) => {
|
|
5620
|
+
return await next();
|
|
5621
|
+
});
|
|
5622
|
+
return builder;
|
|
6318
5623
|
}
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
5624
|
+
};
|
|
5625
|
+
var RouterUtils = {
|
|
5626
|
+
route: (name, params = {}, query = {}) => {
|
|
5627
|
+
let url2 = getNamedRoutePath(name) ?? `/${name}`;
|
|
5628
|
+
for (const [key, value] of Object.entries(params)) {
|
|
5629
|
+
const encoded = encodeURIComponent(value);
|
|
5630
|
+
url2 = url2.replace(`{${key}}`, encoded);
|
|
5631
|
+
url2 = url2.replace(`{${key}?}`, encoded);
|
|
5632
|
+
}
|
|
5633
|
+
if (url2.includes("?}")) {
|
|
5634
|
+
url2 = url2.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
5635
|
+
if (url2.length > 1 && url2.endsWith("/"))
|
|
5636
|
+
url2 = url2.slice(0, -1);
|
|
5637
|
+
}
|
|
5638
|
+
const queryString = new URLSearchParams(query).toString();
|
|
5639
|
+
if (queryString) {
|
|
5640
|
+
url2 += `?${queryString}`;
|
|
6323
5641
|
}
|
|
5642
|
+
return url2;
|
|
5643
|
+
},
|
|
5644
|
+
redirect: (url2, status = 302) => {
|
|
5645
|
+
return new Response(null, {
|
|
5646
|
+
status,
|
|
5647
|
+
headers: { Location: url2 }
|
|
5648
|
+
});
|
|
5649
|
+
},
|
|
5650
|
+
json: (data, status = 200) => {
|
|
5651
|
+
return new Response(JSON.stringify(data), {
|
|
5652
|
+
status,
|
|
5653
|
+
headers: { "Content-Type": "application/json" }
|
|
5654
|
+
});
|
|
6324
5655
|
}
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
function processHtmlTemplate(template, data) {
|
|
6328
|
-
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
6329
|
-
const trimmedKey = key.trim();
|
|
6330
|
-
const value = getNestedValue(data, trimmedKey);
|
|
6331
|
-
return value !== undefined ? String(value) : match;
|
|
6332
|
-
});
|
|
6333
|
-
result = processConditionals(result, data);
|
|
6334
|
-
result = processLoops(result, data);
|
|
6335
|
-
return result;
|
|
6336
|
-
}
|
|
6337
|
-
function processConditionals(template, data) {
|
|
6338
|
-
return template.replace(/\{\{#if ([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (_match, condition, content, alternative = "") => {
|
|
6339
|
-
const conditionTrimmed = condition.trim();
|
|
6340
|
-
const value = getNestedValue(data, conditionTrimmed);
|
|
6341
|
-
return value ? content : alternative;
|
|
6342
|
-
});
|
|
6343
|
-
}
|
|
6344
|
-
function processLoops(template, data) {
|
|
6345
|
-
return template.replace(/\{\{#each ([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, arrayKey, content) => {
|
|
6346
|
-
const array = getNestedValue(data, arrayKey.trim());
|
|
6347
|
-
if (!Array.isArray(array))
|
|
6348
|
-
return "";
|
|
6349
|
-
return array.map((item) => {
|
|
6350
|
-
return content.replace(/\{\{@index\}\}/g, String(array.indexOf(item))).replace(/\{\{this\}\}/g, String(item)).replace(/\{\{this\.([^}]+)\}\}/g, (_matchStr, prop) => {
|
|
6351
|
-
const value = item[prop.trim()];
|
|
6352
|
-
return value !== undefined ? String(value) : "";
|
|
6353
|
-
});
|
|
6354
|
-
}).join("");
|
|
6355
|
-
});
|
|
6356
|
-
}
|
|
6357
|
-
function getNestedValue(obj, path) {
|
|
6358
|
-
return path.split(".").reduce((prev, curr) => {
|
|
6359
|
-
return prev && prev[curr] !== undefined ? prev[curr] : undefined;
|
|
6360
|
-
}, obj);
|
|
6361
|
-
}
|
|
5656
|
+
};
|
|
5657
|
+
var router = new FluentRouter;
|
|
6362
5658
|
|
|
6363
5659
|
// src/router/group-organization.ts
|
|
6364
5660
|
function registerGroupOrganization(RouterClass) {
|
|
@@ -6379,7 +5675,16 @@ function registerGroupOrganization(RouterClass) {
|
|
|
6379
5675
|
];
|
|
6380
5676
|
}
|
|
6381
5677
|
this.currentGroup = newGroup;
|
|
6382
|
-
callback();
|
|
5678
|
+
const result = callback();
|
|
5679
|
+
if (result instanceof Promise) {
|
|
5680
|
+
return result.then(() => {
|
|
5681
|
+
this.currentGroup = previousGroup;
|
|
5682
|
+
return this;
|
|
5683
|
+
}, (error) => {
|
|
5684
|
+
this.currentGroup = previousGroup;
|
|
5685
|
+
throw error;
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
6383
5688
|
this.currentGroup = previousGroup;
|
|
6384
5689
|
return this;
|
|
6385
5690
|
},
|
|
@@ -6576,10 +5881,11 @@ function registerHttpMethods(RouterClass) {
|
|
|
6576
5881
|
this.namedRoutes.set(name, route);
|
|
6577
5882
|
registerNamedRoute(name, route.path);
|
|
6578
5883
|
}
|
|
6579
|
-
if (this.addRouteToCompiler) {
|
|
5884
|
+
if (this.addRouteToCompiler && !domain) {
|
|
6580
5885
|
this.addRouteToCompiler(route);
|
|
6581
5886
|
}
|
|
6582
5887
|
this.routeCache.clear();
|
|
5888
|
+
this._allowedMethodsCache?.clear();
|
|
6583
5889
|
return this;
|
|
6584
5890
|
},
|
|
6585
5891
|
writable: true,
|
|
@@ -6627,6 +5933,13 @@ function registerHttpMethods(RouterClass) {
|
|
|
6627
5933
|
writable: true,
|
|
6628
5934
|
configurable: true
|
|
6629
5935
|
},
|
|
5936
|
+
head: {
|
|
5937
|
+
value(path, handler, type, name, middleware2) {
|
|
5938
|
+
return this.addRoute("HEAD", path, handler, type, name, middleware2);
|
|
5939
|
+
},
|
|
5940
|
+
writable: true,
|
|
5941
|
+
configurable: true
|
|
5942
|
+
},
|
|
6630
5943
|
match: {
|
|
6631
5944
|
value(methods, path, handler, type, name, middleware2) {
|
|
6632
5945
|
for (const method of methods) {
|
|
@@ -6664,6 +5977,12 @@ function registerHttpMethods(RouterClass) {
|
|
|
6664
5977
|
url2 = url2.replace(`{${param}}`, encodeURIComponent(value));
|
|
6665
5978
|
url2 = url2.replace(`{${param}?}`, encodeURIComponent(value));
|
|
6666
5979
|
}
|
|
5980
|
+
if (url2.includes("{")) {
|
|
5981
|
+
url2 = url2.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
5982
|
+
if (url2.length > 1 && url2.endsWith("/")) {
|
|
5983
|
+
url2 = url2.slice(0, -1);
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
6667
5986
|
return url2;
|
|
6668
5987
|
},
|
|
6669
5988
|
writable: true,
|
|
@@ -6874,6 +6193,19 @@ Last error: ${lastError?.message || "Unknown error"}`);
|
|
|
6874
6193
|
function createHandlerResolver(config2) {
|
|
6875
6194
|
return (handler, req) => resolveHandler(handler, req, config2);
|
|
6876
6195
|
}
|
|
6196
|
+
function createHandlerInvoker(handler, config2) {
|
|
6197
|
+
if (handler instanceof Response) {
|
|
6198
|
+
return async () => handler.clone();
|
|
6199
|
+
}
|
|
6200
|
+
if (isCallableHandler(handler)) {
|
|
6201
|
+
return async (req) => wrapResponse(await handler(req));
|
|
6202
|
+
}
|
|
6203
|
+
if (typeof handler === "function" && handler.prototype && typeof handler.prototype.handle === "function") {
|
|
6204
|
+
const HandlerClass = handler;
|
|
6205
|
+
return async (req) => wrapResponse(await new HandlerClass().handle(req));
|
|
6206
|
+
}
|
|
6207
|
+
return (req) => resolveHandler(handler, req, config2);
|
|
6208
|
+
}
|
|
6877
6209
|
|
|
6878
6210
|
// src/router/middleware.ts
|
|
6879
6211
|
function registerMiddlewareHandling(RouterClass) {
|
|
@@ -6886,6 +6218,7 @@ function registerMiddlewareHandling(RouterClass) {
|
|
|
6886
6218
|
this.globalMiddleware.push(resolvedMiddleware);
|
|
6887
6219
|
}
|
|
6888
6220
|
}
|
|
6221
|
+
this._mwEpoch = (this._mwEpoch || 0) + 1;
|
|
6889
6222
|
return this;
|
|
6890
6223
|
},
|
|
6891
6224
|
writable: true,
|
|
@@ -7532,40 +6865,36 @@ class RouteTrie {
|
|
|
7532
6865
|
return null;
|
|
7533
6866
|
}
|
|
7534
6867
|
const segment = segments[segmentIndex];
|
|
7535
|
-
const
|
|
7536
|
-
if (
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
});
|
|
7541
|
-
}
|
|
7542
|
-
if (node.paramChild) {
|
|
7543
|
-
const paramNode = node.paramChild;
|
|
7544
|
-
let matches = true;
|
|
7545
|
-
if (paramNode.pattern) {
|
|
7546
|
-
matches = paramNode.pattern.test(segment);
|
|
7547
|
-
}
|
|
7548
|
-
if (matches && paramNode.paramName) {
|
|
7549
|
-
candidates.push({
|
|
7550
|
-
node: paramNode,
|
|
7551
|
-
newParams: { ...params, [paramNode.paramName]: segment }
|
|
7552
|
-
});
|
|
7553
|
-
}
|
|
6868
|
+
const staticChild = node.children.get(segment);
|
|
6869
|
+
if (staticChild) {
|
|
6870
|
+
const match = this.matchSegments(staticChild, segments, segmentIndex + 1, params, method);
|
|
6871
|
+
if (match)
|
|
6872
|
+
return match;
|
|
7554
6873
|
}
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
const
|
|
6874
|
+
const paramNode = node.paramChild;
|
|
6875
|
+
if (paramNode && paramNode.paramName && (!paramNode.pattern || paramNode.pattern.test(segment))) {
|
|
6876
|
+
const previous = params[paramNode.paramName];
|
|
6877
|
+
params[paramNode.paramName] = segment;
|
|
6878
|
+
const match = this.matchSegments(paramNode, segments, segmentIndex + 1, params, method);
|
|
7558
6879
|
if (match)
|
|
7559
6880
|
return match;
|
|
6881
|
+
if (previous === undefined)
|
|
6882
|
+
delete params[paramNode.paramName];
|
|
6883
|
+
else
|
|
6884
|
+
params[paramNode.paramName] = previous;
|
|
7560
6885
|
}
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
6886
|
+
if (node.wildcardChild) {
|
|
6887
|
+
const compiled = node.wildcardChild.getRoute(method);
|
|
6888
|
+
if (compiled) {
|
|
6889
|
+
params.wildcard = segments.slice(segmentIndex).join("/");
|
|
6890
|
+
return {
|
|
6891
|
+
route: compiled.route,
|
|
6892
|
+
params,
|
|
6893
|
+
score: compiled.priority
|
|
6894
|
+
};
|
|
7566
6895
|
}
|
|
7567
6896
|
}
|
|
7568
|
-
return
|
|
6897
|
+
return null;
|
|
7569
6898
|
}
|
|
7570
6899
|
getAllRoutes() {
|
|
7571
6900
|
return Array.from(this.compiledRoutes.values());
|
|
@@ -7619,7 +6948,7 @@ class RouteTrie {
|
|
|
7619
6948
|
class RouteCompiler {
|
|
7620
6949
|
trie;
|
|
7621
6950
|
matchCache = new Map;
|
|
7622
|
-
|
|
6951
|
+
routeKeys = new Set;
|
|
7623
6952
|
stats = {
|
|
7624
6953
|
totalMatches: 0,
|
|
7625
6954
|
cacheHits: 0,
|
|
@@ -7636,6 +6965,7 @@ class RouteCompiler {
|
|
|
7636
6965
|
enablePriorityOptimization: true,
|
|
7637
6966
|
cacheSize: 1000,
|
|
7638
6967
|
precompilePatterns: true,
|
|
6968
|
+
enableProfiling: false,
|
|
7639
6969
|
...options
|
|
7640
6970
|
};
|
|
7641
6971
|
this.trie = new RouteTrie;
|
|
@@ -7644,10 +6974,11 @@ class RouteCompiler {
|
|
|
7644
6974
|
if (this.options.precompilePatterns) {
|
|
7645
6975
|
this.precompileRoutePattern(route);
|
|
7646
6976
|
}
|
|
7647
|
-
const
|
|
7648
|
-
if (
|
|
6977
|
+
const routeKey = `${route.method}:${route.path}`;
|
|
6978
|
+
if (this.routeKeys.has(routeKey)) {
|
|
7649
6979
|
return false;
|
|
7650
6980
|
}
|
|
6981
|
+
this.routeKeys.add(routeKey);
|
|
7651
6982
|
this.trie.addRoute(route);
|
|
7652
6983
|
if (this.options.enableCaching) {
|
|
7653
6984
|
this.matchCache.clear();
|
|
@@ -7656,15 +6987,6 @@ class RouteCompiler {
|
|
|
7656
6987
|
this.stats.methodDistribution[method] = (this.stats.methodDistribution[method] || 0) + 1;
|
|
7657
6988
|
return true;
|
|
7658
6989
|
}
|
|
7659
|
-
checkForDuplicateRoute(route) {
|
|
7660
|
-
const routes = this.trie.getAllRoutes();
|
|
7661
|
-
for (const existingRoute of routes) {
|
|
7662
|
-
if (existingRoute.route.path === route.path && existingRoute.route.method === route.method) {
|
|
7663
|
-
return true;
|
|
7664
|
-
}
|
|
7665
|
-
}
|
|
7666
|
-
return false;
|
|
7667
|
-
}
|
|
7668
6990
|
patternCache = new Map;
|
|
7669
6991
|
extractParamNames(path) {
|
|
7670
6992
|
const paramNames = [];
|
|
@@ -7707,17 +7029,25 @@ class RouteCompiler {
|
|
|
7707
7029
|
}
|
|
7708
7030
|
}
|
|
7709
7031
|
convertToURLPattern(path, constraints) {
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7032
|
+
const paramTokens = [];
|
|
7033
|
+
const withTokens = path.replace(/\{([^}]+)\}/g, (_m, inner) => {
|
|
7034
|
+
paramTokens.push(inner);
|
|
7035
|
+
return `\x00${paramTokens.length - 1}\x00`;
|
|
7036
|
+
});
|
|
7037
|
+
let regexPattern = withTokens.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
|
|
7038
|
+
regexPattern = regexPattern.replace(/\/?\u0000(\d+)\u0000/g, (token, idx) => {
|
|
7039
|
+
const inner = paramTokens[Number(idx)];
|
|
7040
|
+
const isOptional = inner.endsWith("?");
|
|
7041
|
+
const core = isOptional ? inner.slice(0, -1) : inner;
|
|
7042
|
+
const colonIndex = core.indexOf(":");
|
|
7043
|
+
const name = colonIndex === -1 ? core : core.slice(0, colonIndex);
|
|
7044
|
+
const inlinePattern = colonIndex === -1 ? undefined : core.slice(colonIndex + 1);
|
|
7045
|
+
const pattern = constraints?.[name] ?? inlinePattern ?? "[^/]+";
|
|
7046
|
+
const group = `(?<${name}>${pattern})`;
|
|
7047
|
+
const leadingSlash = token.startsWith("/") ? "/" : "";
|
|
7048
|
+
return isOptional ? `(?:${leadingSlash}${group})?` : `${leadingSlash}${group}`;
|
|
7718
7049
|
});
|
|
7719
|
-
|
|
7720
|
-
const regex = new RegExp(regexPattern);
|
|
7050
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
7721
7051
|
return {
|
|
7722
7052
|
exec: (pathname) => {
|
|
7723
7053
|
const match = pathname.match(regex);
|
|
@@ -7730,16 +7060,16 @@ class RouteCompiler {
|
|
|
7730
7060
|
};
|
|
7731
7061
|
}
|
|
7732
7062
|
match(path, method) {
|
|
7733
|
-
const startTime = performance.now();
|
|
7063
|
+
const startTime = this.options.enableProfiling ? performance.now() : 0;
|
|
7734
7064
|
this.stats.totalMatches++;
|
|
7735
7065
|
if (this.options.enableCaching) {
|
|
7736
7066
|
const cacheKey = `${method}:${path}`;
|
|
7737
|
-
|
|
7067
|
+
const cached = this.matchCache.get(cacheKey);
|
|
7068
|
+
if (cached !== undefined || this.matchCache.has(cacheKey)) {
|
|
7738
7069
|
this.stats.cacheHits++;
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
return cached || null;
|
|
7070
|
+
if (startTime)
|
|
7071
|
+
this.updateMatchTime(startTime);
|
|
7072
|
+
return cached ? { route: cached.route, params: { ...cached.params } } : null;
|
|
7743
7073
|
}
|
|
7744
7074
|
this.stats.cacheMisses++;
|
|
7745
7075
|
}
|
|
@@ -7755,25 +7085,18 @@ class RouteCompiler {
|
|
|
7755
7085
|
const cacheKey = `${method}:${path}`;
|
|
7756
7086
|
this.addToCache(cacheKey, result);
|
|
7757
7087
|
}
|
|
7758
|
-
|
|
7759
|
-
|
|
7088
|
+
if (startTime)
|
|
7089
|
+
this.updateMatchTime(startTime);
|
|
7090
|
+
return result ? { route: result.route, params: { ...result.params } } : null;
|
|
7760
7091
|
}
|
|
7761
7092
|
addToCache(key, value) {
|
|
7762
7093
|
if (this.matchCache.size >= this.options.cacheSize && !this.matchCache.has(key)) {
|
|
7763
|
-
|
|
7764
|
-
|
|
7094
|
+
const lruKey = this.matchCache.keys().next().value;
|
|
7095
|
+
if (lruKey !== undefined) {
|
|
7765
7096
|
this.matchCache.delete(lruKey);
|
|
7766
7097
|
}
|
|
7767
7098
|
}
|
|
7768
7099
|
this.matchCache.set(key, value);
|
|
7769
|
-
this.updateCacheLRU(key);
|
|
7770
|
-
}
|
|
7771
|
-
updateCacheLRU(key) {
|
|
7772
|
-
const index = this.cacheKeys.indexOf(key);
|
|
7773
|
-
if (index !== -1) {
|
|
7774
|
-
this.cacheKeys.splice(index, 1);
|
|
7775
|
-
}
|
|
7776
|
-
this.cacheKeys.push(key);
|
|
7777
7100
|
}
|
|
7778
7101
|
updateMatchTime(startTime) {
|
|
7779
7102
|
const matchTime = performance.now() - startTime;
|
|
@@ -7842,7 +7165,7 @@ class RouteCompiler {
|
|
|
7842
7165
|
clear() {
|
|
7843
7166
|
this.trie.clear();
|
|
7844
7167
|
this.matchCache.clear();
|
|
7845
|
-
this.
|
|
7168
|
+
this.routeKeys.clear();
|
|
7846
7169
|
this.stats = {
|
|
7847
7170
|
totalMatches: 0,
|
|
7848
7171
|
cacheHits: 0,
|
|
@@ -8036,14 +7359,25 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
8036
7359
|
matchRoute: {
|
|
8037
7360
|
value(path, method, domain) {
|
|
8038
7361
|
this.initializeRouteCompiler();
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
if (
|
|
8042
|
-
return;
|
|
7362
|
+
if (domain && this.domains[domain]) {
|
|
7363
|
+
const domainMatch = this.fallbackMatchRoute(path, method, domain);
|
|
7364
|
+
if (domainMatch) {
|
|
7365
|
+
return domainMatch;
|
|
8043
7366
|
}
|
|
8044
|
-
|
|
7367
|
+
} else {
|
|
7368
|
+
const match = this.routeCompiler.match(path, method);
|
|
7369
|
+
if (match && (!domain || !match.route.domain || match.route.domain === domain)) {
|
|
7370
|
+
return match;
|
|
7371
|
+
}
|
|
7372
|
+
const fallback = this.fallbackMatchRoute(path, method, domain);
|
|
7373
|
+
if (fallback) {
|
|
7374
|
+
return fallback;
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
if (method === "HEAD") {
|
|
7378
|
+
return this.matchRoute(path, "GET", domain);
|
|
8045
7379
|
}
|
|
8046
|
-
return
|
|
7380
|
+
return;
|
|
8047
7381
|
},
|
|
8048
7382
|
writable: true,
|
|
8049
7383
|
configurable: true
|
|
@@ -8055,6 +7389,9 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
8055
7389
|
if (this.routeCache.has(cacheKey)) {
|
|
8056
7390
|
return this.routeCache.get(cacheKey);
|
|
8057
7391
|
}
|
|
7392
|
+
if (this.routeCache.size >= 1e4) {
|
|
7393
|
+
this.routeCache.clear();
|
|
7394
|
+
}
|
|
8058
7395
|
if (this.staticRoutes.has(method)) {
|
|
8059
7396
|
const staticRoute = this.staticRoutes.get(method).get(url2.pathname);
|
|
8060
7397
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
@@ -8091,6 +7428,24 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
8091
7428
|
}
|
|
8092
7429
|
}
|
|
8093
7430
|
}
|
|
7431
|
+
const wildcardPools = domain && this.domains[domain] ? [this.domains[domain], this.routes] : [this.routes];
|
|
7432
|
+
for (const pool of wildcardPools) {
|
|
7433
|
+
for (const route of pool) {
|
|
7434
|
+
if (route.method !== method || !route.path.endsWith("*")) {
|
|
7435
|
+
continue;
|
|
7436
|
+
}
|
|
7437
|
+
const basePath = route.path.slice(0, -1);
|
|
7438
|
+
if (url2.pathname.startsWith(basePath)) {
|
|
7439
|
+
const result = {
|
|
7440
|
+
route,
|
|
7441
|
+
params: { wildcard: url2.pathname.slice(basePath.length) }
|
|
7442
|
+
};
|
|
7443
|
+
this.routeCache.set(cacheKey, result);
|
|
7444
|
+
return result;
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7448
|
+
this.routeCache.set(cacheKey, undefined);
|
|
8094
7449
|
return;
|
|
8095
7450
|
},
|
|
8096
7451
|
writable: true,
|
|
@@ -8509,6 +7864,17 @@ function registerRouteMatching(RouterClass) {
|
|
|
8509
7864
|
writable: true,
|
|
8510
7865
|
configurable: true
|
|
8511
7866
|
},
|
|
7867
|
+
withoutNativeDispatch: {
|
|
7868
|
+
value() {
|
|
7869
|
+
const lastRoute = this.routes[this.routes.length - 1];
|
|
7870
|
+
if (lastRoute) {
|
|
7871
|
+
lastRoute.nativeDispatch = false;
|
|
7872
|
+
}
|
|
7873
|
+
return this;
|
|
7874
|
+
},
|
|
7875
|
+
writable: true,
|
|
7876
|
+
configurable: true
|
|
7877
|
+
},
|
|
8512
7878
|
whereNumber: {
|
|
8513
7879
|
value(param) {
|
|
8514
7880
|
return this.where({ [param]: "\\d+" });
|
|
@@ -8571,6 +7937,8 @@ class Router {
|
|
|
8571
7937
|
namedMiddleware = new Map;
|
|
8572
7938
|
conditionalMiddleware = [];
|
|
8573
7939
|
_middlewarePipeline;
|
|
7940
|
+
_mwEpoch = 0;
|
|
7941
|
+
_allowedMethodsCache = new Map;
|
|
8574
7942
|
config = {
|
|
8575
7943
|
verbose: false,
|
|
8576
7944
|
routesPath: "routes",
|
|
@@ -8671,6 +8039,7 @@ class Router {
|
|
|
8671
8039
|
}
|
|
8672
8040
|
invalidateCache() {
|
|
8673
8041
|
this.routeCache.clear();
|
|
8042
|
+
this._allowedMethodsCache.clear();
|
|
8674
8043
|
}
|
|
8675
8044
|
registerRoute(method, path, handler, type, name, middleware2) {
|
|
8676
8045
|
let routePath = path;
|
|
@@ -8985,50 +8354,44 @@ class Router {
|
|
|
8985
8354
|
}
|
|
8986
8355
|
getAllowedMethods(path, domain) {
|
|
8987
8356
|
const url2 = new URL(path, "http://localhost");
|
|
8357
|
+
const cacheKey = `${domain || ""}:${url2.pathname}`;
|
|
8358
|
+
const cached = this._allowedMethodsCache.get(cacheKey);
|
|
8359
|
+
if (cached) {
|
|
8360
|
+
return cached;
|
|
8361
|
+
}
|
|
8988
8362
|
const methods = new Set;
|
|
8989
|
-
const
|
|
8990
|
-
|
|
8991
|
-
if (
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8363
|
+
for (const [method, routesByPath] of this.staticRoutes) {
|
|
8364
|
+
const staticRoute = routesByPath.get(url2.pathname);
|
|
8365
|
+
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
8366
|
+
methods.add(method);
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
8370
|
+
for (const route of potentialRoutes) {
|
|
8371
|
+
if (methods.has(route.method)) {
|
|
8372
|
+
continue;
|
|
8997
8373
|
}
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
if (route.path === url2.pathname) {
|
|
9002
|
-
methods.add(method);
|
|
9003
|
-
break;
|
|
9004
|
-
}
|
|
8374
|
+
if (route.path === url2.pathname) {
|
|
8375
|
+
methods.add(route.method);
|
|
8376
|
+
continue;
|
|
9005
8377
|
}
|
|
9006
|
-
if (
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
const match = route.pattern.exec(url2);
|
|
9010
|
-
if (match) {
|
|
9011
|
-
methods.add(method);
|
|
9012
|
-
break;
|
|
9013
|
-
}
|
|
9014
|
-
}
|
|
9015
|
-
}
|
|
8378
|
+
if (route.pattern && route.pattern.exec(url2)) {
|
|
8379
|
+
methods.add(route.method);
|
|
8380
|
+
continue;
|
|
9016
8381
|
}
|
|
9017
|
-
if (
|
|
9018
|
-
|
|
9019
|
-
for (const route of wildcardRoutes) {
|
|
9020
|
-
const basePath = route.path.slice(0, -1);
|
|
9021
|
-
if (url2.pathname.startsWith(basePath)) {
|
|
9022
|
-
methods.add(method);
|
|
9023
|
-
break;
|
|
9024
|
-
}
|
|
9025
|
-
}
|
|
8382
|
+
if (route.path.endsWith("*") && url2.pathname.startsWith(route.path.slice(0, -1))) {
|
|
8383
|
+
methods.add(route.method);
|
|
9026
8384
|
}
|
|
9027
8385
|
}
|
|
9028
8386
|
if (methods.has("GET")) {
|
|
9029
8387
|
methods.add("HEAD");
|
|
9030
8388
|
}
|
|
9031
|
-
|
|
8389
|
+
const result = Array.from(methods);
|
|
8390
|
+
if (this._allowedMethodsCache.size >= 1e4) {
|
|
8391
|
+
this._allowedMethodsCache.clear();
|
|
8392
|
+
}
|
|
8393
|
+
this._allowedMethodsCache.set(cacheKey, result);
|
|
8394
|
+
return result;
|
|
9032
8395
|
}
|
|
9033
8396
|
matchRoute(path, method, domain) {
|
|
9034
8397
|
const url2 = new URL(path, "http://localhost");
|
|
@@ -9456,12 +8819,15 @@ class Router {
|
|
|
9456
8819
|
}
|
|
9457
8820
|
const result = callback();
|
|
9458
8821
|
if (result instanceof Promise) {
|
|
9459
|
-
result.then(() => {
|
|
8822
|
+
return result.then(() => {
|
|
8823
|
+
this.currentGroup = previousGroup;
|
|
8824
|
+
return this;
|
|
8825
|
+
}, (error) => {
|
|
9460
8826
|
this.currentGroup = previousGroup;
|
|
8827
|
+
throw error;
|
|
9461
8828
|
});
|
|
9462
|
-
} else {
|
|
9463
|
-
this.currentGroup = previousGroup;
|
|
9464
8829
|
}
|
|
8830
|
+
this.currentGroup = previousGroup;
|
|
9465
8831
|
return this;
|
|
9466
8832
|
}
|
|
9467
8833
|
addRoute(route) {
|
|
@@ -9593,10 +8959,48 @@ function applyRequestEnhancements(req, params = {}) {
|
|
|
9593
8959
|
}
|
|
9594
8960
|
|
|
9595
8961
|
// src/request/macros.ts
|
|
8962
|
+
var MOBILE_REGEX = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
8963
|
+
var BOT_REGEX = /bot|crawler|spider|crawling/i;
|
|
8964
|
+
var GLOB_PATTERN_CACHE = new Map;
|
|
8965
|
+
function getParsedURL(req) {
|
|
8966
|
+
const cache = req;
|
|
8967
|
+
if (!cache._parsedURL) {
|
|
8968
|
+
cache._parsedURL = new URL(req.url);
|
|
8969
|
+
}
|
|
8970
|
+
return cache._parsedURL;
|
|
8971
|
+
}
|
|
8972
|
+
function getParsedCookies(req) {
|
|
8973
|
+
const cache = req;
|
|
8974
|
+
if (!cache._parsedCookies) {
|
|
8975
|
+
const cookies = {};
|
|
8976
|
+
const cookieHeader = req.headers.get("cookie");
|
|
8977
|
+
if (cookieHeader) {
|
|
8978
|
+
for (const cookie of cookieHeader.split(";")) {
|
|
8979
|
+
const eqIndex = cookie.indexOf("=");
|
|
8980
|
+
if (eqIndex === -1)
|
|
8981
|
+
continue;
|
|
8982
|
+
const key = cookie.slice(0, eqIndex).trim();
|
|
8983
|
+
if (!key)
|
|
8984
|
+
continue;
|
|
8985
|
+
const value = cookie.slice(eqIndex + 1).trim();
|
|
8986
|
+
try {
|
|
8987
|
+
cookies[key] = decodeURIComponent(value);
|
|
8988
|
+
} catch {
|
|
8989
|
+
cookies[key] = value;
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
}
|
|
8993
|
+
cache._parsedCookies = cookies;
|
|
8994
|
+
}
|
|
8995
|
+
return cache._parsedCookies;
|
|
8996
|
+
}
|
|
8997
|
+
|
|
9596
8998
|
class RequestMacroRegistry {
|
|
9597
8999
|
macros = new Map;
|
|
9598
|
-
|
|
9599
|
-
|
|
9000
|
+
epoch = 0;
|
|
9001
|
+
register(name, handler, accessor = false) {
|
|
9002
|
+
this.macros.set(name, { name, handler, accessor });
|
|
9003
|
+
this.epoch++;
|
|
9600
9004
|
}
|
|
9601
9005
|
get(name) {
|
|
9602
9006
|
return this.macros.get(name);
|
|
@@ -9608,23 +9012,71 @@ class RequestMacroRegistry {
|
|
|
9608
9012
|
return Array.from(this.macros.values());
|
|
9609
9013
|
}
|
|
9610
9014
|
remove(name) {
|
|
9611
|
-
|
|
9015
|
+
const removed = this.macros.delete(name);
|
|
9016
|
+
if (removed)
|
|
9017
|
+
this.epoch++;
|
|
9018
|
+
return removed;
|
|
9612
9019
|
}
|
|
9613
9020
|
clear() {
|
|
9614
9021
|
this.macros.clear();
|
|
9022
|
+
this.epoch++;
|
|
9023
|
+
}
|
|
9024
|
+
getEpoch() {
|
|
9025
|
+
return this.epoch;
|
|
9615
9026
|
}
|
|
9616
9027
|
}
|
|
9617
9028
|
var requestMacroRegistry = new RequestMacroRegistry;
|
|
9029
|
+
var MACRO_PROTO = Symbol("bunRouterMacroProto");
|
|
9030
|
+
var MACRO_BASE = Symbol("bunRouterMacroBase");
|
|
9031
|
+
var macroProtoCache = new WeakMap;
|
|
9032
|
+
var macroProtoEpoch = -1;
|
|
9618
9033
|
|
|
9619
9034
|
class RequestWithMacros {
|
|
9620
9035
|
static macro(name, handler) {
|
|
9621
9036
|
requestMacroRegistry.register(name, handler);
|
|
9622
9037
|
}
|
|
9038
|
+
static macroAccessor(name, getter) {
|
|
9039
|
+
requestMacroRegistry.register(name, getter, true);
|
|
9040
|
+
}
|
|
9623
9041
|
static applyMacros(request2) {
|
|
9624
|
-
const
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9042
|
+
const epoch = requestMacroRegistry.getEpoch();
|
|
9043
|
+
if (epoch !== macroProtoEpoch) {
|
|
9044
|
+
macroProtoCache = new WeakMap;
|
|
9045
|
+
macroProtoEpoch = epoch;
|
|
9046
|
+
}
|
|
9047
|
+
let base = Object.getPrototypeOf(request2) ?? Object.prototype;
|
|
9048
|
+
if (base[MACRO_PROTO]) {
|
|
9049
|
+
if (macroProtoCache.get(base[MACRO_BASE]) === base) {
|
|
9050
|
+
return request2;
|
|
9051
|
+
}
|
|
9052
|
+
base = base[MACRO_BASE];
|
|
9053
|
+
}
|
|
9054
|
+
let proto = macroProtoCache.get(base);
|
|
9055
|
+
if (!proto) {
|
|
9056
|
+
proto = Object.create(base);
|
|
9057
|
+
for (const macro of requestMacroRegistry.all()) {
|
|
9058
|
+
if (macro.accessor) {
|
|
9059
|
+
Object.defineProperty(proto, macro.name, {
|
|
9060
|
+
get: macro.handler,
|
|
9061
|
+
set(value) {
|
|
9062
|
+
Object.defineProperty(this, macro.name, {
|
|
9063
|
+
value,
|
|
9064
|
+
writable: true,
|
|
9065
|
+
configurable: true,
|
|
9066
|
+
enumerable: true
|
|
9067
|
+
});
|
|
9068
|
+
},
|
|
9069
|
+
configurable: true
|
|
9070
|
+
});
|
|
9071
|
+
} else {
|
|
9072
|
+
proto[macro.name] = macro.handler;
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
Object.defineProperty(proto, MACRO_PROTO, { value: true });
|
|
9076
|
+
Object.defineProperty(proto, MACRO_BASE, { value: base });
|
|
9077
|
+
macroProtoCache.set(base, proto);
|
|
9078
|
+
}
|
|
9079
|
+
Object.setPrototypeOf(request2, proto);
|
|
9628
9080
|
return request2;
|
|
9629
9081
|
}
|
|
9630
9082
|
static hasMacro(name) {
|
|
@@ -9654,14 +9106,10 @@ var BuiltInRequestMacros = {
|
|
|
9654
9106
|
return this.headers.get("x-pjax") === "true";
|
|
9655
9107
|
},
|
|
9656
9108
|
isMobile() {
|
|
9657
|
-
|
|
9658
|
-
const mobileRegex = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
9659
|
-
return mobileRegex.test(userAgent);
|
|
9109
|
+
return MOBILE_REGEX.test(this.headers.get("user-agent") || "");
|
|
9660
9110
|
},
|
|
9661
9111
|
isBot() {
|
|
9662
|
-
|
|
9663
|
-
const botRegex = /bot|crawler|spider|crawling/i;
|
|
9664
|
-
return botRegex.test(userAgent);
|
|
9112
|
+
return BOT_REGEX.test(this.headers.get("user-agent") || "");
|
|
9665
9113
|
},
|
|
9666
9114
|
isSecure() {
|
|
9667
9115
|
return this.url.startsWith("https://") || this.headers.get("x-forwarded-proto") === "https" || this.headers.get("x-forwarded-ssl") === "on";
|
|
@@ -9689,6 +9137,17 @@ var BuiltInRequestMacros = {
|
|
|
9689
9137
|
}
|
|
9690
9138
|
return null;
|
|
9691
9139
|
},
|
|
9140
|
+
async rawBody() {
|
|
9141
|
+
if (typeof this._rawBody === "string")
|
|
9142
|
+
return this._rawBody;
|
|
9143
|
+
try {
|
|
9144
|
+
const text = await this.clone().text();
|
|
9145
|
+
this._rawBody = text;
|
|
9146
|
+
return text;
|
|
9147
|
+
} catch {
|
|
9148
|
+
return "";
|
|
9149
|
+
}
|
|
9150
|
+
},
|
|
9692
9151
|
basicAuth() {
|
|
9693
9152
|
const auth2 = this.headers.get("authorization");
|
|
9694
9153
|
if (auth2 && auth2.startsWith("Basic ")) {
|
|
@@ -9793,30 +9252,11 @@ var BuiltInRequestMacros = {
|
|
|
9793
9252
|
return this.params?.[key] || defaultValue;
|
|
9794
9253
|
},
|
|
9795
9254
|
cookie(name, defaultValue) {
|
|
9796
|
-
const
|
|
9797
|
-
|
|
9798
|
-
return defaultValue || null;
|
|
9799
|
-
const cookieArray = cookies.split(";");
|
|
9800
|
-
for (const cookie of cookieArray) {
|
|
9801
|
-
const [key, value] = cookie.trim().split("=");
|
|
9802
|
-
if (key === name) {
|
|
9803
|
-
return decodeURIComponent(value);
|
|
9804
|
-
}
|
|
9805
|
-
}
|
|
9806
|
-
return defaultValue || null;
|
|
9255
|
+
const value = getParsedCookies(this)[name];
|
|
9256
|
+
return value !== undefined ? value : defaultValue || null;
|
|
9807
9257
|
},
|
|
9808
9258
|
cookies() {
|
|
9809
|
-
|
|
9810
|
-
const cookieHeader = this.headers.get("cookie");
|
|
9811
|
-
if (cookieHeader) {
|
|
9812
|
-
cookieHeader.split(";").forEach((cookie) => {
|
|
9813
|
-
const [key, value] = cookie.trim().split("=");
|
|
9814
|
-
if (key && value) {
|
|
9815
|
-
cookies[key] = decodeURIComponent(value);
|
|
9816
|
-
}
|
|
9817
|
-
});
|
|
9818
|
-
}
|
|
9819
|
-
return cookies;
|
|
9259
|
+
return { ...getParsedCookies(this) };
|
|
9820
9260
|
},
|
|
9821
9261
|
file(name) {
|
|
9822
9262
|
return this.files?.find((f) => f.fieldName === name);
|
|
@@ -9825,26 +9265,33 @@ var BuiltInRequestMacros = {
|
|
|
9825
9265
|
return !!this.files?.find((f) => f.fieldName === name);
|
|
9826
9266
|
},
|
|
9827
9267
|
path() {
|
|
9828
|
-
return
|
|
9268
|
+
return getParsedURL(this).pathname;
|
|
9829
9269
|
},
|
|
9830
9270
|
fullUrl() {
|
|
9831
9271
|
return this.url;
|
|
9832
9272
|
},
|
|
9833
9273
|
root() {
|
|
9834
|
-
const url2 =
|
|
9274
|
+
const url2 = getParsedURL(this);
|
|
9835
9275
|
return `${url2.protocol}//${url2.host}`;
|
|
9836
9276
|
},
|
|
9837
9277
|
is(pattern) {
|
|
9838
9278
|
const path = this.path();
|
|
9839
|
-
|
|
9840
|
-
|
|
9279
|
+
let regex = GLOB_PATTERN_CACHE.get(pattern);
|
|
9280
|
+
if (!regex) {
|
|
9281
|
+
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
9282
|
+
regex = new RegExp(`^${regexPattern}$`);
|
|
9283
|
+
if (GLOB_PATTERN_CACHE.size >= 1000) {
|
|
9284
|
+
GLOB_PATTERN_CACHE.clear();
|
|
9285
|
+
}
|
|
9286
|
+
GLOB_PATTERN_CACHE.set(pattern, regex);
|
|
9287
|
+
}
|
|
9841
9288
|
return regex.test(path);
|
|
9842
9289
|
},
|
|
9843
9290
|
route() {
|
|
9844
9291
|
return this.routeName || null;
|
|
9845
9292
|
},
|
|
9846
9293
|
fingerprint() {
|
|
9847
|
-
const url2 =
|
|
9294
|
+
const url2 = getParsedURL(this);
|
|
9848
9295
|
const data = {
|
|
9849
9296
|
method: this.method,
|
|
9850
9297
|
path: url2.pathname,
|
|
@@ -9900,6 +9347,51 @@ function registerBuiltInRequestMacros() {
|
|
|
9900
9347
|
registerBuiltInRequestMacros();
|
|
9901
9348
|
|
|
9902
9349
|
// src/router/server.ts
|
|
9350
|
+
if (!RequestWithMacros.hasMacro("getParam")) {
|
|
9351
|
+
RequestWithMacros.macro("getParam", function(name, defaultValue) {
|
|
9352
|
+
const value = this.params?.[name];
|
|
9353
|
+
return value !== undefined ? value : defaultValue;
|
|
9354
|
+
});
|
|
9355
|
+
}
|
|
9356
|
+
if (!RequestWithMacros.hasMacro("get")) {
|
|
9357
|
+
RequestWithMacros.macro("get", function(key, defaultValue) {
|
|
9358
|
+
const value = getParsedURL(this).searchParams.get(key);
|
|
9359
|
+
return value !== null && value !== undefined ? value : defaultValue;
|
|
9360
|
+
});
|
|
9361
|
+
}
|
|
9362
|
+
RequestWithMacros.macroAccessor("cookies", function() {
|
|
9363
|
+
const req = this;
|
|
9364
|
+
const cookies = () => ({ ...getParsedCookies(req) });
|
|
9365
|
+
for (const [key, value] of Object.entries(getParsedCookies(req))) {
|
|
9366
|
+
Object.defineProperty(cookies, key, {
|
|
9367
|
+
value,
|
|
9368
|
+
writable: true,
|
|
9369
|
+
enumerable: true,
|
|
9370
|
+
configurable: true
|
|
9371
|
+
});
|
|
9372
|
+
}
|
|
9373
|
+
cookies.get = (name) => getParsedCookies(req)[name];
|
|
9374
|
+
cookies.set = (name, value, options = {}) => {
|
|
9375
|
+
if (!req._cookiesToSet) {
|
|
9376
|
+
req._cookiesToSet = [];
|
|
9377
|
+
}
|
|
9378
|
+
req._cookiesToSet.push({ name, value, options });
|
|
9379
|
+
};
|
|
9380
|
+
cookies.delete = (name, options = {}) => {
|
|
9381
|
+
if (!req._cookiesToDelete) {
|
|
9382
|
+
req._cookiesToDelete = [];
|
|
9383
|
+
}
|
|
9384
|
+
req._cookiesToDelete.push({ name, options });
|
|
9385
|
+
};
|
|
9386
|
+
cookies.getAll = () => ({ ...getParsedCookies(req) });
|
|
9387
|
+
Object.defineProperty(req, "cookies", {
|
|
9388
|
+
value: cookies,
|
|
9389
|
+
writable: true,
|
|
9390
|
+
configurable: true,
|
|
9391
|
+
enumerable: true
|
|
9392
|
+
});
|
|
9393
|
+
return cookies;
|
|
9394
|
+
});
|
|
9903
9395
|
function registerServerHandling(RouterClass) {
|
|
9904
9396
|
Object.defineProperties(RouterClass.prototype, {
|
|
9905
9397
|
serve: {
|
|
@@ -9931,6 +9423,13 @@ function registerServerHandling(RouterClass) {
|
|
|
9931
9423
|
if (options.development !== undefined) {
|
|
9932
9424
|
serverOptions.development = options.development;
|
|
9933
9425
|
}
|
|
9426
|
+
this._nativeRoutesEnabled = options.nativeRoutes === true;
|
|
9427
|
+
if (this._nativeRoutesEnabled) {
|
|
9428
|
+
const nativeRoutes = this._buildNativeRoutes();
|
|
9429
|
+
if (nativeRoutes) {
|
|
9430
|
+
serverOptions.routes = nativeRoutes;
|
|
9431
|
+
}
|
|
9432
|
+
}
|
|
9934
9433
|
if (this.wsConfig) {
|
|
9935
9434
|
serverOptions.websocket = this.wsConfig;
|
|
9936
9435
|
}
|
|
@@ -9956,12 +9455,19 @@ function registerServerHandling(RouterClass) {
|
|
|
9956
9455
|
const port = this.serverInstance.port;
|
|
9957
9456
|
const hostname = this.serverInstance.hostname;
|
|
9958
9457
|
this.serverInstance.stop();
|
|
9959
|
-
|
|
9458
|
+
const reloadOptions = {
|
|
9960
9459
|
port,
|
|
9961
9460
|
hostname,
|
|
9962
9461
|
fetch: this.handleRequest.bind(this),
|
|
9963
9462
|
websocket: this.wsConfig || undefined
|
|
9964
|
-
}
|
|
9463
|
+
};
|
|
9464
|
+
if (this._nativeRoutesEnabled) {
|
|
9465
|
+
const nativeRoutes = this._buildNativeRoutes();
|
|
9466
|
+
if (nativeRoutes) {
|
|
9467
|
+
reloadOptions.routes = nativeRoutes;
|
|
9468
|
+
}
|
|
9469
|
+
}
|
|
9470
|
+
this.serverInstance = Bun.serve(reloadOptions);
|
|
9965
9471
|
if (this.config.verbose) {
|
|
9966
9472
|
console.log(`\uD83D\uDD04 Server reloaded at http://${hostname}:${port}`);
|
|
9967
9473
|
}
|
|
@@ -9977,46 +9483,177 @@ function registerServerHandling(RouterClass) {
|
|
|
9977
9483
|
writable: true,
|
|
9978
9484
|
configurable: true
|
|
9979
9485
|
},
|
|
9486
|
+
_dispatchMatchedRoute: {
|
|
9487
|
+
async value(matchedRoute, req, params) {
|
|
9488
|
+
const enhancedReq = this.enhanceRequest(req, params);
|
|
9489
|
+
setCurrentRequest(enhancedReq);
|
|
9490
|
+
enhancedReq.route = matchedRoute;
|
|
9491
|
+
const route = matchedRoute;
|
|
9492
|
+
const epoch = this._mwEpoch || 0;
|
|
9493
|
+
const routeMwLen = route.middleware ? route.middleware.length : 0;
|
|
9494
|
+
let chain = route._compiledChain;
|
|
9495
|
+
if (!chain || route._chainEpoch !== epoch || route._chainMwLen !== routeMwLen) {
|
|
9496
|
+
const invoke = createHandlerInvoker(route.handler, this.config);
|
|
9497
|
+
if (routeMwLen === 0 && this.globalMiddleware.length === 0) {
|
|
9498
|
+
chain = invoke;
|
|
9499
|
+
} else {
|
|
9500
|
+
const middlewareStack = routeMwLen > 0 ? [...this.globalMiddleware, ...route.middleware] : [...this.globalMiddleware];
|
|
9501
|
+
middlewareStack.push((handlerReq, _next) => invoke(handlerReq));
|
|
9502
|
+
chain = this.buildMiddlewareChain(middlewareStack);
|
|
9503
|
+
}
|
|
9504
|
+
route._compiledChain = chain;
|
|
9505
|
+
route._chainEpoch = epoch;
|
|
9506
|
+
route._chainMwLen = routeMwLen;
|
|
9507
|
+
}
|
|
9508
|
+
let response2;
|
|
9509
|
+
try {
|
|
9510
|
+
response2 = await chain(enhancedReq);
|
|
9511
|
+
} catch (error) {
|
|
9512
|
+
if (this.errorHandler) {
|
|
9513
|
+
response2 = await this.errorHandler(error);
|
|
9514
|
+
} else {
|
|
9515
|
+
throw error;
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
if (response2) {
|
|
9519
|
+
return this.applyModifiedCookies(response2, enhancedReq);
|
|
9520
|
+
}
|
|
9521
|
+
return new Response("No response from middleware chain", { status: 500 });
|
|
9522
|
+
},
|
|
9523
|
+
writable: true,
|
|
9524
|
+
configurable: true
|
|
9525
|
+
},
|
|
9526
|
+
_buildNativeRoutes: {
|
|
9527
|
+
value() {
|
|
9528
|
+
const NATIVE_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
|
|
9529
|
+
const convertPath = (path) => {
|
|
9530
|
+
if (path === "*") {
|
|
9531
|
+
return "/*";
|
|
9532
|
+
}
|
|
9533
|
+
const segments = path.split("/");
|
|
9534
|
+
const converted = [];
|
|
9535
|
+
for (let i = 0;i < segments.length; i++) {
|
|
9536
|
+
const segment = segments[i];
|
|
9537
|
+
if (segment === "") {
|
|
9538
|
+
converted.push(segment);
|
|
9539
|
+
continue;
|
|
9540
|
+
}
|
|
9541
|
+
if (segment === "*") {
|
|
9542
|
+
return i === segments.length - 1 ? [...converted, "*"].join("/") : null;
|
|
9543
|
+
}
|
|
9544
|
+
const paramMatch = segment.match(/^\{([A-Z_$][\w$]*)\}$/i);
|
|
9545
|
+
if (paramMatch) {
|
|
9546
|
+
converted.push(`:${paramMatch[1]}`);
|
|
9547
|
+
continue;
|
|
9548
|
+
}
|
|
9549
|
+
if (segment.includes("{") || segment.includes(":") || segment.includes("*")) {
|
|
9550
|
+
return null;
|
|
9551
|
+
}
|
|
9552
|
+
converted.push(segment);
|
|
9553
|
+
}
|
|
9554
|
+
return converted.join("/");
|
|
9555
|
+
};
|
|
9556
|
+
const self = this;
|
|
9557
|
+
const wrapRoute = (route, isWildcard) => {
|
|
9558
|
+
return (req) => {
|
|
9559
|
+
return runWithRequest(req, async () => {
|
|
9560
|
+
try {
|
|
9561
|
+
let params = req.params ?? {};
|
|
9562
|
+
if (isWildcard) {
|
|
9563
|
+
const pathname = new URL(req.url).pathname;
|
|
9564
|
+
const basePath = route.path === "*" ? "/" : route.path.slice(0, -1);
|
|
9565
|
+
params = { ...params, wildcard: pathname.slice(basePath.length) };
|
|
9566
|
+
}
|
|
9567
|
+
return await self._dispatchMatchedRoute(route, req, params);
|
|
9568
|
+
} catch (error) {
|
|
9569
|
+
console.error("Error handling request:", error);
|
|
9570
|
+
if (self.errorHandler) {
|
|
9571
|
+
return self.errorHandler(error);
|
|
9572
|
+
}
|
|
9573
|
+
return new Response(JSON.stringify({
|
|
9574
|
+
success: false,
|
|
9575
|
+
message: "Internal Server Error",
|
|
9576
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9577
|
+
}), {
|
|
9578
|
+
status: 500,
|
|
9579
|
+
headers: {
|
|
9580
|
+
"Content-Type": "application/json",
|
|
9581
|
+
"Access-Control-Allow-Origin": "*",
|
|
9582
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
9583
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, Accept, Origin"
|
|
9584
|
+
}
|
|
9585
|
+
});
|
|
9586
|
+
}
|
|
9587
|
+
});
|
|
9588
|
+
};
|
|
9589
|
+
};
|
|
9590
|
+
const natives = {};
|
|
9591
|
+
const claimedShapes = new Map;
|
|
9592
|
+
for (const route of this.routes) {
|
|
9593
|
+
if (!NATIVE_METHODS.has(route.method)) {
|
|
9594
|
+
continue;
|
|
9595
|
+
}
|
|
9596
|
+
if (route.nativeDispatch === false) {
|
|
9597
|
+
continue;
|
|
9598
|
+
}
|
|
9599
|
+
if (route.constraints && Object.keys(route.constraints).length > 0) {
|
|
9600
|
+
continue;
|
|
9601
|
+
}
|
|
9602
|
+
if (route.handler instanceof Response) {
|
|
9603
|
+
continue;
|
|
9604
|
+
}
|
|
9605
|
+
const bunPath = convertPath(route.path);
|
|
9606
|
+
if (!bunPath) {
|
|
9607
|
+
continue;
|
|
9608
|
+
}
|
|
9609
|
+
const shape = bunPath.replace(/:[^/]+/g, ":p");
|
|
9610
|
+
const claimedBy = claimedShapes.get(shape);
|
|
9611
|
+
if (claimedBy && claimedBy !== bunPath) {
|
|
9612
|
+
continue;
|
|
9613
|
+
}
|
|
9614
|
+
claimedShapes.set(shape, bunPath);
|
|
9615
|
+
const entry = natives[bunPath] ??= {};
|
|
9616
|
+
if (entry[route.method]) {
|
|
9617
|
+
continue;
|
|
9618
|
+
}
|
|
9619
|
+
entry[route.method] = wrapRoute(route, route.path.endsWith("*"));
|
|
9620
|
+
}
|
|
9621
|
+
for (const entry of Object.values(natives)) {
|
|
9622
|
+
if (entry.GET && !entry.HEAD) {
|
|
9623
|
+
entry.HEAD = entry.GET;
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
return Object.keys(natives).length > 0 ? natives : null;
|
|
9627
|
+
},
|
|
9628
|
+
writable: true,
|
|
9629
|
+
configurable: true
|
|
9630
|
+
},
|
|
9980
9631
|
handleRequestImpl: {
|
|
9981
9632
|
async value(req) {
|
|
9982
9633
|
try {
|
|
9983
9634
|
const url2 = new URL(req.url);
|
|
9984
|
-
|
|
9985
|
-
const hostname2 = url2.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
9986
|
-
const optionsMatch = this.matchRoute(url2.pathname, "OPTIONS", hostname2);
|
|
9987
|
-
if (!optionsMatch) {
|
|
9988
|
-
return new Response(null, {
|
|
9989
|
-
status: 204,
|
|
9990
|
-
headers: {
|
|
9991
|
-
"Access-Control-Allow-Origin": "*",
|
|
9992
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
9993
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, Accept, Origin",
|
|
9994
|
-
"Access-Control-Max-Age": "86400",
|
|
9995
|
-
"Access-Control-Allow-Credentials": "true"
|
|
9996
|
-
}
|
|
9997
|
-
});
|
|
9998
|
-
}
|
|
9999
|
-
}
|
|
9635
|
+
req._parsedURL = url2;
|
|
10000
9636
|
const hostname = url2.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
10001
9637
|
const match = this.matchRoute(url2.pathname, req.method, hostname);
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
}
|
|
10010
|
-
const routeHandlerMiddleware = async (req2, _next) => {
|
|
10011
|
-
return await this.resolveHandler(match.route.handler, req2);
|
|
9638
|
+
if (req.method === "OPTIONS" && !match) {
|
|
9639
|
+
const origin = req.headers.get("origin");
|
|
9640
|
+
const preflightHeaders = {
|
|
9641
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
9642
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
9643
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, Accept, Origin",
|
|
9644
|
+
"Access-Control-Max-Age": "86400"
|
|
10012
9645
|
};
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
return this.applyModifiedCookies(response2, enhancedReq);
|
|
9646
|
+
if (origin) {
|
|
9647
|
+
preflightHeaders["Access-Control-Allow-Credentials"] = "true";
|
|
9648
|
+
preflightHeaders.Vary = "Origin";
|
|
10017
9649
|
}
|
|
10018
|
-
return new Response(
|
|
9650
|
+
return new Response(null, { status: 204, headers: preflightHeaders });
|
|
9651
|
+
}
|
|
9652
|
+
if (match) {
|
|
9653
|
+
return await this._dispatchMatchedRoute(match.route, req, match.params);
|
|
10019
9654
|
}
|
|
9655
|
+
const enhancedReq = this.enhanceRequest(req, {});
|
|
9656
|
+
setCurrentRequest(enhancedReq);
|
|
10020
9657
|
const allowedMethods = this.getAllowedMethods(url2.pathname, hostname);
|
|
10021
9658
|
const corsHeaders = {
|
|
10022
9659
|
"Content-Type": "application/json",
|
|
@@ -10089,63 +9726,18 @@ function registerServerHandling(RouterClass) {
|
|
|
10089
9726
|
},
|
|
10090
9727
|
enhanceRequest: {
|
|
10091
9728
|
value(req, params = {}) {
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
parsedCookies[name] = decodeURIComponent(value);
|
|
10103
|
-
}
|
|
10104
|
-
});
|
|
10105
|
-
}
|
|
10106
|
-
return parsedCookies;
|
|
10107
|
-
};
|
|
10108
|
-
const cookies = {
|
|
10109
|
-
get: (name) => getCookies()[name],
|
|
10110
|
-
set: (name, value, options = {}) => {
|
|
10111
|
-
const enhancedRequest = req;
|
|
10112
|
-
if (!enhancedRequest._cookiesToSet) {
|
|
10113
|
-
enhancedRequest._cookiesToSet = [];
|
|
10114
|
-
}
|
|
10115
|
-
enhancedRequest._cookiesToSet.push({ name, value, options });
|
|
10116
|
-
},
|
|
10117
|
-
delete: (name, options = {}) => {
|
|
10118
|
-
const enhancedRequest = req;
|
|
10119
|
-
if (!enhancedRequest._cookiesToDelete) {
|
|
10120
|
-
enhancedRequest._cookiesToDelete = [];
|
|
10121
|
-
}
|
|
10122
|
-
enhancedRequest._cookiesToDelete.push({ name, options });
|
|
10123
|
-
},
|
|
10124
|
-
getAll: () => ({ ...getCookies() })
|
|
10125
|
-
};
|
|
10126
|
-
const enhancedReq = Object.assign(req, {
|
|
10127
|
-
params,
|
|
10128
|
-
cookies: getCookies(),
|
|
10129
|
-
_cookiesToSet: [],
|
|
10130
|
-
_cookiesToDelete: []
|
|
10131
|
-
});
|
|
10132
|
-
RequestWithMacros.applyMacros(enhancedReq);
|
|
10133
|
-
enhancedReq.getParam = (name, defaultValue) => {
|
|
10134
|
-
const value = params?.[name];
|
|
10135
|
-
return value !== undefined ? value : defaultValue;
|
|
10136
|
-
};
|
|
10137
|
-
enhancedReq.cookie = (name, defaultValue) => {
|
|
10138
|
-
const value = getCookies()[name];
|
|
10139
|
-
return value !== undefined ? value : defaultValue ?? null;
|
|
10140
|
-
};
|
|
10141
|
-
if (typeof enhancedReq.get !== "function") {
|
|
10142
|
-
enhancedReq.get = (key, defaultValue) => {
|
|
10143
|
-
const url2 = new URL(req.url);
|
|
10144
|
-
const value = url2.searchParams.get(key);
|
|
10145
|
-
return value !== null && value !== undefined ? value : defaultValue;
|
|
10146
|
-
};
|
|
9729
|
+
const enhancedReq = req;
|
|
9730
|
+
if ("params" in req) {
|
|
9731
|
+
Object.defineProperty(enhancedReq, "params", {
|
|
9732
|
+
value: params,
|
|
9733
|
+
writable: true,
|
|
9734
|
+
configurable: true,
|
|
9735
|
+
enumerable: true
|
|
9736
|
+
});
|
|
9737
|
+
} else {
|
|
9738
|
+
enhancedReq.params = params;
|
|
10147
9739
|
}
|
|
10148
|
-
|
|
9740
|
+
RequestWithMacros.applyMacros(enhancedReq);
|
|
10149
9741
|
return enhancedReq;
|
|
10150
9742
|
},
|
|
10151
9743
|
writable: true,
|
|
@@ -10153,6 +9745,11 @@ function registerServerHandling(RouterClass) {
|
|
|
10153
9745
|
},
|
|
10154
9746
|
applyModifiedCookies: {
|
|
10155
9747
|
value(response2, req) {
|
|
9748
|
+
const hasSet = req._cookiesToSet && req._cookiesToSet.length > 0;
|
|
9749
|
+
const hasDelete = req._cookiesToDelete && req._cookiesToDelete.length > 0;
|
|
9750
|
+
if (!hasSet && !hasDelete) {
|
|
9751
|
+
return response2;
|
|
9752
|
+
}
|
|
10156
9753
|
const newResponse = new Response(response2.body, {
|
|
10157
9754
|
status: response2.status,
|
|
10158
9755
|
statusText: response2.statusText,
|
|
@@ -10702,6 +10299,7 @@ function registerBuiltInResponseMacros() {
|
|
|
10702
10299
|
registerBuiltInResponseMacros();
|
|
10703
10300
|
|
|
10704
10301
|
// src/validation/validator.ts
|
|
10302
|
+
var EMAIL_REGEX = /^[A-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?)*\.[A-Z]{2,}$/i;
|
|
10705
10303
|
var BuiltInRules = {
|
|
10706
10304
|
required: {
|
|
10707
10305
|
name: "required",
|
|
@@ -10740,10 +10338,7 @@ var BuiltInRules = {
|
|
|
10740
10338
|
},
|
|
10741
10339
|
email: {
|
|
10742
10340
|
name: "email",
|
|
10743
|
-
validate: (value) =>
|
|
10744
|
-
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
10745
|
-
return typeof value === "string" && emailRegex.test(value);
|
|
10746
|
-
},
|
|
10341
|
+
validate: (value) => typeof value === "string" && EMAIL_REGEX.test(value),
|
|
10747
10342
|
message: "The :field field must be a valid email address."
|
|
10748
10343
|
},
|
|
10749
10344
|
url: {
|
|
@@ -10913,6 +10508,25 @@ var BuiltInRules = {
|
|
|
10913
10508
|
message: "The :field field must be an object."
|
|
10914
10509
|
}
|
|
10915
10510
|
};
|
|
10511
|
+
var DatabaseRules = {
|
|
10512
|
+
unique: {
|
|
10513
|
+
name: "unique",
|
|
10514
|
+
validate: async (_value, _parameters) => {
|
|
10515
|
+
console.warn('Database validation rule "unique" requires external implementation');
|
|
10516
|
+
return true;
|
|
10517
|
+
},
|
|
10518
|
+
message: "The :field field must be unique."
|
|
10519
|
+
},
|
|
10520
|
+
exists: {
|
|
10521
|
+
name: "exists",
|
|
10522
|
+
validate: async (_value, _parameters) => {
|
|
10523
|
+
console.warn('Database validation rule "exists" requires external implementation');
|
|
10524
|
+
return true;
|
|
10525
|
+
},
|
|
10526
|
+
message: "The selected :field is invalid."
|
|
10527
|
+
}
|
|
10528
|
+
};
|
|
10529
|
+
|
|
10916
10530
|
class Validator {
|
|
10917
10531
|
rules = {};
|
|
10918
10532
|
config;
|
|
@@ -11016,6 +10630,153 @@ function createValidationMiddleware(rules, config2) {
|
|
|
11016
10630
|
};
|
|
11017
10631
|
}
|
|
11018
10632
|
|
|
10633
|
+
class ValidationRuleBuilder {
|
|
10634
|
+
rules = [];
|
|
10635
|
+
required() {
|
|
10636
|
+
this.rules.push("required");
|
|
10637
|
+
return this;
|
|
10638
|
+
}
|
|
10639
|
+
string() {
|
|
10640
|
+
this.rules.push("string");
|
|
10641
|
+
return this;
|
|
10642
|
+
}
|
|
10643
|
+
number() {
|
|
10644
|
+
this.rules.push("number");
|
|
10645
|
+
return this;
|
|
10646
|
+
}
|
|
10647
|
+
integer() {
|
|
10648
|
+
this.rules.push("integer");
|
|
10649
|
+
return this;
|
|
10650
|
+
}
|
|
10651
|
+
boolean() {
|
|
10652
|
+
this.rules.push("boolean");
|
|
10653
|
+
return this;
|
|
10654
|
+
}
|
|
10655
|
+
email() {
|
|
10656
|
+
this.rules.push("email");
|
|
10657
|
+
return this;
|
|
10658
|
+
}
|
|
10659
|
+
url() {
|
|
10660
|
+
this.rules.push("url");
|
|
10661
|
+
return this;
|
|
10662
|
+
}
|
|
10663
|
+
min(value) {
|
|
10664
|
+
this.rules.push(`min:${value}`);
|
|
10665
|
+
return this;
|
|
10666
|
+
}
|
|
10667
|
+
max(value) {
|
|
10668
|
+
this.rules.push(`max:${value}`);
|
|
10669
|
+
return this;
|
|
10670
|
+
}
|
|
10671
|
+
between(min, max) {
|
|
10672
|
+
this.rules.push(`between:${min},${max}`);
|
|
10673
|
+
return this;
|
|
10674
|
+
}
|
|
10675
|
+
in(...values) {
|
|
10676
|
+
this.rules.push(`in:${values.join(",")}`);
|
|
10677
|
+
return this;
|
|
10678
|
+
}
|
|
10679
|
+
notIn(...values) {
|
|
10680
|
+
this.rules.push(`not_in:${values.join(",")}`);
|
|
10681
|
+
return this;
|
|
10682
|
+
}
|
|
10683
|
+
regex(pattern) {
|
|
10684
|
+
this.rules.push(`regex:${pattern}`);
|
|
10685
|
+
return this;
|
|
10686
|
+
}
|
|
10687
|
+
alpha() {
|
|
10688
|
+
this.rules.push("alpha");
|
|
10689
|
+
return this;
|
|
10690
|
+
}
|
|
10691
|
+
alphaNum() {
|
|
10692
|
+
this.rules.push("alpha_num");
|
|
10693
|
+
return this;
|
|
10694
|
+
}
|
|
10695
|
+
alphaDash() {
|
|
10696
|
+
this.rules.push("alpha_dash");
|
|
10697
|
+
return this;
|
|
10698
|
+
}
|
|
10699
|
+
date() {
|
|
10700
|
+
this.rules.push("date");
|
|
10701
|
+
return this;
|
|
10702
|
+
}
|
|
10703
|
+
after(date) {
|
|
10704
|
+
this.rules.push(`after:${date}`);
|
|
10705
|
+
return this;
|
|
10706
|
+
}
|
|
10707
|
+
before(date) {
|
|
10708
|
+
this.rules.push(`before:${date}`);
|
|
10709
|
+
return this;
|
|
10710
|
+
}
|
|
10711
|
+
confirmed(field) {
|
|
10712
|
+
this.rules.push(field ? `confirmed:${field}` : "confirmed");
|
|
10713
|
+
return this;
|
|
10714
|
+
}
|
|
10715
|
+
same(field) {
|
|
10716
|
+
this.rules.push(`same:${field}`);
|
|
10717
|
+
return this;
|
|
10718
|
+
}
|
|
10719
|
+
different(field) {
|
|
10720
|
+
this.rules.push(`different:${field}`);
|
|
10721
|
+
return this;
|
|
10722
|
+
}
|
|
10723
|
+
json() {
|
|
10724
|
+
this.rules.push("json");
|
|
10725
|
+
return this;
|
|
10726
|
+
}
|
|
10727
|
+
uuid() {
|
|
10728
|
+
this.rules.push("uuid");
|
|
10729
|
+
return this;
|
|
10730
|
+
}
|
|
10731
|
+
array() {
|
|
10732
|
+
this.rules.push("array");
|
|
10733
|
+
return this;
|
|
10734
|
+
}
|
|
10735
|
+
object() {
|
|
10736
|
+
this.rules.push("object");
|
|
10737
|
+
return this;
|
|
10738
|
+
}
|
|
10739
|
+
unique(table, column) {
|
|
10740
|
+
this.rules.push(column ? `unique:${table},${column}` : `unique:${table}`);
|
|
10741
|
+
return this;
|
|
10742
|
+
}
|
|
10743
|
+
exists(table, column) {
|
|
10744
|
+
this.rules.push(column ? `exists:${table},${column}` : `exists:${table}`);
|
|
10745
|
+
return this;
|
|
10746
|
+
}
|
|
10747
|
+
custom(ruleName, ...parameters) {
|
|
10748
|
+
this.rules.push(parameters.length > 0 ? `${ruleName}:${parameters.join(",")}` : ruleName);
|
|
10749
|
+
return this;
|
|
10750
|
+
}
|
|
10751
|
+
build() {
|
|
10752
|
+
return this.rules.join("|");
|
|
10753
|
+
}
|
|
10754
|
+
}
|
|
10755
|
+
function rule() {
|
|
10756
|
+
return new ValidationRuleBuilder;
|
|
10757
|
+
}
|
|
10758
|
+
var ValidationHelpers = {
|
|
10759
|
+
rules(rules) {
|
|
10760
|
+
const result = {};
|
|
10761
|
+
for (const [field, fieldRules] of Object.entries(rules)) {
|
|
10762
|
+
result[field] = fieldRules instanceof ValidationRuleBuilder ? fieldRules.build() : fieldRules;
|
|
10763
|
+
}
|
|
10764
|
+
return result;
|
|
10765
|
+
},
|
|
10766
|
+
async validateValue(value, rules, field = "value") {
|
|
10767
|
+
const validator = new Validator;
|
|
10768
|
+
const errors = await validator.validate({ [field]: value }, { [field]: rules });
|
|
10769
|
+
return errors[field] || [];
|
|
10770
|
+
},
|
|
10771
|
+
async passes(value, rules) {
|
|
10772
|
+
const errors = await this.validateValue(value, rules);
|
|
10773
|
+
return errors.length === 0;
|
|
10774
|
+
},
|
|
10775
|
+
async fails(value, rules) {
|
|
10776
|
+
return !await this.passes(value, rules);
|
|
10777
|
+
}
|
|
10778
|
+
};
|
|
10779
|
+
|
|
11019
10780
|
// src/router/validation-integration.ts
|
|
11020
10781
|
class RouteBuilder {
|
|
11021
10782
|
method;
|
|
@@ -11052,7 +10813,7 @@ class RouteBuilder {
|
|
|
11052
10813
|
}
|
|
11053
10814
|
}
|
|
11054
10815
|
|
|
11055
|
-
class
|
|
10816
|
+
class ValidationFluentRouter {
|
|
11056
10817
|
routes = [];
|
|
11057
10818
|
get(path, handler) {
|
|
11058
10819
|
return new RouteBuilder("GET", path, handler);
|
|
@@ -11165,7 +10926,7 @@ var RouteHelpers = {
|
|
|
11165
10926
|
}
|
|
11166
10927
|
};
|
|
11167
10928
|
function createFluentRouter() {
|
|
11168
|
-
return new
|
|
10929
|
+
return new ValidationFluentRouter;
|
|
11169
10930
|
}
|
|
11170
10931
|
var MiddlewareHelpers = {
|
|
11171
10932
|
compose: (...middleware2) => {
|
|
@@ -13819,11 +13580,13 @@ export {
|
|
|
13819
13580
|
singleFileUpload,
|
|
13820
13581
|
signRequest,
|
|
13821
13582
|
setCurrentRequest,
|
|
13583
|
+
setContainer,
|
|
13822
13584
|
session,
|
|
13823
13585
|
securitySuite,
|
|
13824
13586
|
security,
|
|
13825
13587
|
scopedBindingHelpers,
|
|
13826
13588
|
runWithRequest,
|
|
13589
|
+
rule,
|
|
13827
13590
|
router,
|
|
13828
13591
|
routeModelHelpers,
|
|
13829
13592
|
routeCacheManager,
|
|
@@ -13862,10 +13625,12 @@ export {
|
|
|
13862
13625
|
inputValidation,
|
|
13863
13626
|
injectQueryPreservationScript,
|
|
13864
13627
|
imageUpload,
|
|
13628
|
+
globalValidator,
|
|
13865
13629
|
globalMiddlewarePipeline,
|
|
13866
13630
|
getNamedRoutes,
|
|
13867
13631
|
getNamedRoutePath,
|
|
13868
13632
|
getCurrentRequest,
|
|
13633
|
+
getContainer,
|
|
13869
13634
|
getConfig,
|
|
13870
13635
|
generateSessionId,
|
|
13871
13636
|
generateQueryPreservationScript,
|
|
@@ -13878,12 +13643,14 @@ export {
|
|
|
13878
13643
|
extractBearerToken,
|
|
13879
13644
|
extractBasicAuth,
|
|
13880
13645
|
extractApiKey,
|
|
13646
|
+
enableRequestContext,
|
|
13881
13647
|
documentUpload,
|
|
13882
13648
|
defaultQueryPreservationConfig,
|
|
13883
13649
|
defaultConfig,
|
|
13884
13650
|
ddosProtection,
|
|
13885
13651
|
csrf,
|
|
13886
13652
|
createWebSocketTester,
|
|
13653
|
+
createValidationMiddleware,
|
|
13887
13654
|
createTestResponse,
|
|
13888
13655
|
createTestClient,
|
|
13889
13656
|
createSubdomainMiddleware,
|
|
@@ -13905,6 +13672,7 @@ export {
|
|
|
13905
13672
|
createFluentRouter,
|
|
13906
13673
|
createFileUploadTester,
|
|
13907
13674
|
createEnhancementMiddleware,
|
|
13675
|
+
createContainer,
|
|
13908
13676
|
createAuthTester,
|
|
13909
13677
|
cors,
|
|
13910
13678
|
contentSecurityPolicy,
|
|
@@ -13922,7 +13690,11 @@ export {
|
|
|
13922
13690
|
applyRequestEnhancements,
|
|
13923
13691
|
apiKeyAuth,
|
|
13924
13692
|
WebSocketTester,
|
|
13693
|
+
Validator,
|
|
13694
|
+
ValidationRuleBuilder,
|
|
13925
13695
|
ValidationMiddlewareBuilder,
|
|
13696
|
+
ValidationHelpers,
|
|
13697
|
+
ValidationFluentRouter,
|
|
13926
13698
|
ThrottleFactory,
|
|
13927
13699
|
TestResponseWrapper,
|
|
13928
13700
|
TestRequestBuilder,
|
|
@@ -13988,17 +13760,24 @@ export {
|
|
|
13988
13760
|
FileSessionStore,
|
|
13989
13761
|
FileSecurity,
|
|
13990
13762
|
exports_errors as Errors,
|
|
13763
|
+
EnvironmentManager,
|
|
13991
13764
|
EnhancementPresets,
|
|
13992
13765
|
DomainMatcher,
|
|
13993
13766
|
DomainGroupBuilder,
|
|
13994
13767
|
DomainGroup,
|
|
13995
13768
|
Dependencies,
|
|
13769
|
+
DefaultServiceProviderManager,
|
|
13996
13770
|
DatabaseSessionStore,
|
|
13771
|
+
DatabaseRules,
|
|
13997
13772
|
DDoSProtection,
|
|
13998
13773
|
Csrf,
|
|
13999
13774
|
Cors,
|
|
13775
|
+
ContextualContainer,
|
|
14000
13776
|
ContentSecurityPolicy,
|
|
13777
|
+
Container,
|
|
14001
13778
|
CacheInvalidation,
|
|
13779
|
+
BuiltInRules,
|
|
13780
|
+
BindingBuilder,
|
|
14002
13781
|
AuthTester,
|
|
14003
13782
|
auth_default as Auth,
|
|
14004
13783
|
ApiKeyManager
|