blaizejs 0.2.2 → 0.2.3
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/index.cjs +680 -264
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +685 -264
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* blaizejs v0.2.
|
|
2
|
+
* blaizejs v0.2.3
|
|
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
|
|
@@ -159,6 +159,9 @@ function create2(name, version, setup, defaultOptions = {}) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// src/router/create.ts
|
|
163
|
+
var import_node_url = require("url");
|
|
164
|
+
|
|
162
165
|
// src/config.ts
|
|
163
166
|
var config = {};
|
|
164
167
|
function setRuntimeConfig(newConfig) {
|
|
@@ -230,73 +233,75 @@ function getCallerFilePath() {
|
|
|
230
233
|
if (!fileName) {
|
|
231
234
|
throw new Error("Unable to determine caller file name");
|
|
232
235
|
}
|
|
236
|
+
if (fileName.startsWith("file://")) {
|
|
237
|
+
return (0, import_node_url.fileURLToPath)(fileName);
|
|
238
|
+
}
|
|
233
239
|
return fileName;
|
|
234
240
|
} finally {
|
|
235
241
|
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
function getRoutePath() {
|
|
239
|
-
console.log("getRoutePath called");
|
|
240
245
|
const callerPath = getCallerFilePath();
|
|
241
246
|
const routesDir = getRoutesDir();
|
|
242
247
|
const parsedRoute = parseRoutePath(callerPath, routesDir);
|
|
243
|
-
console.log(
|
|
248
|
+
console.log(`\u{1F50E} Parsed route path: ${parsedRoute.routePath} from file: ${callerPath}`);
|
|
244
249
|
return parsedRoute.routePath;
|
|
245
250
|
}
|
|
246
251
|
var createGetRoute = (config2) => {
|
|
247
252
|
validateMethodConfig("GET", config2);
|
|
248
|
-
const
|
|
253
|
+
const path6 = getRoutePath();
|
|
249
254
|
return {
|
|
250
255
|
GET: config2,
|
|
251
|
-
path:
|
|
256
|
+
path: path6
|
|
252
257
|
};
|
|
253
258
|
};
|
|
254
259
|
var createPostRoute = (config2) => {
|
|
255
260
|
validateMethodConfig("POST", config2);
|
|
256
|
-
const
|
|
261
|
+
const path6 = getRoutePath();
|
|
257
262
|
return {
|
|
258
263
|
POST: config2,
|
|
259
|
-
path:
|
|
264
|
+
path: path6
|
|
260
265
|
};
|
|
261
266
|
};
|
|
262
267
|
var createPutRoute = (config2) => {
|
|
263
268
|
validateMethodConfig("PUT", config2);
|
|
264
|
-
const
|
|
269
|
+
const path6 = getRoutePath();
|
|
265
270
|
return {
|
|
266
271
|
PUT: config2,
|
|
267
|
-
path:
|
|
272
|
+
path: path6
|
|
268
273
|
};
|
|
269
274
|
};
|
|
270
275
|
var createDeleteRoute = (config2) => {
|
|
271
276
|
validateMethodConfig("DELETE", config2);
|
|
272
|
-
const
|
|
277
|
+
const path6 = getRoutePath();
|
|
273
278
|
return {
|
|
274
279
|
DELETE: config2,
|
|
275
|
-
path:
|
|
280
|
+
path: path6
|
|
276
281
|
};
|
|
277
282
|
};
|
|
278
283
|
var createPatchRoute = (config2) => {
|
|
279
284
|
validateMethodConfig("PATCH", config2);
|
|
280
|
-
const
|
|
285
|
+
const path6 = getRoutePath();
|
|
281
286
|
return {
|
|
282
287
|
PATCH: config2,
|
|
283
|
-
path:
|
|
288
|
+
path: path6
|
|
284
289
|
};
|
|
285
290
|
};
|
|
286
291
|
var createHeadRoute = (config2) => {
|
|
287
292
|
validateMethodConfig("HEAD", config2);
|
|
288
|
-
const
|
|
293
|
+
const path6 = getRoutePath();
|
|
289
294
|
return {
|
|
290
295
|
HEAD: config2,
|
|
291
|
-
path:
|
|
296
|
+
path: path6
|
|
292
297
|
};
|
|
293
298
|
};
|
|
294
299
|
var createOptionsRoute = (config2) => {
|
|
295
300
|
validateMethodConfig("OPTIONS", config2);
|
|
296
|
-
const
|
|
301
|
+
const path6 = getRoutePath();
|
|
297
302
|
return {
|
|
298
303
|
OPTIONS: config2,
|
|
299
|
-
path:
|
|
304
|
+
path: path6
|
|
300
305
|
};
|
|
301
306
|
};
|
|
302
307
|
function validateMethodConfig(method, config2) {
|
|
@@ -441,7 +446,7 @@ function parseRequestUrl(req) {
|
|
|
441
446
|
const fullUrl = `${protocol}://${host}${originalUrl.startsWith("/") ? "" : "/"}${originalUrl}`;
|
|
442
447
|
try {
|
|
443
448
|
const url = new URL(fullUrl);
|
|
444
|
-
const
|
|
449
|
+
const path6 = url.pathname;
|
|
445
450
|
const query = {};
|
|
446
451
|
url.searchParams.forEach((value, key) => {
|
|
447
452
|
if (query[key] !== void 0) {
|
|
@@ -454,7 +459,7 @@ function parseRequestUrl(req) {
|
|
|
454
459
|
query[key] = value;
|
|
455
460
|
}
|
|
456
461
|
});
|
|
457
|
-
return { path:
|
|
462
|
+
return { path: path6, url, query };
|
|
458
463
|
} catch (error) {
|
|
459
464
|
console.warn(`Invalid URL: ${fullUrl}`, error);
|
|
460
465
|
throw new ParseUrlError(`Invalid URL: ${fullUrl}`);
|
|
@@ -476,7 +481,7 @@ function getProtocol(req) {
|
|
|
476
481
|
return encrypted ? "https" : "http";
|
|
477
482
|
}
|
|
478
483
|
async function createContext(req, res, options = {}) {
|
|
479
|
-
const { path:
|
|
484
|
+
const { path: path6, url, query } = parseRequestUrl(req);
|
|
480
485
|
const method = req.method || "GET";
|
|
481
486
|
const isHttp2 = isHttp2Request(req);
|
|
482
487
|
const protocol = getProtocol(req);
|
|
@@ -485,7 +490,7 @@ async function createContext(req, res, options = {}) {
|
|
|
485
490
|
const responseState = { sent: false };
|
|
486
491
|
const ctx = {
|
|
487
492
|
request: createRequestObject(req, {
|
|
488
|
-
path:
|
|
493
|
+
path: path6,
|
|
489
494
|
url,
|
|
490
495
|
query,
|
|
491
496
|
params,
|
|
@@ -757,13 +762,13 @@ function setBodyError(ctx, type, message, error) {
|
|
|
757
762
|
ctx.state._bodyError = { type, message, error };
|
|
758
763
|
}
|
|
759
764
|
async function readRequestBody(req) {
|
|
760
|
-
return new Promise((
|
|
765
|
+
return new Promise((resolve3, reject) => {
|
|
761
766
|
const chunks = [];
|
|
762
767
|
req.on("data", (chunk) => {
|
|
763
768
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
764
769
|
});
|
|
765
770
|
req.on("end", () => {
|
|
766
|
-
|
|
771
|
+
resolve3(Buffer.concat(chunks).toString("utf8"));
|
|
767
772
|
});
|
|
768
773
|
req.on("error", (err) => {
|
|
769
774
|
reject(err);
|
|
@@ -861,7 +866,7 @@ function createServerInstance(isHttp2, certOptions) {
|
|
|
861
866
|
return http2.createSecureServer(http2ServerOptions);
|
|
862
867
|
}
|
|
863
868
|
function listenOnPort(server, port, host, isHttp2) {
|
|
864
|
-
return new Promise((
|
|
869
|
+
return new Promise((resolve3, reject) => {
|
|
865
870
|
server.listen(port, host, () => {
|
|
866
871
|
const protocol = isHttp2 ? "https" : "http";
|
|
867
872
|
const url = `${protocol}://${host}:${port}`;
|
|
@@ -878,7 +883,7 @@ function listenOnPort(server, port, host, isHttp2) {
|
|
|
878
883
|
|
|
879
884
|
\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}
|
|
880
885
|
`);
|
|
881
|
-
|
|
886
|
+
resolve3();
|
|
882
887
|
});
|
|
883
888
|
server.on("error", (err) => {
|
|
884
889
|
console.error("Server error:", err);
|
|
@@ -922,55 +927,128 @@ async function startServer(serverInstance, serverOptions) {
|
|
|
922
927
|
}
|
|
923
928
|
|
|
924
929
|
// src/server/stop.ts
|
|
930
|
+
var isShuttingDown = false;
|
|
925
931
|
async function stopServer(serverInstance, options = {}) {
|
|
926
932
|
const server = serverInstance.server;
|
|
927
933
|
const events = serverInstance.events;
|
|
934
|
+
if (isShuttingDown) {
|
|
935
|
+
console.log("\u26A0\uFE0F Shutdown already in progress, ignoring duplicate shutdown request");
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
928
938
|
if (!server) {
|
|
929
939
|
return;
|
|
930
940
|
}
|
|
931
|
-
|
|
941
|
+
isShuttingDown = true;
|
|
942
|
+
const timeout = options.timeout || 5e3;
|
|
932
943
|
try {
|
|
933
944
|
if (options.onStopping) {
|
|
934
945
|
await options.onStopping();
|
|
935
946
|
}
|
|
936
947
|
events.emit("stopping");
|
|
937
|
-
|
|
948
|
+
if (serverInstance.router && typeof serverInstance.router.close === "function") {
|
|
949
|
+
console.log("\u{1F50C} Closing router watchers...");
|
|
950
|
+
try {
|
|
951
|
+
await Promise.race([
|
|
952
|
+
serverInstance.router.close(),
|
|
953
|
+
new Promise(
|
|
954
|
+
(_, reject) => setTimeout(() => reject(new Error("Router close timeout")), 2e3)
|
|
955
|
+
)
|
|
956
|
+
]);
|
|
957
|
+
console.log("\u2705 Router watchers closed");
|
|
958
|
+
} catch (error) {
|
|
959
|
+
console.error("\u274C Error closing router watchers:", error);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
await Promise.race([
|
|
964
|
+
serverInstance.pluginManager.onServerStop(serverInstance, server),
|
|
965
|
+
new Promise(
|
|
966
|
+
(_, reject) => setTimeout(() => reject(new Error("Plugin stop timeout")), 2e3)
|
|
967
|
+
)
|
|
968
|
+
]);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
console.error("\u274C Plugin stop timeout:", error);
|
|
971
|
+
}
|
|
972
|
+
const closePromise = new Promise((resolve3, reject) => {
|
|
973
|
+
server.close((err) => {
|
|
974
|
+
if (err) return reject(err);
|
|
975
|
+
resolve3();
|
|
976
|
+
});
|
|
977
|
+
});
|
|
938
978
|
const timeoutPromise = new Promise((_, reject) => {
|
|
939
979
|
setTimeout(() => {
|
|
940
|
-
reject(new Error("Server shutdown
|
|
980
|
+
reject(new Error("Server shutdown timeout"));
|
|
941
981
|
}, timeout);
|
|
942
982
|
});
|
|
943
|
-
const closePromise = new Promise((resolve2, reject) => {
|
|
944
|
-
server.close((err) => {
|
|
945
|
-
if (err) {
|
|
946
|
-
return reject(err);
|
|
947
|
-
}
|
|
948
|
-
resolve2();
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
983
|
await Promise.race([closePromise, timeoutPromise]);
|
|
952
|
-
|
|
984
|
+
try {
|
|
985
|
+
await Promise.race([
|
|
986
|
+
serverInstance.pluginManager.terminatePlugins(serverInstance),
|
|
987
|
+
new Promise(
|
|
988
|
+
(_, reject) => setTimeout(() => reject(new Error("Plugin terminate timeout")), 1e3)
|
|
989
|
+
)
|
|
990
|
+
]);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
console.error("\u274C Plugin terminate timeout:", error);
|
|
993
|
+
}
|
|
953
994
|
if (options.onStopped) {
|
|
954
995
|
await options.onStopped();
|
|
955
996
|
}
|
|
956
997
|
events.emit("stopped");
|
|
957
998
|
serverInstance.server = null;
|
|
999
|
+
console.log("\u2705 Graceful shutdown completed");
|
|
1000
|
+
isShuttingDown = false;
|
|
958
1001
|
} catch (error) {
|
|
1002
|
+
isShuttingDown = false;
|
|
1003
|
+
console.error("\u26A0\uFE0F Shutdown error (forcing exit):", error);
|
|
1004
|
+
if (server && typeof server.close === "function") {
|
|
1005
|
+
server.close();
|
|
1006
|
+
}
|
|
1007
|
+
if (process.env.NODE_ENV === "development") {
|
|
1008
|
+
console.log("\u{1F504} Forcing exit for development restart...");
|
|
1009
|
+
process.exit(0);
|
|
1010
|
+
}
|
|
959
1011
|
events.emit("error", error);
|
|
960
1012
|
throw error;
|
|
961
1013
|
}
|
|
962
1014
|
}
|
|
963
1015
|
function registerSignalHandlers(stopFn) {
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1016
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
1017
|
+
if (isDevelopment) {
|
|
1018
|
+
const sigintHandler = () => {
|
|
1019
|
+
console.log("\u{1F4E4} SIGINT received, forcing exit for development restart...");
|
|
1020
|
+
process.exit(0);
|
|
1021
|
+
};
|
|
1022
|
+
const sigtermHandler = () => {
|
|
1023
|
+
console.log("\u{1F4E4} SIGTERM received, forcing exit for development restart...");
|
|
1024
|
+
process.exit(0);
|
|
1025
|
+
};
|
|
1026
|
+
process.on("SIGINT", sigintHandler);
|
|
1027
|
+
process.on("SIGTERM", sigtermHandler);
|
|
1028
|
+
return {
|
|
1029
|
+
unregister: () => {
|
|
1030
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1031
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
} else {
|
|
1035
|
+
const sigintHandler = () => {
|
|
1036
|
+
console.log("\u{1F4E4} SIGINT received, starting graceful shutdown...");
|
|
1037
|
+
stopFn().catch(console.error);
|
|
1038
|
+
};
|
|
1039
|
+
const sigtermHandler = () => {
|
|
1040
|
+
console.log("\u{1F4E4} SIGTERM received, starting graceful shutdown...");
|
|
1041
|
+
stopFn().catch(console.error);
|
|
1042
|
+
};
|
|
1043
|
+
process.on("SIGINT", sigintHandler);
|
|
1044
|
+
process.on("SIGTERM", sigtermHandler);
|
|
1045
|
+
return {
|
|
1046
|
+
unregister: () => {
|
|
1047
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1048
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
974
1052
|
}
|
|
975
1053
|
|
|
976
1054
|
// src/server/validation.ts
|
|
@@ -1176,59 +1254,33 @@ function validatePlugin(plugin, options = {}) {
|
|
|
1176
1254
|
}
|
|
1177
1255
|
}
|
|
1178
1256
|
|
|
1179
|
-
// src/router/discovery/
|
|
1257
|
+
// src/router/discovery/cache.ts
|
|
1258
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
1180
1259
|
var fs3 = __toESM(require("fs/promises"), 1);
|
|
1260
|
+
var import_node_module = require("module");
|
|
1181
1261
|
var path3 = __toESM(require("path"), 1);
|
|
1182
|
-
async function findRouteFiles(routesDir, options = {}) {
|
|
1183
|
-
const absoluteDir = path3.isAbsolute(routesDir) ? routesDir : path3.resolve(process.cwd(), routesDir);
|
|
1184
|
-
console.log("Creating router with routes directory:", absoluteDir);
|
|
1185
|
-
try {
|
|
1186
|
-
const stats = await fs3.stat(absoluteDir);
|
|
1187
|
-
if (!stats.isDirectory()) {
|
|
1188
|
-
throw new Error(`Route directory is not a directory: ${absoluteDir}`);
|
|
1189
|
-
}
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
if (error.code === "ENOENT") {
|
|
1192
|
-
throw new Error(`Route directory not found: ${absoluteDir}`);
|
|
1193
|
-
}
|
|
1194
|
-
throw error;
|
|
1195
|
-
}
|
|
1196
|
-
const routeFiles = [];
|
|
1197
|
-
const ignore = options.ignore || ["node_modules", ".git"];
|
|
1198
|
-
async function scanDirectory(dir) {
|
|
1199
|
-
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1200
|
-
for (const entry of entries) {
|
|
1201
|
-
const fullPath = path3.join(dir, entry.name);
|
|
1202
|
-
if (entry.isDirectory() && ignore.includes(entry.name)) {
|
|
1203
|
-
continue;
|
|
1204
|
-
}
|
|
1205
|
-
if (entry.isDirectory()) {
|
|
1206
|
-
await scanDirectory(fullPath);
|
|
1207
|
-
} else if (isRouteFile(entry.name)) {
|
|
1208
|
-
routeFiles.push(fullPath);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
await scanDirectory(absoluteDir);
|
|
1213
|
-
return routeFiles;
|
|
1214
|
-
}
|
|
1215
|
-
function isRouteFile(filename) {
|
|
1216
|
-
return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js"));
|
|
1217
|
-
}
|
|
1218
1262
|
|
|
1219
1263
|
// src/router/discovery/loader.ts
|
|
1220
1264
|
async function dynamicImport(filePath) {
|
|
1221
|
-
|
|
1265
|
+
const cacheBuster = `?t=${Date.now()}`;
|
|
1266
|
+
const importPath = filePath + cacheBuster;
|
|
1267
|
+
try {
|
|
1268
|
+
const module2 = await import(importPath);
|
|
1269
|
+
console.log(`\u2705 Successfully imported module`);
|
|
1270
|
+
return module2;
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1273
|
+
console.log(`\u26A0\uFE0F Error importing with cache buster, trying original path:`, errorMessage);
|
|
1274
|
+
return import(filePath);
|
|
1275
|
+
}
|
|
1222
1276
|
}
|
|
1223
1277
|
async function loadRouteModule(filePath, basePath) {
|
|
1224
1278
|
try {
|
|
1225
1279
|
const parsedRoute = parseRoutePath(filePath, basePath);
|
|
1226
|
-
console.log("parsedRoute:", parsedRoute);
|
|
1227
1280
|
const module2 = await dynamicImport(filePath);
|
|
1228
|
-
console.log("Module exports:", Object.keys(module2));
|
|
1281
|
+
console.log("\u{1F4E6} Module exports:", Object.keys(module2));
|
|
1229
1282
|
const routes = [];
|
|
1230
1283
|
if (module2.default && typeof module2.default === "object") {
|
|
1231
|
-
console.log("Found default export:", module2.default);
|
|
1232
1284
|
const route = {
|
|
1233
1285
|
...module2.default,
|
|
1234
1286
|
path: parsedRoute.routePath
|
|
@@ -1241,7 +1293,6 @@ async function loadRouteModule(filePath, basePath) {
|
|
|
1241
1293
|
}
|
|
1242
1294
|
const potentialRoute = exportValue;
|
|
1243
1295
|
if (isValidRoute(potentialRoute)) {
|
|
1244
|
-
console.log(`Found named route export: ${exportName}`, potentialRoute);
|
|
1245
1296
|
const route = {
|
|
1246
1297
|
...potentialRoute,
|
|
1247
1298
|
// Use the route's own path if it has one, otherwise derive from file
|
|
@@ -1254,7 +1305,7 @@ async function loadRouteModule(filePath, basePath) {
|
|
|
1254
1305
|
console.warn(`Route file ${filePath} does not export any valid route definitions`);
|
|
1255
1306
|
return [];
|
|
1256
1307
|
}
|
|
1257
|
-
console.log(
|
|
1308
|
+
console.log(`\u2705 Successfully Loaded ${routes.length} route(s)`);
|
|
1258
1309
|
return routes;
|
|
1259
1310
|
} catch (error) {
|
|
1260
1311
|
console.error(`Failed to load route module ${filePath}:`, error);
|
|
@@ -1272,25 +1323,223 @@ function isValidRoute(obj) {
|
|
|
1272
1323
|
return hasHttpMethod;
|
|
1273
1324
|
}
|
|
1274
1325
|
|
|
1275
|
-
// src/router/discovery/
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1326
|
+
// src/router/discovery/cache.ts
|
|
1327
|
+
var import_meta = {};
|
|
1328
|
+
var fileRouteCache = /* @__PURE__ */ new Map();
|
|
1329
|
+
async function processChangedFile(filePath, routesDir, updateCache = true) {
|
|
1330
|
+
const stat3 = await fs3.stat(filePath);
|
|
1331
|
+
const lastModified = stat3.mtime.getTime();
|
|
1332
|
+
const cachedEntry = fileRouteCache.get(filePath);
|
|
1333
|
+
if (updateCache && cachedEntry && cachedEntry.timestamp === lastModified) {
|
|
1334
|
+
return cachedEntry.routes;
|
|
1335
|
+
}
|
|
1336
|
+
invalidateModuleCache(filePath);
|
|
1337
|
+
const routes = await loadRouteModule(filePath, routesDir);
|
|
1338
|
+
if (updateCache) {
|
|
1339
|
+
const hash = hashRoutes(routes);
|
|
1340
|
+
fileRouteCache.set(filePath, {
|
|
1341
|
+
routes,
|
|
1342
|
+
timestamp: lastModified,
|
|
1343
|
+
hash
|
|
1344
|
+
});
|
|
1286
1345
|
}
|
|
1287
1346
|
return routes;
|
|
1288
1347
|
}
|
|
1348
|
+
function hasRouteContentChanged(filePath, newRoutes) {
|
|
1349
|
+
const cachedEntry = fileRouteCache.get(filePath);
|
|
1350
|
+
if (!cachedEntry) {
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
const newHash = hashRoutes(newRoutes);
|
|
1354
|
+
return cachedEntry.hash !== newHash;
|
|
1355
|
+
}
|
|
1356
|
+
function clearFileCache(filePath) {
|
|
1357
|
+
if (filePath) {
|
|
1358
|
+
fileRouteCache.delete(filePath);
|
|
1359
|
+
} else {
|
|
1360
|
+
fileRouteCache.clear();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function hashRoutes(routes) {
|
|
1364
|
+
const routeData = routes.map((route) => ({
|
|
1365
|
+
path: route.path,
|
|
1366
|
+
methods: Object.keys(route).filter((key) => key !== "path").sort().map((method) => {
|
|
1367
|
+
const methodDef = route[method];
|
|
1368
|
+
const handlerString = methodDef?.handler ? methodDef.handler.toString() : null;
|
|
1369
|
+
return {
|
|
1370
|
+
method,
|
|
1371
|
+
// Include handler function string for change detection
|
|
1372
|
+
handler: handlerString,
|
|
1373
|
+
// Include middleware if present
|
|
1374
|
+
middleware: methodDef?.middleware ? methodDef.middleware.length : 0,
|
|
1375
|
+
// Include schema structure (but not full serialization which can be unstable)
|
|
1376
|
+
hasSchema: !!methodDef?.schema,
|
|
1377
|
+
schemaKeys: methodDef?.schema ? Object.keys(methodDef.schema).sort() : []
|
|
1378
|
+
};
|
|
1379
|
+
})
|
|
1380
|
+
}));
|
|
1381
|
+
const dataString = JSON.stringify(routeData);
|
|
1382
|
+
const hash = crypto.createHash("md5").update(dataString).digest("hex");
|
|
1383
|
+
return hash;
|
|
1384
|
+
}
|
|
1385
|
+
function invalidateModuleCache(filePath) {
|
|
1386
|
+
try {
|
|
1387
|
+
const absolutePath = path3.resolve(filePath);
|
|
1388
|
+
if (typeof require !== "undefined") {
|
|
1389
|
+
delete require.cache[absolutePath];
|
|
1390
|
+
try {
|
|
1391
|
+
const resolvedPath = require.resolve(absolutePath);
|
|
1392
|
+
delete require.cache[resolvedPath];
|
|
1393
|
+
} catch (resolveError) {
|
|
1394
|
+
const errorMessage = resolveError instanceof Error ? resolveError.message : String(resolveError);
|
|
1395
|
+
console.log(`\u26A0\uFE0F Could not resolve path: ${errorMessage}`);
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
try {
|
|
1399
|
+
const require2 = (0, import_node_module.createRequire)(import_meta.url);
|
|
1400
|
+
delete require2.cache[absolutePath];
|
|
1401
|
+
try {
|
|
1402
|
+
const resolvedPath = require2.resolve(absolutePath);
|
|
1403
|
+
delete require2.cache[resolvedPath];
|
|
1404
|
+
} catch {
|
|
1405
|
+
console.log(`\u26A0\uFE0F Could not resolve ESM path`);
|
|
1406
|
+
}
|
|
1407
|
+
} catch {
|
|
1408
|
+
console.log(`\u26A0\uFE0F createRequire not available in pure ESM`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
console.log(`\u26A0\uFE0F Error during module cache invalidation for ${filePath}:`, error);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1289
1415
|
|
|
1290
|
-
// src/router/discovery/
|
|
1416
|
+
// src/router/discovery/parallel.ts
|
|
1417
|
+
var os = __toESM(require("os"), 1);
|
|
1418
|
+
|
|
1419
|
+
// src/router/discovery/finder.ts
|
|
1420
|
+
var fs4 = __toESM(require("fs/promises"), 1);
|
|
1291
1421
|
var path4 = __toESM(require("path"), 1);
|
|
1422
|
+
async function findRouteFiles(routesDir, options = {}) {
|
|
1423
|
+
const absoluteDir = path4.isAbsolute(routesDir) ? routesDir : path4.resolve(process.cwd(), routesDir);
|
|
1424
|
+
console.log("Creating router with routes directory:", absoluteDir);
|
|
1425
|
+
try {
|
|
1426
|
+
const stats = await fs4.stat(absoluteDir);
|
|
1427
|
+
if (!stats.isDirectory()) {
|
|
1428
|
+
throw new Error(`Route directory is not a directory: ${absoluteDir}`);
|
|
1429
|
+
}
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
if (error.code === "ENOENT") {
|
|
1432
|
+
throw new Error(`Route directory not found: ${absoluteDir}`);
|
|
1433
|
+
}
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
const routeFiles = [];
|
|
1437
|
+
const ignore = options.ignore || ["node_modules", ".git"];
|
|
1438
|
+
async function scanDirectory(dir) {
|
|
1439
|
+
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1440
|
+
for (const entry of entries) {
|
|
1441
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1442
|
+
if (entry.isDirectory() && ignore.includes(entry.name)) {
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (entry.isDirectory()) {
|
|
1446
|
+
await scanDirectory(fullPath);
|
|
1447
|
+
} else if (isRouteFile(entry.name)) {
|
|
1448
|
+
routeFiles.push(fullPath);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
await scanDirectory(absoluteDir);
|
|
1453
|
+
return routeFiles;
|
|
1454
|
+
}
|
|
1455
|
+
function isRouteFile(filename) {
|
|
1456
|
+
return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js"));
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/router/discovery/parallel.ts
|
|
1460
|
+
async function processFilesInParallel(filePaths, processor, concurrency = Math.max(1, Math.floor(os.cpus().length / 2))) {
|
|
1461
|
+
const chunks = chunkArray(filePaths, concurrency);
|
|
1462
|
+
const results = [];
|
|
1463
|
+
for (const chunk of chunks) {
|
|
1464
|
+
const chunkResults = await Promise.allSettled(chunk.map((filePath) => processor(filePath)));
|
|
1465
|
+
const successfulResults = chunkResults.filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
1466
|
+
results.push(...successfulResults);
|
|
1467
|
+
}
|
|
1468
|
+
return results;
|
|
1469
|
+
}
|
|
1470
|
+
async function loadInitialRoutesParallel(routesDir) {
|
|
1471
|
+
const files = await findRouteFiles(routesDir);
|
|
1472
|
+
const routeArrays = await processFilesInParallel(
|
|
1473
|
+
files,
|
|
1474
|
+
(filePath) => processChangedFile(filePath, routesDir)
|
|
1475
|
+
);
|
|
1476
|
+
return routeArrays.flat();
|
|
1477
|
+
}
|
|
1478
|
+
function chunkArray(array, chunkSize) {
|
|
1479
|
+
const chunks = [];
|
|
1480
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
1481
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
1482
|
+
}
|
|
1483
|
+
return chunks;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/router/discovery/profiler.ts
|
|
1487
|
+
var profilerState = {
|
|
1488
|
+
fileChanges: 0,
|
|
1489
|
+
totalReloadTime: 0,
|
|
1490
|
+
averageReloadTime: 0,
|
|
1491
|
+
slowReloads: []
|
|
1492
|
+
};
|
|
1493
|
+
function trackReloadPerformance(filePath, startTime) {
|
|
1494
|
+
const duration = Date.now() - startTime;
|
|
1495
|
+
profilerState.fileChanges++;
|
|
1496
|
+
profilerState.totalReloadTime += duration;
|
|
1497
|
+
profilerState.averageReloadTime = profilerState.totalReloadTime / profilerState.fileChanges;
|
|
1498
|
+
if (duration > 100) {
|
|
1499
|
+
profilerState.slowReloads.push({ file: filePath, time: duration });
|
|
1500
|
+
if (profilerState.slowReloads.length > 10) {
|
|
1501
|
+
profilerState.slowReloads.shift();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (process.env.NODE_ENV === "development") {
|
|
1505
|
+
const emoji = duration < 50 ? "\u26A1" : duration < 100 ? "\u{1F504}" : "\u{1F40C}";
|
|
1506
|
+
console.log(`${emoji} Route reload: ${filePath} (${duration}ms)`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
function withPerformanceTracking(fn, filePath) {
|
|
1510
|
+
console.log(`Tracking performance for: ${filePath}`);
|
|
1511
|
+
return async (...args) => {
|
|
1512
|
+
const startTime = Date.now();
|
|
1513
|
+
try {
|
|
1514
|
+
const result = await fn(...args);
|
|
1515
|
+
trackReloadPerformance(filePath, startTime);
|
|
1516
|
+
return result;
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
trackReloadPerformance(filePath, startTime);
|
|
1519
|
+
throw error;
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/router/discovery/watchers.ts
|
|
1525
|
+
var path5 = __toESM(require("path"), 1);
|
|
1292
1526
|
var import_chokidar = require("chokidar");
|
|
1293
1527
|
function watchRoutes(routesDir, options = {}) {
|
|
1528
|
+
const debounceMs = options.debounceMs || 16;
|
|
1529
|
+
const debouncedCallbacks = /* @__PURE__ */ new Map();
|
|
1530
|
+
function createDebouncedCallback(fn, filePath) {
|
|
1531
|
+
return (...args) => {
|
|
1532
|
+
const existingTimeout = debouncedCallbacks.get(filePath);
|
|
1533
|
+
if (existingTimeout) {
|
|
1534
|
+
clearTimeout(existingTimeout);
|
|
1535
|
+
}
|
|
1536
|
+
const timeoutId = setTimeout(() => {
|
|
1537
|
+
fn(...args);
|
|
1538
|
+
debouncedCallbacks.delete(filePath);
|
|
1539
|
+
}, debounceMs);
|
|
1540
|
+
debouncedCallbacks.set(filePath, timeoutId);
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1294
1543
|
const routesByPath = /* @__PURE__ */ new Map();
|
|
1295
1544
|
async function loadInitialRoutes() {
|
|
1296
1545
|
try {
|
|
@@ -1306,28 +1555,34 @@ function watchRoutes(routesDir, options = {}) {
|
|
|
1306
1555
|
}
|
|
1307
1556
|
async function loadAndNotify(filePath) {
|
|
1308
1557
|
try {
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1558
|
+
const existingRoutes = routesByPath.get(filePath);
|
|
1559
|
+
const newRoutes = await processChangedFile(filePath, routesDir, false);
|
|
1560
|
+
if (!newRoutes || newRoutes.length === 0) {
|
|
1311
1561
|
return;
|
|
1312
1562
|
}
|
|
1313
|
-
|
|
1563
|
+
if (existingRoutes && !hasRouteContentChanged(filePath, newRoutes)) {
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
await processChangedFile(filePath, routesDir, true);
|
|
1567
|
+
const normalizedPath = path5.normalize(filePath);
|
|
1314
1568
|
if (existingRoutes) {
|
|
1315
|
-
routesByPath.set(filePath,
|
|
1569
|
+
routesByPath.set(filePath, newRoutes);
|
|
1316
1570
|
if (options.onRouteChanged) {
|
|
1317
|
-
options.onRouteChanged(
|
|
1571
|
+
options.onRouteChanged(normalizedPath, newRoutes);
|
|
1318
1572
|
}
|
|
1319
1573
|
} else {
|
|
1320
|
-
routesByPath.set(filePath,
|
|
1574
|
+
routesByPath.set(filePath, newRoutes);
|
|
1321
1575
|
if (options.onRouteAdded) {
|
|
1322
|
-
options.onRouteAdded(
|
|
1576
|
+
options.onRouteAdded(normalizedPath, newRoutes);
|
|
1323
1577
|
}
|
|
1324
1578
|
}
|
|
1325
1579
|
} catch (error) {
|
|
1580
|
+
console.log(`\u26A0\uFE0F Error processing file ${filePath}:`, error);
|
|
1326
1581
|
handleError(error);
|
|
1327
1582
|
}
|
|
1328
1583
|
}
|
|
1329
1584
|
function handleRemoved(filePath) {
|
|
1330
|
-
const normalizedPath =
|
|
1585
|
+
const normalizedPath = path5.normalize(filePath);
|
|
1331
1586
|
const routes = routesByPath.get(normalizedPath);
|
|
1332
1587
|
if (routes && routes.length > 0 && options.onRouteRemoved) {
|
|
1333
1588
|
options.onRouteRemoved(normalizedPath, routes);
|
|
@@ -1338,33 +1593,53 @@ function watchRoutes(routesDir, options = {}) {
|
|
|
1338
1593
|
if (options.onError && error instanceof Error) {
|
|
1339
1594
|
options.onError(error);
|
|
1340
1595
|
} else {
|
|
1341
|
-
console.error("Route watcher error:", error);
|
|
1596
|
+
console.error("\u26A0\uFE0F Route watcher error:", error);
|
|
1342
1597
|
}
|
|
1343
1598
|
}
|
|
1344
1599
|
const watcher = (0, import_chokidar.watch)(routesDir, {
|
|
1600
|
+
// Much faster response times
|
|
1601
|
+
awaitWriteFinish: {
|
|
1602
|
+
stabilityThreshold: 50,
|
|
1603
|
+
// Reduced from 300ms
|
|
1604
|
+
pollInterval: 10
|
|
1605
|
+
// Reduced from 100ms
|
|
1606
|
+
},
|
|
1607
|
+
// Performance optimizations
|
|
1608
|
+
usePolling: false,
|
|
1609
|
+
atomic: true,
|
|
1610
|
+
followSymlinks: false,
|
|
1611
|
+
depth: 10,
|
|
1612
|
+
// More aggressive ignoring
|
|
1345
1613
|
ignored: [
|
|
1346
1614
|
/(^|[/\\])\../,
|
|
1347
|
-
// Ignore dot files
|
|
1348
1615
|
/node_modules/,
|
|
1616
|
+
/\.git/,
|
|
1617
|
+
/\.DS_Store/,
|
|
1618
|
+
/Thumbs\.db/,
|
|
1619
|
+
/\.(test|spec)\.(ts|js)$/,
|
|
1620
|
+
/\.d\.ts$/,
|
|
1621
|
+
/\.map$/,
|
|
1622
|
+
/~$/,
|
|
1349
1623
|
...options.ignore || []
|
|
1350
|
-
]
|
|
1351
|
-
persistent: true,
|
|
1352
|
-
ignoreInitial: false,
|
|
1353
|
-
awaitWriteFinish: {
|
|
1354
|
-
stabilityThreshold: 300,
|
|
1355
|
-
pollInterval: 100
|
|
1356
|
-
}
|
|
1624
|
+
]
|
|
1357
1625
|
});
|
|
1358
|
-
watcher.on("add",
|
|
1626
|
+
watcher.on("add", (filePath) => {
|
|
1627
|
+
const debouncedLoad = createDebouncedCallback(loadAndNotify, filePath);
|
|
1628
|
+
debouncedLoad(filePath);
|
|
1629
|
+
}).on("change", (filePath) => {
|
|
1630
|
+
const debouncedLoad = createDebouncedCallback(loadAndNotify, filePath);
|
|
1631
|
+
debouncedLoad(filePath);
|
|
1632
|
+
}).on("unlink", (filePath) => {
|
|
1633
|
+
const debouncedRemove = createDebouncedCallback(handleRemoved, filePath);
|
|
1634
|
+
debouncedRemove(filePath);
|
|
1635
|
+
}).on("error", handleError);
|
|
1359
1636
|
loadInitialRoutes().catch(handleError);
|
|
1360
1637
|
return {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
* Get all currently loaded routes (flattened)
|
|
1367
|
-
*/
|
|
1638
|
+
close: () => {
|
|
1639
|
+
debouncedCallbacks.forEach((timeout) => clearTimeout(timeout));
|
|
1640
|
+
debouncedCallbacks.clear();
|
|
1641
|
+
return watcher.close();
|
|
1642
|
+
},
|
|
1368
1643
|
getRoutes: () => {
|
|
1369
1644
|
const allRoutes = [];
|
|
1370
1645
|
for (const routes of routesByPath.values()) {
|
|
@@ -1372,9 +1647,6 @@ function watchRoutes(routesDir, options = {}) {
|
|
|
1372
1647
|
}
|
|
1373
1648
|
return allRoutes;
|
|
1374
1649
|
},
|
|
1375
|
-
/**
|
|
1376
|
-
* Get routes organized by file path
|
|
1377
|
-
*/
|
|
1378
1650
|
getRoutesByFile: () => new Map(routesByPath)
|
|
1379
1651
|
};
|
|
1380
1652
|
}
|
|
@@ -1585,8 +1857,8 @@ async function executeHandler(ctx, routeOptions, params) {
|
|
|
1585
1857
|
}
|
|
1586
1858
|
|
|
1587
1859
|
// src/router/matching/params.ts
|
|
1588
|
-
function extractParams(
|
|
1589
|
-
const match = pattern.exec(
|
|
1860
|
+
function extractParams(path6, pattern, paramNames) {
|
|
1861
|
+
const match = pattern.exec(path6);
|
|
1590
1862
|
if (!match) {
|
|
1591
1863
|
return {};
|
|
1592
1864
|
}
|
|
@@ -1596,15 +1868,15 @@ function extractParams(path5, pattern, paramNames) {
|
|
|
1596
1868
|
}
|
|
1597
1869
|
return params;
|
|
1598
1870
|
}
|
|
1599
|
-
function compilePathPattern(
|
|
1871
|
+
function compilePathPattern(path6) {
|
|
1600
1872
|
const paramNames = [];
|
|
1601
|
-
if (
|
|
1873
|
+
if (path6 === "/") {
|
|
1602
1874
|
return {
|
|
1603
1875
|
pattern: /^\/$/,
|
|
1604
1876
|
paramNames: []
|
|
1605
1877
|
};
|
|
1606
1878
|
}
|
|
1607
|
-
let patternString =
|
|
1879
|
+
let patternString = path6.replace(/([.+*?^$(){}|\\])/g, "\\$1");
|
|
1608
1880
|
patternString = patternString.replace(/\/:([^/]+)/g, (_, paramName) => {
|
|
1609
1881
|
paramNames.push(paramName);
|
|
1610
1882
|
return "/([^/]+)";
|
|
@@ -1627,10 +1899,10 @@ function createMatcher() {
|
|
|
1627
1899
|
/**
|
|
1628
1900
|
* Add a route to the matcher
|
|
1629
1901
|
*/
|
|
1630
|
-
add(
|
|
1631
|
-
const { pattern, paramNames } = compilePathPattern(
|
|
1902
|
+
add(path6, method, routeOptions) {
|
|
1903
|
+
const { pattern, paramNames } = compilePathPattern(path6);
|
|
1632
1904
|
const newRoute = {
|
|
1633
|
-
path:
|
|
1905
|
+
path: path6,
|
|
1634
1906
|
method,
|
|
1635
1907
|
pattern,
|
|
1636
1908
|
paramNames,
|
|
@@ -1643,17 +1915,33 @@ function createMatcher() {
|
|
|
1643
1915
|
routes.splice(insertIndex, 0, newRoute);
|
|
1644
1916
|
}
|
|
1645
1917
|
},
|
|
1918
|
+
/**
|
|
1919
|
+
* Remove a route from the matcher by path
|
|
1920
|
+
*/
|
|
1921
|
+
remove(path6) {
|
|
1922
|
+
for (let i = routes.length - 1; i >= 0; i--) {
|
|
1923
|
+
if (routes[i].path === path6) {
|
|
1924
|
+
routes.splice(i, 1);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1928
|
+
/**
|
|
1929
|
+
* Clear all routes from the matcher
|
|
1930
|
+
*/
|
|
1931
|
+
clear() {
|
|
1932
|
+
routes.length = 0;
|
|
1933
|
+
},
|
|
1646
1934
|
/**
|
|
1647
1935
|
* Match a URL path to a route
|
|
1648
1936
|
*/
|
|
1649
|
-
match(
|
|
1650
|
-
const pathname =
|
|
1937
|
+
match(path6, method) {
|
|
1938
|
+
const pathname = path6.split("?")[0];
|
|
1651
1939
|
if (!pathname) return null;
|
|
1652
1940
|
for (const route of routes) {
|
|
1653
1941
|
if (route.method !== method) continue;
|
|
1654
1942
|
const match = route.pattern.exec(pathname);
|
|
1655
1943
|
if (match) {
|
|
1656
|
-
const params = extractParams(
|
|
1944
|
+
const params = extractParams(path6, route.pattern, route.paramNames);
|
|
1657
1945
|
return {
|
|
1658
1946
|
route: route.routeOptions,
|
|
1659
1947
|
params
|
|
@@ -1661,14 +1949,14 @@ function createMatcher() {
|
|
|
1661
1949
|
}
|
|
1662
1950
|
}
|
|
1663
1951
|
const matchingPath = routes.find(
|
|
1664
|
-
(route) => route.method !== method && route.pattern.test(
|
|
1952
|
+
(route) => route.method !== method && route.pattern.test(path6)
|
|
1665
1953
|
);
|
|
1666
1954
|
if (matchingPath) {
|
|
1667
1955
|
return {
|
|
1668
1956
|
route: null,
|
|
1669
1957
|
params: {},
|
|
1670
1958
|
methodNotAllowed: true,
|
|
1671
|
-
allowedMethods: routes.filter((route) => route.pattern.test(
|
|
1959
|
+
allowedMethods: routes.filter((route) => route.pattern.test(path6)).map((route) => route.method)
|
|
1672
1960
|
};
|
|
1673
1961
|
}
|
|
1674
1962
|
return null;
|
|
@@ -1685,16 +1973,93 @@ function createMatcher() {
|
|
|
1685
1973
|
/**
|
|
1686
1974
|
* Find routes matching a specific path
|
|
1687
1975
|
*/
|
|
1688
|
-
findRoutes(
|
|
1689
|
-
return routes.filter((route) => route.pattern.test(
|
|
1976
|
+
findRoutes(path6) {
|
|
1977
|
+
return routes.filter((route) => route.pattern.test(path6)).map((route) => ({
|
|
1690
1978
|
path: route.path,
|
|
1691
1979
|
method: route.method,
|
|
1692
|
-
params: extractParams(
|
|
1980
|
+
params: extractParams(path6, route.pattern, route.paramNames)
|
|
1693
1981
|
}));
|
|
1694
1982
|
}
|
|
1695
1983
|
};
|
|
1696
1984
|
}
|
|
1697
1985
|
|
|
1986
|
+
// src/router/registry/fast-registry.ts
|
|
1987
|
+
function createRouteRegistry() {
|
|
1988
|
+
return {
|
|
1989
|
+
routesByPath: /* @__PURE__ */ new Map(),
|
|
1990
|
+
routesByFile: /* @__PURE__ */ new Map(),
|
|
1991
|
+
pathToFile: /* @__PURE__ */ new Map()
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
function updateRoutesFromFile(registry, filePath, newRoutes) {
|
|
1995
|
+
console.log(`Updating routes from file: ${filePath}`);
|
|
1996
|
+
const oldPaths = registry.routesByFile.get(filePath) || /* @__PURE__ */ new Set();
|
|
1997
|
+
const newPaths = new Set(newRoutes.map((r) => r.path));
|
|
1998
|
+
const added = newRoutes.filter((r) => !oldPaths.has(r.path));
|
|
1999
|
+
const removed = Array.from(oldPaths).filter((p) => !newPaths.has(p));
|
|
2000
|
+
const potentiallyChanged = newRoutes.filter((r) => oldPaths.has(r.path));
|
|
2001
|
+
const changed = potentiallyChanged.filter((route) => {
|
|
2002
|
+
const existingRoute = registry.routesByPath.get(route.path);
|
|
2003
|
+
return !existingRoute || !routesEqual(existingRoute, route);
|
|
2004
|
+
});
|
|
2005
|
+
applyRouteUpdates(registry, filePath, { added, removed, changed });
|
|
2006
|
+
return { added, removed, changed };
|
|
2007
|
+
}
|
|
2008
|
+
function getAllRoutesFromRegistry(registry) {
|
|
2009
|
+
return Array.from(registry.routesByPath.values());
|
|
2010
|
+
}
|
|
2011
|
+
function applyRouteUpdates(registry, filePath, updates) {
|
|
2012
|
+
const { added, removed, changed } = updates;
|
|
2013
|
+
removed.forEach((path6) => {
|
|
2014
|
+
registry.routesByPath.delete(path6);
|
|
2015
|
+
registry.pathToFile.delete(path6);
|
|
2016
|
+
});
|
|
2017
|
+
[...added, ...changed].forEach((route) => {
|
|
2018
|
+
registry.routesByPath.set(route.path, route);
|
|
2019
|
+
registry.pathToFile.set(route.path, filePath);
|
|
2020
|
+
});
|
|
2021
|
+
const allPathsForFile = /* @__PURE__ */ new Set([
|
|
2022
|
+
...added.map((r) => r.path),
|
|
2023
|
+
...changed.map((r) => r.path),
|
|
2024
|
+
...Array.from(registry.routesByFile.get(filePath) || []).filter((p) => !removed.includes(p))
|
|
2025
|
+
]);
|
|
2026
|
+
if (allPathsForFile.size > 0) {
|
|
2027
|
+
registry.routesByFile.set(filePath, allPathsForFile);
|
|
2028
|
+
} else {
|
|
2029
|
+
registry.routesByFile.delete(filePath);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function routesEqual(route1, route2) {
|
|
2033
|
+
if (route1.path !== route2.path) return false;
|
|
2034
|
+
const methods1 = Object.keys(route1).filter((k) => k !== "path").sort();
|
|
2035
|
+
const methods2 = Object.keys(route2).filter((k) => k !== "path").sort();
|
|
2036
|
+
if (methods1.length !== methods2.length) return false;
|
|
2037
|
+
return methods1.every((method) => {
|
|
2038
|
+
const handler1 = route1[method];
|
|
2039
|
+
const handler2 = route2[method];
|
|
2040
|
+
return typeof handler1 === typeof handler2;
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// src/router/utils/matching-helpers.ts
|
|
2045
|
+
function addRouteToMatcher(route, matcher) {
|
|
2046
|
+
Object.entries(route).forEach(([method, methodOptions]) => {
|
|
2047
|
+
if (method === "path" || !methodOptions) return;
|
|
2048
|
+
matcher.add(route.path, method, methodOptions);
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
function removeRouteFromMatcher(path6, matcher) {
|
|
2052
|
+
if ("remove" in matcher && typeof matcher.remove === "function") {
|
|
2053
|
+
matcher.remove(path6);
|
|
2054
|
+
} else {
|
|
2055
|
+
console.warn("Matcher does not support selective removal, consider adding remove() method");
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
function updateRouteInMatcher(route, matcher) {
|
|
2059
|
+
removeRouteFromMatcher(route.path, matcher);
|
|
2060
|
+
addRouteToMatcher(route, matcher);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1698
2063
|
// src/router/router.ts
|
|
1699
2064
|
var DEFAULT_ROUTER_OPTIONS = {
|
|
1700
2065
|
routesDir: "./routes",
|
|
@@ -1709,46 +2074,55 @@ function createRouter(options) {
|
|
|
1709
2074
|
if (options.basePath && !options.basePath.startsWith("/")) {
|
|
1710
2075
|
console.warn("Base path does nothing");
|
|
1711
2076
|
}
|
|
1712
|
-
const
|
|
2077
|
+
const registry = createRouteRegistry();
|
|
1713
2078
|
const matcher = createMatcher();
|
|
1714
2079
|
let initialized = false;
|
|
1715
2080
|
let initializationPromise = null;
|
|
1716
2081
|
let _watchers = null;
|
|
1717
|
-
const routeSources = /* @__PURE__ */ new Map();
|
|
1718
2082
|
const routeDirectories = /* @__PURE__ */ new Set([routerOptions.routesDir]);
|
|
1719
|
-
function
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
2083
|
+
function applyMatcherChanges(changes) {
|
|
2084
|
+
console.log("\n\u{1F527} APPLYING MATCHER CHANGES:");
|
|
2085
|
+
console.log(` Adding ${changes.added.length} routes`);
|
|
2086
|
+
console.log(` Removing ${changes.removed.length} routes`);
|
|
2087
|
+
console.log(` Updating ${changes.changed.length} routes`);
|
|
2088
|
+
changes.removed.forEach((routePath) => {
|
|
2089
|
+
console.log(` \u2796 Removing: ${routePath}`);
|
|
2090
|
+
removeRouteFromMatcher(routePath, matcher);
|
|
2091
|
+
});
|
|
2092
|
+
changes.added.forEach((route) => {
|
|
2093
|
+
const methods = Object.keys(route).filter((key) => key !== "path");
|
|
2094
|
+
console.log(` \u2795 Adding: ${route.path} [${methods.join(", ")}]`);
|
|
2095
|
+
addRouteToMatcher(route, matcher);
|
|
2096
|
+
});
|
|
2097
|
+
changes.changed.forEach((route) => {
|
|
2098
|
+
const methods = Object.keys(route).filter((key) => key !== "path");
|
|
2099
|
+
console.log(` \u{1F504} Updating: ${route.path} [${methods.join(", ")}]`);
|
|
2100
|
+
updateRouteInMatcher(route, matcher);
|
|
2101
|
+
});
|
|
2102
|
+
console.log("\u2705 Matcher changes applied\n");
|
|
2103
|
+
}
|
|
2104
|
+
function addRoutesWithSource(routes, source) {
|
|
2105
|
+
try {
|
|
2106
|
+
const changes = updateRoutesFromFile(registry, source, routes);
|
|
2107
|
+
applyMatcherChanges(changes);
|
|
2108
|
+
return changes;
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
console.error(`\u26A0\uFE0F Route conflicts from ${source}:`, error);
|
|
2111
|
+
throw error;
|
|
1731
2112
|
}
|
|
1732
|
-
routeSources.set(route.path, [...existingSources, source]);
|
|
1733
|
-
addRouteInternal(route);
|
|
1734
2113
|
}
|
|
1735
2114
|
async function loadRoutesFromDirectory(directory, source, prefix) {
|
|
1736
2115
|
try {
|
|
1737
|
-
const discoveredRoutes = await
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
...route,
|
|
1743
|
-
path: `${prefix}${route.path}`
|
|
1744
|
-
} : route;
|
|
1745
|
-
addRouteWithSource(finalRoute, source);
|
|
1746
|
-
}
|
|
2116
|
+
const discoveredRoutes = await loadInitialRoutesParallel(directory);
|
|
2117
|
+
const finalRoutes = discoveredRoutes.map(
|
|
2118
|
+
(route) => prefix ? { ...route, path: `${prefix}${route.path}` } : route
|
|
2119
|
+
);
|
|
2120
|
+
const changes = addRoutesWithSource(finalRoutes, source);
|
|
1747
2121
|
console.log(
|
|
1748
|
-
`Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""}`
|
|
2122
|
+
`Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""} (${changes.added.length} added, ${changes.changed.length} changed, ${changes.removed.length} removed)`
|
|
1749
2123
|
);
|
|
1750
2124
|
} catch (error) {
|
|
1751
|
-
console.error(
|
|
2125
|
+
console.error(`\u26A0\uFE0F Failed to load routes from ${source}:`, error);
|
|
1752
2126
|
throw error;
|
|
1753
2127
|
}
|
|
1754
2128
|
}
|
|
@@ -1758,105 +2132,126 @@ function createRouter(options) {
|
|
|
1758
2132
|
}
|
|
1759
2133
|
initializationPromise = (async () => {
|
|
1760
2134
|
try {
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2135
|
+
await Promise.all(
|
|
2136
|
+
Array.from(routeDirectories).map(
|
|
2137
|
+
(directory) => loadRoutesFromDirectory(directory, directory)
|
|
2138
|
+
)
|
|
2139
|
+
);
|
|
1764
2140
|
if (routerOptions.watchMode) {
|
|
1765
|
-
|
|
2141
|
+
setupOptimizedWatching();
|
|
1766
2142
|
}
|
|
1767
2143
|
initialized = true;
|
|
1768
2144
|
} catch (error) {
|
|
1769
|
-
console.error("Failed to initialize router:", error);
|
|
2145
|
+
console.error("\u26A0\uFE0F Failed to initialize router:", error);
|
|
1770
2146
|
throw error;
|
|
1771
2147
|
}
|
|
1772
2148
|
})();
|
|
1773
2149
|
return initializationPromise;
|
|
1774
2150
|
}
|
|
1775
|
-
function
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
|
|
2151
|
+
function setupOptimizedWatching() {
|
|
2152
|
+
if (!_watchers) {
|
|
2153
|
+
_watchers = /* @__PURE__ */ new Map();
|
|
2154
|
+
}
|
|
2155
|
+
for (const directory of routeDirectories) {
|
|
2156
|
+
if (!_watchers.has(directory)) {
|
|
2157
|
+
const watcher = watchRoutes(directory, {
|
|
2158
|
+
debounceMs: 16,
|
|
2159
|
+
// ~60fps debouncing
|
|
2160
|
+
ignore: ["node_modules", ".git"],
|
|
2161
|
+
onRouteAdded: (filepath, addedRoutes) => {
|
|
2162
|
+
try {
|
|
2163
|
+
const changes = updateRoutesFromFile(registry, filepath, addedRoutes);
|
|
2164
|
+
applyMatcherChanges(changes);
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
console.error(`Error adding routes from ${directory}:`, error);
|
|
2167
|
+
}
|
|
2168
|
+
},
|
|
2169
|
+
onRouteChanged: withPerformanceTracking(
|
|
2170
|
+
async (filepath, changedRoutes) => {
|
|
2171
|
+
try {
|
|
2172
|
+
console.log(`Processing changes for ${filepath}`);
|
|
2173
|
+
const changes = updateRoutesFromFile(registry, filepath, changedRoutes);
|
|
2174
|
+
console.log(
|
|
2175
|
+
`Changes detected: ${changes.added.length} added, ${changes.changed.length} changed, ${changes.removed.length} removed`
|
|
2176
|
+
);
|
|
2177
|
+
applyMatcherChanges(changes);
|
|
2178
|
+
console.log(
|
|
2179
|
+
`Route changes applied: ${changes.added.length} added, ${changes.changed.length} changed, ${changes.removed.length} removed`
|
|
2180
|
+
);
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
console.error(`\u26A0\uFE0F Error updating routes from ${directory}:`, error);
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
directory
|
|
2186
|
+
),
|
|
2187
|
+
onRouteRemoved: (filePath, removedRoutes) => {
|
|
2188
|
+
console.log(`File removed: ${filePath} with ${removedRoutes.length} routes`);
|
|
2189
|
+
try {
|
|
2190
|
+
removedRoutes.forEach((route) => {
|
|
2191
|
+
removeRouteFromMatcher(route.path, matcher);
|
|
2192
|
+
});
|
|
2193
|
+
clearFileCache(filePath);
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
console.error(`\u26A0\uFE0F Error removing routes from ${filePath}:`, error);
|
|
1810
2196
|
}
|
|
2197
|
+
},
|
|
2198
|
+
onError: (error) => {
|
|
2199
|
+
console.error(`\u26A0\uFE0F Route watcher error for ${directory}:`, error);
|
|
1811
2200
|
}
|
|
1812
|
-
const finalRoute = prefix ? { ...route, path: finalPath } : route;
|
|
1813
|
-
addRouteWithSource(finalRoute, source);
|
|
1814
2201
|
});
|
|
2202
|
+
_watchers.set(directory, watcher);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
function setupWatcherForNewDirectory(directory, prefix) {
|
|
2207
|
+
if (!_watchers) {
|
|
2208
|
+
_watchers = /* @__PURE__ */ new Map();
|
|
2209
|
+
}
|
|
2210
|
+
const watcher = watchRoutes(directory, {
|
|
2211
|
+
debounceMs: 16,
|
|
2212
|
+
ignore: ["node_modules", ".git"],
|
|
2213
|
+
onRouteAdded: (filePath, addedRoutes) => {
|
|
2214
|
+
try {
|
|
2215
|
+
const finalRoutes = addedRoutes.map(
|
|
2216
|
+
(route) => prefix ? { ...route, path: `${prefix}${route.path}` } : route
|
|
2217
|
+
);
|
|
2218
|
+
const changes = updateRoutesFromFile(registry, filePath, finalRoutes);
|
|
2219
|
+
applyMatcherChanges(changes);
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
console.error(`\u26A0\uFE0F Error adding routes from ${directory}:`, error);
|
|
2222
|
+
}
|
|
1815
2223
|
},
|
|
2224
|
+
onRouteChanged: withPerformanceTracking(async (filePath, changedRoutes) => {
|
|
2225
|
+
try {
|
|
2226
|
+
const finalRoutes = changedRoutes.map(
|
|
2227
|
+
(route) => prefix ? { ...route, path: `${prefix}${route.path}` } : route
|
|
2228
|
+
);
|
|
2229
|
+
const changes = updateRoutesFromFile(registry, filePath, finalRoutes);
|
|
2230
|
+
applyMatcherChanges(changes);
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
console.error(`\u26A0\uFE0F Error updating routes from ${directory}:`, error);
|
|
2233
|
+
}
|
|
2234
|
+
}, directory),
|
|
1816
2235
|
onRouteRemoved: (filePath, removedRoutes) => {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
}
|
|
1827
|
-
const sources = routeSources.get(finalPath) || [];
|
|
1828
|
-
const filteredSources = sources.filter((s) => s !== source);
|
|
1829
|
-
if (filteredSources.length > 0) {
|
|
1830
|
-
routeSources.set(finalPath, filteredSources);
|
|
1831
|
-
} else {
|
|
1832
|
-
routeSources.delete(finalPath);
|
|
1833
|
-
}
|
|
1834
|
-
});
|
|
2236
|
+
try {
|
|
2237
|
+
removedRoutes.forEach((route) => {
|
|
2238
|
+
const finalPath = prefix ? `${prefix}${route.path}` : route.path;
|
|
2239
|
+
removeRouteFromMatcher(finalPath, matcher);
|
|
2240
|
+
});
|
|
2241
|
+
clearFileCache(filePath);
|
|
2242
|
+
} catch (error) {
|
|
2243
|
+
console.error(`Error removing routes from ${filePath}:`, error);
|
|
2244
|
+
}
|
|
1835
2245
|
},
|
|
1836
2246
|
onError: (error) => {
|
|
1837
|
-
console.error(
|
|
2247
|
+
console.error(`\u26A0\uFE0F Route watcher error for ${directory}:`, error);
|
|
1838
2248
|
}
|
|
1839
|
-
};
|
|
1840
|
-
}
|
|
1841
|
-
function setupWatcherForDirectory(directory, source, prefix) {
|
|
1842
|
-
const callbacks = createWatcherCallbacks(directory, source, prefix);
|
|
1843
|
-
const watcher = watchRoutes(directory, {
|
|
1844
|
-
ignore: ["node_modules", ".git"],
|
|
1845
|
-
...callbacks
|
|
1846
2249
|
});
|
|
1847
|
-
if (!_watchers) {
|
|
1848
|
-
_watchers = /* @__PURE__ */ new Map();
|
|
1849
|
-
}
|
|
1850
2250
|
_watchers.set(directory, watcher);
|
|
1851
2251
|
return watcher;
|
|
1852
2252
|
}
|
|
1853
|
-
function setupWatcherForAllDirectories() {
|
|
1854
|
-
for (const directory of routeDirectories) {
|
|
1855
|
-
setupWatcherForDirectory(directory, directory);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
2253
|
initialize().catch((error) => {
|
|
1859
|
-
console.error("Failed to initialize router on creation:", error);
|
|
2254
|
+
console.error("\u26A0\uFE0F Failed to initialize router on creation:", error);
|
|
1860
2255
|
});
|
|
1861
2256
|
return {
|
|
1862
2257
|
/**
|
|
@@ -1864,17 +2259,23 @@ function createRouter(options) {
|
|
|
1864
2259
|
*/
|
|
1865
2260
|
async handleRequest(ctx) {
|
|
1866
2261
|
if (!initialized) {
|
|
2262
|
+
console.log("\u{1F504} Router not initialized, initializing...");
|
|
1867
2263
|
await initialize();
|
|
1868
2264
|
}
|
|
1869
|
-
const { method, path:
|
|
1870
|
-
|
|
2265
|
+
const { method, path: path6 } = ctx.request;
|
|
2266
|
+
console.log(`
|
|
2267
|
+
\u{1F4E5} Handling request: ${method} ${path6}`);
|
|
2268
|
+
const match = matcher.match(path6, method);
|
|
1871
2269
|
if (!match) {
|
|
2270
|
+
console.log(`\u274C No match found for: ${method} ${path6}`);
|
|
1872
2271
|
ctx.response.status(404).json({ error: "Not Found" });
|
|
1873
2272
|
return;
|
|
1874
2273
|
}
|
|
2274
|
+
console.log(`\u2705 Route matched: ${method} ${path6}`);
|
|
2275
|
+
console.log(` Params: ${JSON.stringify(match.params)}`);
|
|
1875
2276
|
if (match.methodNotAllowed) {
|
|
1876
2277
|
ctx.response.status(405).json({
|
|
1877
|
-
error: "Method Not Allowed",
|
|
2278
|
+
error: "\u274C Method Not Allowed",
|
|
1878
2279
|
allowed: match.allowedMethods
|
|
1879
2280
|
});
|
|
1880
2281
|
if (match.allowedMethods && match.allowedMethods.length > 0) {
|
|
@@ -1893,19 +2294,28 @@ function createRouter(options) {
|
|
|
1893
2294
|
}
|
|
1894
2295
|
},
|
|
1895
2296
|
/**
|
|
1896
|
-
* Get all registered routes
|
|
2297
|
+
* Get all registered routes (using optimized registry)
|
|
1897
2298
|
*/
|
|
1898
2299
|
getRoutes() {
|
|
1899
|
-
return
|
|
2300
|
+
return getAllRoutesFromRegistry(registry);
|
|
1900
2301
|
},
|
|
1901
2302
|
/**
|
|
1902
2303
|
* Add a route programmatically
|
|
1903
2304
|
*/
|
|
1904
2305
|
addRoute(route) {
|
|
1905
|
-
|
|
2306
|
+
const changes = updateRoutesFromFile(registry, "programmatic", [route]);
|
|
2307
|
+
applyMatcherChanges(changes);
|
|
1906
2308
|
},
|
|
1907
2309
|
/**
|
|
1908
|
-
* Add
|
|
2310
|
+
* Add multiple routes programmatically with batch processing
|
|
2311
|
+
*/
|
|
2312
|
+
addRoutes(routes) {
|
|
2313
|
+
const changes = updateRoutesFromFile(registry, "programmatic", routes);
|
|
2314
|
+
applyMatcherChanges(changes);
|
|
2315
|
+
return changes;
|
|
2316
|
+
},
|
|
2317
|
+
/**
|
|
2318
|
+
* Add a route directory (for plugins) with optimized loading
|
|
1909
2319
|
*/
|
|
1910
2320
|
async addRouteDirectory(directory, options2 = {}) {
|
|
1911
2321
|
if (routeDirectories.has(directory)) {
|
|
@@ -1916,21 +2326,27 @@ function createRouter(options) {
|
|
|
1916
2326
|
if (initialized) {
|
|
1917
2327
|
await loadRoutesFromDirectory(directory, directory, options2.prefix);
|
|
1918
2328
|
if (routerOptions.watchMode) {
|
|
1919
|
-
|
|
2329
|
+
setupWatcherForNewDirectory(directory, options2.prefix);
|
|
1920
2330
|
}
|
|
1921
2331
|
}
|
|
1922
2332
|
},
|
|
1923
2333
|
/**
|
|
1924
|
-
* Get route conflicts
|
|
2334
|
+
* Get route conflicts (using registry)
|
|
1925
2335
|
*/
|
|
1926
2336
|
getRouteConflicts() {
|
|
1927
2337
|
const conflicts = [];
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2338
|
+
return conflicts;
|
|
2339
|
+
},
|
|
2340
|
+
/**
|
|
2341
|
+
* Close watchers and cleanup (useful for testing)
|
|
2342
|
+
*/
|
|
2343
|
+
async close() {
|
|
2344
|
+
if (_watchers) {
|
|
2345
|
+
for (const watcher of _watchers.values()) {
|
|
2346
|
+
await watcher.close();
|
|
1931
2347
|
}
|
|
2348
|
+
_watchers.clear();
|
|
1932
2349
|
}
|
|
1933
|
-
return conflicts;
|
|
1934
2350
|
}
|
|
1935
2351
|
};
|
|
1936
2352
|
}
|