blaizejs 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-QQCQRHXJ.js +38 -0
- package/dist/chunk-QQCQRHXJ.js.map +1 -0
- package/dist/chunk-SRD3AB6T.js +38 -0
- package/dist/chunk-SRD3AB6T.js.map +1 -0
- package/dist/chunk-TCPQMZ23.js +148 -0
- package/dist/chunk-TCPQMZ23.js.map +1 -0
- package/dist/index.cjs +1747 -412
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1189 -143
- package/dist/index.d.ts +1189 -143
- package/dist/index.js +1471 -409
- package/dist/index.js.map +1 -1
- package/dist/internal-server-error-CVRDTBLL.js +16 -0
- package/dist/internal-server-error-CVRDTBLL.js.map +1 -0
- package/dist/payload-too-large-error-PAYLDBZT.js +29 -0
- package/dist/payload-too-large-error-PAYLDBZT.js.map +1 -0
- package/dist/unsupported-media-type-error-MQZD7YQJ.js +29 -0
- package/dist/unsupported-media-type-error-MQZD7YQJ.js.map +1 -0
- package/dist/validation-error-CM6IKIJU.js +16 -0
- package/dist/validation-error-CM6IKIJU.js.map +1 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* blaizejs v0.
|
|
2
|
+
* blaizejs v0.3.0
|
|
3
3
|
* A blazing-fast, TypeScript-first Node.js framework with HTTP/2 support, file-based routing, powerful middleware system, and end-to-end type safety for building modern APIs.
|
|
4
4
|
*
|
|
5
5
|
* Copyright (c) 2025 BlaizeJS Contributors
|
|
6
6
|
* @license MIT
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import {
|
|
10
|
+
InternalServerError
|
|
11
|
+
} from "./chunk-QQCQRHXJ.js";
|
|
12
|
+
import {
|
|
13
|
+
ValidationError
|
|
14
|
+
} from "./chunk-SRD3AB6T.js";
|
|
15
|
+
import {
|
|
16
|
+
BlaizeError,
|
|
17
|
+
ErrorSeverity,
|
|
18
|
+
ErrorType,
|
|
19
|
+
__require,
|
|
20
|
+
generateCorrelationId,
|
|
21
|
+
getCurrentCorrelationId,
|
|
22
|
+
isBodyParseError
|
|
23
|
+
} from "./chunk-TCPQMZ23.js";
|
|
9
24
|
|
|
10
25
|
// src/middleware/execute.ts
|
|
11
26
|
function execute(middleware, ctx, next) {
|
|
@@ -107,6 +122,9 @@ function create2(name, version, setup, defaultOptions = {}) {
|
|
|
107
122
|
};
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
// src/router/create.ts
|
|
126
|
+
import { fileURLToPath } from "node:url";
|
|
127
|
+
|
|
110
128
|
// src/config.ts
|
|
111
129
|
var config = {};
|
|
112
130
|
function setRuntimeConfig(newConfig) {
|
|
@@ -178,73 +196,75 @@ function getCallerFilePath() {
|
|
|
178
196
|
if (!fileName) {
|
|
179
197
|
throw new Error("Unable to determine caller file name");
|
|
180
198
|
}
|
|
199
|
+
if (fileName.startsWith("file://")) {
|
|
200
|
+
return fileURLToPath(fileName);
|
|
201
|
+
}
|
|
181
202
|
return fileName;
|
|
182
203
|
} finally {
|
|
183
204
|
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
184
205
|
}
|
|
185
206
|
}
|
|
186
207
|
function getRoutePath() {
|
|
187
|
-
console.log("getRoutePath called");
|
|
188
208
|
const callerPath = getCallerFilePath();
|
|
189
209
|
const routesDir = getRoutesDir();
|
|
190
210
|
const parsedRoute = parseRoutePath(callerPath, routesDir);
|
|
191
|
-
console.log(
|
|
211
|
+
console.log(`\u{1F50E} Parsed route path: ${parsedRoute.routePath} from file: ${callerPath}`);
|
|
192
212
|
return parsedRoute.routePath;
|
|
193
213
|
}
|
|
194
214
|
var createGetRoute = (config2) => {
|
|
195
215
|
validateMethodConfig("GET", config2);
|
|
196
|
-
const
|
|
216
|
+
const path6 = getRoutePath();
|
|
197
217
|
return {
|
|
198
218
|
GET: config2,
|
|
199
|
-
path:
|
|
219
|
+
path: path6
|
|
200
220
|
};
|
|
201
221
|
};
|
|
202
222
|
var createPostRoute = (config2) => {
|
|
203
223
|
validateMethodConfig("POST", config2);
|
|
204
|
-
const
|
|
224
|
+
const path6 = getRoutePath();
|
|
205
225
|
return {
|
|
206
226
|
POST: config2,
|
|
207
|
-
path:
|
|
227
|
+
path: path6
|
|
208
228
|
};
|
|
209
229
|
};
|
|
210
230
|
var createPutRoute = (config2) => {
|
|
211
231
|
validateMethodConfig("PUT", config2);
|
|
212
|
-
const
|
|
232
|
+
const path6 = getRoutePath();
|
|
213
233
|
return {
|
|
214
234
|
PUT: config2,
|
|
215
|
-
path:
|
|
235
|
+
path: path6
|
|
216
236
|
};
|
|
217
237
|
};
|
|
218
238
|
var createDeleteRoute = (config2) => {
|
|
219
239
|
validateMethodConfig("DELETE", config2);
|
|
220
|
-
const
|
|
240
|
+
const path6 = getRoutePath();
|
|
221
241
|
return {
|
|
222
242
|
DELETE: config2,
|
|
223
|
-
path:
|
|
243
|
+
path: path6
|
|
224
244
|
};
|
|
225
245
|
};
|
|
226
246
|
var createPatchRoute = (config2) => {
|
|
227
247
|
validateMethodConfig("PATCH", config2);
|
|
228
|
-
const
|
|
248
|
+
const path6 = getRoutePath();
|
|
229
249
|
return {
|
|
230
250
|
PATCH: config2,
|
|
231
|
-
path:
|
|
251
|
+
path: path6
|
|
232
252
|
};
|
|
233
253
|
};
|
|
234
254
|
var createHeadRoute = (config2) => {
|
|
235
255
|
validateMethodConfig("HEAD", config2);
|
|
236
|
-
const
|
|
256
|
+
const path6 = getRoutePath();
|
|
237
257
|
return {
|
|
238
258
|
HEAD: config2,
|
|
239
|
-
path:
|
|
259
|
+
path: path6
|
|
240
260
|
};
|
|
241
261
|
};
|
|
242
262
|
var createOptionsRoute = (config2) => {
|
|
243
263
|
validateMethodConfig("OPTIONS", config2);
|
|
244
|
-
const
|
|
264
|
+
const path6 = getRoutePath();
|
|
245
265
|
return {
|
|
246
266
|
OPTIONS: config2,
|
|
247
|
-
path:
|
|
267
|
+
path: path6
|
|
248
268
|
};
|
|
249
269
|
};
|
|
250
270
|
function validateMethodConfig(method, config2) {
|
|
@@ -380,8 +400,474 @@ function runWithContext(context, callback) {
|
|
|
380
400
|
return contextStorage.run(context, callback);
|
|
381
401
|
}
|
|
382
402
|
|
|
403
|
+
// src/upload/multipart-parser.ts
|
|
404
|
+
import { randomUUID } from "node:crypto";
|
|
405
|
+
import { createWriteStream } from "node:fs";
|
|
406
|
+
import { tmpdir } from "node:os";
|
|
407
|
+
import { join as join2 } from "node:path";
|
|
408
|
+
import { Readable } from "node:stream";
|
|
409
|
+
|
|
410
|
+
// src/upload/utils.ts
|
|
411
|
+
var BOUNDARY_REGEX = /boundary=([^;]+)/i;
|
|
412
|
+
var CONTENT_DISPOSITION_REGEX = /Content-Disposition:\s*form-data;\s*name="([^"]+)"(?:;[\s\r\n]*filename="([^"]*)")?/i;
|
|
413
|
+
var CONTENT_TYPE_REGEX = /Content-Type:\s*([^\r\n]+)/i;
|
|
414
|
+
var MULTIPART_REGEX = /multipart\/form-data/i;
|
|
415
|
+
function extractBoundary(contentType) {
|
|
416
|
+
const match = contentType.match(BOUNDARY_REGEX);
|
|
417
|
+
if (!match || !match[1]) return null;
|
|
418
|
+
let boundary = match[1].trim();
|
|
419
|
+
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
420
|
+
boundary = boundary.slice(1, -1);
|
|
421
|
+
}
|
|
422
|
+
return boundary || null;
|
|
423
|
+
}
|
|
424
|
+
function parseContentDisposition(headers) {
|
|
425
|
+
const match = headers.match(CONTENT_DISPOSITION_REGEX);
|
|
426
|
+
if (!match || !match[1]) return null;
|
|
427
|
+
return {
|
|
428
|
+
name: match[1],
|
|
429
|
+
filename: match[2] !== void 0 ? match[2] : void 0
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function parseContentType(headers) {
|
|
433
|
+
const match = headers.match(CONTENT_TYPE_REGEX);
|
|
434
|
+
return match && match[1]?.trim() ? match[1].trim() : "application/octet-stream";
|
|
435
|
+
}
|
|
436
|
+
function isMultipartContent(contentType) {
|
|
437
|
+
return MULTIPART_REGEX.test(contentType);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/upload/multipart-parser.ts
|
|
441
|
+
var DEFAULT_OPTIONS = {
|
|
442
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
443
|
+
// 10MB
|
|
444
|
+
maxFiles: 10,
|
|
445
|
+
maxFieldSize: 1 * 1024 * 1024,
|
|
446
|
+
// 1MB
|
|
447
|
+
allowedMimeTypes: [],
|
|
448
|
+
allowedExtensions: [],
|
|
449
|
+
strategy: "stream",
|
|
450
|
+
tempDir: tmpdir(),
|
|
451
|
+
computeHash: false
|
|
452
|
+
};
|
|
453
|
+
function createParserState(boundary, options = {}) {
|
|
454
|
+
return {
|
|
455
|
+
boundary: Buffer.from(`--${boundary}`),
|
|
456
|
+
options: { ...DEFAULT_OPTIONS, ...options },
|
|
457
|
+
fields: /* @__PURE__ */ new Map(),
|
|
458
|
+
files: /* @__PURE__ */ new Map(),
|
|
459
|
+
buffer: Buffer.alloc(0),
|
|
460
|
+
stage: "boundary",
|
|
461
|
+
currentHeaders: "",
|
|
462
|
+
currentField: null,
|
|
463
|
+
currentFilename: void 0,
|
|
464
|
+
currentMimetype: "application/octet-stream",
|
|
465
|
+
currentContentLength: 0,
|
|
466
|
+
fileCount: 0,
|
|
467
|
+
fieldCount: 0,
|
|
468
|
+
currentBufferChunks: [],
|
|
469
|
+
currentStream: null,
|
|
470
|
+
currentTempPath: null,
|
|
471
|
+
currentWriteStream: null,
|
|
472
|
+
streamController: null,
|
|
473
|
+
cleanupTasks: [],
|
|
474
|
+
// Track validation state
|
|
475
|
+
hasFoundValidBoundary: false,
|
|
476
|
+
hasProcessedAnyPart: false,
|
|
477
|
+
isFinished: false
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function processChunk(state, chunk) {
|
|
481
|
+
const newBuffer = Buffer.concat([state.buffer, chunk]);
|
|
482
|
+
let currentState = { ...state, buffer: newBuffer };
|
|
483
|
+
while (currentState.buffer.length > 0 && !currentState.isFinished) {
|
|
484
|
+
const nextState = await processCurrentStage(currentState);
|
|
485
|
+
if (nextState === currentState) break;
|
|
486
|
+
currentState = nextState;
|
|
487
|
+
}
|
|
488
|
+
return currentState;
|
|
489
|
+
}
|
|
490
|
+
async function processCurrentStage(state) {
|
|
491
|
+
switch (state.stage) {
|
|
492
|
+
case "boundary":
|
|
493
|
+
return processBoundary(state);
|
|
494
|
+
case "headers":
|
|
495
|
+
return processHeaders(state);
|
|
496
|
+
case "content":
|
|
497
|
+
return processContent(state);
|
|
498
|
+
default: {
|
|
499
|
+
const { InternalServerError: InternalServerError2 } = await import("./internal-server-error-CVRDTBLL.js");
|
|
500
|
+
throw new InternalServerError2(`Invalid parser stage`, {
|
|
501
|
+
operation: state.stage
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function processBoundary(state) {
|
|
507
|
+
const boundaryIndex = state.buffer.indexOf(state.boundary);
|
|
508
|
+
if (boundaryIndex === -1) return state;
|
|
509
|
+
const hasFoundValidBoundary = true;
|
|
510
|
+
let buffer = state.buffer.subarray(boundaryIndex + state.boundary.length);
|
|
511
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("--"))) {
|
|
512
|
+
return {
|
|
513
|
+
...state,
|
|
514
|
+
buffer,
|
|
515
|
+
hasFoundValidBoundary,
|
|
516
|
+
isFinished: true,
|
|
517
|
+
stage: "boundary"
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("\r\n"))) {
|
|
521
|
+
buffer = buffer.subarray(2);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
...state,
|
|
525
|
+
buffer,
|
|
526
|
+
hasFoundValidBoundary,
|
|
527
|
+
stage: "headers",
|
|
528
|
+
currentHeaders: ""
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async function processHeaders(state) {
|
|
532
|
+
const headerEnd = state.buffer.indexOf("\r\n\r\n");
|
|
533
|
+
if (headerEnd === -1) return state;
|
|
534
|
+
const headers = state.buffer.subarray(0, headerEnd).toString("utf8");
|
|
535
|
+
const buffer = state.buffer.subarray(headerEnd + 4);
|
|
536
|
+
const disposition = parseContentDisposition(headers);
|
|
537
|
+
if (!disposition) {
|
|
538
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
539
|
+
throw new ValidationError2("Missing or invalid Content-Disposition header");
|
|
540
|
+
}
|
|
541
|
+
const mimetype = parseContentType(headers);
|
|
542
|
+
const isFile = disposition.filename !== void 0;
|
|
543
|
+
if (isFile && state.fileCount >= state.options.maxFiles) {
|
|
544
|
+
const { PayloadTooLargeError } = await import("./payload-too-large-error-PAYLDBZT.js");
|
|
545
|
+
throw new PayloadTooLargeError("Too many files in upload", {
|
|
546
|
+
fileCount: state.fileCount + 1,
|
|
547
|
+
maxFiles: state.options.maxFiles,
|
|
548
|
+
filename: disposition.filename
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (isFile && state.options.allowedMimeTypes.length > 0 && !state.options.allowedMimeTypes.includes(mimetype)) {
|
|
552
|
+
const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-MQZD7YQJ.js");
|
|
553
|
+
throw new UnsupportedMediaTypeError("File type not allowed", {
|
|
554
|
+
receivedMimeType: mimetype,
|
|
555
|
+
allowedMimeTypes: state.options.allowedMimeTypes,
|
|
556
|
+
filename: disposition.filename
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
...state,
|
|
561
|
+
buffer,
|
|
562
|
+
stage: "content",
|
|
563
|
+
currentHeaders: headers,
|
|
564
|
+
currentField: disposition.name,
|
|
565
|
+
currentFilename: disposition.filename,
|
|
566
|
+
currentMimetype: mimetype,
|
|
567
|
+
currentContentLength: 0,
|
|
568
|
+
fileCount: isFile ? state.fileCount + 1 : state.fileCount,
|
|
569
|
+
fieldCount: isFile ? state.fieldCount : state.fieldCount + 1,
|
|
570
|
+
currentBufferChunks: []
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function processContent(state) {
|
|
574
|
+
const nextBoundaryIndex = state.buffer.indexOf(state.boundary);
|
|
575
|
+
let contentChunk;
|
|
576
|
+
let isComplete = false;
|
|
577
|
+
let buffer = state.buffer;
|
|
578
|
+
if (nextBoundaryIndex === -1) {
|
|
579
|
+
const safeLength = Math.max(0, state.buffer.length - state.boundary.length);
|
|
580
|
+
if (safeLength === 0) return state;
|
|
581
|
+
contentChunk = state.buffer.subarray(0, safeLength);
|
|
582
|
+
buffer = state.buffer.subarray(safeLength);
|
|
583
|
+
} else {
|
|
584
|
+
const contentEnd = Math.max(0, nextBoundaryIndex - 2);
|
|
585
|
+
contentChunk = state.buffer.subarray(0, contentEnd);
|
|
586
|
+
buffer = state.buffer.subarray(nextBoundaryIndex);
|
|
587
|
+
isComplete = true;
|
|
588
|
+
}
|
|
589
|
+
let updatedState = { ...state, buffer };
|
|
590
|
+
if (contentChunk.length > 0) {
|
|
591
|
+
updatedState = await processContentChunk(updatedState, contentChunk);
|
|
592
|
+
}
|
|
593
|
+
if (isComplete) {
|
|
594
|
+
updatedState = await finalizeCurrentPart(updatedState);
|
|
595
|
+
updatedState = {
|
|
596
|
+
...updatedState,
|
|
597
|
+
stage: "boundary",
|
|
598
|
+
hasProcessedAnyPart: true
|
|
599
|
+
// Mark that we've processed at least one part
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return updatedState;
|
|
603
|
+
}
|
|
604
|
+
async function processContentChunk(state, chunk) {
|
|
605
|
+
const newContentLength = state.currentContentLength + chunk.length;
|
|
606
|
+
const maxSize = state.currentFilename !== void 0 ? state.options.maxFileSize : state.options.maxFieldSize;
|
|
607
|
+
if (newContentLength > maxSize) {
|
|
608
|
+
const isFile = state.currentFilename !== void 0;
|
|
609
|
+
const { PayloadTooLargeError } = await import("./payload-too-large-error-PAYLDBZT.js");
|
|
610
|
+
const payloadErrorDetals = state.currentField ? {
|
|
611
|
+
contentType: isFile ? "file" : "field",
|
|
612
|
+
currentSize: newContentLength,
|
|
613
|
+
maxSize,
|
|
614
|
+
field: state.currentField,
|
|
615
|
+
filename: state.currentFilename
|
|
616
|
+
} : {
|
|
617
|
+
contentType: isFile ? "file" : "field",
|
|
618
|
+
currentSize: newContentLength,
|
|
619
|
+
maxSize,
|
|
620
|
+
filename: state.currentFilename
|
|
621
|
+
};
|
|
622
|
+
throw new PayloadTooLargeError(
|
|
623
|
+
`${isFile ? "File" : "Field"} size exceeds limit`,
|
|
624
|
+
payloadErrorDetals
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (state.currentFilename !== void 0) {
|
|
628
|
+
return processFileChunk(state, chunk, newContentLength);
|
|
629
|
+
} else {
|
|
630
|
+
return {
|
|
631
|
+
...state,
|
|
632
|
+
currentContentLength: newContentLength,
|
|
633
|
+
currentBufferChunks: [...state.currentBufferChunks, chunk]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function processFileChunk(state, chunk, newContentLength) {
|
|
638
|
+
switch (state.options.strategy) {
|
|
639
|
+
case "memory":
|
|
640
|
+
return {
|
|
641
|
+
...state,
|
|
642
|
+
currentContentLength: newContentLength,
|
|
643
|
+
currentBufferChunks: [...state.currentBufferChunks, chunk]
|
|
644
|
+
};
|
|
645
|
+
case "stream":
|
|
646
|
+
if (state.streamController) {
|
|
647
|
+
state.streamController.enqueue(chunk);
|
|
648
|
+
}
|
|
649
|
+
return { ...state, currentContentLength: newContentLength };
|
|
650
|
+
case "temp":
|
|
651
|
+
if (state.currentWriteStream) {
|
|
652
|
+
await writeToStream(state.currentWriteStream, chunk);
|
|
653
|
+
}
|
|
654
|
+
return { ...state, currentContentLength: newContentLength };
|
|
655
|
+
default: {
|
|
656
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
657
|
+
throw new ValidationError2(`Invalid parsing strategy`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function initializeFileProcessing(state) {
|
|
662
|
+
if (state.currentFilename === void 0) return state;
|
|
663
|
+
switch (state.options.strategy) {
|
|
664
|
+
case "memory":
|
|
665
|
+
return { ...state, currentBufferChunks: [] };
|
|
666
|
+
case "stream": {
|
|
667
|
+
let streamController = null;
|
|
668
|
+
const stream = new ReadableStream({
|
|
669
|
+
start: (controller) => {
|
|
670
|
+
streamController = controller;
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
return {
|
|
674
|
+
...state,
|
|
675
|
+
currentStream: stream,
|
|
676
|
+
// Type cast for Node.js compatibility
|
|
677
|
+
streamController
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
case "temp": {
|
|
681
|
+
const tempPath = join2(state.options.tempDir, `upload-${randomUUID()}`);
|
|
682
|
+
const writeStream = createWriteStream(tempPath);
|
|
683
|
+
const cleanupTask = async () => {
|
|
684
|
+
try {
|
|
685
|
+
const { unlink } = await import("node:fs/promises");
|
|
686
|
+
await unlink(tempPath);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
console.warn(`Failed to cleanup temp file: ${tempPath}`, error);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
return {
|
|
692
|
+
...state,
|
|
693
|
+
currentTempPath: tempPath,
|
|
694
|
+
currentWriteStream: writeStream,
|
|
695
|
+
cleanupTasks: [...state.cleanupTasks, cleanupTask]
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
default: {
|
|
699
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
700
|
+
throw new ValidationError2(`Invalid file processing strategy`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async function finalizeCurrentPart(state) {
|
|
705
|
+
if (!state.currentField) return resetCurrentPart(state);
|
|
706
|
+
if (state.currentFilename !== void 0) {
|
|
707
|
+
return finalizeFile(state);
|
|
708
|
+
} else {
|
|
709
|
+
return finalizeField(state);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function finalizeFile(state) {
|
|
713
|
+
if (!state.currentField || state.currentFilename === void 0) {
|
|
714
|
+
return resetCurrentPart(state);
|
|
715
|
+
}
|
|
716
|
+
let stream;
|
|
717
|
+
let buffer;
|
|
718
|
+
let tempPath;
|
|
719
|
+
switch (state.options.strategy) {
|
|
720
|
+
case "memory":
|
|
721
|
+
buffer = Buffer.concat(state.currentBufferChunks);
|
|
722
|
+
stream = Readable.from(buffer);
|
|
723
|
+
break;
|
|
724
|
+
case "stream":
|
|
725
|
+
if (state.streamController) {
|
|
726
|
+
state.streamController.close();
|
|
727
|
+
}
|
|
728
|
+
stream = state.currentStream;
|
|
729
|
+
break;
|
|
730
|
+
case "temp":
|
|
731
|
+
if (state.currentWriteStream) {
|
|
732
|
+
await closeStream(state.currentWriteStream);
|
|
733
|
+
}
|
|
734
|
+
tempPath = state.currentTempPath;
|
|
735
|
+
stream = Readable.from(Buffer.alloc(0));
|
|
736
|
+
break;
|
|
737
|
+
default: {
|
|
738
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
739
|
+
throw new ValidationError2(`Invalid file finalization strategy`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const file = {
|
|
743
|
+
filename: state.currentFilename,
|
|
744
|
+
fieldname: state.currentField,
|
|
745
|
+
mimetype: state.currentMimetype,
|
|
746
|
+
size: state.currentContentLength,
|
|
747
|
+
stream,
|
|
748
|
+
buffer,
|
|
749
|
+
tempPath
|
|
750
|
+
};
|
|
751
|
+
const updatedFiles = addToCollection(state.files, state.currentField, file);
|
|
752
|
+
return {
|
|
753
|
+
...resetCurrentPart(state),
|
|
754
|
+
files: updatedFiles
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function finalizeField(state) {
|
|
758
|
+
if (!state.currentField) return resetCurrentPart(state);
|
|
759
|
+
const value = Buffer.concat(state.currentBufferChunks).toString("utf8");
|
|
760
|
+
const updatedFields = addToCollection(state.fields, state.currentField, value);
|
|
761
|
+
return {
|
|
762
|
+
...resetCurrentPart(state),
|
|
763
|
+
fields: updatedFields
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function resetCurrentPart(state) {
|
|
767
|
+
return {
|
|
768
|
+
...state,
|
|
769
|
+
currentField: null,
|
|
770
|
+
currentFilename: void 0,
|
|
771
|
+
currentContentLength: 0,
|
|
772
|
+
currentBufferChunks: [],
|
|
773
|
+
currentStream: null,
|
|
774
|
+
streamController: null,
|
|
775
|
+
currentTempPath: null,
|
|
776
|
+
currentWriteStream: null
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function addToCollection(collection, key, value) {
|
|
780
|
+
const newCollection = new Map(collection);
|
|
781
|
+
const existing = newCollection.get(key) || [];
|
|
782
|
+
newCollection.set(key, [...existing, value]);
|
|
783
|
+
return newCollection;
|
|
784
|
+
}
|
|
785
|
+
async function finalize(state) {
|
|
786
|
+
if (!state.hasFoundValidBoundary) {
|
|
787
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
788
|
+
throw new ValidationError2("No valid multipart boundary found");
|
|
789
|
+
}
|
|
790
|
+
if (state.hasFoundValidBoundary && !state.hasProcessedAnyPart) {
|
|
791
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
792
|
+
throw new ValidationError2("Empty multipart request");
|
|
793
|
+
}
|
|
794
|
+
const fields = {};
|
|
795
|
+
for (const [key, values] of state.fields.entries()) {
|
|
796
|
+
fields[key] = values.length === 1 ? values[0] : values;
|
|
797
|
+
}
|
|
798
|
+
const files = {};
|
|
799
|
+
for (const [key, fileList] of state.files.entries()) {
|
|
800
|
+
files[key] = fileList.length === 1 ? fileList[0] : fileList;
|
|
801
|
+
}
|
|
802
|
+
return { fields, files };
|
|
803
|
+
}
|
|
804
|
+
async function cleanup(state) {
|
|
805
|
+
await Promise.allSettled(state.cleanupTasks.map((task) => task()));
|
|
806
|
+
if (state.streamController) {
|
|
807
|
+
state.streamController.close();
|
|
808
|
+
}
|
|
809
|
+
if (state.currentWriteStream) {
|
|
810
|
+
await closeStream(state.currentWriteStream);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function writeToStream(stream, chunk) {
|
|
814
|
+
return new Promise((resolve3, reject) => {
|
|
815
|
+
stream.write(chunk, (error) => {
|
|
816
|
+
if (error) reject(error);
|
|
817
|
+
else resolve3();
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
async function closeStream(stream) {
|
|
822
|
+
return new Promise((resolve3) => {
|
|
823
|
+
stream.end(() => resolve3());
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
async function parseMultipartRequest(request, options = {}) {
|
|
827
|
+
const contentType = request.headers["content-type"] || "";
|
|
828
|
+
const boundary = extractBoundary(contentType);
|
|
829
|
+
if (!boundary) {
|
|
830
|
+
const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-MQZD7YQJ.js");
|
|
831
|
+
throw new UnsupportedMediaTypeError("Missing boundary in multipart content-type", {
|
|
832
|
+
receivedContentType: contentType,
|
|
833
|
+
expectedFormat: "multipart/form-data; boundary=..."
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
let state = createParserState(boundary, options);
|
|
837
|
+
if (state.currentFilename !== void 0) {
|
|
838
|
+
state = await initializeFileProcessing(state);
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
for await (const chunk of request) {
|
|
842
|
+
state = await processChunk(state, chunk);
|
|
843
|
+
}
|
|
844
|
+
return finalize(state);
|
|
845
|
+
} finally {
|
|
846
|
+
await cleanup(state);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
383
850
|
// src/context/create.ts
|
|
384
851
|
var CONTENT_TYPE_HEADER = "Content-Type";
|
|
852
|
+
var DEFAULT_BODY_LIMITS = {
|
|
853
|
+
json: 512 * 1024,
|
|
854
|
+
// 512KB - Most APIs should be much smaller
|
|
855
|
+
form: 1024 * 1024,
|
|
856
|
+
// 1MB - Reasonable for form submissions
|
|
857
|
+
text: 5 * 1024 * 1024,
|
|
858
|
+
// 5MB - Documents, logs, code files
|
|
859
|
+
multipart: {
|
|
860
|
+
maxFileSize: 50 * 1024 * 1024,
|
|
861
|
+
// 50MB per file
|
|
862
|
+
maxTotalSize: 100 * 1024 * 1024,
|
|
863
|
+
// 100MB total request
|
|
864
|
+
maxFiles: 10,
|
|
865
|
+
maxFieldSize: 1024 * 1024
|
|
866
|
+
// 1MB for form fields
|
|
867
|
+
},
|
|
868
|
+
raw: 10 * 1024 * 1024
|
|
869
|
+
// 10MB for unknown content types
|
|
870
|
+
};
|
|
385
871
|
function parseRequestUrl(req) {
|
|
386
872
|
const originalUrl = req.url || "/";
|
|
387
873
|
const host = req.headers.host || "localhost";
|
|
@@ -389,7 +875,7 @@ function parseRequestUrl(req) {
|
|
|
389
875
|
const fullUrl = `${protocol}://${host}${originalUrl.startsWith("/") ? "" : "/"}${originalUrl}`;
|
|
390
876
|
try {
|
|
391
877
|
const url = new URL(fullUrl);
|
|
392
|
-
const
|
|
878
|
+
const path6 = url.pathname;
|
|
393
879
|
const query = {};
|
|
394
880
|
url.searchParams.forEach((value, key) => {
|
|
395
881
|
if (query[key] !== void 0) {
|
|
@@ -402,7 +888,7 @@ function parseRequestUrl(req) {
|
|
|
402
888
|
query[key] = value;
|
|
403
889
|
}
|
|
404
890
|
});
|
|
405
|
-
return { path:
|
|
891
|
+
return { path: path6, url, query };
|
|
406
892
|
} catch (error) {
|
|
407
893
|
console.warn(`Invalid URL: ${fullUrl}`, error);
|
|
408
894
|
throw new ParseUrlError(`Invalid URL: ${fullUrl}`);
|
|
@@ -424,7 +910,7 @@ function getProtocol(req) {
|
|
|
424
910
|
return encrypted ? "https" : "http";
|
|
425
911
|
}
|
|
426
912
|
async function createContext(req, res, options = {}) {
|
|
427
|
-
const { path:
|
|
913
|
+
const { path: path6, url, query } = parseRequestUrl(req);
|
|
428
914
|
const method = req.method || "GET";
|
|
429
915
|
const isHttp2 = isHttp2Request(req);
|
|
430
916
|
const protocol = getProtocol(req);
|
|
@@ -433,7 +919,7 @@ async function createContext(req, res, options = {}) {
|
|
|
433
919
|
const responseState = { sent: false };
|
|
434
920
|
const ctx = {
|
|
435
921
|
request: createRequestObject(req, {
|
|
436
|
-
path:
|
|
922
|
+
path: path6,
|
|
437
923
|
url,
|
|
438
924
|
query,
|
|
439
925
|
params,
|
|
@@ -446,7 +932,7 @@ async function createContext(req, res, options = {}) {
|
|
|
446
932
|
};
|
|
447
933
|
ctx.response = createResponseObject(res, responseState, ctx);
|
|
448
934
|
if (options.parseBody) {
|
|
449
|
-
await parseBodyIfNeeded(req, ctx);
|
|
935
|
+
await parseBodyIfNeeded(req, ctx, options);
|
|
450
936
|
}
|
|
451
937
|
return ctx;
|
|
452
938
|
}
|
|
@@ -622,34 +1108,60 @@ function createStreamResponder(res, responseState) {
|
|
|
622
1108
|
});
|
|
623
1109
|
};
|
|
624
1110
|
}
|
|
625
|
-
async function parseBodyIfNeeded(req, ctx) {
|
|
1111
|
+
async function parseBodyIfNeeded(req, ctx, options = {}) {
|
|
626
1112
|
if (shouldSkipParsing(req.method)) {
|
|
627
1113
|
return;
|
|
628
1114
|
}
|
|
629
1115
|
const contentType = req.headers["content-type"] || "";
|
|
630
1116
|
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
|
631
|
-
if (contentLength === 0
|
|
1117
|
+
if (contentLength === 0) {
|
|
632
1118
|
return;
|
|
633
1119
|
}
|
|
1120
|
+
const limits = {
|
|
1121
|
+
json: options.bodyLimits?.json ?? DEFAULT_BODY_LIMITS.json,
|
|
1122
|
+
form: options.bodyLimits?.form ?? DEFAULT_BODY_LIMITS.form,
|
|
1123
|
+
text: options.bodyLimits?.text ?? DEFAULT_BODY_LIMITS.text,
|
|
1124
|
+
raw: options.bodyLimits?.raw ?? DEFAULT_BODY_LIMITS.raw,
|
|
1125
|
+
multipart: {
|
|
1126
|
+
maxFileSize: options.bodyLimits?.multipart?.maxFileSize ?? DEFAULT_BODY_LIMITS.multipart.maxFileSize,
|
|
1127
|
+
maxFiles: options.bodyLimits?.multipart?.maxFiles ?? DEFAULT_BODY_LIMITS.multipart.maxFiles,
|
|
1128
|
+
maxFieldSize: options.bodyLimits?.multipart?.maxFieldSize ?? DEFAULT_BODY_LIMITS.multipart.maxFieldSize,
|
|
1129
|
+
maxTotalSize: options.bodyLimits?.multipart?.maxTotalSize ?? DEFAULT_BODY_LIMITS.multipart.maxTotalSize
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
634
1132
|
try {
|
|
635
|
-
|
|
1133
|
+
if (contentType.includes("application/json")) {
|
|
1134
|
+
if (contentLength > limits.json) {
|
|
1135
|
+
throw new Error(`JSON body too large: ${contentLength} > ${limits.json} bytes`);
|
|
1136
|
+
}
|
|
1137
|
+
await parseJsonBody(req, ctx);
|
|
1138
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1139
|
+
if (contentLength > limits.form) {
|
|
1140
|
+
throw new Error(`Form body too large: ${contentLength} > ${limits.form} bytes`);
|
|
1141
|
+
}
|
|
1142
|
+
await parseFormUrlEncodedBody(req, ctx);
|
|
1143
|
+
} else if (contentType.includes("text/")) {
|
|
1144
|
+
if (contentLength > limits.text) {
|
|
1145
|
+
throw new Error(`Text body too large: ${contentLength} > ${limits.text} bytes`);
|
|
1146
|
+
}
|
|
1147
|
+
await parseTextBody(req, ctx);
|
|
1148
|
+
} else if (isMultipartContent(contentType)) {
|
|
1149
|
+
await parseMultipartBody(req, ctx, limits.multipart);
|
|
1150
|
+
} else {
|
|
1151
|
+
if (contentLength > limits.raw) {
|
|
1152
|
+
throw new Error(`Request body too large: ${contentLength} > ${limits.raw} bytes`);
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
636
1156
|
} catch (error) {
|
|
637
|
-
|
|
1157
|
+
const errorType = contentType.includes("multipart") ? "multipart_parse_error" : "body_read_error";
|
|
1158
|
+
setBodyError(ctx, errorType, "Error reading request body", error);
|
|
638
1159
|
}
|
|
639
1160
|
}
|
|
640
1161
|
function shouldSkipParsing(method) {
|
|
641
1162
|
const skipMethods = ["GET", "HEAD", "OPTIONS"];
|
|
642
1163
|
return skipMethods.includes(method || "GET");
|
|
643
1164
|
}
|
|
644
|
-
async function parseBodyByContentType(req, ctx, contentType) {
|
|
645
|
-
if (contentType.includes("application/json")) {
|
|
646
|
-
await parseJsonBody(req, ctx);
|
|
647
|
-
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
648
|
-
await parseFormUrlEncodedBody(req, ctx);
|
|
649
|
-
} else if (contentType.includes("text/")) {
|
|
650
|
-
await parseTextBody(req, ctx);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
1165
|
async function parseJsonBody(req, ctx) {
|
|
654
1166
|
const body = await readRequestBody(req);
|
|
655
1167
|
if (!body) {
|
|
@@ -701,17 +1213,36 @@ async function parseTextBody(req, ctx) {
|
|
|
701
1213
|
ctx.request.body = body;
|
|
702
1214
|
}
|
|
703
1215
|
}
|
|
1216
|
+
async function parseMultipartBody(req, ctx, multipartLimits) {
|
|
1217
|
+
try {
|
|
1218
|
+
const limits = multipartLimits || DEFAULT_BODY_LIMITS.multipart;
|
|
1219
|
+
const multipartData = await parseMultipartRequest(req, {
|
|
1220
|
+
strategy: "stream",
|
|
1221
|
+
maxFileSize: limits.maxFileSize,
|
|
1222
|
+
maxFiles: limits.maxFiles,
|
|
1223
|
+
maxFieldSize: limits.maxFieldSize
|
|
1224
|
+
// Could add total size validation here
|
|
1225
|
+
});
|
|
1226
|
+
ctx.request.multipart = multipartData;
|
|
1227
|
+
ctx.request.files = multipartData.files;
|
|
1228
|
+
ctx.request.body = multipartData.fields;
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
ctx.request.body = null;
|
|
1231
|
+
setBodyError(ctx, "multipart_parse_error", "Failed to parse multipart data", error);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
704
1234
|
function setBodyError(ctx, type, message, error) {
|
|
705
|
-
|
|
1235
|
+
const bodyError = { type, message, error };
|
|
1236
|
+
ctx.state._bodyError = bodyError;
|
|
706
1237
|
}
|
|
707
1238
|
async function readRequestBody(req) {
|
|
708
|
-
return new Promise((
|
|
1239
|
+
return new Promise((resolve3, reject) => {
|
|
709
1240
|
const chunks = [];
|
|
710
1241
|
req.on("data", (chunk) => {
|
|
711
1242
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
712
1243
|
});
|
|
713
1244
|
req.on("end", () => {
|
|
714
|
-
|
|
1245
|
+
resolve3(Buffer.concat(chunks).toString("utf8"));
|
|
715
1246
|
});
|
|
716
1247
|
req.on("error", (err) => {
|
|
717
1248
|
reject(err);
|
|
@@ -719,6 +1250,102 @@ async function readRequestBody(req) {
|
|
|
719
1250
|
});
|
|
720
1251
|
}
|
|
721
1252
|
|
|
1253
|
+
// src/errors/not-found-error.ts
|
|
1254
|
+
var NotFoundError = class extends BlaizeError {
|
|
1255
|
+
/**
|
|
1256
|
+
* Creates a new NotFoundError instance
|
|
1257
|
+
*
|
|
1258
|
+
* @param title - Human-readable error message
|
|
1259
|
+
* @param details - Optional context about the missing resource
|
|
1260
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
1261
|
+
*/
|
|
1262
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
1263
|
+
super(
|
|
1264
|
+
"NOT_FOUND" /* NOT_FOUND */,
|
|
1265
|
+
title,
|
|
1266
|
+
404,
|
|
1267
|
+
// HTTP 404 Not Found
|
|
1268
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
1269
|
+
details
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// src/errors/boundary.ts
|
|
1275
|
+
function isHandledError(error) {
|
|
1276
|
+
return error instanceof BlaizeError;
|
|
1277
|
+
}
|
|
1278
|
+
function formatErrorResponse(error) {
|
|
1279
|
+
if (isHandledError(error)) {
|
|
1280
|
+
return {
|
|
1281
|
+
type: error.type,
|
|
1282
|
+
title: error.title,
|
|
1283
|
+
status: error.status,
|
|
1284
|
+
correlationId: error.correlationId,
|
|
1285
|
+
timestamp: error.timestamp.toISOString(),
|
|
1286
|
+
details: error.details
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const correlationId = generateCorrelationId();
|
|
1290
|
+
let originalMessage;
|
|
1291
|
+
if (error instanceof Error) {
|
|
1292
|
+
originalMessage = error.message;
|
|
1293
|
+
} else if (error === null || error === void 0) {
|
|
1294
|
+
originalMessage = "Unknown error occurred";
|
|
1295
|
+
} else {
|
|
1296
|
+
originalMessage = String(error);
|
|
1297
|
+
}
|
|
1298
|
+
const wrappedError = new InternalServerError(
|
|
1299
|
+
"Internal Server Error",
|
|
1300
|
+
{ originalMessage },
|
|
1301
|
+
correlationId
|
|
1302
|
+
);
|
|
1303
|
+
return {
|
|
1304
|
+
type: wrappedError.type,
|
|
1305
|
+
title: wrappedError.title,
|
|
1306
|
+
status: wrappedError.status,
|
|
1307
|
+
correlationId: wrappedError.correlationId,
|
|
1308
|
+
timestamp: wrappedError.timestamp.toISOString(),
|
|
1309
|
+
details: wrappedError.details
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function extractOrGenerateCorrelationId(headerGetter) {
|
|
1313
|
+
return headerGetter("x-correlation-id") ?? generateCorrelationId();
|
|
1314
|
+
}
|
|
1315
|
+
function setErrorResponseHeaders(headerSetter, correlationId) {
|
|
1316
|
+
headerSetter("x-correlation-id", correlationId);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/middleware/error-boundary.ts
|
|
1320
|
+
function createErrorBoundary(options = {}) {
|
|
1321
|
+
const { debug = false } = options;
|
|
1322
|
+
const middlewareFn = async (ctx, next) => {
|
|
1323
|
+
try {
|
|
1324
|
+
await next();
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (ctx.response.sent) {
|
|
1327
|
+
if (debug) {
|
|
1328
|
+
console.error("Error occurred after response was sent:", error);
|
|
1329
|
+
}
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (debug) {
|
|
1333
|
+
console.error("Error boundary caught error:", error);
|
|
1334
|
+
}
|
|
1335
|
+
const correlationId = extractOrGenerateCorrelationId(ctx.request.header);
|
|
1336
|
+
const errorResponse = formatErrorResponse(error);
|
|
1337
|
+
errorResponse.correlationId = correlationId;
|
|
1338
|
+
setErrorResponseHeaders(ctx.response.header, correlationId);
|
|
1339
|
+
ctx.response.status(errorResponse.status).json(errorResponse);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
return {
|
|
1343
|
+
name: "ErrorBoundary",
|
|
1344
|
+
execute: middlewareFn,
|
|
1345
|
+
debug
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
722
1349
|
// src/server/request-handler.ts
|
|
723
1350
|
function createRequestHandler(serverInstance) {
|
|
724
1351
|
return async (req, res) => {
|
|
@@ -727,32 +1354,20 @@ function createRequestHandler(serverInstance) {
|
|
|
727
1354
|
parseBody: true
|
|
728
1355
|
// Enable automatic body parsing
|
|
729
1356
|
});
|
|
730
|
-
const
|
|
1357
|
+
const errorBoundary = createErrorBoundary();
|
|
1358
|
+
const allMiddleware = [errorBoundary, ...serverInstance.middleware];
|
|
1359
|
+
const handler = compose(allMiddleware);
|
|
731
1360
|
await runWithContext(context, async () => {
|
|
732
|
-
|
|
733
|
-
|
|
1361
|
+
await handler(context, async () => {
|
|
1362
|
+
if (!context.response.sent) {
|
|
1363
|
+
await serverInstance.router.handleRequest(context);
|
|
734
1364
|
if (!context.response.sent) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
error: "Not Found",
|
|
739
|
-
message: `Route not found: ${context.request.method} ${context.request.path}`
|
|
740
|
-
});
|
|
741
|
-
}
|
|
1365
|
+
throw new NotFoundError(
|
|
1366
|
+
`Route not found: ${context.request.method} ${context.request.path}`
|
|
1367
|
+
);
|
|
742
1368
|
}
|
|
743
|
-
});
|
|
744
|
-
} catch (error) {
|
|
745
|
-
console.error("Error processing request:", error);
|
|
746
|
-
if (!context.response.sent) {
|
|
747
|
-
context.response.json(
|
|
748
|
-
{
|
|
749
|
-
error: "Internal Server Error",
|
|
750
|
-
message: process.env.NODE_ENV === "development" ? error || "Unknown error" : "An error occurred processing your request"
|
|
751
|
-
},
|
|
752
|
-
500
|
|
753
|
-
);
|
|
754
1369
|
}
|
|
755
|
-
}
|
|
1370
|
+
});
|
|
756
1371
|
});
|
|
757
1372
|
} catch (error) {
|
|
758
1373
|
console.error("Error creating context:", error);
|
|
@@ -809,7 +1424,7 @@ function createServerInstance(isHttp2, certOptions) {
|
|
|
809
1424
|
return http2.createSecureServer(http2ServerOptions);
|
|
810
1425
|
}
|
|
811
1426
|
function listenOnPort(server, port, host, isHttp2) {
|
|
812
|
-
return new Promise((
|
|
1427
|
+
return new Promise((resolve3, reject) => {
|
|
813
1428
|
server.listen(port, host, () => {
|
|
814
1429
|
const protocol = isHttp2 ? "https" : "http";
|
|
815
1430
|
const url = `${protocol}://${host}:${port}`;
|
|
@@ -826,7 +1441,7 @@ function listenOnPort(server, port, host, isHttp2) {
|
|
|
826
1441
|
|
|
827
1442
|
\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}\u{1F525}
|
|
828
1443
|
`);
|
|
829
|
-
|
|
1444
|
+
resolve3();
|
|
830
1445
|
});
|
|
831
1446
|
server.on("error", (err) => {
|
|
832
1447
|
console.error("Server error:", err);
|
|
@@ -870,55 +1485,128 @@ async function startServer(serverInstance, serverOptions) {
|
|
|
870
1485
|
}
|
|
871
1486
|
|
|
872
1487
|
// src/server/stop.ts
|
|
1488
|
+
var isShuttingDown = false;
|
|
873
1489
|
async function stopServer(serverInstance, options = {}) {
|
|
874
1490
|
const server = serverInstance.server;
|
|
875
1491
|
const events = serverInstance.events;
|
|
1492
|
+
if (isShuttingDown) {
|
|
1493
|
+
console.log("\u26A0\uFE0F Shutdown already in progress, ignoring duplicate shutdown request");
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
876
1496
|
if (!server) {
|
|
877
1497
|
return;
|
|
878
1498
|
}
|
|
879
|
-
|
|
1499
|
+
isShuttingDown = true;
|
|
1500
|
+
const timeout = options.timeout || 5e3;
|
|
880
1501
|
try {
|
|
881
1502
|
if (options.onStopping) {
|
|
882
1503
|
await options.onStopping();
|
|
883
1504
|
}
|
|
884
1505
|
events.emit("stopping");
|
|
885
|
-
|
|
1506
|
+
if (serverInstance.router && typeof serverInstance.router.close === "function") {
|
|
1507
|
+
console.log("\u{1F50C} Closing router watchers...");
|
|
1508
|
+
try {
|
|
1509
|
+
await Promise.race([
|
|
1510
|
+
serverInstance.router.close(),
|
|
1511
|
+
new Promise(
|
|
1512
|
+
(_, reject) => setTimeout(() => reject(new Error("Router close timeout")), 2e3)
|
|
1513
|
+
)
|
|
1514
|
+
]);
|
|
1515
|
+
console.log("\u2705 Router watchers closed");
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
console.error("\u274C Error closing router watchers:", error);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
try {
|
|
1521
|
+
await Promise.race([
|
|
1522
|
+
serverInstance.pluginManager.onServerStop(serverInstance, server),
|
|
1523
|
+
new Promise(
|
|
1524
|
+
(_, reject) => setTimeout(() => reject(new Error("Plugin stop timeout")), 2e3)
|
|
1525
|
+
)
|
|
1526
|
+
]);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
console.error("\u274C Plugin stop timeout:", error);
|
|
1529
|
+
}
|
|
1530
|
+
const closePromise = new Promise((resolve3, reject) => {
|
|
1531
|
+
server.close((err) => {
|
|
1532
|
+
if (err) return reject(err);
|
|
1533
|
+
resolve3();
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
886
1536
|
const timeoutPromise = new Promise((_, reject) => {
|
|
887
1537
|
setTimeout(() => {
|
|
888
|
-
reject(new Error("Server shutdown
|
|
1538
|
+
reject(new Error("Server shutdown timeout"));
|
|
889
1539
|
}, timeout);
|
|
890
1540
|
});
|
|
891
|
-
const closePromise = new Promise((resolve2, reject) => {
|
|
892
|
-
server.close((err) => {
|
|
893
|
-
if (err) {
|
|
894
|
-
return reject(err);
|
|
895
|
-
}
|
|
896
|
-
resolve2();
|
|
897
|
-
});
|
|
898
|
-
});
|
|
899
1541
|
await Promise.race([closePromise, timeoutPromise]);
|
|
900
|
-
|
|
1542
|
+
try {
|
|
1543
|
+
await Promise.race([
|
|
1544
|
+
serverInstance.pluginManager.terminatePlugins(serverInstance),
|
|
1545
|
+
new Promise(
|
|
1546
|
+
(_, reject) => setTimeout(() => reject(new Error("Plugin terminate timeout")), 1e3)
|
|
1547
|
+
)
|
|
1548
|
+
]);
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
console.error("\u274C Plugin terminate timeout:", error);
|
|
1551
|
+
}
|
|
901
1552
|
if (options.onStopped) {
|
|
902
1553
|
await options.onStopped();
|
|
903
1554
|
}
|
|
904
1555
|
events.emit("stopped");
|
|
905
1556
|
serverInstance.server = null;
|
|
1557
|
+
console.log("\u2705 Graceful shutdown completed");
|
|
1558
|
+
isShuttingDown = false;
|
|
906
1559
|
} catch (error) {
|
|
1560
|
+
isShuttingDown = false;
|
|
1561
|
+
console.error("\u26A0\uFE0F Shutdown error (forcing exit):", error);
|
|
1562
|
+
if (server && typeof server.close === "function") {
|
|
1563
|
+
server.close();
|
|
1564
|
+
}
|
|
1565
|
+
if (process.env.NODE_ENV === "development") {
|
|
1566
|
+
console.log("\u{1F504} Forcing exit for development restart...");
|
|
1567
|
+
process.exit(0);
|
|
1568
|
+
}
|
|
907
1569
|
events.emit("error", error);
|
|
908
1570
|
throw error;
|
|
909
1571
|
}
|
|
910
1572
|
}
|
|
911
1573
|
function registerSignalHandlers(stopFn) {
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1574
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
1575
|
+
if (isDevelopment) {
|
|
1576
|
+
const sigintHandler = () => {
|
|
1577
|
+
console.log("\u{1F4E4} SIGINT received, forcing exit for development restart...");
|
|
1578
|
+
process.exit(0);
|
|
1579
|
+
};
|
|
1580
|
+
const sigtermHandler = () => {
|
|
1581
|
+
console.log("\u{1F4E4} SIGTERM received, forcing exit for development restart...");
|
|
1582
|
+
process.exit(0);
|
|
1583
|
+
};
|
|
1584
|
+
process.on("SIGINT", sigintHandler);
|
|
1585
|
+
process.on("SIGTERM", sigtermHandler);
|
|
1586
|
+
return {
|
|
1587
|
+
unregister: () => {
|
|
1588
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1589
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
} else {
|
|
1593
|
+
const sigintHandler = () => {
|
|
1594
|
+
console.log("\u{1F4E4} SIGINT received, starting graceful shutdown...");
|
|
1595
|
+
stopFn().catch(console.error);
|
|
1596
|
+
};
|
|
1597
|
+
const sigtermHandler = () => {
|
|
1598
|
+
console.log("\u{1F4E4} SIGTERM received, starting graceful shutdown...");
|
|
1599
|
+
stopFn().catch(console.error);
|
|
1600
|
+
};
|
|
1601
|
+
process.on("SIGINT", sigintHandler);
|
|
1602
|
+
process.on("SIGTERM", sigtermHandler);
|
|
1603
|
+
return {
|
|
1604
|
+
unregister: () => {
|
|
1605
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1606
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
922
1610
|
}
|
|
923
1611
|
|
|
924
1612
|
// src/server/validation.ts
|
|
@@ -1124,59 +1812,33 @@ function validatePlugin(plugin, options = {}) {
|
|
|
1124
1812
|
}
|
|
1125
1813
|
}
|
|
1126
1814
|
|
|
1127
|
-
// src/router/discovery/
|
|
1815
|
+
// src/router/discovery/cache.ts
|
|
1816
|
+
import * as crypto from "node:crypto";
|
|
1128
1817
|
import * as fs3 from "node:fs/promises";
|
|
1818
|
+
import { createRequire } from "node:module";
|
|
1129
1819
|
import * as path3 from "node:path";
|
|
1130
|
-
async function findRouteFiles(routesDir, options = {}) {
|
|
1131
|
-
const absoluteDir = path3.isAbsolute(routesDir) ? routesDir : path3.resolve(process.cwd(), routesDir);
|
|
1132
|
-
console.log("Creating router with routes directory:", absoluteDir);
|
|
1133
|
-
try {
|
|
1134
|
-
const stats = await fs3.stat(absoluteDir);
|
|
1135
|
-
if (!stats.isDirectory()) {
|
|
1136
|
-
throw new Error(`Route directory is not a directory: ${absoluteDir}`);
|
|
1137
|
-
}
|
|
1138
|
-
} catch (error) {
|
|
1139
|
-
if (error.code === "ENOENT") {
|
|
1140
|
-
throw new Error(`Route directory not found: ${absoluteDir}`);
|
|
1141
|
-
}
|
|
1142
|
-
throw error;
|
|
1143
|
-
}
|
|
1144
|
-
const routeFiles = [];
|
|
1145
|
-
const ignore = options.ignore || ["node_modules", ".git"];
|
|
1146
|
-
async function scanDirectory(dir) {
|
|
1147
|
-
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1148
|
-
for (const entry of entries) {
|
|
1149
|
-
const fullPath = path3.join(dir, entry.name);
|
|
1150
|
-
if (entry.isDirectory() && ignore.includes(entry.name)) {
|
|
1151
|
-
continue;
|
|
1152
|
-
}
|
|
1153
|
-
if (entry.isDirectory()) {
|
|
1154
|
-
await scanDirectory(fullPath);
|
|
1155
|
-
} else if (isRouteFile(entry.name)) {
|
|
1156
|
-
routeFiles.push(fullPath);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
await scanDirectory(absoluteDir);
|
|
1161
|
-
return routeFiles;
|
|
1162
|
-
}
|
|
1163
|
-
function isRouteFile(filename) {
|
|
1164
|
-
return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js"));
|
|
1165
|
-
}
|
|
1166
1820
|
|
|
1167
1821
|
// src/router/discovery/loader.ts
|
|
1168
1822
|
async function dynamicImport(filePath) {
|
|
1169
|
-
|
|
1823
|
+
const cacheBuster = `?t=${Date.now()}`;
|
|
1824
|
+
const importPath = filePath + cacheBuster;
|
|
1825
|
+
try {
|
|
1826
|
+
const module = await import(importPath);
|
|
1827
|
+
console.log(`\u2705 Successfully imported module`);
|
|
1828
|
+
return module;
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1831
|
+
console.log(`\u26A0\uFE0F Error importing with cache buster, trying original path:`, errorMessage);
|
|
1832
|
+
return import(filePath);
|
|
1833
|
+
}
|
|
1170
1834
|
}
|
|
1171
1835
|
async function loadRouteModule(filePath, basePath) {
|
|
1172
1836
|
try {
|
|
1173
1837
|
const parsedRoute = parseRoutePath(filePath, basePath);
|
|
1174
|
-
console.log("parsedRoute:", parsedRoute);
|
|
1175
1838
|
const module = await dynamicImport(filePath);
|
|
1176
|
-
console.log("Module exports:", Object.keys(module));
|
|
1839
|
+
console.log("\u{1F4E6} Module exports:", Object.keys(module));
|
|
1177
1840
|
const routes = [];
|
|
1178
1841
|
if (module.default && typeof module.default === "object") {
|
|
1179
|
-
console.log("Found default export:", module.default);
|
|
1180
1842
|
const route = {
|
|
1181
1843
|
...module.default,
|
|
1182
1844
|
path: parsedRoute.routePath
|
|
@@ -1189,7 +1851,6 @@ async function loadRouteModule(filePath, basePath) {
|
|
|
1189
1851
|
}
|
|
1190
1852
|
const potentialRoute = exportValue;
|
|
1191
1853
|
if (isValidRoute(potentialRoute)) {
|
|
1192
|
-
console.log(`Found named route export: ${exportName}`, potentialRoute);
|
|
1193
1854
|
const route = {
|
|
1194
1855
|
...potentialRoute,
|
|
1195
1856
|
// Use the route's own path if it has one, otherwise derive from file
|
|
@@ -1202,7 +1863,7 @@ async function loadRouteModule(filePath, basePath) {
|
|
|
1202
1863
|
console.warn(`Route file ${filePath} does not export any valid route definitions`);
|
|
1203
1864
|
return [];
|
|
1204
1865
|
}
|
|
1205
|
-
console.log(
|
|
1866
|
+
console.log(`\u2705 Successfully Loaded ${routes.length} route(s)`);
|
|
1206
1867
|
return routes;
|
|
1207
1868
|
} catch (error) {
|
|
1208
1869
|
console.error(`Failed to load route module ${filePath}:`, error);
|
|
@@ -1220,25 +1881,222 @@ function isValidRoute(obj) {
|
|
|
1220
1881
|
return hasHttpMethod;
|
|
1221
1882
|
}
|
|
1222
1883
|
|
|
1223
|
-
// src/router/discovery/
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1884
|
+
// src/router/discovery/cache.ts
|
|
1885
|
+
var fileRouteCache = /* @__PURE__ */ new Map();
|
|
1886
|
+
async function processChangedFile(filePath, routesDir, updateCache = true) {
|
|
1887
|
+
const stat3 = await fs3.stat(filePath);
|
|
1888
|
+
const lastModified = stat3.mtime.getTime();
|
|
1889
|
+
const cachedEntry = fileRouteCache.get(filePath);
|
|
1890
|
+
if (updateCache && cachedEntry && cachedEntry.timestamp === lastModified) {
|
|
1891
|
+
return cachedEntry.routes;
|
|
1892
|
+
}
|
|
1893
|
+
invalidateModuleCache(filePath);
|
|
1894
|
+
const routes = await loadRouteModule(filePath, routesDir);
|
|
1895
|
+
if (updateCache) {
|
|
1896
|
+
const hash = hashRoutes(routes);
|
|
1897
|
+
fileRouteCache.set(filePath, {
|
|
1898
|
+
routes,
|
|
1899
|
+
timestamp: lastModified,
|
|
1900
|
+
hash
|
|
1901
|
+
});
|
|
1234
1902
|
}
|
|
1235
1903
|
return routes;
|
|
1236
1904
|
}
|
|
1905
|
+
function hasRouteContentChanged(filePath, newRoutes) {
|
|
1906
|
+
const cachedEntry = fileRouteCache.get(filePath);
|
|
1907
|
+
if (!cachedEntry) {
|
|
1908
|
+
return true;
|
|
1909
|
+
}
|
|
1910
|
+
const newHash = hashRoutes(newRoutes);
|
|
1911
|
+
return cachedEntry.hash !== newHash;
|
|
1912
|
+
}
|
|
1913
|
+
function clearFileCache(filePath) {
|
|
1914
|
+
if (filePath) {
|
|
1915
|
+
fileRouteCache.delete(filePath);
|
|
1916
|
+
} else {
|
|
1917
|
+
fileRouteCache.clear();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function hashRoutes(routes) {
|
|
1921
|
+
const routeData = routes.map((route) => ({
|
|
1922
|
+
path: route.path,
|
|
1923
|
+
methods: Object.keys(route).filter((key) => key !== "path").sort().map((method) => {
|
|
1924
|
+
const methodDef = route[method];
|
|
1925
|
+
const handlerString = methodDef?.handler ? methodDef.handler.toString() : null;
|
|
1926
|
+
return {
|
|
1927
|
+
method,
|
|
1928
|
+
// Include handler function string for change detection
|
|
1929
|
+
handler: handlerString,
|
|
1930
|
+
// Include middleware if present
|
|
1931
|
+
middleware: methodDef?.middleware ? methodDef.middleware.length : 0,
|
|
1932
|
+
// Include schema structure (but not full serialization which can be unstable)
|
|
1933
|
+
hasSchema: !!methodDef?.schema,
|
|
1934
|
+
schemaKeys: methodDef?.schema ? Object.keys(methodDef.schema).sort() : []
|
|
1935
|
+
};
|
|
1936
|
+
})
|
|
1937
|
+
}));
|
|
1938
|
+
const dataString = JSON.stringify(routeData);
|
|
1939
|
+
const hash = crypto.createHash("md5").update(dataString).digest("hex");
|
|
1940
|
+
return hash;
|
|
1941
|
+
}
|
|
1942
|
+
function invalidateModuleCache(filePath) {
|
|
1943
|
+
try {
|
|
1944
|
+
const absolutePath = path3.resolve(filePath);
|
|
1945
|
+
if (typeof __require !== "undefined") {
|
|
1946
|
+
delete __require.cache[absolutePath];
|
|
1947
|
+
try {
|
|
1948
|
+
const resolvedPath = __require.resolve(absolutePath);
|
|
1949
|
+
delete __require.cache[resolvedPath];
|
|
1950
|
+
} catch (resolveError) {
|
|
1951
|
+
const errorMessage = resolveError instanceof Error ? resolveError.message : String(resolveError);
|
|
1952
|
+
console.log(`\u26A0\uFE0F Could not resolve path: ${errorMessage}`);
|
|
1953
|
+
}
|
|
1954
|
+
} else {
|
|
1955
|
+
try {
|
|
1956
|
+
const require2 = createRequire(import.meta.url);
|
|
1957
|
+
delete require2.cache[absolutePath];
|
|
1958
|
+
try {
|
|
1959
|
+
const resolvedPath = require2.resolve(absolutePath);
|
|
1960
|
+
delete require2.cache[resolvedPath];
|
|
1961
|
+
} catch {
|
|
1962
|
+
console.log(`\u26A0\uFE0F Could not resolve ESM path`);
|
|
1963
|
+
}
|
|
1964
|
+
} catch {
|
|
1965
|
+
console.log(`\u26A0\uFE0F createRequire not available in pure ESM`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
console.log(`\u26A0\uFE0F Error during module cache invalidation for ${filePath}:`, error);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1237
1972
|
|
|
1238
|
-
// src/router/discovery/
|
|
1973
|
+
// src/router/discovery/parallel.ts
|
|
1974
|
+
import * as os from "node:os";
|
|
1975
|
+
|
|
1976
|
+
// src/router/discovery/finder.ts
|
|
1977
|
+
import * as fs4 from "node:fs/promises";
|
|
1239
1978
|
import * as path4 from "node:path";
|
|
1979
|
+
async function findRouteFiles(routesDir, options = {}) {
|
|
1980
|
+
const absoluteDir = path4.isAbsolute(routesDir) ? routesDir : path4.resolve(process.cwd(), routesDir);
|
|
1981
|
+
console.log("Creating router with routes directory:", absoluteDir);
|
|
1982
|
+
try {
|
|
1983
|
+
const stats = await fs4.stat(absoluteDir);
|
|
1984
|
+
if (!stats.isDirectory()) {
|
|
1985
|
+
throw new Error(`Route directory is not a directory: ${absoluteDir}`);
|
|
1986
|
+
}
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
if (error.code === "ENOENT") {
|
|
1989
|
+
throw new Error(`Route directory not found: ${absoluteDir}`);
|
|
1990
|
+
}
|
|
1991
|
+
throw error;
|
|
1992
|
+
}
|
|
1993
|
+
const routeFiles = [];
|
|
1994
|
+
const ignore = options.ignore || ["node_modules", ".git"];
|
|
1995
|
+
async function scanDirectory(dir) {
|
|
1996
|
+
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1997
|
+
for (const entry of entries) {
|
|
1998
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1999
|
+
if (entry.isDirectory() && ignore.includes(entry.name)) {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
if (entry.isDirectory()) {
|
|
2003
|
+
await scanDirectory(fullPath);
|
|
2004
|
+
} else if (isRouteFile(entry.name)) {
|
|
2005
|
+
routeFiles.push(fullPath);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
await scanDirectory(absoluteDir);
|
|
2010
|
+
return routeFiles;
|
|
2011
|
+
}
|
|
2012
|
+
function isRouteFile(filename) {
|
|
2013
|
+
return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js"));
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// src/router/discovery/parallel.ts
|
|
2017
|
+
async function processFilesInParallel(filePaths, processor, concurrency = Math.max(1, Math.floor(os.cpus().length / 2))) {
|
|
2018
|
+
const chunks = chunkArray(filePaths, concurrency);
|
|
2019
|
+
const results = [];
|
|
2020
|
+
for (const chunk of chunks) {
|
|
2021
|
+
const chunkResults = await Promise.allSettled(chunk.map((filePath) => processor(filePath)));
|
|
2022
|
+
const successfulResults = chunkResults.filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
2023
|
+
results.push(...successfulResults);
|
|
2024
|
+
}
|
|
2025
|
+
return results;
|
|
2026
|
+
}
|
|
2027
|
+
async function loadInitialRoutesParallel(routesDir) {
|
|
2028
|
+
const files = await findRouteFiles(routesDir);
|
|
2029
|
+
const routeArrays = await processFilesInParallel(
|
|
2030
|
+
files,
|
|
2031
|
+
(filePath) => processChangedFile(filePath, routesDir)
|
|
2032
|
+
);
|
|
2033
|
+
return routeArrays.flat();
|
|
2034
|
+
}
|
|
2035
|
+
function chunkArray(array, chunkSize) {
|
|
2036
|
+
const chunks = [];
|
|
2037
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
2038
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
2039
|
+
}
|
|
2040
|
+
return chunks;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/router/discovery/profiler.ts
|
|
2044
|
+
var profilerState = {
|
|
2045
|
+
fileChanges: 0,
|
|
2046
|
+
totalReloadTime: 0,
|
|
2047
|
+
averageReloadTime: 0,
|
|
2048
|
+
slowReloads: []
|
|
2049
|
+
};
|
|
2050
|
+
function trackReloadPerformance(filePath, startTime) {
|
|
2051
|
+
const duration = Date.now() - startTime;
|
|
2052
|
+
profilerState.fileChanges++;
|
|
2053
|
+
profilerState.totalReloadTime += duration;
|
|
2054
|
+
profilerState.averageReloadTime = profilerState.totalReloadTime / profilerState.fileChanges;
|
|
2055
|
+
if (duration > 100) {
|
|
2056
|
+
profilerState.slowReloads.push({ file: filePath, time: duration });
|
|
2057
|
+
if (profilerState.slowReloads.length > 10) {
|
|
2058
|
+
profilerState.slowReloads.shift();
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (process.env.NODE_ENV === "development") {
|
|
2062
|
+
const emoji = duration < 50 ? "\u26A1" : duration < 100 ? "\u{1F504}" : "\u{1F40C}";
|
|
2063
|
+
console.log(`${emoji} Route reload: ${filePath} (${duration}ms)`);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
function withPerformanceTracking(fn, filePath) {
|
|
2067
|
+
console.log(`Tracking performance for: ${filePath}`);
|
|
2068
|
+
return async (...args) => {
|
|
2069
|
+
const startTime = Date.now();
|
|
2070
|
+
try {
|
|
2071
|
+
const result = await fn(...args);
|
|
2072
|
+
trackReloadPerformance(filePath, startTime);
|
|
2073
|
+
return result;
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
trackReloadPerformance(filePath, startTime);
|
|
2076
|
+
throw error;
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/router/discovery/watchers.ts
|
|
2082
|
+
import * as path5 from "node:path";
|
|
1240
2083
|
import { watch } from "chokidar";
|
|
1241
2084
|
function watchRoutes(routesDir, options = {}) {
|
|
2085
|
+
const debounceMs = options.debounceMs || 16;
|
|
2086
|
+
const debouncedCallbacks = /* @__PURE__ */ new Map();
|
|
2087
|
+
function createDebouncedCallback(fn, filePath) {
|
|
2088
|
+
return (...args) => {
|
|
2089
|
+
const existingTimeout = debouncedCallbacks.get(filePath);
|
|
2090
|
+
if (existingTimeout) {
|
|
2091
|
+
clearTimeout(existingTimeout);
|
|
2092
|
+
}
|
|
2093
|
+
const timeoutId = setTimeout(() => {
|
|
2094
|
+
fn(...args);
|
|
2095
|
+
debouncedCallbacks.delete(filePath);
|
|
2096
|
+
}, debounceMs);
|
|
2097
|
+
debouncedCallbacks.set(filePath, timeoutId);
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
1242
2100
|
const routesByPath = /* @__PURE__ */ new Map();
|
|
1243
2101
|
async function loadInitialRoutes() {
|
|
1244
2102
|
try {
|
|
@@ -1254,28 +2112,34 @@ function watchRoutes(routesDir, options = {}) {
|
|
|
1254
2112
|
}
|
|
1255
2113
|
async function loadAndNotify(filePath) {
|
|
1256
2114
|
try {
|
|
1257
|
-
const
|
|
1258
|
-
|
|
2115
|
+
const existingRoutes = routesByPath.get(filePath);
|
|
2116
|
+
const newRoutes = await processChangedFile(filePath, routesDir, false);
|
|
2117
|
+
if (!newRoutes || newRoutes.length === 0) {
|
|
1259
2118
|
return;
|
|
1260
2119
|
}
|
|
1261
|
-
|
|
2120
|
+
if (existingRoutes && !hasRouteContentChanged(filePath, newRoutes)) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
await processChangedFile(filePath, routesDir, true);
|
|
2124
|
+
const normalizedPath = path5.normalize(filePath);
|
|
1262
2125
|
if (existingRoutes) {
|
|
1263
|
-
routesByPath.set(filePath,
|
|
2126
|
+
routesByPath.set(filePath, newRoutes);
|
|
1264
2127
|
if (options.onRouteChanged) {
|
|
1265
|
-
options.onRouteChanged(
|
|
2128
|
+
options.onRouteChanged(normalizedPath, newRoutes);
|
|
1266
2129
|
}
|
|
1267
2130
|
} else {
|
|
1268
|
-
routesByPath.set(filePath,
|
|
2131
|
+
routesByPath.set(filePath, newRoutes);
|
|
1269
2132
|
if (options.onRouteAdded) {
|
|
1270
|
-
options.onRouteAdded(
|
|
2133
|
+
options.onRouteAdded(normalizedPath, newRoutes);
|
|
1271
2134
|
}
|
|
1272
2135
|
}
|
|
1273
2136
|
} catch (error) {
|
|
2137
|
+
console.log(`\u26A0\uFE0F Error processing file ${filePath}:`, error);
|
|
1274
2138
|
handleError(error);
|
|
1275
2139
|
}
|
|
1276
2140
|
}
|
|
1277
2141
|
function handleRemoved(filePath) {
|
|
1278
|
-
const normalizedPath =
|
|
2142
|
+
const normalizedPath = path5.normalize(filePath);
|
|
1279
2143
|
const routes = routesByPath.get(normalizedPath);
|
|
1280
2144
|
if (routes && routes.length > 0 && options.onRouteRemoved) {
|
|
1281
2145
|
options.onRouteRemoved(normalizedPath, routes);
|
|
@@ -1286,33 +2150,53 @@ function watchRoutes(routesDir, options = {}) {
|
|
|
1286
2150
|
if (options.onError && error instanceof Error) {
|
|
1287
2151
|
options.onError(error);
|
|
1288
2152
|
} else {
|
|
1289
|
-
console.error("Route watcher error:", error);
|
|
2153
|
+
console.error("\u26A0\uFE0F Route watcher error:", error);
|
|
1290
2154
|
}
|
|
1291
2155
|
}
|
|
1292
2156
|
const watcher = watch(routesDir, {
|
|
2157
|
+
// Much faster response times
|
|
2158
|
+
awaitWriteFinish: {
|
|
2159
|
+
stabilityThreshold: 50,
|
|
2160
|
+
// Reduced from 300ms
|
|
2161
|
+
pollInterval: 10
|
|
2162
|
+
// Reduced from 100ms
|
|
2163
|
+
},
|
|
2164
|
+
// Performance optimizations
|
|
2165
|
+
usePolling: false,
|
|
2166
|
+
atomic: true,
|
|
2167
|
+
followSymlinks: false,
|
|
2168
|
+
depth: 10,
|
|
2169
|
+
// More aggressive ignoring
|
|
1293
2170
|
ignored: [
|
|
1294
2171
|
/(^|[/\\])\../,
|
|
1295
|
-
// Ignore dot files
|
|
1296
2172
|
/node_modules/,
|
|
2173
|
+
/\.git/,
|
|
2174
|
+
/\.DS_Store/,
|
|
2175
|
+
/Thumbs\.db/,
|
|
2176
|
+
/\.(test|spec)\.(ts|js)$/,
|
|
2177
|
+
/\.d\.ts$/,
|
|
2178
|
+
/\.map$/,
|
|
2179
|
+
/~$/,
|
|
1297
2180
|
...options.ignore || []
|
|
1298
|
-
]
|
|
1299
|
-
persistent: true,
|
|
1300
|
-
ignoreInitial: false,
|
|
1301
|
-
awaitWriteFinish: {
|
|
1302
|
-
stabilityThreshold: 300,
|
|
1303
|
-
pollInterval: 100
|
|
1304
|
-
}
|
|
2181
|
+
]
|
|
1305
2182
|
});
|
|
1306
|
-
watcher.on("add",
|
|
2183
|
+
watcher.on("add", (filePath) => {
|
|
2184
|
+
const debouncedLoad = createDebouncedCallback(loadAndNotify, filePath);
|
|
2185
|
+
debouncedLoad(filePath);
|
|
2186
|
+
}).on("change", (filePath) => {
|
|
2187
|
+
const debouncedLoad = createDebouncedCallback(loadAndNotify, filePath);
|
|
2188
|
+
debouncedLoad(filePath);
|
|
2189
|
+
}).on("unlink", (filePath) => {
|
|
2190
|
+
const debouncedRemove = createDebouncedCallback(handleRemoved, filePath);
|
|
2191
|
+
debouncedRemove(filePath);
|
|
2192
|
+
}).on("error", handleError);
|
|
1307
2193
|
loadInitialRoutes().catch(handleError);
|
|
1308
2194
|
return {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
* Get all currently loaded routes (flattened)
|
|
1315
|
-
*/
|
|
2195
|
+
close: () => {
|
|
2196
|
+
debouncedCallbacks.forEach((timeout) => clearTimeout(timeout));
|
|
2197
|
+
debouncedCallbacks.clear();
|
|
2198
|
+
return watcher.close();
|
|
2199
|
+
},
|
|
1316
2200
|
getRoutes: () => {
|
|
1317
2201
|
const allRoutes = [];
|
|
1318
2202
|
for (const routes of routesByPath.values()) {
|
|
@@ -1320,88 +2204,12 @@ function watchRoutes(routesDir, options = {}) {
|
|
|
1320
2204
|
}
|
|
1321
2205
|
return allRoutes;
|
|
1322
2206
|
},
|
|
1323
|
-
/**
|
|
1324
|
-
* Get routes organized by file path
|
|
1325
|
-
*/
|
|
1326
2207
|
getRoutesByFile: () => new Map(routesByPath)
|
|
1327
2208
|
};
|
|
1328
2209
|
}
|
|
1329
2210
|
|
|
1330
|
-
// src/router/
|
|
1331
|
-
|
|
1332
|
-
if (options.log) {
|
|
1333
|
-
console.error("Route error:", error);
|
|
1334
|
-
}
|
|
1335
|
-
const status = getErrorStatus(error);
|
|
1336
|
-
const response = {
|
|
1337
|
-
error: getErrorType(error),
|
|
1338
|
-
message: getErrorMessage(error)
|
|
1339
|
-
};
|
|
1340
|
-
if (options.detailed) {
|
|
1341
|
-
if (error instanceof Error) {
|
|
1342
|
-
response.stack = error.stack;
|
|
1343
|
-
}
|
|
1344
|
-
if (error && typeof error === "object" && "details" in error && error.details) {
|
|
1345
|
-
response.details = error.details;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
ctx.response.status(status).json(response);
|
|
1349
|
-
}
|
|
1350
|
-
function getErrorStatus(error) {
|
|
1351
|
-
if (error && typeof error === "object") {
|
|
1352
|
-
if ("status" in error && typeof error.status === "number") {
|
|
1353
|
-
return error.status;
|
|
1354
|
-
}
|
|
1355
|
-
if ("statusCode" in error && typeof error.statusCode === "number") {
|
|
1356
|
-
return error.statusCode;
|
|
1357
|
-
}
|
|
1358
|
-
if ("code" in error && typeof error.code === "string") {
|
|
1359
|
-
return getStatusFromCode(error.code);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
return 500;
|
|
1363
|
-
}
|
|
1364
|
-
function getStatusFromCode(code) {
|
|
1365
|
-
switch (code) {
|
|
1366
|
-
case "NOT_FOUND":
|
|
1367
|
-
return 404;
|
|
1368
|
-
case "UNAUTHORIZED":
|
|
1369
|
-
return 401;
|
|
1370
|
-
case "FORBIDDEN":
|
|
1371
|
-
return 403;
|
|
1372
|
-
case "BAD_REQUEST":
|
|
1373
|
-
return 400;
|
|
1374
|
-
case "CONFLICT":
|
|
1375
|
-
return 409;
|
|
1376
|
-
default:
|
|
1377
|
-
return 500;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
function getErrorType(error) {
|
|
1381
|
-
if (error && typeof error === "object") {
|
|
1382
|
-
if ("type" in error && typeof error.type === "string") {
|
|
1383
|
-
return error.type;
|
|
1384
|
-
}
|
|
1385
|
-
if ("name" in error && typeof error.name === "string") {
|
|
1386
|
-
return error.name;
|
|
1387
|
-
}
|
|
1388
|
-
if (error instanceof Error) {
|
|
1389
|
-
return error.constructor.name;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
return "Error";
|
|
1393
|
-
}
|
|
1394
|
-
function getErrorMessage(error) {
|
|
1395
|
-
if (error instanceof Error) {
|
|
1396
|
-
return error.message;
|
|
1397
|
-
}
|
|
1398
|
-
if (error && typeof error === "object") {
|
|
1399
|
-
if ("message" in error && typeof error.message === "string") {
|
|
1400
|
-
return error.message;
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
return String(error);
|
|
1404
|
-
}
|
|
2211
|
+
// src/router/validation/schema.ts
|
|
2212
|
+
import { z as z6 } from "zod";
|
|
1405
2213
|
|
|
1406
2214
|
// src/router/validation/body.ts
|
|
1407
2215
|
import { z as z2 } from "zod";
|
|
@@ -1442,35 +2250,45 @@ function validateResponse(response, schema) {
|
|
|
1442
2250
|
// src/router/validation/schema.ts
|
|
1443
2251
|
function createRequestValidator(schema, debug = false) {
|
|
1444
2252
|
const middlewareFn = async (ctx, next) => {
|
|
1445
|
-
const errors = {};
|
|
1446
2253
|
if (schema.params && ctx.request.params) {
|
|
1447
2254
|
try {
|
|
1448
2255
|
ctx.request.params = validateParams(ctx.request.params, schema.params);
|
|
1449
2256
|
} catch (error) {
|
|
1450
|
-
|
|
2257
|
+
const fieldErrors = extractZodFieldErrors(error);
|
|
2258
|
+
const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
|
|
2259
|
+
throw new ValidationError("Request validation failed", {
|
|
2260
|
+
fields: fieldErrors,
|
|
2261
|
+
errorCount,
|
|
2262
|
+
section: "params"
|
|
2263
|
+
});
|
|
1451
2264
|
}
|
|
1452
2265
|
}
|
|
1453
2266
|
if (schema.query && ctx.request.query) {
|
|
1454
2267
|
try {
|
|
1455
2268
|
ctx.request.query = validateQuery(ctx.request.query, schema.query);
|
|
1456
2269
|
} catch (error) {
|
|
1457
|
-
|
|
2270
|
+
const fieldErrors = extractZodFieldErrors(error);
|
|
2271
|
+
const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
|
|
2272
|
+
throw new ValidationError("Request validation failed", {
|
|
2273
|
+
fields: fieldErrors,
|
|
2274
|
+
errorCount,
|
|
2275
|
+
section: "query"
|
|
2276
|
+
});
|
|
1458
2277
|
}
|
|
1459
2278
|
}
|
|
1460
2279
|
if (schema.body) {
|
|
1461
2280
|
try {
|
|
1462
2281
|
ctx.request.body = validateBody(ctx.request.body, schema.body);
|
|
1463
2282
|
} catch (error) {
|
|
1464
|
-
|
|
2283
|
+
const fieldErrors = extractZodFieldErrors(error);
|
|
2284
|
+
const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
|
|
2285
|
+
throw new ValidationError("Request validation failed", {
|
|
2286
|
+
fields: fieldErrors,
|
|
2287
|
+
errorCount,
|
|
2288
|
+
section: "body"
|
|
2289
|
+
});
|
|
1465
2290
|
}
|
|
1466
2291
|
}
|
|
1467
|
-
if (Object.keys(errors).length > 0) {
|
|
1468
|
-
ctx.response.status(400).json({
|
|
1469
|
-
error: "Validation Error",
|
|
1470
|
-
details: errors
|
|
1471
|
-
});
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
2292
|
await next();
|
|
1475
2293
|
};
|
|
1476
2294
|
return {
|
|
@@ -1489,12 +2307,11 @@ function createResponseValidator(responseSchema, debug = false) {
|
|
|
1489
2307
|
return originalJson.call(ctx.response, validatedBody, status);
|
|
1490
2308
|
} catch (error) {
|
|
1491
2309
|
ctx.response.json = originalJson;
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
2310
|
+
throw new InternalServerError("Response validation failed", {
|
|
2311
|
+
responseSchema: responseSchema.description || "Unknown schema",
|
|
2312
|
+
validationError: extractZodFieldErrors(error),
|
|
2313
|
+
originalResponse: body
|
|
1496
2314
|
});
|
|
1497
|
-
return ctx.response;
|
|
1498
2315
|
}
|
|
1499
2316
|
};
|
|
1500
2317
|
await next();
|
|
@@ -1505,11 +2322,25 @@ function createResponseValidator(responseSchema, debug = false) {
|
|
|
1505
2322
|
debug
|
|
1506
2323
|
};
|
|
1507
2324
|
}
|
|
1508
|
-
function
|
|
1509
|
-
if (error
|
|
1510
|
-
|
|
2325
|
+
function extractZodFieldErrors(error) {
|
|
2326
|
+
if (error instanceof z6.ZodError) {
|
|
2327
|
+
const fieldErrorMap = /* @__PURE__ */ new Map();
|
|
2328
|
+
for (const issue of error.issues) {
|
|
2329
|
+
const fieldPath = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
2330
|
+
if (!fieldErrorMap.has(fieldPath)) {
|
|
2331
|
+
fieldErrorMap.set(fieldPath, []);
|
|
2332
|
+
}
|
|
2333
|
+
fieldErrorMap.get(fieldPath).push(issue.message);
|
|
2334
|
+
}
|
|
2335
|
+
return Array.from(fieldErrorMap.entries()).map(([field, messages]) => ({
|
|
2336
|
+
field,
|
|
2337
|
+
messages
|
|
2338
|
+
}));
|
|
2339
|
+
}
|
|
2340
|
+
if (error instanceof Error) {
|
|
2341
|
+
return [{ field: "unknown", messages: [error.message] }];
|
|
1511
2342
|
}
|
|
1512
|
-
return
|
|
2343
|
+
return [{ field: "unknown", messages: [String(error)] }];
|
|
1513
2344
|
}
|
|
1514
2345
|
|
|
1515
2346
|
// src/router/handlers/executor.ts
|
|
@@ -1533,8 +2364,8 @@ async function executeHandler(ctx, routeOptions, params) {
|
|
|
1533
2364
|
}
|
|
1534
2365
|
|
|
1535
2366
|
// src/router/matching/params.ts
|
|
1536
|
-
function extractParams(
|
|
1537
|
-
const match = pattern.exec(
|
|
2367
|
+
function extractParams(path6, pattern, paramNames) {
|
|
2368
|
+
const match = pattern.exec(path6);
|
|
1538
2369
|
if (!match) {
|
|
1539
2370
|
return {};
|
|
1540
2371
|
}
|
|
@@ -1544,15 +2375,15 @@ function extractParams(path5, pattern, paramNames) {
|
|
|
1544
2375
|
}
|
|
1545
2376
|
return params;
|
|
1546
2377
|
}
|
|
1547
|
-
function compilePathPattern(
|
|
2378
|
+
function compilePathPattern(path6) {
|
|
1548
2379
|
const paramNames = [];
|
|
1549
|
-
if (
|
|
2380
|
+
if (path6 === "/") {
|
|
1550
2381
|
return {
|
|
1551
2382
|
pattern: /^\/$/,
|
|
1552
2383
|
paramNames: []
|
|
1553
2384
|
};
|
|
1554
2385
|
}
|
|
1555
|
-
let patternString =
|
|
2386
|
+
let patternString = path6.replace(/([.+*?^$(){}|\\])/g, "\\$1");
|
|
1556
2387
|
patternString = patternString.replace(/\/:([^/]+)/g, (_, paramName) => {
|
|
1557
2388
|
paramNames.push(paramName);
|
|
1558
2389
|
return "/([^/]+)";
|
|
@@ -1575,10 +2406,10 @@ function createMatcher() {
|
|
|
1575
2406
|
/**
|
|
1576
2407
|
* Add a route to the matcher
|
|
1577
2408
|
*/
|
|
1578
|
-
add(
|
|
1579
|
-
const { pattern, paramNames } = compilePathPattern(
|
|
2409
|
+
add(path6, method, routeOptions) {
|
|
2410
|
+
const { pattern, paramNames } = compilePathPattern(path6);
|
|
1580
2411
|
const newRoute = {
|
|
1581
|
-
path:
|
|
2412
|
+
path: path6,
|
|
1582
2413
|
method,
|
|
1583
2414
|
pattern,
|
|
1584
2415
|
paramNames,
|
|
@@ -1591,17 +2422,33 @@ function createMatcher() {
|
|
|
1591
2422
|
routes.splice(insertIndex, 0, newRoute);
|
|
1592
2423
|
}
|
|
1593
2424
|
},
|
|
2425
|
+
/**
|
|
2426
|
+
* Remove a route from the matcher by path
|
|
2427
|
+
*/
|
|
2428
|
+
remove(path6) {
|
|
2429
|
+
for (let i = routes.length - 1; i >= 0; i--) {
|
|
2430
|
+
if (routes[i].path === path6) {
|
|
2431
|
+
routes.splice(i, 1);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
},
|
|
2435
|
+
/**
|
|
2436
|
+
* Clear all routes from the matcher
|
|
2437
|
+
*/
|
|
2438
|
+
clear() {
|
|
2439
|
+
routes.length = 0;
|
|
2440
|
+
},
|
|
1594
2441
|
/**
|
|
1595
2442
|
* Match a URL path to a route
|
|
1596
2443
|
*/
|
|
1597
|
-
match(
|
|
1598
|
-
const pathname =
|
|
2444
|
+
match(path6, method) {
|
|
2445
|
+
const pathname = path6.split("?")[0];
|
|
1599
2446
|
if (!pathname) return null;
|
|
1600
2447
|
for (const route of routes) {
|
|
1601
2448
|
if (route.method !== method) continue;
|
|
1602
2449
|
const match = route.pattern.exec(pathname);
|
|
1603
2450
|
if (match) {
|
|
1604
|
-
const params = extractParams(
|
|
2451
|
+
const params = extractParams(path6, route.pattern, route.paramNames);
|
|
1605
2452
|
return {
|
|
1606
2453
|
route: route.routeOptions,
|
|
1607
2454
|
params
|
|
@@ -1609,14 +2456,14 @@ function createMatcher() {
|
|
|
1609
2456
|
}
|
|
1610
2457
|
}
|
|
1611
2458
|
const matchingPath = routes.find(
|
|
1612
|
-
(route) => route.method !== method && route.pattern.test(
|
|
2459
|
+
(route) => route.method !== method && route.pattern.test(path6)
|
|
1613
2460
|
);
|
|
1614
2461
|
if (matchingPath) {
|
|
1615
2462
|
return {
|
|
1616
2463
|
route: null,
|
|
1617
2464
|
params: {},
|
|
1618
2465
|
methodNotAllowed: true,
|
|
1619
|
-
allowedMethods: routes.filter((route) => route.pattern.test(
|
|
2466
|
+
allowedMethods: routes.filter((route) => route.pattern.test(path6)).map((route) => route.method)
|
|
1620
2467
|
};
|
|
1621
2468
|
}
|
|
1622
2469
|
return null;
|
|
@@ -1633,16 +2480,93 @@ function createMatcher() {
|
|
|
1633
2480
|
/**
|
|
1634
2481
|
* Find routes matching a specific path
|
|
1635
2482
|
*/
|
|
1636
|
-
findRoutes(
|
|
1637
|
-
return routes.filter((route) => route.pattern.test(
|
|
2483
|
+
findRoutes(path6) {
|
|
2484
|
+
return routes.filter((route) => route.pattern.test(path6)).map((route) => ({
|
|
1638
2485
|
path: route.path,
|
|
1639
2486
|
method: route.method,
|
|
1640
|
-
params: extractParams(
|
|
2487
|
+
params: extractParams(path6, route.pattern, route.paramNames)
|
|
1641
2488
|
}));
|
|
1642
2489
|
}
|
|
1643
2490
|
};
|
|
1644
2491
|
}
|
|
1645
2492
|
|
|
2493
|
+
// src/router/registry/fast-registry.ts
|
|
2494
|
+
function createRouteRegistry() {
|
|
2495
|
+
return {
|
|
2496
|
+
routesByPath: /* @__PURE__ */ new Map(),
|
|
2497
|
+
routesByFile: /* @__PURE__ */ new Map(),
|
|
2498
|
+
pathToFile: /* @__PURE__ */ new Map()
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
function updateRoutesFromFile(registry, filePath, newRoutes) {
|
|
2502
|
+
console.log(`Updating routes from file: ${filePath}`);
|
|
2503
|
+
const oldPaths = registry.routesByFile.get(filePath) || /* @__PURE__ */ new Set();
|
|
2504
|
+
const newPaths = new Set(newRoutes.map((r) => r.path));
|
|
2505
|
+
const added = newRoutes.filter((r) => !oldPaths.has(r.path));
|
|
2506
|
+
const removed = Array.from(oldPaths).filter((p) => !newPaths.has(p));
|
|
2507
|
+
const potentiallyChanged = newRoutes.filter((r) => oldPaths.has(r.path));
|
|
2508
|
+
const changed = potentiallyChanged.filter((route) => {
|
|
2509
|
+
const existingRoute = registry.routesByPath.get(route.path);
|
|
2510
|
+
return !existingRoute || !routesEqual(existingRoute, route);
|
|
2511
|
+
});
|
|
2512
|
+
applyRouteUpdates(registry, filePath, { added, removed, changed });
|
|
2513
|
+
return { added, removed, changed };
|
|
2514
|
+
}
|
|
2515
|
+
function getAllRoutesFromRegistry(registry) {
|
|
2516
|
+
return Array.from(registry.routesByPath.values());
|
|
2517
|
+
}
|
|
2518
|
+
function applyRouteUpdates(registry, filePath, updates) {
|
|
2519
|
+
const { added, removed, changed } = updates;
|
|
2520
|
+
removed.forEach((path6) => {
|
|
2521
|
+
registry.routesByPath.delete(path6);
|
|
2522
|
+
registry.pathToFile.delete(path6);
|
|
2523
|
+
});
|
|
2524
|
+
[...added, ...changed].forEach((route) => {
|
|
2525
|
+
registry.routesByPath.set(route.path, route);
|
|
2526
|
+
registry.pathToFile.set(route.path, filePath);
|
|
2527
|
+
});
|
|
2528
|
+
const allPathsForFile = /* @__PURE__ */ new Set([
|
|
2529
|
+
...added.map((r) => r.path),
|
|
2530
|
+
...changed.map((r) => r.path),
|
|
2531
|
+
...Array.from(registry.routesByFile.get(filePath) || []).filter((p) => !removed.includes(p))
|
|
2532
|
+
]);
|
|
2533
|
+
if (allPathsForFile.size > 0) {
|
|
2534
|
+
registry.routesByFile.set(filePath, allPathsForFile);
|
|
2535
|
+
} else {
|
|
2536
|
+
registry.routesByFile.delete(filePath);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
function routesEqual(route1, route2) {
|
|
2540
|
+
if (route1.path !== route2.path) return false;
|
|
2541
|
+
const methods1 = Object.keys(route1).filter((k) => k !== "path").sort();
|
|
2542
|
+
const methods2 = Object.keys(route2).filter((k) => k !== "path").sort();
|
|
2543
|
+
if (methods1.length !== methods2.length) return false;
|
|
2544
|
+
return methods1.every((method) => {
|
|
2545
|
+
const handler1 = route1[method];
|
|
2546
|
+
const handler2 = route2[method];
|
|
2547
|
+
return typeof handler1 === typeof handler2;
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// src/router/utils/matching-helpers.ts
|
|
2552
|
+
function addRouteToMatcher(route, matcher) {
|
|
2553
|
+
Object.entries(route).forEach(([method, methodOptions]) => {
|
|
2554
|
+
if (method === "path" || !methodOptions) return;
|
|
2555
|
+
matcher.add(route.path, method, methodOptions);
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
function removeRouteFromMatcher(path6, matcher) {
|
|
2559
|
+
if ("remove" in matcher && typeof matcher.remove === "function") {
|
|
2560
|
+
matcher.remove(path6);
|
|
2561
|
+
} else {
|
|
2562
|
+
console.warn("Matcher does not support selective removal, consider adding remove() method");
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function updateRouteInMatcher(route, matcher) {
|
|
2566
|
+
removeRouteFromMatcher(route.path, matcher);
|
|
2567
|
+
addRouteToMatcher(route, matcher);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
1646
2570
|
// src/router/router.ts
|
|
1647
2571
|
var DEFAULT_ROUTER_OPTIONS = {
|
|
1648
2572
|
routesDir: "./routes",
|
|
@@ -1657,46 +2581,55 @@ function createRouter(options) {
|
|
|
1657
2581
|
if (options.basePath && !options.basePath.startsWith("/")) {
|
|
1658
2582
|
console.warn("Base path does nothing");
|
|
1659
2583
|
}
|
|
1660
|
-
const
|
|
2584
|
+
const registry = createRouteRegistry();
|
|
1661
2585
|
const matcher = createMatcher();
|
|
1662
2586
|
let initialized = false;
|
|
1663
2587
|
let initializationPromise = null;
|
|
1664
2588
|
let _watchers = null;
|
|
1665
|
-
const routeSources = /* @__PURE__ */ new Map();
|
|
1666
2589
|
const routeDirectories = /* @__PURE__ */ new Set([routerOptions.routesDir]);
|
|
1667
|
-
function
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2590
|
+
function applyMatcherChanges(changes) {
|
|
2591
|
+
console.log("\n\u{1F527} APPLYING MATCHER CHANGES:");
|
|
2592
|
+
console.log(` Adding ${changes.added.length} routes`);
|
|
2593
|
+
console.log(` Removing ${changes.removed.length} routes`);
|
|
2594
|
+
console.log(` Updating ${changes.changed.length} routes`);
|
|
2595
|
+
changes.removed.forEach((routePath) => {
|
|
2596
|
+
console.log(` \u2796 Removing: ${routePath}`);
|
|
2597
|
+
removeRouteFromMatcher(routePath, matcher);
|
|
2598
|
+
});
|
|
2599
|
+
changes.added.forEach((route) => {
|
|
2600
|
+
const methods = Object.keys(route).filter((key) => key !== "path");
|
|
2601
|
+
console.log(` \u2795 Adding: ${route.path} [${methods.join(", ")}]`);
|
|
2602
|
+
addRouteToMatcher(route, matcher);
|
|
2603
|
+
});
|
|
2604
|
+
changes.changed.forEach((route) => {
|
|
2605
|
+
const methods = Object.keys(route).filter((key) => key !== "path");
|
|
2606
|
+
console.log(` \u{1F504} Updating: ${route.path} [${methods.join(", ")}]`);
|
|
2607
|
+
updateRouteInMatcher(route, matcher);
|
|
2608
|
+
});
|
|
2609
|
+
console.log("\u2705 Matcher changes applied\n");
|
|
2610
|
+
}
|
|
2611
|
+
function addRoutesWithSource(routes, source) {
|
|
2612
|
+
try {
|
|
2613
|
+
const changes = updateRoutesFromFile(registry, source, routes);
|
|
2614
|
+
applyMatcherChanges(changes);
|
|
2615
|
+
return changes;
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
console.error(`\u26A0\uFE0F Route conflicts from ${source}:`, error);
|
|
2618
|
+
throw error;
|
|
1679
2619
|
}
|
|
1680
|
-
routeSources.set(route.path, [...existingSources, source]);
|
|
1681
|
-
addRouteInternal(route);
|
|
1682
2620
|
}
|
|
1683
2621
|
async function loadRoutesFromDirectory(directory, source, prefix) {
|
|
1684
2622
|
try {
|
|
1685
|
-
const discoveredRoutes = await
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
...route,
|
|
1691
|
-
path: `${prefix}${route.path}`
|
|
1692
|
-
} : route;
|
|
1693
|
-
addRouteWithSource(finalRoute, source);
|
|
1694
|
-
}
|
|
2623
|
+
const discoveredRoutes = await loadInitialRoutesParallel(directory);
|
|
2624
|
+
const finalRoutes = discoveredRoutes.map(
|
|
2625
|
+
(route) => prefix ? { ...route, path: `${prefix}${route.path}` } : route
|
|
2626
|
+
);
|
|
2627
|
+
const changes = addRoutesWithSource(finalRoutes, source);
|
|
1695
2628
|
console.log(
|
|
1696
|
-
`Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""}`
|
|
2629
|
+
`Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""} (${changes.added.length} added, ${changes.changed.length} changed, ${changes.removed.length} removed)`
|
|
1697
2630
|
);
|
|
1698
2631
|
} catch (error) {
|
|
1699
|
-
console.error(
|
|
2632
|
+
console.error(`\u26A0\uFE0F Failed to load routes from ${source}:`, error);
|
|
1700
2633
|
throw error;
|
|
1701
2634
|
}
|
|
1702
2635
|
}
|
|
@@ -1706,105 +2639,126 @@ function createRouter(options) {
|
|
|
1706
2639
|
}
|
|
1707
2640
|
initializationPromise = (async () => {
|
|
1708
2641
|
try {
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2642
|
+
await Promise.all(
|
|
2643
|
+
Array.from(routeDirectories).map(
|
|
2644
|
+
(directory) => loadRoutesFromDirectory(directory, directory)
|
|
2645
|
+
)
|
|
2646
|
+
);
|
|
1712
2647
|
if (routerOptions.watchMode) {
|
|
1713
|
-
|
|
2648
|
+
setupOptimizedWatching();
|
|
1714
2649
|
}
|
|
1715
2650
|
initialized = true;
|
|
1716
2651
|
} catch (error) {
|
|
1717
|
-
console.error("Failed to initialize router:", error);
|
|
2652
|
+
console.error("\u26A0\uFE0F Failed to initialize router:", error);
|
|
1718
2653
|
throw error;
|
|
1719
2654
|
}
|
|
1720
2655
|
})();
|
|
1721
2656
|
return initializationPromise;
|
|
1722
2657
|
}
|
|
1723
|
-
function
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
}
|
|
1757
|
-
|
|
2658
|
+
function setupOptimizedWatching() {
|
|
2659
|
+
if (!_watchers) {
|
|
2660
|
+
_watchers = /* @__PURE__ */ new Map();
|
|
2661
|
+
}
|
|
2662
|
+
for (const directory of routeDirectories) {
|
|
2663
|
+
if (!_watchers.has(directory)) {
|
|
2664
|
+
const watcher = watchRoutes(directory, {
|
|
2665
|
+
debounceMs: 16,
|
|
2666
|
+
// ~60fps debouncing
|
|
2667
|
+
ignore: ["node_modules", ".git"],
|
|
2668
|
+
onRouteAdded: (filepath, addedRoutes) => {
|
|
2669
|
+
try {
|
|
2670
|
+
const changes = updateRoutesFromFile(registry, filepath, addedRoutes);
|
|
2671
|
+
applyMatcherChanges(changes);
|
|
2672
|
+
} catch (error) {
|
|
2673
|
+
console.error(`Error adding routes from ${directory}:`, error);
|
|
2674
|
+
}
|
|
2675
|
+
},
|
|
2676
|
+
onRouteChanged: withPerformanceTracking(
|
|
2677
|
+
async (filepath, changedRoutes) => {
|
|
2678
|
+
try {
|
|
2679
|
+
console.log(`Processing changes for ${filepath}`);
|
|
2680
|
+
const changes = updateRoutesFromFile(registry, filepath, changedRoutes);
|
|
2681
|
+
console.log(
|
|
2682
|
+
`Changes detected: ${changes.added.length} added, ${changes.changed.length} changed, ${changes.removed.length} removed`
|
|
2683
|
+
);
|
|
2684
|
+
applyMatcherChanges(changes);
|
|
2685
|
+
console.log(
|
|
2686
|
+
`Route changes applied: ${changes.added.length} added, ${changes.changed.length} changed, ${changes.removed.length} removed`
|
|
2687
|
+
);
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
console.error(`\u26A0\uFE0F Error updating routes from ${directory}:`, error);
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2692
|
+
directory
|
|
2693
|
+
),
|
|
2694
|
+
onRouteRemoved: (filePath, removedRoutes) => {
|
|
2695
|
+
console.log(`File removed: ${filePath} with ${removedRoutes.length} routes`);
|
|
2696
|
+
try {
|
|
2697
|
+
removedRoutes.forEach((route) => {
|
|
2698
|
+
removeRouteFromMatcher(route.path, matcher);
|
|
2699
|
+
});
|
|
2700
|
+
clearFileCache(filePath);
|
|
2701
|
+
} catch (error) {
|
|
2702
|
+
console.error(`\u26A0\uFE0F Error removing routes from ${filePath}:`, error);
|
|
1758
2703
|
}
|
|
2704
|
+
},
|
|
2705
|
+
onError: (error) => {
|
|
2706
|
+
console.error(`\u26A0\uFE0F Route watcher error for ${directory}:`, error);
|
|
1759
2707
|
}
|
|
1760
|
-
const finalRoute = prefix ? { ...route, path: finalPath } : route;
|
|
1761
|
-
addRouteWithSource(finalRoute, source);
|
|
1762
2708
|
});
|
|
2709
|
+
_watchers.set(directory, watcher);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
function setupWatcherForNewDirectory(directory, prefix) {
|
|
2714
|
+
if (!_watchers) {
|
|
2715
|
+
_watchers = /* @__PURE__ */ new Map();
|
|
2716
|
+
}
|
|
2717
|
+
const watcher = watchRoutes(directory, {
|
|
2718
|
+
debounceMs: 16,
|
|
2719
|
+
ignore: ["node_modules", ".git"],
|
|
2720
|
+
onRouteAdded: (filePath, addedRoutes) => {
|
|
2721
|
+
try {
|
|
2722
|
+
const finalRoutes = addedRoutes.map(
|
|
2723
|
+
(route) => prefix ? { ...route, path: `${prefix}${route.path}` } : route
|
|
2724
|
+
);
|
|
2725
|
+
const changes = updateRoutesFromFile(registry, filePath, finalRoutes);
|
|
2726
|
+
applyMatcherChanges(changes);
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
console.error(`\u26A0\uFE0F Error adding routes from ${directory}:`, error);
|
|
2729
|
+
}
|
|
1763
2730
|
},
|
|
2731
|
+
onRouteChanged: withPerformanceTracking(async (filePath, changedRoutes) => {
|
|
2732
|
+
try {
|
|
2733
|
+
const finalRoutes = changedRoutes.map(
|
|
2734
|
+
(route) => prefix ? { ...route, path: `${prefix}${route.path}` } : route
|
|
2735
|
+
);
|
|
2736
|
+
const changes = updateRoutesFromFile(registry, filePath, finalRoutes);
|
|
2737
|
+
applyMatcherChanges(changes);
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
console.error(`\u26A0\uFE0F Error updating routes from ${directory}:`, error);
|
|
2740
|
+
}
|
|
2741
|
+
}, directory),
|
|
1764
2742
|
onRouteRemoved: (filePath, removedRoutes) => {
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
}
|
|
1775
|
-
const sources = routeSources.get(finalPath) || [];
|
|
1776
|
-
const filteredSources = sources.filter((s) => s !== source);
|
|
1777
|
-
if (filteredSources.length > 0) {
|
|
1778
|
-
routeSources.set(finalPath, filteredSources);
|
|
1779
|
-
} else {
|
|
1780
|
-
routeSources.delete(finalPath);
|
|
1781
|
-
}
|
|
1782
|
-
});
|
|
2743
|
+
try {
|
|
2744
|
+
removedRoutes.forEach((route) => {
|
|
2745
|
+
const finalPath = prefix ? `${prefix}${route.path}` : route.path;
|
|
2746
|
+
removeRouteFromMatcher(finalPath, matcher);
|
|
2747
|
+
});
|
|
2748
|
+
clearFileCache(filePath);
|
|
2749
|
+
} catch (error) {
|
|
2750
|
+
console.error(`Error removing routes from ${filePath}:`, error);
|
|
2751
|
+
}
|
|
1783
2752
|
},
|
|
1784
2753
|
onError: (error) => {
|
|
1785
|
-
console.error(
|
|
2754
|
+
console.error(`\u26A0\uFE0F Route watcher error for ${directory}:`, error);
|
|
1786
2755
|
}
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
function setupWatcherForDirectory(directory, source, prefix) {
|
|
1790
|
-
const callbacks = createWatcherCallbacks(directory, source, prefix);
|
|
1791
|
-
const watcher = watchRoutes(directory, {
|
|
1792
|
-
ignore: ["node_modules", ".git"],
|
|
1793
|
-
...callbacks
|
|
1794
2756
|
});
|
|
1795
|
-
if (!_watchers) {
|
|
1796
|
-
_watchers = /* @__PURE__ */ new Map();
|
|
1797
|
-
}
|
|
1798
2757
|
_watchers.set(directory, watcher);
|
|
1799
2758
|
return watcher;
|
|
1800
2759
|
}
|
|
1801
|
-
function setupWatcherForAllDirectories() {
|
|
1802
|
-
for (const directory of routeDirectories) {
|
|
1803
|
-
setupWatcherForDirectory(directory, directory);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
2760
|
initialize().catch((error) => {
|
|
1807
|
-
console.error("Failed to initialize router on creation:", error);
|
|
2761
|
+
console.error("\u26A0\uFE0F Failed to initialize router on creation:", error);
|
|
1808
2762
|
});
|
|
1809
2763
|
return {
|
|
1810
2764
|
/**
|
|
@@ -1812,17 +2766,22 @@ function createRouter(options) {
|
|
|
1812
2766
|
*/
|
|
1813
2767
|
async handleRequest(ctx) {
|
|
1814
2768
|
if (!initialized) {
|
|
2769
|
+
console.log("\u{1F504} Router not initialized, initializing...");
|
|
1815
2770
|
await initialize();
|
|
1816
2771
|
}
|
|
1817
|
-
const { method, path:
|
|
1818
|
-
|
|
2772
|
+
const { method, path: path6 } = ctx.request;
|
|
2773
|
+
console.log(`
|
|
2774
|
+
\u{1F4E5} Handling request: ${method} ${path6}`);
|
|
2775
|
+
const match = matcher.match(path6, method);
|
|
1819
2776
|
if (!match) {
|
|
1820
|
-
|
|
1821
|
-
|
|
2777
|
+
console.log(`\u274C No match found for: ${method} ${path6}`);
|
|
2778
|
+
throw new NotFoundError("Not found");
|
|
1822
2779
|
}
|
|
2780
|
+
console.log(`\u2705 Route matched: ${method} ${path6}`);
|
|
2781
|
+
console.log(` Params: ${JSON.stringify(match.params)}`);
|
|
1823
2782
|
if (match.methodNotAllowed) {
|
|
1824
2783
|
ctx.response.status(405).json({
|
|
1825
|
-
error: "Method Not Allowed",
|
|
2784
|
+
error: "\u274C Method Not Allowed",
|
|
1826
2785
|
allowed: match.allowedMethods
|
|
1827
2786
|
});
|
|
1828
2787
|
if (match.allowedMethods && match.allowedMethods.length > 0) {
|
|
@@ -1831,29 +2790,31 @@ function createRouter(options) {
|
|
|
1831
2790
|
return;
|
|
1832
2791
|
}
|
|
1833
2792
|
ctx.request.params = match.params;
|
|
1834
|
-
|
|
1835
|
-
await executeHandler(ctx, match.route, match.params);
|
|
1836
|
-
} catch (error) {
|
|
1837
|
-
handleRouteError(ctx, error, {
|
|
1838
|
-
detailed: process.env.NODE_ENV !== "production",
|
|
1839
|
-
log: true
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
2793
|
+
await executeHandler(ctx, match.route, match.params);
|
|
1842
2794
|
},
|
|
1843
2795
|
/**
|
|
1844
|
-
* Get all registered routes
|
|
2796
|
+
* Get all registered routes (using optimized registry)
|
|
1845
2797
|
*/
|
|
1846
2798
|
getRoutes() {
|
|
1847
|
-
return
|
|
2799
|
+
return getAllRoutesFromRegistry(registry);
|
|
1848
2800
|
},
|
|
1849
2801
|
/**
|
|
1850
2802
|
* Add a route programmatically
|
|
1851
2803
|
*/
|
|
1852
2804
|
addRoute(route) {
|
|
1853
|
-
|
|
2805
|
+
const changes = updateRoutesFromFile(registry, "programmatic", [route]);
|
|
2806
|
+
applyMatcherChanges(changes);
|
|
2807
|
+
},
|
|
2808
|
+
/**
|
|
2809
|
+
* Add multiple routes programmatically with batch processing
|
|
2810
|
+
*/
|
|
2811
|
+
addRoutes(routes) {
|
|
2812
|
+
const changes = updateRoutesFromFile(registry, "programmatic", routes);
|
|
2813
|
+
applyMatcherChanges(changes);
|
|
2814
|
+
return changes;
|
|
1854
2815
|
},
|
|
1855
2816
|
/**
|
|
1856
|
-
* Add a route directory (for plugins)
|
|
2817
|
+
* Add a route directory (for plugins) with optimized loading
|
|
1857
2818
|
*/
|
|
1858
2819
|
async addRouteDirectory(directory, options2 = {}) {
|
|
1859
2820
|
if (routeDirectories.has(directory)) {
|
|
@@ -1864,27 +2825,33 @@ function createRouter(options) {
|
|
|
1864
2825
|
if (initialized) {
|
|
1865
2826
|
await loadRoutesFromDirectory(directory, directory, options2.prefix);
|
|
1866
2827
|
if (routerOptions.watchMode) {
|
|
1867
|
-
|
|
2828
|
+
setupWatcherForNewDirectory(directory, options2.prefix);
|
|
1868
2829
|
}
|
|
1869
2830
|
}
|
|
1870
2831
|
},
|
|
1871
2832
|
/**
|
|
1872
|
-
* Get route conflicts
|
|
2833
|
+
* Get route conflicts (using registry)
|
|
1873
2834
|
*/
|
|
1874
2835
|
getRouteConflicts() {
|
|
1875
2836
|
const conflicts = [];
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2837
|
+
return conflicts;
|
|
2838
|
+
},
|
|
2839
|
+
/**
|
|
2840
|
+
* Close watchers and cleanup (useful for testing)
|
|
2841
|
+
*/
|
|
2842
|
+
async close() {
|
|
2843
|
+
if (_watchers) {
|
|
2844
|
+
for (const watcher of _watchers.values()) {
|
|
2845
|
+
await watcher.close();
|
|
1879
2846
|
}
|
|
2847
|
+
_watchers.clear();
|
|
1880
2848
|
}
|
|
1881
|
-
return conflicts;
|
|
1882
2849
|
}
|
|
1883
2850
|
};
|
|
1884
2851
|
}
|
|
1885
2852
|
|
|
1886
2853
|
// src/server/create.ts
|
|
1887
|
-
var
|
|
2854
|
+
var DEFAULT_OPTIONS2 = {
|
|
1888
2855
|
port: 3e3,
|
|
1889
2856
|
host: "localhost",
|
|
1890
2857
|
routesDir: "./routes",
|
|
@@ -1895,7 +2862,7 @@ var DEFAULT_OPTIONS = {
|
|
|
1895
2862
|
plugins: []
|
|
1896
2863
|
};
|
|
1897
2864
|
function createServerOptions(options = {}) {
|
|
1898
|
-
const baseOptions = { ...
|
|
2865
|
+
const baseOptions = { ...DEFAULT_OPTIONS2 };
|
|
1899
2866
|
setRuntimeConfig({ routesDir: options.routesDir || baseOptions.routesDir });
|
|
1900
2867
|
return {
|
|
1901
2868
|
port: options.port ?? baseOptions.port,
|
|
@@ -2015,6 +2982,90 @@ function create3(options = {}) {
|
|
|
2015
2982
|
return serverInstance;
|
|
2016
2983
|
}
|
|
2017
2984
|
|
|
2985
|
+
// src/errors/unauthorized-error.ts
|
|
2986
|
+
var UnauthorizedError = class extends BlaizeError {
|
|
2987
|
+
/**
|
|
2988
|
+
* Creates a new UnauthorizedError instance
|
|
2989
|
+
*
|
|
2990
|
+
* @param title - Human-readable error message
|
|
2991
|
+
* @param details - Optional authentication context
|
|
2992
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
2993
|
+
*/
|
|
2994
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
2995
|
+
super(
|
|
2996
|
+
"UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
2997
|
+
title,
|
|
2998
|
+
401,
|
|
2999
|
+
// HTTP 401 Unauthorized
|
|
3000
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3001
|
+
details
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
|
|
3006
|
+
// src/errors/forbidden-error.ts
|
|
3007
|
+
var ForbiddenError = class extends BlaizeError {
|
|
3008
|
+
/**
|
|
3009
|
+
* Creates a new ForbiddenError instance
|
|
3010
|
+
*
|
|
3011
|
+
* @param title - Human-readable error message
|
|
3012
|
+
* @param details - Optional permission context
|
|
3013
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
3014
|
+
*/
|
|
3015
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
3016
|
+
super(
|
|
3017
|
+
"FORBIDDEN" /* FORBIDDEN */,
|
|
3018
|
+
title,
|
|
3019
|
+
403,
|
|
3020
|
+
// HTTP 403 Forbidden
|
|
3021
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3022
|
+
details
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
|
|
3027
|
+
// src/errors/conflict-error.ts
|
|
3028
|
+
var ConflictError = class extends BlaizeError {
|
|
3029
|
+
/**
|
|
3030
|
+
* Creates a new ConflictError instance
|
|
3031
|
+
*
|
|
3032
|
+
* @param title - Human-readable error message
|
|
3033
|
+
* @param details - Optional conflict context
|
|
3034
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
3035
|
+
*/
|
|
3036
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
3037
|
+
super(
|
|
3038
|
+
"CONFLICT" /* CONFLICT */,
|
|
3039
|
+
title,
|
|
3040
|
+
409,
|
|
3041
|
+
// HTTP 409 Conflict
|
|
3042
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3043
|
+
details
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
|
|
3048
|
+
// src/errors/rate-limit-error.ts
|
|
3049
|
+
var RateLimitError = class extends BlaizeError {
|
|
3050
|
+
/**
|
|
3051
|
+
* Creates a new RateLimitError instance
|
|
3052
|
+
*
|
|
3053
|
+
* @param title - Human-readable error message
|
|
3054
|
+
* @param details - Optional rate limit context
|
|
3055
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
3056
|
+
*/
|
|
3057
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
3058
|
+
super(
|
|
3059
|
+
"RATE_LIMITED" /* RATE_LIMITED */,
|
|
3060
|
+
title,
|
|
3061
|
+
429,
|
|
3062
|
+
// HTTP 429 Too Many Requests
|
|
3063
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3064
|
+
details
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
|
|
2018
3069
|
// src/index.ts
|
|
2019
3070
|
var VERSION = "0.1.0";
|
|
2020
3071
|
var ServerAPI = { createServer: create3 };
|
|
@@ -2045,11 +3096,21 @@ var Blaize = {
|
|
|
2045
3096
|
var index_default = Blaize;
|
|
2046
3097
|
export {
|
|
2047
3098
|
Blaize,
|
|
3099
|
+
BlaizeError,
|
|
3100
|
+
ConflictError,
|
|
3101
|
+
ErrorSeverity,
|
|
3102
|
+
ErrorType,
|
|
3103
|
+
ForbiddenError,
|
|
3104
|
+
InternalServerError,
|
|
2048
3105
|
MiddlewareAPI,
|
|
3106
|
+
NotFoundError,
|
|
2049
3107
|
PluginsAPI,
|
|
3108
|
+
RateLimitError,
|
|
2050
3109
|
RouterAPI,
|
|
2051
3110
|
ServerAPI,
|
|
3111
|
+
UnauthorizedError,
|
|
2052
3112
|
VERSION,
|
|
3113
|
+
ValidationError,
|
|
2053
3114
|
compose,
|
|
2054
3115
|
createDeleteRoute,
|
|
2055
3116
|
createGetRoute,
|
|
@@ -2061,6 +3122,7 @@ export {
|
|
|
2061
3122
|
createPostRoute,
|
|
2062
3123
|
createPutRoute,
|
|
2063
3124
|
create3 as createServer,
|
|
2064
|
-
index_default as default
|
|
3125
|
+
index_default as default,
|
|
3126
|
+
isBodyParseError
|
|
2065
3127
|
};
|
|
2066
3128
|
//# sourceMappingURL=index.js.map
|