blaizejs 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +1567 -1567
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +687 -22
- package/dist/index.d.ts +687 -22
- package/dist/index.js +1567 -1567
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* blaizejs v0.
|
|
3
|
-
* A blazing-fast,
|
|
2
|
+
* blaizejs v0.2.2
|
|
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
|
|
@@ -59,31 +59,6 @@ __export(index_exports, {
|
|
|
59
59
|
});
|
|
60
60
|
module.exports = __toCommonJS(index_exports);
|
|
61
61
|
|
|
62
|
-
// src/middleware/create.ts
|
|
63
|
-
function create(handlerOrOptions) {
|
|
64
|
-
if (typeof handlerOrOptions === "function") {
|
|
65
|
-
return {
|
|
66
|
-
name: "anonymous",
|
|
67
|
-
// Default name for function middleware
|
|
68
|
-
execute: handlerOrOptions,
|
|
69
|
-
debug: false
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
const { name = "anonymous", handler, skip, debug = false } = handlerOrOptions;
|
|
73
|
-
const middleware = {
|
|
74
|
-
name,
|
|
75
|
-
execute: handler,
|
|
76
|
-
debug
|
|
77
|
-
};
|
|
78
|
-
if (skip !== void 0) {
|
|
79
|
-
return {
|
|
80
|
-
...middleware,
|
|
81
|
-
skip
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return middleware;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
62
|
// src/middleware/execute.ts
|
|
88
63
|
function execute(middleware, ctx, next) {
|
|
89
64
|
if (!middleware) {
|
|
@@ -131,6 +106,31 @@ function compose(middlewareStack) {
|
|
|
131
106
|
};
|
|
132
107
|
}
|
|
133
108
|
|
|
109
|
+
// src/middleware/create.ts
|
|
110
|
+
function create(handlerOrOptions) {
|
|
111
|
+
if (typeof handlerOrOptions === "function") {
|
|
112
|
+
return {
|
|
113
|
+
name: "anonymous",
|
|
114
|
+
// Default name for function middleware
|
|
115
|
+
execute: handlerOrOptions,
|
|
116
|
+
debug: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const { name = "anonymous", handler, skip, debug = false } = handlerOrOptions;
|
|
120
|
+
const middleware = {
|
|
121
|
+
name,
|
|
122
|
+
execute: handler,
|
|
123
|
+
debug
|
|
124
|
+
};
|
|
125
|
+
if (skip !== void 0) {
|
|
126
|
+
return {
|
|
127
|
+
...middleware,
|
|
128
|
+
skip
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return middleware;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
134
|
// src/plugins/create.ts
|
|
135
135
|
function create2(name, version, setup, defaultOptions = {}) {
|
|
136
136
|
if (!name || typeof name !== "string") {
|
|
@@ -159,48 +159,20 @@ function create2(name, version, setup, defaultOptions = {}) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// src/
|
|
163
|
-
var
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const absoluteDir = path.isAbsolute(routesDir) ? routesDir : path.resolve(process.cwd(), routesDir);
|
|
167
|
-
console.log("Creating router with routes directory:", absoluteDir);
|
|
168
|
-
try {
|
|
169
|
-
const stats = await fs.stat(absoluteDir);
|
|
170
|
-
if (!stats.isDirectory()) {
|
|
171
|
-
throw new Error(`Route directory is not a directory: ${absoluteDir}`);
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (error.code === "ENOENT") {
|
|
175
|
-
throw new Error(`Route directory not found: ${absoluteDir}`);
|
|
176
|
-
}
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
const routeFiles = [];
|
|
180
|
-
const ignore = options.ignore || ["node_modules", ".git"];
|
|
181
|
-
async function scanDirectory(dir) {
|
|
182
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
183
|
-
for (const entry of entries) {
|
|
184
|
-
const fullPath = path.join(dir, entry.name);
|
|
185
|
-
if (entry.isDirectory() && ignore.includes(entry.name)) {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (entry.isDirectory()) {
|
|
189
|
-
await scanDirectory(fullPath);
|
|
190
|
-
} else if (isRouteFile(entry.name)) {
|
|
191
|
-
routeFiles.push(fullPath);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
await scanDirectory(absoluteDir);
|
|
196
|
-
return routeFiles;
|
|
162
|
+
// src/config.ts
|
|
163
|
+
var config = {};
|
|
164
|
+
function setRuntimeConfig(newConfig) {
|
|
165
|
+
config = { ...config, ...newConfig };
|
|
197
166
|
}
|
|
198
|
-
function
|
|
199
|
-
|
|
167
|
+
function getRoutesDir() {
|
|
168
|
+
if (!config.routesDir) {
|
|
169
|
+
throw new Error("Routes directory not configured. Make sure server is properly initialized.");
|
|
170
|
+
}
|
|
171
|
+
return config.routesDir;
|
|
200
172
|
}
|
|
201
173
|
|
|
202
174
|
// src/router/discovery/parser.ts
|
|
203
|
-
var
|
|
175
|
+
var path = __toESM(require("path"), 1);
|
|
204
176
|
function parseRoutePath(filePath, basePath) {
|
|
205
177
|
if (filePath.startsWith("file://")) {
|
|
206
178
|
filePath = filePath.replace("file://", "");
|
|
@@ -220,7 +192,7 @@ function parseRoutePath(filePath, basePath) {
|
|
|
220
192
|
relativePath = relativePath.substring(1);
|
|
221
193
|
}
|
|
222
194
|
} else {
|
|
223
|
-
relativePath =
|
|
195
|
+
relativePath = path.relative(forwardSlashBasePath, forwardSlashFilePath).replace(/\\/g, "/");
|
|
224
196
|
}
|
|
225
197
|
relativePath = relativePath.replace(/\.[^.]+$/, "");
|
|
226
198
|
const segments = relativePath.split("/").filter(Boolean);
|
|
@@ -244,1697 +216,1725 @@ function parseRoutePath(filePath, basePath) {
|
|
|
244
216
|
};
|
|
245
217
|
}
|
|
246
218
|
|
|
247
|
-
// src/router/
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
async function loadRouteModule(filePath, basePath) {
|
|
219
|
+
// src/router/create.ts
|
|
220
|
+
function getCallerFilePath() {
|
|
221
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
252
222
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (module2.default && typeof module2.default === "object") {
|
|
259
|
-
console.log("Found default export:", module2.default);
|
|
260
|
-
const route = {
|
|
261
|
-
...module2.default,
|
|
262
|
-
path: parsedRoute.routePath
|
|
263
|
-
};
|
|
264
|
-
routes.push(route);
|
|
223
|
+
Error.prepareStackTrace = (_, stack2) => stack2;
|
|
224
|
+
const stack = new Error().stack;
|
|
225
|
+
const callerFrame = stack[3];
|
|
226
|
+
if (!callerFrame || typeof callerFrame.getFileName !== "function") {
|
|
227
|
+
throw new Error("Unable to determine caller file frame");
|
|
265
228
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
const potentialRoute = exportValue;
|
|
271
|
-
if (isValidRoute(potentialRoute)) {
|
|
272
|
-
console.log(`Found named route export: ${exportName}`, potentialRoute);
|
|
273
|
-
const route = {
|
|
274
|
-
...potentialRoute,
|
|
275
|
-
// Use the route's own path if it has one, otherwise derive from file
|
|
276
|
-
path: parsedRoute.routePath
|
|
277
|
-
};
|
|
278
|
-
routes.push(route);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
if (routes.length === 0) {
|
|
282
|
-
console.warn(`Route file ${filePath} does not export any valid route definitions`);
|
|
283
|
-
return [];
|
|
229
|
+
const fileName = callerFrame.getFileName();
|
|
230
|
+
if (!fileName) {
|
|
231
|
+
throw new Error("Unable to determine caller file name");
|
|
284
232
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
console.error(`Failed to load route module ${filePath}:`, error);
|
|
289
|
-
return [];
|
|
233
|
+
return fileName;
|
|
234
|
+
} finally {
|
|
235
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
290
236
|
}
|
|
291
237
|
}
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
);
|
|
300
|
-
return hasHttpMethod;
|
|
238
|
+
function getRoutePath() {
|
|
239
|
+
console.log("getRoutePath called");
|
|
240
|
+
const callerPath = getCallerFilePath();
|
|
241
|
+
const routesDir = getRoutesDir();
|
|
242
|
+
const parsedRoute = parseRoutePath(callerPath, routesDir);
|
|
243
|
+
console.log(`Parsed route path: ${parsedRoute.routePath} from file: ${callerPath}`);
|
|
244
|
+
return parsedRoute.routePath;
|
|
301
245
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
246
|
+
var createGetRoute = (config2) => {
|
|
247
|
+
validateMethodConfig("GET", config2);
|
|
248
|
+
const path5 = getRoutePath();
|
|
249
|
+
return {
|
|
250
|
+
GET: config2,
|
|
251
|
+
path: path5
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
var createPostRoute = (config2) => {
|
|
255
|
+
validateMethodConfig("POST", config2);
|
|
256
|
+
const path5 = getRoutePath();
|
|
257
|
+
return {
|
|
258
|
+
POST: config2,
|
|
259
|
+
path: path5
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
var createPutRoute = (config2) => {
|
|
263
|
+
validateMethodConfig("PUT", config2);
|
|
264
|
+
const path5 = getRoutePath();
|
|
265
|
+
return {
|
|
266
|
+
PUT: config2,
|
|
267
|
+
path: path5
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
var createDeleteRoute = (config2) => {
|
|
271
|
+
validateMethodConfig("DELETE", config2);
|
|
272
|
+
const path5 = getRoutePath();
|
|
273
|
+
return {
|
|
274
|
+
DELETE: config2,
|
|
275
|
+
path: path5
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
var createPatchRoute = (config2) => {
|
|
279
|
+
validateMethodConfig("PATCH", config2);
|
|
280
|
+
const path5 = getRoutePath();
|
|
281
|
+
return {
|
|
282
|
+
PATCH: config2,
|
|
283
|
+
path: path5
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
var createHeadRoute = (config2) => {
|
|
287
|
+
validateMethodConfig("HEAD", config2);
|
|
288
|
+
const path5 = getRoutePath();
|
|
289
|
+
return {
|
|
290
|
+
HEAD: config2,
|
|
291
|
+
path: path5
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
var createOptionsRoute = (config2) => {
|
|
295
|
+
validateMethodConfig("OPTIONS", config2);
|
|
296
|
+
const path5 = getRoutePath();
|
|
297
|
+
return {
|
|
298
|
+
OPTIONS: config2,
|
|
299
|
+
path: path5
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
function validateMethodConfig(method, config2) {
|
|
303
|
+
if (!config2.handler || typeof config2.handler !== "function") {
|
|
304
|
+
throw new Error(`Handler for method ${method} must be a function`);
|
|
314
305
|
}
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// src/router/discovery/watchers.ts
|
|
319
|
-
var path3 = __toESM(require("path"), 1);
|
|
320
|
-
var import_chokidar = require("chokidar");
|
|
321
|
-
function watchRoutes(routesDir, options = {}) {
|
|
322
|
-
const routesByPath = /* @__PURE__ */ new Map();
|
|
323
|
-
async function loadInitialRoutes() {
|
|
324
|
-
try {
|
|
325
|
-
const files = await findRouteFiles(routesDir, {
|
|
326
|
-
ignore: options.ignore
|
|
327
|
-
});
|
|
328
|
-
for (const filePath of files) {
|
|
329
|
-
await loadAndNotify(filePath);
|
|
330
|
-
}
|
|
331
|
-
} catch (error) {
|
|
332
|
-
handleError(error);
|
|
333
|
-
}
|
|
306
|
+
if (config2.middleware && !Array.isArray(config2.middleware)) {
|
|
307
|
+
throw new Error(`Middleware for method ${method} must be an array`);
|
|
334
308
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
if (options.onRouteChanged) {
|
|
345
|
-
options.onRouteChanged(routes);
|
|
346
|
-
}
|
|
347
|
-
} else {
|
|
348
|
-
routesByPath.set(filePath, routes);
|
|
349
|
-
if (options.onRouteAdded) {
|
|
350
|
-
options.onRouteAdded(routes);
|
|
351
|
-
}
|
|
309
|
+
if (config2.schema) {
|
|
310
|
+
validateSchema(method, config2.schema);
|
|
311
|
+
}
|
|
312
|
+
switch (method) {
|
|
313
|
+
case "GET":
|
|
314
|
+
case "HEAD":
|
|
315
|
+
case "DELETE":
|
|
316
|
+
if (config2.schema?.body) {
|
|
317
|
+
console.warn(`Warning: ${method} requests typically don't have request bodies`);
|
|
352
318
|
}
|
|
353
|
-
|
|
354
|
-
handleError(error);
|
|
355
|
-
}
|
|
319
|
+
break;
|
|
356
320
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
routesByPath.delete(normalizedPath);
|
|
321
|
+
}
|
|
322
|
+
function validateSchema(method, schema) {
|
|
323
|
+
const { params, query, body, response } = schema;
|
|
324
|
+
if (params && (!params._def || typeof params.parse !== "function")) {
|
|
325
|
+
throw new Error(`Params schema for ${method} must be a valid Zod schema`);
|
|
364
326
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
options.onError(error);
|
|
368
|
-
} else {
|
|
369
|
-
console.error("Route watcher error:", error);
|
|
370
|
-
}
|
|
327
|
+
if (query && (!query._def || typeof query.parse !== "function")) {
|
|
328
|
+
throw new Error(`Query schema for ${method} must be a valid Zod schema`);
|
|
371
329
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
/(^|[/\\])\../,
|
|
375
|
-
// Ignore dot files
|
|
376
|
-
/node_modules/,
|
|
377
|
-
...options.ignore || []
|
|
378
|
-
],
|
|
379
|
-
persistent: true,
|
|
380
|
-
ignoreInitial: false,
|
|
381
|
-
awaitWriteFinish: {
|
|
382
|
-
stabilityThreshold: 300,
|
|
383
|
-
pollInterval: 100
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
watcher.on("add", loadAndNotify).on("change", loadAndNotify).on("unlink", handleRemoved).on("error", handleError);
|
|
387
|
-
loadInitialRoutes().catch(handleError);
|
|
388
|
-
return {
|
|
389
|
-
/**
|
|
390
|
-
* Close the watcher
|
|
391
|
-
*/
|
|
392
|
-
close: () => watcher.close(),
|
|
393
|
-
/**
|
|
394
|
-
* Get all currently loaded routes (flattened)
|
|
395
|
-
*/
|
|
396
|
-
getRoutes: () => {
|
|
397
|
-
const allRoutes = [];
|
|
398
|
-
for (const routes of routesByPath.values()) {
|
|
399
|
-
allRoutes.push(...routes);
|
|
400
|
-
}
|
|
401
|
-
return allRoutes;
|
|
402
|
-
},
|
|
403
|
-
/**
|
|
404
|
-
* Get routes organized by file path
|
|
405
|
-
*/
|
|
406
|
-
getRoutesByFile: () => new Map(routesByPath)
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// src/router/handlers/error.ts
|
|
411
|
-
function handleRouteError(ctx, error, options = {}) {
|
|
412
|
-
if (options.log) {
|
|
413
|
-
console.error("Route error:", error);
|
|
330
|
+
if (body && (!body._def || typeof body.parse !== "function")) {
|
|
331
|
+
throw new Error(`Body schema for ${method} must be a valid Zod schema`);
|
|
414
332
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
error: getErrorType(error),
|
|
418
|
-
message: getErrorMessage(error)
|
|
419
|
-
};
|
|
420
|
-
if (options.detailed) {
|
|
421
|
-
if (error instanceof Error) {
|
|
422
|
-
response.stack = error.stack;
|
|
423
|
-
}
|
|
424
|
-
if (error && typeof error === "object" && "details" in error && error.details) {
|
|
425
|
-
response.details = error.details;
|
|
426
|
-
}
|
|
333
|
+
if (response && (!response._def || typeof response.parse !== "function")) {
|
|
334
|
+
throw new Error(`Response schema for ${method} must be a valid Zod schema`);
|
|
427
335
|
}
|
|
428
|
-
ctx.response.status(status).json(response);
|
|
429
336
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
337
|
+
|
|
338
|
+
// src/server/create.ts
|
|
339
|
+
var import_node_async_hooks2 = require("async_hooks");
|
|
340
|
+
var import_node_events = __toESM(require("events"), 1);
|
|
341
|
+
|
|
342
|
+
// src/server/start.ts
|
|
343
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
344
|
+
var http = __toESM(require("http"), 1);
|
|
345
|
+
var http2 = __toESM(require("http2"), 1);
|
|
346
|
+
|
|
347
|
+
// src/server/dev-certificate.ts
|
|
348
|
+
var fs = __toESM(require("fs"), 1);
|
|
349
|
+
var path2 = __toESM(require("path"), 1);
|
|
350
|
+
var selfsigned = __toESM(require("selfsigned"), 1);
|
|
351
|
+
async function generateDevCertificates() {
|
|
352
|
+
const certDir = path2.join(process.cwd(), ".blaizejs", "certs");
|
|
353
|
+
const keyPath = path2.join(certDir, "dev.key");
|
|
354
|
+
const certPath = path2.join(certDir, "dev.cert");
|
|
355
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
356
|
+
return {
|
|
357
|
+
keyFile: keyPath,
|
|
358
|
+
certFile: certPath
|
|
359
|
+
};
|
|
441
360
|
}
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
function getStatusFromCode(code) {
|
|
445
|
-
switch (code) {
|
|
446
|
-
case "NOT_FOUND":
|
|
447
|
-
return 404;
|
|
448
|
-
case "UNAUTHORIZED":
|
|
449
|
-
return 401;
|
|
450
|
-
case "FORBIDDEN":
|
|
451
|
-
return 403;
|
|
452
|
-
case "BAD_REQUEST":
|
|
453
|
-
return 400;
|
|
454
|
-
case "CONFLICT":
|
|
455
|
-
return 409;
|
|
456
|
-
default:
|
|
457
|
-
return 500;
|
|
361
|
+
if (!fs.existsSync(certDir)) {
|
|
362
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
458
363
|
}
|
|
364
|
+
const attrs = [{ name: "commonName", value: "localhost" }];
|
|
365
|
+
const options = {
|
|
366
|
+
days: 365,
|
|
367
|
+
algorithm: "sha256",
|
|
368
|
+
keySize: 2048,
|
|
369
|
+
extensions: [
|
|
370
|
+
{ name: "basicConstraints", cA: true },
|
|
371
|
+
{
|
|
372
|
+
name: "keyUsage",
|
|
373
|
+
keyCertSign: true,
|
|
374
|
+
digitalSignature: true,
|
|
375
|
+
nonRepudiation: true,
|
|
376
|
+
keyEncipherment: true,
|
|
377
|
+
dataEncipherment: true
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: "extKeyUsage",
|
|
381
|
+
serverAuth: true,
|
|
382
|
+
clientAuth: true
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "subjectAltName",
|
|
386
|
+
altNames: [
|
|
387
|
+
{ type: 2, value: "localhost" },
|
|
388
|
+
{ type: 7, ip: "127.0.0.1" }
|
|
389
|
+
]
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
};
|
|
393
|
+
const pems = selfsigned.generate(attrs, options);
|
|
394
|
+
fs.writeFileSync(keyPath, Buffer.from(pems.private, "utf-8"));
|
|
395
|
+
fs.writeFileSync(certPath, Buffer.from(pems.cert, "utf-8"));
|
|
396
|
+
console.log(`
|
|
397
|
+
\u{1F512} Generated self-signed certificates for development at ${certDir}
|
|
398
|
+
`);
|
|
399
|
+
return {
|
|
400
|
+
keyFile: keyPath,
|
|
401
|
+
certFile: certPath
|
|
402
|
+
};
|
|
459
403
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
return error.name;
|
|
467
|
-
}
|
|
468
|
-
if (error instanceof Error) {
|
|
469
|
-
return error.constructor.name;
|
|
470
|
-
}
|
|
404
|
+
|
|
405
|
+
// src/context/errors.ts
|
|
406
|
+
var ResponseSentError = class extends Error {
|
|
407
|
+
constructor(message = "\u274C Response has already been sent") {
|
|
408
|
+
super(message);
|
|
409
|
+
this.name = "ResponseSentError";
|
|
471
410
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return error.message;
|
|
411
|
+
};
|
|
412
|
+
var ResponseSentHeaderError = class extends ResponseSentError {
|
|
413
|
+
constructor(message = "Cannot set header after response has been sent") {
|
|
414
|
+
super(message);
|
|
477
415
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
416
|
+
};
|
|
417
|
+
var ResponseSentContentError = class extends ResponseSentError {
|
|
418
|
+
constructor(message = "Cannot set content type after response has been sent") {
|
|
419
|
+
super(message);
|
|
482
420
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
var import_zod = require("zod");
|
|
488
|
-
function validateBody(body, schema) {
|
|
489
|
-
if (schema instanceof import_zod.z.ZodObject) {
|
|
490
|
-
return schema.strict().parse(body);
|
|
421
|
+
};
|
|
422
|
+
var ParseUrlError = class extends ResponseSentError {
|
|
423
|
+
constructor(message = "Invalide URL") {
|
|
424
|
+
super(message);
|
|
491
425
|
}
|
|
492
|
-
|
|
493
|
-
}
|
|
426
|
+
};
|
|
494
427
|
|
|
495
|
-
// src/
|
|
496
|
-
var
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
return schema.parse(params);
|
|
428
|
+
// src/context/store.ts
|
|
429
|
+
var import_node_async_hooks = require("async_hooks");
|
|
430
|
+
var contextStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
431
|
+
function runWithContext(context, callback) {
|
|
432
|
+
return contextStorage.run(context, callback);
|
|
502
433
|
}
|
|
503
434
|
|
|
504
|
-
// src/
|
|
505
|
-
var
|
|
506
|
-
function
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
435
|
+
// src/context/create.ts
|
|
436
|
+
var CONTENT_TYPE_HEADER = "Content-Type";
|
|
437
|
+
function parseRequestUrl(req) {
|
|
438
|
+
const originalUrl = req.url || "/";
|
|
439
|
+
const host = req.headers.host || "localhost";
|
|
440
|
+
const protocol = req.socket && req.socket.encrypted ? "https" : "http";
|
|
441
|
+
const fullUrl = `${protocol}://${host}${originalUrl.startsWith("/") ? "" : "/"}${originalUrl}`;
|
|
442
|
+
try {
|
|
443
|
+
const url = new URL(fullUrl);
|
|
444
|
+
const path5 = url.pathname;
|
|
445
|
+
const query = {};
|
|
446
|
+
url.searchParams.forEach((value, key) => {
|
|
447
|
+
if (query[key] !== void 0) {
|
|
448
|
+
if (Array.isArray(query[key])) {
|
|
449
|
+
query[key].push(value);
|
|
450
|
+
} else {
|
|
451
|
+
query[key] = [query[key], value];
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
query[key] = value;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
return { path: path5, url, query };
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.warn(`Invalid URL: ${fullUrl}`, error);
|
|
460
|
+
throw new ParseUrlError(`Invalid URL: ${fullUrl}`);
|
|
518
461
|
}
|
|
519
|
-
return schema.parse(response);
|
|
520
462
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
if (schema.query && ctx.request.query) {
|
|
534
|
-
try {
|
|
535
|
-
ctx.request.query = validateQuery(ctx.request.query, schema.query);
|
|
536
|
-
} catch (error) {
|
|
537
|
-
errors.query = formatValidationError(error);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
if (schema.body) {
|
|
541
|
-
try {
|
|
542
|
-
ctx.request.body = validateBody(ctx.request.body, schema.body);
|
|
543
|
-
} catch (error) {
|
|
544
|
-
errors.body = formatValidationError(error);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
if (Object.keys(errors).length > 0) {
|
|
548
|
-
ctx.response.status(400).json({
|
|
549
|
-
error: "Validation Error",
|
|
550
|
-
details: errors
|
|
551
|
-
});
|
|
552
|
-
return;
|
|
463
|
+
function isHttp2Request(req) {
|
|
464
|
+
return "stream" in req || "httpVersionMajor" in req && req.httpVersionMajor === 2;
|
|
465
|
+
}
|
|
466
|
+
function getProtocol(req) {
|
|
467
|
+
const encrypted = req.socket && req.socket.encrypted;
|
|
468
|
+
const forwardedProto = req.headers["x-forwarded-proto"];
|
|
469
|
+
if (forwardedProto) {
|
|
470
|
+
if (Array.isArray(forwardedProto)) {
|
|
471
|
+
return forwardedProto[0]?.split(",")[0]?.trim() || "http";
|
|
472
|
+
} else {
|
|
473
|
+
return forwardedProto.split(",")[0]?.trim() || "http";
|
|
553
474
|
}
|
|
554
|
-
|
|
475
|
+
}
|
|
476
|
+
return encrypted ? "https" : "http";
|
|
477
|
+
}
|
|
478
|
+
async function createContext(req, res, options = {}) {
|
|
479
|
+
const { path: path5, url, query } = parseRequestUrl(req);
|
|
480
|
+
const method = req.method || "GET";
|
|
481
|
+
const isHttp2 = isHttp2Request(req);
|
|
482
|
+
const protocol = getProtocol(req);
|
|
483
|
+
const params = {};
|
|
484
|
+
const state = { ...options.initialState || {} };
|
|
485
|
+
const responseState = { sent: false };
|
|
486
|
+
const ctx = {
|
|
487
|
+
request: createRequestObject(req, {
|
|
488
|
+
path: path5,
|
|
489
|
+
url,
|
|
490
|
+
query,
|
|
491
|
+
params,
|
|
492
|
+
method,
|
|
493
|
+
isHttp2,
|
|
494
|
+
protocol
|
|
495
|
+
}),
|
|
496
|
+
response: {},
|
|
497
|
+
state
|
|
555
498
|
};
|
|
499
|
+
ctx.response = createResponseObject(res, responseState, ctx);
|
|
500
|
+
if (options.parseBody) {
|
|
501
|
+
await parseBodyIfNeeded(req, ctx);
|
|
502
|
+
}
|
|
503
|
+
return ctx;
|
|
504
|
+
}
|
|
505
|
+
function createRequestObject(req, info) {
|
|
556
506
|
return {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
507
|
+
raw: req,
|
|
508
|
+
...info,
|
|
509
|
+
header: createRequestHeaderGetter(req),
|
|
510
|
+
headers: createRequestHeadersGetter(req),
|
|
511
|
+
body: void 0
|
|
560
512
|
};
|
|
561
513
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
514
|
+
function createRequestHeaderGetter(req) {
|
|
515
|
+
return (name) => {
|
|
516
|
+
const value = req.headers[name.toLowerCase()];
|
|
517
|
+
if (Array.isArray(value)) {
|
|
518
|
+
return value.join(", ");
|
|
519
|
+
}
|
|
520
|
+
return value || void 0;
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function createRequestHeadersGetter(req) {
|
|
524
|
+
const headerGetter = createRequestHeaderGetter(req);
|
|
525
|
+
return (names) => {
|
|
526
|
+
if (names && Array.isArray(names) && names.length > 0) {
|
|
527
|
+
return names.reduce((acc, name) => {
|
|
528
|
+
acc[name] = headerGetter(name);
|
|
529
|
+
return acc;
|
|
530
|
+
}, {});
|
|
531
|
+
} else {
|
|
532
|
+
return Object.entries(req.headers).reduce(
|
|
533
|
+
(acc, [key, value]) => {
|
|
534
|
+
acc[key] = Array.isArray(value) ? value.join(", ") : value || void 0;
|
|
535
|
+
return acc;
|
|
536
|
+
},
|
|
537
|
+
{}
|
|
538
|
+
);
|
|
539
|
+
}
|
|
581
540
|
};
|
|
541
|
+
}
|
|
542
|
+
function createResponseObject(res, responseState, ctx) {
|
|
582
543
|
return {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
544
|
+
raw: res,
|
|
545
|
+
get sent() {
|
|
546
|
+
return responseState.sent;
|
|
547
|
+
},
|
|
548
|
+
status: createStatusSetter(res, responseState, ctx),
|
|
549
|
+
header: createHeaderSetter(res, responseState, ctx),
|
|
550
|
+
headers: createHeadersSetter(res, responseState, ctx),
|
|
551
|
+
type: createContentTypeSetter(res, responseState, ctx),
|
|
552
|
+
json: createJsonResponder(res, responseState),
|
|
553
|
+
text: createTextResponder(res, responseState),
|
|
554
|
+
html: createHtmlResponder(res, responseState),
|
|
555
|
+
redirect: createRedirectResponder(res, responseState),
|
|
556
|
+
stream: createStreamResponder(res, responseState)
|
|
586
557
|
};
|
|
587
558
|
}
|
|
588
|
-
function
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
559
|
+
function createStatusSetter(res, responseState, ctx) {
|
|
560
|
+
return function statusSetter(code) {
|
|
561
|
+
if (responseState.sent) {
|
|
562
|
+
throw new ResponseSentError();
|
|
563
|
+
}
|
|
564
|
+
res.statusCode = code;
|
|
565
|
+
return ctx.response;
|
|
566
|
+
};
|
|
593
567
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (routeOptions.schema) {
|
|
599
|
-
if (routeOptions.schema.params || routeOptions.schema.query || routeOptions.schema.body) {
|
|
600
|
-
middleware.unshift(createRequestValidator(routeOptions.schema));
|
|
568
|
+
function createHeaderSetter(res, responseState, ctx) {
|
|
569
|
+
return function headerSetter(name, value) {
|
|
570
|
+
if (responseState.sent) {
|
|
571
|
+
throw new ResponseSentHeaderError();
|
|
601
572
|
}
|
|
602
|
-
|
|
603
|
-
|
|
573
|
+
res.setHeader(name, value);
|
|
574
|
+
return ctx.response;
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function createHeadersSetter(res, responseState, ctx) {
|
|
578
|
+
return function headersSetter(headers) {
|
|
579
|
+
if (responseState.sent) {
|
|
580
|
+
throw new ResponseSentHeaderError();
|
|
604
581
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
await handler(ctx, async () => {
|
|
608
|
-
const result = await routeOptions.handler(ctx, params);
|
|
609
|
-
if (!ctx.response.sent && result !== void 0) {
|
|
610
|
-
ctx.response.json(result);
|
|
582
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
583
|
+
res.setHeader(name, value);
|
|
611
584
|
}
|
|
612
|
-
|
|
585
|
+
return ctx.response;
|
|
586
|
+
};
|
|
613
587
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
623
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
624
|
-
}
|
|
625
|
-
return params;
|
|
588
|
+
function createContentTypeSetter(res, responseState, ctx) {
|
|
589
|
+
return function typeSetter(type) {
|
|
590
|
+
if (responseState.sent) {
|
|
591
|
+
throw new ResponseSentContentError();
|
|
592
|
+
}
|
|
593
|
+
res.setHeader(CONTENT_TYPE_HEADER, type);
|
|
594
|
+
return ctx.response;
|
|
595
|
+
};
|
|
626
596
|
}
|
|
627
|
-
function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return "/([^/]+)";
|
|
639
|
-
}).replace(/\/\[([^\]]+)\]/g, (_, paramName) => {
|
|
640
|
-
paramNames.push(paramName);
|
|
641
|
-
return "/([^/]+)";
|
|
642
|
-
});
|
|
643
|
-
patternString = `${patternString}(?:/)?`;
|
|
644
|
-
const pattern = new RegExp(`^${patternString}$`);
|
|
645
|
-
return {
|
|
646
|
-
pattern,
|
|
647
|
-
paramNames
|
|
597
|
+
function createJsonResponder(res, responseState) {
|
|
598
|
+
return function jsonResponder(body, status) {
|
|
599
|
+
if (responseState.sent) {
|
|
600
|
+
throw new ResponseSentError();
|
|
601
|
+
}
|
|
602
|
+
if (status !== void 0) {
|
|
603
|
+
res.statusCode = status;
|
|
604
|
+
}
|
|
605
|
+
res.setHeader(CONTENT_TYPE_HEADER, "application/json");
|
|
606
|
+
res.end(JSON.stringify(body));
|
|
607
|
+
responseState.sent = true;
|
|
648
608
|
};
|
|
649
609
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
return {
|
|
655
|
-
/**
|
|
656
|
-
* Add a route to the matcher
|
|
657
|
-
*/
|
|
658
|
-
add(path5, method, routeOptions) {
|
|
659
|
-
const { pattern, paramNames } = compilePathPattern(path5);
|
|
660
|
-
const newRoute = {
|
|
661
|
-
path: path5,
|
|
662
|
-
method,
|
|
663
|
-
pattern,
|
|
664
|
-
paramNames,
|
|
665
|
-
routeOptions
|
|
666
|
-
};
|
|
667
|
-
const insertIndex = routes.findIndex((route) => paramNames.length < route.paramNames.length);
|
|
668
|
-
if (insertIndex === -1) {
|
|
669
|
-
routes.push(newRoute);
|
|
670
|
-
} else {
|
|
671
|
-
routes.splice(insertIndex, 0, newRoute);
|
|
672
|
-
}
|
|
673
|
-
},
|
|
674
|
-
/**
|
|
675
|
-
* Match a URL path to a route
|
|
676
|
-
*/
|
|
677
|
-
match(path5, method) {
|
|
678
|
-
const pathname = path5.split("?")[0];
|
|
679
|
-
if (!pathname) return null;
|
|
680
|
-
for (const route of routes) {
|
|
681
|
-
if (route.method !== method) continue;
|
|
682
|
-
const match = route.pattern.exec(pathname);
|
|
683
|
-
if (match) {
|
|
684
|
-
const params = extractParams(path5, route.pattern, route.paramNames);
|
|
685
|
-
return {
|
|
686
|
-
route: route.routeOptions,
|
|
687
|
-
params
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
const matchingPath = routes.find(
|
|
692
|
-
(route) => route.method !== method && route.pattern.test(path5)
|
|
693
|
-
);
|
|
694
|
-
if (matchingPath) {
|
|
695
|
-
return {
|
|
696
|
-
route: null,
|
|
697
|
-
params: {},
|
|
698
|
-
methodNotAllowed: true,
|
|
699
|
-
allowedMethods: routes.filter((route) => route.pattern.test(path5)).map((route) => route.method)
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
return null;
|
|
703
|
-
},
|
|
704
|
-
/**
|
|
705
|
-
* Get all registered routes
|
|
706
|
-
*/
|
|
707
|
-
getRoutes() {
|
|
708
|
-
return routes.map((route) => ({
|
|
709
|
-
path: route.path,
|
|
710
|
-
method: route.method
|
|
711
|
-
}));
|
|
712
|
-
},
|
|
713
|
-
/**
|
|
714
|
-
* Find routes matching a specific path
|
|
715
|
-
*/
|
|
716
|
-
findRoutes(path5) {
|
|
717
|
-
return routes.filter((route) => route.pattern.test(path5)).map((route) => ({
|
|
718
|
-
path: route.path,
|
|
719
|
-
method: route.method,
|
|
720
|
-
params: extractParams(path5, route.pattern, route.paramNames)
|
|
721
|
-
}));
|
|
610
|
+
function createTextResponder(res, responseState) {
|
|
611
|
+
return function textResponder(body, status) {
|
|
612
|
+
if (responseState.sent) {
|
|
613
|
+
throw new ResponseSentError();
|
|
722
614
|
}
|
|
615
|
+
if (status !== void 0) {
|
|
616
|
+
res.statusCode = status;
|
|
617
|
+
}
|
|
618
|
+
res.setHeader(CONTENT_TYPE_HEADER, "text/plain");
|
|
619
|
+
res.end(body);
|
|
620
|
+
responseState.sent = true;
|
|
723
621
|
};
|
|
724
622
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
623
|
+
function createHtmlResponder(res, responseState) {
|
|
624
|
+
return function htmlResponder(body, status) {
|
|
625
|
+
if (responseState.sent) {
|
|
626
|
+
throw new ResponseSentError();
|
|
627
|
+
}
|
|
628
|
+
if (status !== void 0) {
|
|
629
|
+
res.statusCode = status;
|
|
630
|
+
}
|
|
631
|
+
res.setHeader(CONTENT_TYPE_HEADER, "text/html");
|
|
632
|
+
res.end(body);
|
|
633
|
+
responseState.sent = true;
|
|
736
634
|
};
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
let initialized = false;
|
|
743
|
-
let initializationPromise = null;
|
|
744
|
-
let _watchers = null;
|
|
745
|
-
const routeSources = /* @__PURE__ */ new Map();
|
|
746
|
-
const routeDirectories = /* @__PURE__ */ new Set([routerOptions.routesDir]);
|
|
747
|
-
function addRouteWithSource(route, source) {
|
|
748
|
-
const existingSources = routeSources.get(route.path) || [];
|
|
749
|
-
if (existingSources.includes(source)) {
|
|
750
|
-
console.warn(`Skipping duplicate route: ${route.path} from ${source}`);
|
|
751
|
-
return;
|
|
635
|
+
}
|
|
636
|
+
function createRedirectResponder(res, responseState) {
|
|
637
|
+
return function redirectResponder(url, status = 302) {
|
|
638
|
+
if (responseState.sent) {
|
|
639
|
+
throw new ResponseSentError();
|
|
752
640
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
641
|
+
res.statusCode = status;
|
|
642
|
+
res.setHeader("Location", url);
|
|
643
|
+
res.end();
|
|
644
|
+
responseState.sent = true;
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function createStreamResponder(res, responseState) {
|
|
648
|
+
return function streamResponder(readable, options = {}) {
|
|
649
|
+
if (responseState.sent) {
|
|
650
|
+
throw new ResponseSentError();
|
|
759
651
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
async function loadRoutesFromDirectory(directory, source, prefix) {
|
|
764
|
-
try {
|
|
765
|
-
const discoveredRoutes = await findRoutes(directory, {
|
|
766
|
-
basePath: routerOptions.basePath
|
|
767
|
-
});
|
|
768
|
-
for (const route of discoveredRoutes) {
|
|
769
|
-
const finalRoute = prefix ? {
|
|
770
|
-
...route,
|
|
771
|
-
path: `${prefix}${route.path}`
|
|
772
|
-
} : route;
|
|
773
|
-
addRouteWithSource(finalRoute, source);
|
|
774
|
-
}
|
|
775
|
-
console.log(
|
|
776
|
-
`Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""}`
|
|
777
|
-
);
|
|
778
|
-
} catch (error) {
|
|
779
|
-
console.error(`Failed to load routes from ${source}:`, error);
|
|
780
|
-
throw error;
|
|
652
|
+
if (options.status !== void 0) {
|
|
653
|
+
res.statusCode = options.status;
|
|
781
654
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (initialized || initializationPromise) {
|
|
785
|
-
return initializationPromise;
|
|
655
|
+
if (options.contentType) {
|
|
656
|
+
res.setHeader(CONTENT_TYPE_HEADER, options.contentType);
|
|
786
657
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
await loadRoutesFromDirectory(directory, directory);
|
|
791
|
-
}
|
|
792
|
-
if (routerOptions.watchMode) {
|
|
793
|
-
setupWatcherForAllDirectories();
|
|
794
|
-
}
|
|
795
|
-
initialized = true;
|
|
796
|
-
} catch (error) {
|
|
797
|
-
console.error("Failed to initialize router:", error);
|
|
798
|
-
throw error;
|
|
658
|
+
if (options.headers) {
|
|
659
|
+
for (const [name, value] of Object.entries(options.headers)) {
|
|
660
|
+
res.setHeader(name, value);
|
|
799
661
|
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
routes.push(route);
|
|
805
|
-
Object.entries(route).forEach(([method, methodOptions]) => {
|
|
806
|
-
if (method === "path" || !methodOptions) return;
|
|
807
|
-
matcher.add(route.path, method, methodOptions);
|
|
662
|
+
}
|
|
663
|
+
readable.pipe(res);
|
|
664
|
+
readable.on("end", () => {
|
|
665
|
+
responseState.sent = true;
|
|
808
666
|
});
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
addedRoutes.map((r) => r.path)
|
|
816
|
-
);
|
|
817
|
-
addedRoutes.forEach((route) => {
|
|
818
|
-
const finalRoute = prefix ? { ...route, path: `${prefix}${route.path}` } : route;
|
|
819
|
-
addRouteWithSource(finalRoute, source);
|
|
820
|
-
});
|
|
821
|
-
},
|
|
822
|
-
onRouteChanged: (changedRoutes) => {
|
|
823
|
-
console.log(
|
|
824
|
-
`${changedRoutes.length} route(s) changed in ${directory}:`,
|
|
825
|
-
changedRoutes.map((r) => r.path)
|
|
826
|
-
);
|
|
827
|
-
changedRoutes.forEach((route) => {
|
|
828
|
-
const finalPath = prefix ? `${prefix}${route.path}` : route.path;
|
|
829
|
-
const index = routes.findIndex((r) => r.path === finalPath);
|
|
830
|
-
if (index >= 0) {
|
|
831
|
-
routes.splice(index, 1);
|
|
832
|
-
const sources = routeSources.get(finalPath) || [];
|
|
833
|
-
const filteredSources = sources.filter((s) => s !== source);
|
|
834
|
-
if (filteredSources.length > 0) {
|
|
835
|
-
routeSources.set(finalPath, filteredSources);
|
|
836
|
-
} else {
|
|
837
|
-
routeSources.delete(finalPath);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
const finalRoute = prefix ? { ...route, path: finalPath } : route;
|
|
841
|
-
addRouteWithSource(finalRoute, source);
|
|
842
|
-
});
|
|
843
|
-
},
|
|
844
|
-
onRouteRemoved: (filePath, removedRoutes) => {
|
|
845
|
-
console.log(
|
|
846
|
-
`File removed from ${directory}: ${filePath} with ${removedRoutes.length} route(s):`,
|
|
847
|
-
removedRoutes.map((r) => r.path)
|
|
848
|
-
);
|
|
849
|
-
removedRoutes.forEach((route) => {
|
|
850
|
-
const finalPath = prefix ? `${prefix}${route.path}` : route.path;
|
|
851
|
-
const index = routes.findIndex((r) => r.path === finalPath);
|
|
852
|
-
if (index >= 0) {
|
|
853
|
-
routes.splice(index, 1);
|
|
854
|
-
}
|
|
855
|
-
const sources = routeSources.get(finalPath) || [];
|
|
856
|
-
const filteredSources = sources.filter((s) => s !== source);
|
|
857
|
-
if (filteredSources.length > 0) {
|
|
858
|
-
routeSources.set(finalPath, filteredSources);
|
|
859
|
-
} else {
|
|
860
|
-
routeSources.delete(finalPath);
|
|
861
|
-
}
|
|
862
|
-
});
|
|
863
|
-
},
|
|
864
|
-
onError: (error) => {
|
|
865
|
-
console.error(`Route watcher error for ${directory}:`, error);
|
|
667
|
+
readable.on("error", (err) => {
|
|
668
|
+
console.error("Stream error:", err);
|
|
669
|
+
if (!responseState.sent) {
|
|
670
|
+
res.statusCode = 500;
|
|
671
|
+
res.end("Stream error");
|
|
672
|
+
responseState.sent = true;
|
|
866
673
|
}
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
function setupWatcherForDirectory(directory, source, prefix) {
|
|
870
|
-
const callbacks = createWatcherCallbacks(directory, source, prefix);
|
|
871
|
-
const watcher = watchRoutes(directory, {
|
|
872
|
-
ignore: ["node_modules", ".git"],
|
|
873
|
-
...callbacks
|
|
874
674
|
});
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
return
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async function parseBodyIfNeeded(req, ctx) {
|
|
678
|
+
if (shouldSkipParsing(req.method)) {
|
|
679
|
+
return;
|
|
880
680
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
681
|
+
const contentType = req.headers["content-type"] || "";
|
|
682
|
+
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
|
683
|
+
if (contentLength === 0 || contentLength > 1048576) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
await parseBodyByContentType(req, ctx, contentType);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
setBodyError(ctx, "body_read_error", "Error reading request body", error);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function shouldSkipParsing(method) {
|
|
693
|
+
const skipMethods = ["GET", "HEAD", "OPTIONS"];
|
|
694
|
+
return skipMethods.includes(method || "GET");
|
|
695
|
+
}
|
|
696
|
+
async function parseBodyByContentType(req, ctx, contentType) {
|
|
697
|
+
if (contentType.includes("application/json")) {
|
|
698
|
+
await parseJsonBody(req, ctx);
|
|
699
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
700
|
+
await parseFormUrlEncodedBody(req, ctx);
|
|
701
|
+
} else if (contentType.includes("text/")) {
|
|
702
|
+
await parseTextBody(req, ctx);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function parseJsonBody(req, ctx) {
|
|
706
|
+
const body = await readRequestBody(req);
|
|
707
|
+
if (!body) {
|
|
708
|
+
console.warn("Empty body, skipping JSON parsing");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (body.trim() === "null") {
|
|
712
|
+
console.warn('Body is the string "null"');
|
|
713
|
+
ctx.request.body = null;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
const json = JSON.parse(body);
|
|
718
|
+
ctx.request.body = json;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
ctx.request.body = null;
|
|
721
|
+
setBodyError(ctx, "json_parse_error", "Invalid JSON in request body", error);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function parseFormUrlEncodedBody(req, ctx) {
|
|
725
|
+
const body = await readRequestBody(req);
|
|
726
|
+
if (!body) return;
|
|
727
|
+
try {
|
|
728
|
+
ctx.request.body = parseUrlEncodedData(body);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
ctx.request.body = null;
|
|
731
|
+
setBodyError(ctx, "form_parse_error", "Invalid form data in request body", error);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function parseUrlEncodedData(body) {
|
|
735
|
+
const params = new URLSearchParams(body);
|
|
736
|
+
const formData = {};
|
|
737
|
+
params.forEach((value, key) => {
|
|
738
|
+
if (formData[key] !== void 0) {
|
|
739
|
+
if (Array.isArray(formData[key])) {
|
|
740
|
+
formData[key].push(value);
|
|
741
|
+
} else {
|
|
742
|
+
formData[key] = [formData[key], value];
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
formData[key] = value;
|
|
884
746
|
}
|
|
747
|
+
});
|
|
748
|
+
return formData;
|
|
749
|
+
}
|
|
750
|
+
async function parseTextBody(req, ctx) {
|
|
751
|
+
const body = await readRequestBody(req);
|
|
752
|
+
if (body) {
|
|
753
|
+
ctx.request.body = body;
|
|
885
754
|
}
|
|
886
|
-
|
|
887
|
-
|
|
755
|
+
}
|
|
756
|
+
function setBodyError(ctx, type, message, error) {
|
|
757
|
+
ctx.state._bodyError = { type, message, error };
|
|
758
|
+
}
|
|
759
|
+
async function readRequestBody(req) {
|
|
760
|
+
return new Promise((resolve2, reject) => {
|
|
761
|
+
const chunks = [];
|
|
762
|
+
req.on("data", (chunk) => {
|
|
763
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
764
|
+
});
|
|
765
|
+
req.on("end", () => {
|
|
766
|
+
resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
767
|
+
});
|
|
768
|
+
req.on("error", (err) => {
|
|
769
|
+
reject(err);
|
|
770
|
+
});
|
|
888
771
|
});
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
* Get all registered routes
|
|
925
|
-
*/
|
|
926
|
-
getRoutes() {
|
|
927
|
-
return [...routes];
|
|
928
|
-
},
|
|
929
|
-
/**
|
|
930
|
-
* Add a route programmatically
|
|
931
|
-
*/
|
|
932
|
-
addRoute(route) {
|
|
933
|
-
addRouteInternal(route);
|
|
934
|
-
},
|
|
935
|
-
/**
|
|
936
|
-
* Add a route directory (for plugins)
|
|
937
|
-
*/
|
|
938
|
-
async addRouteDirectory(directory, options2 = {}) {
|
|
939
|
-
if (routeDirectories.has(directory)) {
|
|
940
|
-
console.warn(`Route directory ${directory} already registered`);
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
routeDirectories.add(directory);
|
|
944
|
-
if (initialized) {
|
|
945
|
-
await loadRoutesFromDirectory(directory, directory, options2.prefix);
|
|
946
|
-
if (routerOptions.watchMode) {
|
|
947
|
-
setupWatcherForDirectory(directory, directory, options2.prefix);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
},
|
|
951
|
-
/**
|
|
952
|
-
* Get route conflicts
|
|
953
|
-
*/
|
|
954
|
-
getRouteConflicts() {
|
|
955
|
-
const conflicts = [];
|
|
956
|
-
for (const [path5, sources] of routeSources.entries()) {
|
|
957
|
-
if (sources.length > 1) {
|
|
958
|
-
conflicts.push({ path: path5, sources });
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/server/request-handler.ts
|
|
775
|
+
function createRequestHandler(serverInstance) {
|
|
776
|
+
return async (req, res) => {
|
|
777
|
+
try {
|
|
778
|
+
const context = await createContext(req, res, {
|
|
779
|
+
parseBody: true
|
|
780
|
+
// Enable automatic body parsing
|
|
781
|
+
});
|
|
782
|
+
const handler = compose(serverInstance.middleware);
|
|
783
|
+
await runWithContext(context, async () => {
|
|
784
|
+
try {
|
|
785
|
+
await handler(context, async () => {
|
|
786
|
+
if (!context.response.sent) {
|
|
787
|
+
await serverInstance.router.handleRequest(context);
|
|
788
|
+
if (!context.response.sent) {
|
|
789
|
+
context.response.status(404).json({
|
|
790
|
+
error: "Not Found",
|
|
791
|
+
message: `Route not found: ${context.request.method} ${context.request.path}`
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error("Error processing request:", error);
|
|
798
|
+
if (!context.response.sent) {
|
|
799
|
+
context.response.json(
|
|
800
|
+
{
|
|
801
|
+
error: "Internal Server Error",
|
|
802
|
+
message: process.env.NODE_ENV === "development" ? error || "Unknown error" : "An error occurred processing your request"
|
|
803
|
+
},
|
|
804
|
+
500
|
|
805
|
+
);
|
|
806
|
+
}
|
|
959
807
|
}
|
|
960
|
-
}
|
|
961
|
-
|
|
808
|
+
});
|
|
809
|
+
} catch (error) {
|
|
810
|
+
console.error("Error creating context:", error);
|
|
811
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
812
|
+
res.end(
|
|
813
|
+
JSON.stringify({
|
|
814
|
+
error: "Internal Server Error",
|
|
815
|
+
message: "Failed to process request"
|
|
816
|
+
})
|
|
817
|
+
);
|
|
962
818
|
}
|
|
963
819
|
};
|
|
964
820
|
}
|
|
965
821
|
|
|
966
|
-
// src/
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
function getRoutesDir() {
|
|
972
|
-
if (!config.routesDir) {
|
|
973
|
-
throw new Error("Routes directory not configured. Make sure server is properly initialized.");
|
|
822
|
+
// src/server/start.ts
|
|
823
|
+
async function prepareCertificates(http2Options) {
|
|
824
|
+
if (!http2Options.enabled) {
|
|
825
|
+
return {};
|
|
974
826
|
}
|
|
975
|
-
|
|
827
|
+
const { keyFile, certFile } = http2Options;
|
|
828
|
+
const isDevMode = process.env.NODE_ENV === "development";
|
|
829
|
+
const certificatesMissing = !keyFile || !certFile;
|
|
830
|
+
if (certificatesMissing && isDevMode) {
|
|
831
|
+
const devCerts = await generateDevCertificates();
|
|
832
|
+
return devCerts;
|
|
833
|
+
}
|
|
834
|
+
if (certificatesMissing) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
"HTTP/2 requires SSL certificates. Provide keyFile and certFile in http2 options. In development, set NODE_ENV=development to generate them automatically."
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return { keyFile, certFile };
|
|
976
840
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
841
|
+
function createServerInstance(isHttp2, certOptions) {
|
|
842
|
+
if (!isHttp2) {
|
|
843
|
+
return http.createServer();
|
|
844
|
+
}
|
|
845
|
+
const http2ServerOptions = {
|
|
846
|
+
allowHTTP1: true
|
|
847
|
+
// Allow fallback to HTTP/1.1
|
|
848
|
+
};
|
|
981
849
|
try {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const callerFrame = stack[3];
|
|
985
|
-
if (!callerFrame || typeof callerFrame.getFileName !== "function") {
|
|
986
|
-
throw new Error("Unable to determine caller file frame");
|
|
850
|
+
if (certOptions.keyFile) {
|
|
851
|
+
http2ServerOptions.key = fs2.readFileSync(certOptions.keyFile);
|
|
987
852
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
throw new Error("Unable to determine caller file name");
|
|
853
|
+
if (certOptions.certFile) {
|
|
854
|
+
http2ServerOptions.cert = fs2.readFileSync(certOptions.certFile);
|
|
991
855
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
856
|
+
} catch (err) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`Failed to read certificate files: ${err instanceof Error ? err.message : String(err)}`
|
|
859
|
+
);
|
|
995
860
|
}
|
|
861
|
+
return http2.createSecureServer(http2ServerOptions);
|
|
996
862
|
}
|
|
997
|
-
function
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
863
|
+
function listenOnPort(server, port, host, isHttp2) {
|
|
864
|
+
return new Promise((resolve2, reject) => {
|
|
865
|
+
server.listen(port, host, () => {
|
|
866
|
+
const protocol = isHttp2 ? "https" : "http";
|
|
867
|
+
const url = `${protocol}://${host}:${port}`;
|
|
868
|
+
console.log(`
|
|
869
|
+
\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}
|
|
870
|
+
|
|
871
|
+
\u26A1 BlaizeJS DEVELOPMENT SERVER HOT AND READY \u26A1
|
|
872
|
+
|
|
873
|
+
\u{1F680} Server: ${url}
|
|
874
|
+
\u{1F525} Hot Reload: Enabled
|
|
875
|
+
\u{1F6E0}\uFE0F Mode: Development
|
|
876
|
+
|
|
877
|
+
Time to build something amazing! \u{1F680}
|
|
878
|
+
|
|
879
|
+
\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
|
+
`);
|
|
881
|
+
resolve2();
|
|
882
|
+
});
|
|
883
|
+
server.on("error", (err) => {
|
|
884
|
+
console.error("Server error:", err);
|
|
885
|
+
reject(err);
|
|
886
|
+
});
|
|
887
|
+
});
|
|
1004
888
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
path: path5
|
|
1011
|
-
};
|
|
1012
|
-
};
|
|
1013
|
-
var createPostRoute = (config2) => {
|
|
1014
|
-
validateMethodConfig("POST", config2);
|
|
1015
|
-
const path5 = getRoutePath();
|
|
1016
|
-
return {
|
|
1017
|
-
POST: config2,
|
|
1018
|
-
path: path5
|
|
1019
|
-
};
|
|
1020
|
-
};
|
|
1021
|
-
var createPutRoute = (config2) => {
|
|
1022
|
-
validateMethodConfig("PUT", config2);
|
|
1023
|
-
const path5 = getRoutePath();
|
|
1024
|
-
return {
|
|
1025
|
-
PUT: config2,
|
|
1026
|
-
path: path5
|
|
1027
|
-
};
|
|
1028
|
-
};
|
|
1029
|
-
var createDeleteRoute = (config2) => {
|
|
1030
|
-
validateMethodConfig("DELETE", config2);
|
|
1031
|
-
const path5 = getRoutePath();
|
|
1032
|
-
return {
|
|
1033
|
-
DELETE: config2,
|
|
1034
|
-
path: path5
|
|
1035
|
-
};
|
|
1036
|
-
};
|
|
1037
|
-
var createPatchRoute = (config2) => {
|
|
1038
|
-
validateMethodConfig("PATCH", config2);
|
|
1039
|
-
const path5 = getRoutePath();
|
|
1040
|
-
return {
|
|
1041
|
-
PATCH: config2,
|
|
1042
|
-
path: path5
|
|
1043
|
-
};
|
|
1044
|
-
};
|
|
1045
|
-
var createHeadRoute = (config2) => {
|
|
1046
|
-
validateMethodConfig("HEAD", config2);
|
|
1047
|
-
const path5 = getRoutePath();
|
|
1048
|
-
return {
|
|
1049
|
-
HEAD: config2,
|
|
1050
|
-
path: path5
|
|
1051
|
-
};
|
|
1052
|
-
};
|
|
1053
|
-
var createOptionsRoute = (config2) => {
|
|
1054
|
-
validateMethodConfig("OPTIONS", config2);
|
|
1055
|
-
const path5 = getRoutePath();
|
|
1056
|
-
return {
|
|
1057
|
-
OPTIONS: config2,
|
|
1058
|
-
path: path5
|
|
1059
|
-
};
|
|
1060
|
-
};
|
|
1061
|
-
function validateMethodConfig(method, config2) {
|
|
1062
|
-
if (!config2.handler || typeof config2.handler !== "function") {
|
|
1063
|
-
throw new Error(`Handler for method ${method} must be a function`);
|
|
889
|
+
async function initializePlugins(serverInstance) {
|
|
890
|
+
for (const plugin of serverInstance.plugins) {
|
|
891
|
+
if (typeof plugin.initialize === "function") {
|
|
892
|
+
await plugin.initialize(serverInstance);
|
|
893
|
+
}
|
|
1064
894
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
895
|
+
}
|
|
896
|
+
async function startServer(serverInstance, serverOptions) {
|
|
897
|
+
if (serverInstance.server) {
|
|
898
|
+
return;
|
|
1067
899
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
900
|
+
try {
|
|
901
|
+
const port = serverOptions.port;
|
|
902
|
+
const host = serverOptions.host;
|
|
903
|
+
await initializePlugins(serverInstance);
|
|
904
|
+
const http2Options = serverOptions.http2 || { enabled: true };
|
|
905
|
+
const isHttp2 = !!http2Options.enabled;
|
|
906
|
+
const certOptions = await prepareCertificates(http2Options);
|
|
907
|
+
if (serverOptions.http2 && certOptions.keyFile && certOptions.certFile) {
|
|
908
|
+
serverOptions.http2.keyFile = certOptions.keyFile;
|
|
909
|
+
serverOptions.http2.certFile = certOptions.certFile;
|
|
910
|
+
}
|
|
911
|
+
const server = createServerInstance(isHttp2, certOptions);
|
|
912
|
+
serverInstance.server = server;
|
|
913
|
+
serverInstance.port = port;
|
|
914
|
+
serverInstance.host = host;
|
|
915
|
+
const requestHandler = createRequestHandler(serverInstance);
|
|
916
|
+
server.on("request", requestHandler);
|
|
917
|
+
await listenOnPort(server, port, host, isHttp2);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
console.error("Failed to start server:", error);
|
|
920
|
+
throw error;
|
|
1070
921
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/server/stop.ts
|
|
925
|
+
async function stopServer(serverInstance, options = {}) {
|
|
926
|
+
const server = serverInstance.server;
|
|
927
|
+
const events = serverInstance.events;
|
|
928
|
+
if (!server) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const timeout = options.timeout || 3e4;
|
|
932
|
+
try {
|
|
933
|
+
if (options.onStopping) {
|
|
934
|
+
await options.onStopping();
|
|
935
|
+
}
|
|
936
|
+
events.emit("stopping");
|
|
937
|
+
await serverInstance.pluginManager.onServerStop(serverInstance, server);
|
|
938
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
939
|
+
setTimeout(() => {
|
|
940
|
+
reject(new Error("Server shutdown timed out waiting for requests to complete"));
|
|
941
|
+
}, timeout);
|
|
942
|
+
});
|
|
943
|
+
const closePromise = new Promise((resolve2, reject) => {
|
|
944
|
+
server.close((err) => {
|
|
945
|
+
if (err) {
|
|
946
|
+
return reject(err);
|
|
947
|
+
}
|
|
948
|
+
resolve2();
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
await Promise.race([closePromise, timeoutPromise]);
|
|
952
|
+
await serverInstance.pluginManager.terminatePlugins(serverInstance);
|
|
953
|
+
if (options.onStopped) {
|
|
954
|
+
await options.onStopped();
|
|
955
|
+
}
|
|
956
|
+
events.emit("stopped");
|
|
957
|
+
serverInstance.server = null;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
events.emit("error", error);
|
|
960
|
+
throw error;
|
|
1079
961
|
}
|
|
1080
962
|
}
|
|
1081
|
-
function
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
963
|
+
function registerSignalHandlers(stopFn) {
|
|
964
|
+
const sigintHandler = () => stopFn().catch(console.error);
|
|
965
|
+
const sigtermHandler = () => stopFn().catch(console.error);
|
|
966
|
+
process.on("SIGINT", sigintHandler);
|
|
967
|
+
process.on("SIGTERM", sigtermHandler);
|
|
968
|
+
return {
|
|
969
|
+
unregister: () => {
|
|
970
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
971
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// src/server/validation.ts
|
|
977
|
+
var import_zod = require("zod");
|
|
978
|
+
var middlewareSchema = import_zod.z.custom(
|
|
979
|
+
(data) => data !== null && typeof data === "object" && "execute" in data && typeof data.execute === "function",
|
|
980
|
+
{
|
|
981
|
+
message: "Expected middleware to have an execute function"
|
|
1085
982
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
983
|
+
);
|
|
984
|
+
var pluginSchema = import_zod.z.custom(
|
|
985
|
+
(data) => data !== null && typeof data === "object" && "register" in data && typeof data.register === "function",
|
|
986
|
+
{
|
|
987
|
+
message: "Expected a valid plugin object with a register method"
|
|
1088
988
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
989
|
+
);
|
|
990
|
+
var http2Schema = import_zod.z.object({
|
|
991
|
+
enabled: import_zod.z.boolean().optional().default(true),
|
|
992
|
+
keyFile: import_zod.z.string().optional(),
|
|
993
|
+
certFile: import_zod.z.string().optional()
|
|
994
|
+
}).refine(
|
|
995
|
+
(data) => {
|
|
996
|
+
if (data.enabled && process.env.NODE_ENV === "production") {
|
|
997
|
+
return data.keyFile && data.certFile;
|
|
998
|
+
}
|
|
999
|
+
return true;
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
message: "When HTTP/2 is enabled (outside of development mode), both keyFile and certFile must be provided"
|
|
1091
1003
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1004
|
+
);
|
|
1005
|
+
var serverOptionsSchema = import_zod.z.object({
|
|
1006
|
+
port: import_zod.z.number().int().positive().optional().default(3e3),
|
|
1007
|
+
host: import_zod.z.string().optional().default("localhost"),
|
|
1008
|
+
routesDir: import_zod.z.string().optional().default("./routes"),
|
|
1009
|
+
http2: http2Schema.optional().default({
|
|
1010
|
+
enabled: true
|
|
1011
|
+
}),
|
|
1012
|
+
middleware: import_zod.z.array(middlewareSchema).optional().default([]),
|
|
1013
|
+
plugins: import_zod.z.array(pluginSchema).optional().default([])
|
|
1014
|
+
});
|
|
1015
|
+
function validateServerOptions(options) {
|
|
1016
|
+
try {
|
|
1017
|
+
return serverOptionsSchema.parse(options);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
if (error instanceof import_zod.z.ZodError) {
|
|
1020
|
+
const formattedError = error.format();
|
|
1021
|
+
throw new Error(`Invalid server options: ${JSON.stringify(formattedError, null, 2)}`);
|
|
1022
|
+
}
|
|
1023
|
+
throw new Error(`Invalid server options: ${String(error)}`);
|
|
1094
1024
|
}
|
|
1095
1025
|
}
|
|
1096
1026
|
|
|
1097
|
-
// src/
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
var http2 = __toESM(require("http2"), 1);
|
|
1105
|
-
|
|
1106
|
-
// src/server/dev-certificate.ts
|
|
1107
|
-
var fs2 = __toESM(require("fs"), 1);
|
|
1108
|
-
var path4 = __toESM(require("path"), 1);
|
|
1109
|
-
var selfsigned = __toESM(require("selfsigned"), 1);
|
|
1110
|
-
async function generateDevCertificates() {
|
|
1111
|
-
const certDir = path4.join(process.cwd(), ".blaizejs", "certs");
|
|
1112
|
-
const keyPath = path4.join(certDir, "dev.key");
|
|
1113
|
-
const certPath = path4.join(certDir, "dev.cert");
|
|
1114
|
-
if (fs2.existsSync(keyPath) && fs2.existsSync(certPath)) {
|
|
1115
|
-
return {
|
|
1116
|
-
keyFile: keyPath,
|
|
1117
|
-
certFile: certPath
|
|
1118
|
-
};
|
|
1027
|
+
// src/plugins/lifecycle.ts
|
|
1028
|
+
function createPluginLifecycleManager(options = {}) {
|
|
1029
|
+
const { continueOnError = true, debug = false, onError } = options;
|
|
1030
|
+
function log(message, ...args) {
|
|
1031
|
+
if (debug) {
|
|
1032
|
+
console.log(`[PluginLifecycle] ${message}`, ...args);
|
|
1033
|
+
}
|
|
1119
1034
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1035
|
+
function handleError(plugin, phase, error) {
|
|
1036
|
+
const errorMessage = `Plugin ${plugin.name} failed during ${phase}: ${error.message}`;
|
|
1037
|
+
if (onError) {
|
|
1038
|
+
onError(plugin, phase, error);
|
|
1039
|
+
} else {
|
|
1040
|
+
console.error(errorMessage, error);
|
|
1041
|
+
}
|
|
1042
|
+
if (!continueOnError) {
|
|
1043
|
+
throw new Error(errorMessage);
|
|
1044
|
+
}
|
|
1122
1045
|
}
|
|
1123
|
-
const attrs = [{ name: "commonName", value: "localhost" }];
|
|
1124
|
-
const options = {
|
|
1125
|
-
days: 365,
|
|
1126
|
-
algorithm: "sha256",
|
|
1127
|
-
keySize: 2048,
|
|
1128
|
-
extensions: [
|
|
1129
|
-
{ name: "basicConstraints", cA: true },
|
|
1130
|
-
{
|
|
1131
|
-
name: "keyUsage",
|
|
1132
|
-
keyCertSign: true,
|
|
1133
|
-
digitalSignature: true,
|
|
1134
|
-
nonRepudiation: true,
|
|
1135
|
-
keyEncipherment: true,
|
|
1136
|
-
dataEncipherment: true
|
|
1137
|
-
},
|
|
1138
|
-
{
|
|
1139
|
-
name: "extKeyUsage",
|
|
1140
|
-
serverAuth: true,
|
|
1141
|
-
clientAuth: true
|
|
1142
|
-
},
|
|
1143
|
-
{
|
|
1144
|
-
name: "subjectAltName",
|
|
1145
|
-
altNames: [
|
|
1146
|
-
{ type: 2, value: "localhost" },
|
|
1147
|
-
{ type: 7, ip: "127.0.0.1" }
|
|
1148
|
-
]
|
|
1149
|
-
}
|
|
1150
|
-
]
|
|
1151
|
-
};
|
|
1152
|
-
const pems = selfsigned.generate(attrs, options);
|
|
1153
|
-
fs2.writeFileSync(keyPath, Buffer.from(pems.private, "utf-8"));
|
|
1154
|
-
fs2.writeFileSync(certPath, Buffer.from(pems.cert, "utf-8"));
|
|
1155
|
-
console.log(`
|
|
1156
|
-
\u{1F512} Generated self-signed certificates for development at ${certDir}
|
|
1157
|
-
`);
|
|
1158
1046
|
return {
|
|
1159
|
-
|
|
1160
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* Initialize all plugins
|
|
1049
|
+
*/
|
|
1050
|
+
async initializePlugins(server) {
|
|
1051
|
+
log("Initializing plugins...");
|
|
1052
|
+
for (const plugin of server.plugins) {
|
|
1053
|
+
if (plugin.initialize) {
|
|
1054
|
+
try {
|
|
1055
|
+
log(`Initializing plugin: ${plugin.name}`);
|
|
1056
|
+
await plugin.initialize(server);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
handleError(plugin, "initialize", error);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
log(`Initialized ${server.plugins.length} plugins`);
|
|
1063
|
+
},
|
|
1064
|
+
/**
|
|
1065
|
+
* Terminate all plugins in reverse order
|
|
1066
|
+
*/
|
|
1067
|
+
async terminatePlugins(server) {
|
|
1068
|
+
log("Terminating plugins...");
|
|
1069
|
+
const pluginsToTerminate = [...server.plugins].reverse();
|
|
1070
|
+
for (const plugin of pluginsToTerminate) {
|
|
1071
|
+
if (plugin.terminate) {
|
|
1072
|
+
try {
|
|
1073
|
+
log(`Terminating plugin: ${plugin.name}`);
|
|
1074
|
+
await plugin.terminate(server);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
handleError(plugin, "terminate", error);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
log(`Terminated ${pluginsToTerminate.length} plugins`);
|
|
1081
|
+
},
|
|
1082
|
+
/**
|
|
1083
|
+
* Notify plugins that the server has started
|
|
1084
|
+
*/
|
|
1085
|
+
async onServerStart(server, httpServer) {
|
|
1086
|
+
log("Notifying plugins of server start...");
|
|
1087
|
+
for (const plugin of server.plugins) {
|
|
1088
|
+
if (plugin.onServerStart) {
|
|
1089
|
+
try {
|
|
1090
|
+
log(`Notifying plugin of server start: ${plugin.name}`);
|
|
1091
|
+
await plugin.onServerStart(httpServer);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
handleError(plugin, "onServerStart", error);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
/**
|
|
1099
|
+
* Notify plugins that the server is stopping
|
|
1100
|
+
*/
|
|
1101
|
+
async onServerStop(server, httpServer) {
|
|
1102
|
+
log("Notifying plugins of server stop...");
|
|
1103
|
+
const pluginsToNotify = [...server.plugins].reverse();
|
|
1104
|
+
for (const plugin of pluginsToNotify) {
|
|
1105
|
+
if (plugin.onServerStop) {
|
|
1106
|
+
try {
|
|
1107
|
+
log(`Notifying plugin of server stop: ${plugin.name}`);
|
|
1108
|
+
await plugin.onServerStop(httpServer);
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
handleError(plugin, "onServerStop", error);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1161
1115
|
};
|
|
1162
1116
|
}
|
|
1163
1117
|
|
|
1164
|
-
// src/
|
|
1165
|
-
var
|
|
1166
|
-
constructor(message
|
|
1167
|
-
super(message);
|
|
1168
|
-
this.
|
|
1118
|
+
// src/plugins/errors.ts
|
|
1119
|
+
var PluginValidationError = class extends Error {
|
|
1120
|
+
constructor(pluginName, message) {
|
|
1121
|
+
super(`Plugin validation error${pluginName ? ` for "${pluginName}"` : ""}: ${message}`);
|
|
1122
|
+
this.pluginName = pluginName;
|
|
1123
|
+
this.name = "PluginValidationError";
|
|
1169
1124
|
}
|
|
1170
1125
|
};
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1126
|
+
|
|
1127
|
+
// src/plugins/validation.ts
|
|
1128
|
+
var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
1129
|
+
"core",
|
|
1130
|
+
"server",
|
|
1131
|
+
"router",
|
|
1132
|
+
"middleware",
|
|
1133
|
+
"context",
|
|
1134
|
+
"blaize",
|
|
1135
|
+
"blaizejs"
|
|
1136
|
+
]);
|
|
1137
|
+
var VALID_NAME_PATTERN = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
|
|
1138
|
+
var VALID_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9-.]+)?(?:\+[a-zA-Z0-9-.]+)?$/;
|
|
1139
|
+
function validatePlugin(plugin, options = {}) {
|
|
1140
|
+
const { requireVersion = true, validateNameFormat = true, checkReservedNames = true } = options;
|
|
1141
|
+
if (!plugin || typeof plugin !== "object") {
|
|
1142
|
+
throw new PluginValidationError("", "Plugin must be an object");
|
|
1174
1143
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
super(message);
|
|
1144
|
+
const p = plugin;
|
|
1145
|
+
if (!p.name || typeof p.name !== "string") {
|
|
1146
|
+
throw new PluginValidationError("", "Plugin must have a name (string)");
|
|
1179
1147
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1148
|
+
if (validateNameFormat && !VALID_NAME_PATTERN.test(p.name)) {
|
|
1149
|
+
throw new PluginValidationError(
|
|
1150
|
+
p.name,
|
|
1151
|
+
"Plugin name must be lowercase letters, numbers, and hyphens only"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (checkReservedNames && RESERVED_NAMES.has(p.name.toLowerCase())) {
|
|
1155
|
+
throw new PluginValidationError(p.name, `Plugin name "${p.name}" is reserved`);
|
|
1156
|
+
}
|
|
1157
|
+
if (requireVersion) {
|
|
1158
|
+
if (!p.version || typeof p.version !== "string") {
|
|
1159
|
+
throw new PluginValidationError(p.name, "Plugin must have a version (string)");
|
|
1160
|
+
}
|
|
1161
|
+
if (!VALID_VERSION_PATTERN.test(p.version)) {
|
|
1162
|
+
throw new PluginValidationError(
|
|
1163
|
+
p.name,
|
|
1164
|
+
'Plugin version must follow semantic versioning (e.g., "1.0.0")'
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (!p.register || typeof p.register !== "function") {
|
|
1169
|
+
throw new PluginValidationError(p.name, "Plugin must have a register method (function)");
|
|
1170
|
+
}
|
|
1171
|
+
const lifecycleMethods = ["initialize", "terminate", "onServerStart", "onServerStop"];
|
|
1172
|
+
for (const method of lifecycleMethods) {
|
|
1173
|
+
if (p[method] && typeof p[method] !== "function") {
|
|
1174
|
+
throw new PluginValidationError(p.name, `Plugin ${method} must be a function if provided`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/router/discovery/finder.ts
|
|
1180
|
+
var fs3 = __toESM(require("fs/promises"), 1);
|
|
1181
|
+
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
|
+
}
|
|
1184
1211
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
function runWithContext(context, callback) {
|
|
1191
|
-
return contextStorage.run(context, callback);
|
|
1212
|
+
await scanDirectory(absoluteDir);
|
|
1213
|
+
return routeFiles;
|
|
1214
|
+
}
|
|
1215
|
+
function isRouteFile(filename) {
|
|
1216
|
+
return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js"));
|
|
1192
1217
|
}
|
|
1193
1218
|
|
|
1194
|
-
// src/
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const protocol = req.socket && req.socket.encrypted ? "https" : "http";
|
|
1200
|
-
const fullUrl = `${protocol}://${host}${originalUrl.startsWith("/") ? "" : "/"}${originalUrl}`;
|
|
1219
|
+
// src/router/discovery/loader.ts
|
|
1220
|
+
async function dynamicImport(filePath) {
|
|
1221
|
+
return import(filePath);
|
|
1222
|
+
}
|
|
1223
|
+
async function loadRouteModule(filePath, basePath) {
|
|
1201
1224
|
try {
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1225
|
+
const parsedRoute = parseRoutePath(filePath, basePath);
|
|
1226
|
+
console.log("parsedRoute:", parsedRoute);
|
|
1227
|
+
const module2 = await dynamicImport(filePath);
|
|
1228
|
+
console.log("Module exports:", Object.keys(module2));
|
|
1229
|
+
const routes = [];
|
|
1230
|
+
if (module2.default && typeof module2.default === "object") {
|
|
1231
|
+
console.log("Found default export:", module2.default);
|
|
1232
|
+
const route = {
|
|
1233
|
+
...module2.default,
|
|
1234
|
+
path: parsedRoute.routePath
|
|
1235
|
+
};
|
|
1236
|
+
routes.push(route);
|
|
1237
|
+
}
|
|
1238
|
+
Object.entries(module2).forEach(([exportName, exportValue]) => {
|
|
1239
|
+
if (exportName === "default" || !exportValue || typeof exportValue !== "object") {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const potentialRoute = exportValue;
|
|
1243
|
+
if (isValidRoute(potentialRoute)) {
|
|
1244
|
+
console.log(`Found named route export: ${exportName}`, potentialRoute);
|
|
1245
|
+
const route = {
|
|
1246
|
+
...potentialRoute,
|
|
1247
|
+
// Use the route's own path if it has one, otherwise derive from file
|
|
1248
|
+
path: parsedRoute.routePath
|
|
1249
|
+
};
|
|
1250
|
+
routes.push(route);
|
|
1214
1251
|
}
|
|
1215
1252
|
});
|
|
1216
|
-
|
|
1253
|
+
if (routes.length === 0) {
|
|
1254
|
+
console.warn(`Route file ${filePath} does not export any valid route definitions`);
|
|
1255
|
+
return [];
|
|
1256
|
+
}
|
|
1257
|
+
console.log(`Loaded ${routes.length} route(s) from ${filePath}`);
|
|
1258
|
+
return routes;
|
|
1217
1259
|
} catch (error) {
|
|
1218
|
-
console.
|
|
1219
|
-
|
|
1260
|
+
console.error(`Failed to load route module ${filePath}:`, error);
|
|
1261
|
+
return [];
|
|
1220
1262
|
}
|
|
1221
1263
|
}
|
|
1222
|
-
function
|
|
1223
|
-
|
|
1264
|
+
function isValidRoute(obj) {
|
|
1265
|
+
if (!obj || typeof obj !== "object") {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
1269
|
+
const hasHttpMethod = httpMethods.some(
|
|
1270
|
+
(method) => obj[method] && typeof obj[method] === "object" && obj[method].handler
|
|
1271
|
+
);
|
|
1272
|
+
return hasHttpMethod;
|
|
1224
1273
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1274
|
+
|
|
1275
|
+
// src/router/discovery/index.ts
|
|
1276
|
+
async function findRoutes(routesDir, options = {}) {
|
|
1277
|
+
const routeFiles = await findRouteFiles(routesDir, {
|
|
1278
|
+
ignore: options.ignore
|
|
1279
|
+
});
|
|
1280
|
+
const routes = [];
|
|
1281
|
+
for (const filePath of routeFiles) {
|
|
1282
|
+
const moduleRoutes = await loadRouteModule(filePath, routesDir);
|
|
1283
|
+
if (moduleRoutes.length > 0) {
|
|
1284
|
+
routes.push(...moduleRoutes);
|
|
1233
1285
|
}
|
|
1234
1286
|
}
|
|
1235
|
-
return
|
|
1287
|
+
return routes;
|
|
1236
1288
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
}),
|
|
1255
|
-
response: {},
|
|
1256
|
-
state
|
|
1257
|
-
};
|
|
1258
|
-
ctx.response = createResponseObject(res, responseState, ctx);
|
|
1259
|
-
if (options.parseBody) {
|
|
1260
|
-
await parseBodyIfNeeded(req, ctx);
|
|
1289
|
+
|
|
1290
|
+
// src/router/discovery/watchers.ts
|
|
1291
|
+
var path4 = __toESM(require("path"), 1);
|
|
1292
|
+
var import_chokidar = require("chokidar");
|
|
1293
|
+
function watchRoutes(routesDir, options = {}) {
|
|
1294
|
+
const routesByPath = /* @__PURE__ */ new Map();
|
|
1295
|
+
async function loadInitialRoutes() {
|
|
1296
|
+
try {
|
|
1297
|
+
const files = await findRouteFiles(routesDir, {
|
|
1298
|
+
ignore: options.ignore
|
|
1299
|
+
});
|
|
1300
|
+
for (const filePath of files) {
|
|
1301
|
+
await loadAndNotify(filePath);
|
|
1302
|
+
}
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
handleError(error);
|
|
1305
|
+
}
|
|
1261
1306
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1307
|
+
async function loadAndNotify(filePath) {
|
|
1308
|
+
try {
|
|
1309
|
+
const routes = await loadRouteModule(filePath, routesDir);
|
|
1310
|
+
if (!routes || routes.length === 0) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const existingRoutes = routesByPath.get(filePath);
|
|
1314
|
+
if (existingRoutes) {
|
|
1315
|
+
routesByPath.set(filePath, routes);
|
|
1316
|
+
if (options.onRouteChanged) {
|
|
1317
|
+
options.onRouteChanged(routes);
|
|
1318
|
+
}
|
|
1319
|
+
} else {
|
|
1320
|
+
routesByPath.set(filePath, routes);
|
|
1321
|
+
if (options.onRouteAdded) {
|
|
1322
|
+
options.onRouteAdded(routes);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
handleError(error);
|
|
1278
1327
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1328
|
+
}
|
|
1329
|
+
function handleRemoved(filePath) {
|
|
1330
|
+
const normalizedPath = path4.normalize(filePath);
|
|
1331
|
+
const routes = routesByPath.get(normalizedPath);
|
|
1332
|
+
if (routes && routes.length > 0 && options.onRouteRemoved) {
|
|
1333
|
+
options.onRouteRemoved(normalizedPath, routes);
|
|
1334
|
+
}
|
|
1335
|
+
routesByPath.delete(normalizedPath);
|
|
1336
|
+
}
|
|
1337
|
+
function handleError(error) {
|
|
1338
|
+
if (options.onError && error instanceof Error) {
|
|
1339
|
+
options.onError(error);
|
|
1290
1340
|
} else {
|
|
1291
|
-
|
|
1292
|
-
(acc, [key, value]) => {
|
|
1293
|
-
acc[key] = Array.isArray(value) ? value.join(", ") : value || void 0;
|
|
1294
|
-
return acc;
|
|
1295
|
-
},
|
|
1296
|
-
{}
|
|
1297
|
-
);
|
|
1341
|
+
console.error("Route watcher error:", error);
|
|
1298
1342
|
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
text: createTextResponder(res, responseState),
|
|
1313
|
-
html: createHtmlResponder(res, responseState),
|
|
1314
|
-
redirect: createRedirectResponder(res, responseState),
|
|
1315
|
-
stream: createStreamResponder(res, responseState)
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
function createStatusSetter(res, responseState, ctx) {
|
|
1319
|
-
return function statusSetter(code) {
|
|
1320
|
-
if (responseState.sent) {
|
|
1321
|
-
throw new ResponseSentError();
|
|
1343
|
+
}
|
|
1344
|
+
const watcher = (0, import_chokidar.watch)(routesDir, {
|
|
1345
|
+
ignored: [
|
|
1346
|
+
/(^|[/\\])\../,
|
|
1347
|
+
// Ignore dot files
|
|
1348
|
+
/node_modules/,
|
|
1349
|
+
...options.ignore || []
|
|
1350
|
+
],
|
|
1351
|
+
persistent: true,
|
|
1352
|
+
ignoreInitial: false,
|
|
1353
|
+
awaitWriteFinish: {
|
|
1354
|
+
stabilityThreshold: 300,
|
|
1355
|
+
pollInterval: 100
|
|
1322
1356
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1357
|
+
});
|
|
1358
|
+
watcher.on("add", loadAndNotify).on("change", loadAndNotify).on("unlink", handleRemoved).on("error", handleError);
|
|
1359
|
+
loadInitialRoutes().catch(handleError);
|
|
1360
|
+
return {
|
|
1361
|
+
/**
|
|
1362
|
+
* Close the watcher
|
|
1363
|
+
*/
|
|
1364
|
+
close: () => watcher.close(),
|
|
1365
|
+
/**
|
|
1366
|
+
* Get all currently loaded routes (flattened)
|
|
1367
|
+
*/
|
|
1368
|
+
getRoutes: () => {
|
|
1369
|
+
const allRoutes = [];
|
|
1370
|
+
for (const routes of routesByPath.values()) {
|
|
1371
|
+
allRoutes.push(...routes);
|
|
1372
|
+
}
|
|
1373
|
+
return allRoutes;
|
|
1374
|
+
},
|
|
1375
|
+
/**
|
|
1376
|
+
* Get routes organized by file path
|
|
1377
|
+
*/
|
|
1378
|
+
getRoutesByFile: () => new Map(routesByPath)
|
|
1325
1379
|
};
|
|
1326
1380
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1381
|
+
|
|
1382
|
+
// src/router/handlers/error.ts
|
|
1383
|
+
function handleRouteError(ctx, error, options = {}) {
|
|
1384
|
+
if (options.log) {
|
|
1385
|
+
console.error("Route error:", error);
|
|
1386
|
+
}
|
|
1387
|
+
const status = getErrorStatus(error);
|
|
1388
|
+
const response = {
|
|
1389
|
+
error: getErrorType(error),
|
|
1390
|
+
message: getErrorMessage(error)
|
|
1334
1391
|
};
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if (responseState.sent) {
|
|
1339
|
-
throw new ResponseSentHeaderError();
|
|
1392
|
+
if (options.detailed) {
|
|
1393
|
+
if (error instanceof Error) {
|
|
1394
|
+
response.stack = error.stack;
|
|
1340
1395
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1396
|
+
if (error && typeof error === "object" && "details" in error && error.details) {
|
|
1397
|
+
response.details = error.details;
|
|
1343
1398
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1399
|
+
}
|
|
1400
|
+
ctx.response.status(status).json(response);
|
|
1346
1401
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1349
|
-
if (
|
|
1350
|
-
|
|
1402
|
+
function getErrorStatus(error) {
|
|
1403
|
+
if (error && typeof error === "object") {
|
|
1404
|
+
if ("status" in error && typeof error.status === "number") {
|
|
1405
|
+
return error.status;
|
|
1351
1406
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
};
|
|
1355
|
-
}
|
|
1356
|
-
function createJsonResponder(res, responseState) {
|
|
1357
|
-
return function jsonResponder(body, status) {
|
|
1358
|
-
if (responseState.sent) {
|
|
1359
|
-
throw new ResponseSentError();
|
|
1407
|
+
if ("statusCode" in error && typeof error.statusCode === "number") {
|
|
1408
|
+
return error.statusCode;
|
|
1360
1409
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
|
|
1410
|
+
if ("code" in error && typeof error.code === "string") {
|
|
1411
|
+
return getStatusFromCode(error.code);
|
|
1363
1412
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
responseState.sent = true;
|
|
1367
|
-
};
|
|
1413
|
+
}
|
|
1414
|
+
return 500;
|
|
1368
1415
|
}
|
|
1369
|
-
function
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1416
|
+
function getStatusFromCode(code) {
|
|
1417
|
+
switch (code) {
|
|
1418
|
+
case "NOT_FOUND":
|
|
1419
|
+
return 404;
|
|
1420
|
+
case "UNAUTHORIZED":
|
|
1421
|
+
return 401;
|
|
1422
|
+
case "FORBIDDEN":
|
|
1423
|
+
return 403;
|
|
1424
|
+
case "BAD_REQUEST":
|
|
1425
|
+
return 400;
|
|
1426
|
+
case "CONFLICT":
|
|
1427
|
+
return 409;
|
|
1428
|
+
default:
|
|
1429
|
+
return 500;
|
|
1430
|
+
}
|
|
1381
1431
|
}
|
|
1382
|
-
function
|
|
1383
|
-
|
|
1384
|
-
if (
|
|
1385
|
-
|
|
1432
|
+
function getErrorType(error) {
|
|
1433
|
+
if (error && typeof error === "object") {
|
|
1434
|
+
if ("type" in error && typeof error.type === "string") {
|
|
1435
|
+
return error.type;
|
|
1386
1436
|
}
|
|
1387
|
-
if (
|
|
1388
|
-
|
|
1437
|
+
if ("name" in error && typeof error.name === "string") {
|
|
1438
|
+
return error.name;
|
|
1389
1439
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
responseState.sent = true;
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
function createRedirectResponder(res, responseState) {
|
|
1396
|
-
return function redirectResponder(url, status = 302) {
|
|
1397
|
-
if (responseState.sent) {
|
|
1398
|
-
throw new ResponseSentError();
|
|
1440
|
+
if (error instanceof Error) {
|
|
1441
|
+
return error.constructor.name;
|
|
1399
1442
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
res.end();
|
|
1403
|
-
responseState.sent = true;
|
|
1404
|
-
};
|
|
1443
|
+
}
|
|
1444
|
+
return "Error";
|
|
1405
1445
|
}
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1446
|
+
function getErrorMessage(error) {
|
|
1447
|
+
if (error instanceof Error) {
|
|
1448
|
+
return error.message;
|
|
1449
|
+
}
|
|
1450
|
+
if (error && typeof error === "object") {
|
|
1451
|
+
if ("message" in error && typeof error.message === "string") {
|
|
1452
|
+
return error.message;
|
|
1410
1453
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1454
|
+
}
|
|
1455
|
+
return String(error);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/router/validation/body.ts
|
|
1459
|
+
var import_zod2 = require("zod");
|
|
1460
|
+
function validateBody(body, schema) {
|
|
1461
|
+
if (schema instanceof import_zod2.z.ZodObject) {
|
|
1462
|
+
return schema.strict().parse(body);
|
|
1463
|
+
}
|
|
1464
|
+
return schema.parse(body);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/router/validation/params.ts
|
|
1468
|
+
var import_zod3 = require("zod");
|
|
1469
|
+
function validateParams(params, schema) {
|
|
1470
|
+
if (schema instanceof import_zod3.z.ZodObject) {
|
|
1471
|
+
return schema.strict().parse(params);
|
|
1472
|
+
}
|
|
1473
|
+
return schema.parse(params);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/router/validation/query.ts
|
|
1477
|
+
var import_zod4 = require("zod");
|
|
1478
|
+
function validateQuery(query, schema) {
|
|
1479
|
+
if (schema instanceof import_zod4.z.ZodObject) {
|
|
1480
|
+
return schema.strict().parse(query);
|
|
1481
|
+
}
|
|
1482
|
+
return schema.parse(query);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/router/validation/response.ts
|
|
1486
|
+
var import_zod5 = require("zod");
|
|
1487
|
+
function validateResponse(response, schema) {
|
|
1488
|
+
if (schema instanceof import_zod5.z.ZodObject) {
|
|
1489
|
+
return schema.strict().parse(response);
|
|
1490
|
+
}
|
|
1491
|
+
return schema.parse(response);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/router/validation/schema.ts
|
|
1495
|
+
function createRequestValidator(schema, debug = false) {
|
|
1496
|
+
const middlewareFn = async (ctx, next) => {
|
|
1497
|
+
const errors = {};
|
|
1498
|
+
if (schema.params && ctx.request.params) {
|
|
1499
|
+
try {
|
|
1500
|
+
ctx.request.params = validateParams(ctx.request.params, schema.params);
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
errors.params = formatValidationError(error);
|
|
1503
|
+
}
|
|
1413
1504
|
}
|
|
1414
|
-
if (
|
|
1415
|
-
|
|
1505
|
+
if (schema.query && ctx.request.query) {
|
|
1506
|
+
try {
|
|
1507
|
+
ctx.request.query = validateQuery(ctx.request.query, schema.query);
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
errors.query = formatValidationError(error);
|
|
1510
|
+
}
|
|
1416
1511
|
}
|
|
1417
|
-
if (
|
|
1418
|
-
|
|
1419
|
-
|
|
1512
|
+
if (schema.body) {
|
|
1513
|
+
try {
|
|
1514
|
+
ctx.request.body = validateBody(ctx.request.body, schema.body);
|
|
1515
|
+
} catch (error) {
|
|
1516
|
+
errors.body = formatValidationError(error);
|
|
1420
1517
|
}
|
|
1421
1518
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1519
|
+
if (Object.keys(errors).length > 0) {
|
|
1520
|
+
ctx.response.status(400).json({
|
|
1521
|
+
error: "Validation Error",
|
|
1522
|
+
details: errors
|
|
1523
|
+
});
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
await next();
|
|
1527
|
+
};
|
|
1528
|
+
return {
|
|
1529
|
+
name: "RequestValidator",
|
|
1530
|
+
execute: middlewareFn,
|
|
1531
|
+
debug
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function createResponseValidator(responseSchema, debug = false) {
|
|
1535
|
+
const middlewareFn = async (ctx, next) => {
|
|
1536
|
+
const originalJson = ctx.response.json;
|
|
1537
|
+
ctx.response.json = (body, status) => {
|
|
1538
|
+
try {
|
|
1539
|
+
const validatedBody = validateResponse(body, responseSchema);
|
|
1540
|
+
ctx.response.json = originalJson;
|
|
1541
|
+
return originalJson.call(ctx.response, validatedBody, status);
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
ctx.response.json = originalJson;
|
|
1544
|
+
console.error("Response validation error:", error);
|
|
1545
|
+
ctx.response.status(500).json({
|
|
1546
|
+
error: "Internal Server Error",
|
|
1547
|
+
message: "Response validation failed"
|
|
1548
|
+
});
|
|
1549
|
+
return ctx.response;
|
|
1432
1550
|
}
|
|
1433
|
-
}
|
|
1551
|
+
};
|
|
1552
|
+
await next();
|
|
1553
|
+
};
|
|
1554
|
+
return {
|
|
1555
|
+
name: "ResponseValidator",
|
|
1556
|
+
execute: middlewareFn,
|
|
1557
|
+
debug
|
|
1434
1558
|
};
|
|
1435
1559
|
}
|
|
1436
|
-
|
|
1437
|
-
if (
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
const contentType = req.headers["content-type"] || "";
|
|
1441
|
-
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
|
1442
|
-
if (contentLength === 0 || contentLength > 1048576) {
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
try {
|
|
1446
|
-
await parseBodyByContentType(req, ctx, contentType);
|
|
1447
|
-
} catch (error) {
|
|
1448
|
-
setBodyError(ctx, "body_read_error", "Error reading request body", error);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
function shouldSkipParsing(method) {
|
|
1452
|
-
const skipMethods = ["GET", "HEAD", "OPTIONS"];
|
|
1453
|
-
return skipMethods.includes(method || "GET");
|
|
1454
|
-
}
|
|
1455
|
-
async function parseBodyByContentType(req, ctx, contentType) {
|
|
1456
|
-
if (contentType.includes("application/json")) {
|
|
1457
|
-
await parseJsonBody(req, ctx);
|
|
1458
|
-
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1459
|
-
await parseFormUrlEncodedBody(req, ctx);
|
|
1460
|
-
} else if (contentType.includes("text/")) {
|
|
1461
|
-
await parseTextBody(req, ctx);
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
async function parseJsonBody(req, ctx) {
|
|
1465
|
-
const body = await readRequestBody(req);
|
|
1466
|
-
if (!body) {
|
|
1467
|
-
console.warn("Empty body, skipping JSON parsing");
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
if (body.trim() === "null") {
|
|
1471
|
-
console.warn('Body is the string "null"');
|
|
1472
|
-
ctx.request.body = null;
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
try {
|
|
1476
|
-
const json = JSON.parse(body);
|
|
1477
|
-
ctx.request.body = json;
|
|
1478
|
-
} catch (error) {
|
|
1479
|
-
ctx.request.body = null;
|
|
1480
|
-
setBodyError(ctx, "json_parse_error", "Invalid JSON in request body", error);
|
|
1560
|
+
function formatValidationError(error) {
|
|
1561
|
+
if (error && typeof error === "object" && "format" in error && typeof error.format === "function") {
|
|
1562
|
+
return error.format();
|
|
1481
1563
|
}
|
|
1564
|
+
return error instanceof Error ? error.message : String(error);
|
|
1482
1565
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1566
|
+
|
|
1567
|
+
// src/router/handlers/executor.ts
|
|
1568
|
+
async function executeHandler(ctx, routeOptions, params) {
|
|
1569
|
+
const middleware = [...routeOptions.middleware || []];
|
|
1570
|
+
if (routeOptions.schema) {
|
|
1571
|
+
if (routeOptions.schema.params || routeOptions.schema.query || routeOptions.schema.body) {
|
|
1572
|
+
middleware.unshift(createRequestValidator(routeOptions.schema));
|
|
1573
|
+
}
|
|
1574
|
+
if (routeOptions.schema.response) {
|
|
1575
|
+
middleware.push(createResponseValidator(routeOptions.schema.response));
|
|
1576
|
+
}
|
|
1491
1577
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (formData[key] !== void 0) {
|
|
1498
|
-
if (Array.isArray(formData[key])) {
|
|
1499
|
-
formData[key].push(value);
|
|
1500
|
-
} else {
|
|
1501
|
-
formData[key] = [formData[key], value];
|
|
1502
|
-
}
|
|
1503
|
-
} else {
|
|
1504
|
-
formData[key] = value;
|
|
1578
|
+
const handler = compose([...middleware]);
|
|
1579
|
+
await handler(ctx, async () => {
|
|
1580
|
+
const result = await routeOptions.handler(ctx, params);
|
|
1581
|
+
if (!ctx.response.sent && result !== void 0) {
|
|
1582
|
+
ctx.response.json(result);
|
|
1505
1583
|
}
|
|
1506
1584
|
});
|
|
1507
|
-
return formData;
|
|
1508
1585
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1586
|
+
|
|
1587
|
+
// src/router/matching/params.ts
|
|
1588
|
+
function extractParams(path5, pattern, paramNames) {
|
|
1589
|
+
const match = pattern.exec(path5);
|
|
1590
|
+
if (!match) {
|
|
1591
|
+
return {};
|
|
1513
1592
|
}
|
|
1593
|
+
const params = {};
|
|
1594
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
1595
|
+
params[paramNames[i]] = match[i + 1] || "";
|
|
1596
|
+
}
|
|
1597
|
+
return params;
|
|
1514
1598
|
}
|
|
1515
|
-
function
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1599
|
+
function compilePathPattern(path5) {
|
|
1600
|
+
const paramNames = [];
|
|
1601
|
+
if (path5 === "/") {
|
|
1602
|
+
return {
|
|
1603
|
+
pattern: /^\/$/,
|
|
1604
|
+
paramNames: []
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
let patternString = path5.replace(/([.+*?^$(){}|\\])/g, "\\$1");
|
|
1608
|
+
patternString = patternString.replace(/\/:([^/]+)/g, (_, paramName) => {
|
|
1609
|
+
paramNames.push(paramName);
|
|
1610
|
+
return "/([^/]+)";
|
|
1611
|
+
}).replace(/\/\[([^\]]+)\]/g, (_, paramName) => {
|
|
1612
|
+
paramNames.push(paramName);
|
|
1613
|
+
return "/([^/]+)";
|
|
1530
1614
|
});
|
|
1615
|
+
patternString = `${patternString}(?:/)?`;
|
|
1616
|
+
const pattern = new RegExp(`^${patternString}$`);
|
|
1617
|
+
return {
|
|
1618
|
+
pattern,
|
|
1619
|
+
paramNames
|
|
1620
|
+
};
|
|
1531
1621
|
}
|
|
1532
1622
|
|
|
1533
|
-
// src/
|
|
1534
|
-
function
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1623
|
+
// src/router/matching/matcher.ts
|
|
1624
|
+
function createMatcher() {
|
|
1625
|
+
const routes = [];
|
|
1626
|
+
return {
|
|
1627
|
+
/**
|
|
1628
|
+
* Add a route to the matcher
|
|
1629
|
+
*/
|
|
1630
|
+
add(path5, method, routeOptions) {
|
|
1631
|
+
const { pattern, paramNames } = compilePathPattern(path5);
|
|
1632
|
+
const newRoute = {
|
|
1633
|
+
path: path5,
|
|
1634
|
+
method,
|
|
1635
|
+
pattern,
|
|
1636
|
+
paramNames,
|
|
1637
|
+
routeOptions
|
|
1638
|
+
};
|
|
1639
|
+
const insertIndex = routes.findIndex((route) => paramNames.length < route.paramNames.length);
|
|
1640
|
+
if (insertIndex === -1) {
|
|
1641
|
+
routes.push(newRoute);
|
|
1642
|
+
} else {
|
|
1643
|
+
routes.splice(insertIndex, 0, newRoute);
|
|
1644
|
+
}
|
|
1645
|
+
},
|
|
1646
|
+
/**
|
|
1647
|
+
* Match a URL path to a route
|
|
1648
|
+
*/
|
|
1649
|
+
match(path5, method) {
|
|
1650
|
+
const pathname = path5.split("?")[0];
|
|
1651
|
+
if (!pathname) return null;
|
|
1652
|
+
for (const route of routes) {
|
|
1653
|
+
if (route.method !== method) continue;
|
|
1654
|
+
const match = route.pattern.exec(pathname);
|
|
1655
|
+
if (match) {
|
|
1656
|
+
const params = extractParams(path5, route.pattern, route.paramNames);
|
|
1657
|
+
return {
|
|
1658
|
+
route: route.routeOptions,
|
|
1659
|
+
params
|
|
1660
|
+
};
|
|
1566
1661
|
}
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1571
|
-
res.end(
|
|
1572
|
-
JSON.stringify({
|
|
1573
|
-
error: "Internal Server Error",
|
|
1574
|
-
message: "Failed to process request"
|
|
1575
|
-
})
|
|
1662
|
+
}
|
|
1663
|
+
const matchingPath = routes.find(
|
|
1664
|
+
(route) => route.method !== method && route.pattern.test(path5)
|
|
1576
1665
|
);
|
|
1666
|
+
if (matchingPath) {
|
|
1667
|
+
return {
|
|
1668
|
+
route: null,
|
|
1669
|
+
params: {},
|
|
1670
|
+
methodNotAllowed: true,
|
|
1671
|
+
allowedMethods: routes.filter((route) => route.pattern.test(path5)).map((route) => route.method)
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
return null;
|
|
1675
|
+
},
|
|
1676
|
+
/**
|
|
1677
|
+
* Get all registered routes
|
|
1678
|
+
*/
|
|
1679
|
+
getRoutes() {
|
|
1680
|
+
return routes.map((route) => ({
|
|
1681
|
+
path: route.path,
|
|
1682
|
+
method: route.method
|
|
1683
|
+
}));
|
|
1684
|
+
},
|
|
1685
|
+
/**
|
|
1686
|
+
* Find routes matching a specific path
|
|
1687
|
+
*/
|
|
1688
|
+
findRoutes(path5) {
|
|
1689
|
+
return routes.filter((route) => route.pattern.test(path5)).map((route) => ({
|
|
1690
|
+
path: route.path,
|
|
1691
|
+
method: route.method,
|
|
1692
|
+
params: extractParams(path5, route.pattern, route.paramNames)
|
|
1693
|
+
}));
|
|
1577
1694
|
}
|
|
1578
1695
|
};
|
|
1579
1696
|
}
|
|
1580
1697
|
|
|
1581
|
-
// src/
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
return devCerts;
|
|
1592
|
-
}
|
|
1593
|
-
if (certificatesMissing) {
|
|
1594
|
-
throw new Error(
|
|
1595
|
-
"HTTP/2 requires SSL certificates. Provide keyFile and certFile in http2 options. In development, set NODE_ENV=development to generate them automatically."
|
|
1596
|
-
);
|
|
1597
|
-
}
|
|
1598
|
-
return { keyFile, certFile };
|
|
1599
|
-
}
|
|
1600
|
-
function createServerInstance(isHttp2, certOptions) {
|
|
1601
|
-
if (!isHttp2) {
|
|
1602
|
-
return http.createServer();
|
|
1603
|
-
}
|
|
1604
|
-
const http2ServerOptions = {
|
|
1605
|
-
allowHTTP1: true
|
|
1606
|
-
// Allow fallback to HTTP/1.1
|
|
1698
|
+
// src/router/router.ts
|
|
1699
|
+
var DEFAULT_ROUTER_OPTIONS = {
|
|
1700
|
+
routesDir: "./routes",
|
|
1701
|
+
basePath: "/",
|
|
1702
|
+
watchMode: process.env.NODE_ENV === "development"
|
|
1703
|
+
};
|
|
1704
|
+
function createRouter(options) {
|
|
1705
|
+
const routerOptions = {
|
|
1706
|
+
...DEFAULT_ROUTER_OPTIONS,
|
|
1707
|
+
...options
|
|
1607
1708
|
};
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
http2ServerOptions.key = fs3.readFileSync(certOptions.keyFile);
|
|
1611
|
-
}
|
|
1612
|
-
if (certOptions.certFile) {
|
|
1613
|
-
http2ServerOptions.cert = fs3.readFileSync(certOptions.certFile);
|
|
1614
|
-
}
|
|
1615
|
-
} catch (err) {
|
|
1616
|
-
throw new Error(
|
|
1617
|
-
`Failed to read certificate files: ${err instanceof Error ? err.message : String(err)}`
|
|
1618
|
-
);
|
|
1709
|
+
if (options.basePath && !options.basePath.startsWith("/")) {
|
|
1710
|
+
console.warn("Base path does nothing");
|
|
1619
1711
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
\u{1F680} Server: ${url}
|
|
1633
|
-
\u{1F525} Hot Reload: Enabled
|
|
1634
|
-
\u{1F6E0}\uFE0F Mode: Development
|
|
1635
|
-
|
|
1636
|
-
Time to build something amazing! \u{1F680}
|
|
1637
|
-
|
|
1638
|
-
\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}
|
|
1639
|
-
`);
|
|
1640
|
-
resolve2();
|
|
1641
|
-
});
|
|
1642
|
-
server.on("error", (err) => {
|
|
1643
|
-
console.error("Server error:", err);
|
|
1644
|
-
reject(err);
|
|
1645
|
-
});
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
async function initializePlugins(serverInstance) {
|
|
1649
|
-
for (const plugin of serverInstance.plugins) {
|
|
1650
|
-
if (typeof plugin.initialize === "function") {
|
|
1651
|
-
await plugin.initialize(serverInstance);
|
|
1712
|
+
const routes = [];
|
|
1713
|
+
const matcher = createMatcher();
|
|
1714
|
+
let initialized = false;
|
|
1715
|
+
let initializationPromise = null;
|
|
1716
|
+
let _watchers = null;
|
|
1717
|
+
const routeSources = /* @__PURE__ */ new Map();
|
|
1718
|
+
const routeDirectories = /* @__PURE__ */ new Set([routerOptions.routesDir]);
|
|
1719
|
+
function addRouteWithSource(route, source) {
|
|
1720
|
+
const existingSources = routeSources.get(route.path) || [];
|
|
1721
|
+
if (existingSources.includes(source)) {
|
|
1722
|
+
console.warn(`Skipping duplicate route: ${route.path} from ${source}`);
|
|
1723
|
+
return;
|
|
1652
1724
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
try {
|
|
1660
|
-
const port = serverOptions.port;
|
|
1661
|
-
const host = serverOptions.host;
|
|
1662
|
-
await initializePlugins(serverInstance);
|
|
1663
|
-
const http2Options = serverOptions.http2 || { enabled: true };
|
|
1664
|
-
const isHttp2 = !!http2Options.enabled;
|
|
1665
|
-
const certOptions = await prepareCertificates(http2Options);
|
|
1666
|
-
if (serverOptions.http2 && certOptions.keyFile && certOptions.certFile) {
|
|
1667
|
-
serverOptions.http2.keyFile = certOptions.keyFile;
|
|
1668
|
-
serverOptions.http2.certFile = certOptions.certFile;
|
|
1725
|
+
if (existingSources.length > 0) {
|
|
1726
|
+
const conflictError = new Error(
|
|
1727
|
+
`Route conflict for path "${route.path}": already defined in ${existingSources.join(", ")}, now being added from ${source}`
|
|
1728
|
+
);
|
|
1729
|
+
console.error(conflictError.message);
|
|
1730
|
+
throw conflictError;
|
|
1669
1731
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
serverInstance.port = port;
|
|
1673
|
-
serverInstance.host = host;
|
|
1674
|
-
const requestHandler = createRequestHandler(serverInstance);
|
|
1675
|
-
server.on("request", requestHandler);
|
|
1676
|
-
await listenOnPort(server, port, host, isHttp2);
|
|
1677
|
-
} catch (error) {
|
|
1678
|
-
console.error("Failed to start server:", error);
|
|
1679
|
-
throw error;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// src/server/stop.ts
|
|
1684
|
-
async function stopServer(serverInstance, options = {}) {
|
|
1685
|
-
const server = serverInstance.server;
|
|
1686
|
-
const events = serverInstance.events;
|
|
1687
|
-
if (!server) {
|
|
1688
|
-
return;
|
|
1732
|
+
routeSources.set(route.path, [...existingSources, source]);
|
|
1733
|
+
addRouteInternal(route);
|
|
1689
1734
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
}
|
|
1695
|
-
events.emit("stopping");
|
|
1696
|
-
await serverInstance.pluginManager.onServerStop(serverInstance, server);
|
|
1697
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1698
|
-
setTimeout(() => {
|
|
1699
|
-
reject(new Error("Server shutdown timed out waiting for requests to complete"));
|
|
1700
|
-
}, timeout);
|
|
1701
|
-
});
|
|
1702
|
-
const closePromise = new Promise((resolve2, reject) => {
|
|
1703
|
-
server.close((err) => {
|
|
1704
|
-
if (err) {
|
|
1705
|
-
return reject(err);
|
|
1706
|
-
}
|
|
1707
|
-
resolve2();
|
|
1735
|
+
async function loadRoutesFromDirectory(directory, source, prefix) {
|
|
1736
|
+
try {
|
|
1737
|
+
const discoveredRoutes = await findRoutes(directory, {
|
|
1738
|
+
basePath: routerOptions.basePath
|
|
1708
1739
|
});
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1740
|
+
for (const route of discoveredRoutes) {
|
|
1741
|
+
const finalRoute = prefix ? {
|
|
1742
|
+
...route,
|
|
1743
|
+
path: `${prefix}${route.path}`
|
|
1744
|
+
} : route;
|
|
1745
|
+
addRouteWithSource(finalRoute, source);
|
|
1746
|
+
}
|
|
1747
|
+
console.log(
|
|
1748
|
+
`Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""}`
|
|
1749
|
+
);
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
console.error(`Failed to load routes from ${source}:`, error);
|
|
1752
|
+
throw error;
|
|
1714
1753
|
}
|
|
1715
|
-
events.emit("stopped");
|
|
1716
|
-
serverInstance.server = null;
|
|
1717
|
-
} catch (error) {
|
|
1718
|
-
events.emit("error", error);
|
|
1719
|
-
throw error;
|
|
1720
1754
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const sigtermHandler = () => stopFn().catch(console.error);
|
|
1725
|
-
process.on("SIGINT", sigintHandler);
|
|
1726
|
-
process.on("SIGTERM", sigtermHandler);
|
|
1727
|
-
return {
|
|
1728
|
-
unregister: () => {
|
|
1729
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
1730
|
-
process.removeListener("SIGTERM", sigtermHandler);
|
|
1755
|
+
async function initialize() {
|
|
1756
|
+
if (initialized || initializationPromise) {
|
|
1757
|
+
return initializationPromise;
|
|
1731
1758
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
);
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1759
|
+
initializationPromise = (async () => {
|
|
1760
|
+
try {
|
|
1761
|
+
for (const directory of routeDirectories) {
|
|
1762
|
+
await loadRoutesFromDirectory(directory, directory);
|
|
1763
|
+
}
|
|
1764
|
+
if (routerOptions.watchMode) {
|
|
1765
|
+
setupWatcherForAllDirectories();
|
|
1766
|
+
}
|
|
1767
|
+
initialized = true;
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
console.error("Failed to initialize router:", error);
|
|
1770
|
+
throw error;
|
|
1771
|
+
}
|
|
1772
|
+
})();
|
|
1773
|
+
return initializationPromise;
|
|
1747
1774
|
}
|
|
1748
|
-
)
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
})
|
|
1754
|
-
(data) => {
|
|
1755
|
-
if (data.enabled && process.env.NODE_ENV === "production") {
|
|
1756
|
-
return data.keyFile && data.certFile;
|
|
1757
|
-
}
|
|
1758
|
-
return true;
|
|
1759
|
-
},
|
|
1760
|
-
{
|
|
1761
|
-
message: "When HTTP/2 is enabled (outside of development mode), both keyFile and certFile must be provided"
|
|
1775
|
+
function addRouteInternal(route) {
|
|
1776
|
+
routes.push(route);
|
|
1777
|
+
Object.entries(route).forEach(([method, methodOptions]) => {
|
|
1778
|
+
if (method === "path" || !methodOptions) return;
|
|
1779
|
+
matcher.add(route.path, method, methodOptions);
|
|
1780
|
+
});
|
|
1762
1781
|
}
|
|
1763
|
-
)
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
});
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1782
|
+
function createWatcherCallbacks(directory, source, prefix) {
|
|
1783
|
+
return {
|
|
1784
|
+
onRouteAdded: (addedRoutes) => {
|
|
1785
|
+
console.log(
|
|
1786
|
+
`${addedRoutes.length} route(s) added from ${directory}:`,
|
|
1787
|
+
addedRoutes.map((r) => r.path)
|
|
1788
|
+
);
|
|
1789
|
+
addedRoutes.forEach((route) => {
|
|
1790
|
+
const finalRoute = prefix ? { ...route, path: `${prefix}${route.path}` } : route;
|
|
1791
|
+
addRouteWithSource(finalRoute, source);
|
|
1792
|
+
});
|
|
1793
|
+
},
|
|
1794
|
+
onRouteChanged: (changedRoutes) => {
|
|
1795
|
+
console.log(
|
|
1796
|
+
`${changedRoutes.length} route(s) changed in ${directory}:`,
|
|
1797
|
+
changedRoutes.map((r) => r.path)
|
|
1798
|
+
);
|
|
1799
|
+
changedRoutes.forEach((route) => {
|
|
1800
|
+
const finalPath = prefix ? `${prefix}${route.path}` : route.path;
|
|
1801
|
+
const index = routes.findIndex((r) => r.path === finalPath);
|
|
1802
|
+
if (index >= 0) {
|
|
1803
|
+
routes.splice(index, 1);
|
|
1804
|
+
const sources = routeSources.get(finalPath) || [];
|
|
1805
|
+
const filteredSources = sources.filter((s) => s !== source);
|
|
1806
|
+
if (filteredSources.length > 0) {
|
|
1807
|
+
routeSources.set(finalPath, filteredSources);
|
|
1808
|
+
} else {
|
|
1809
|
+
routeSources.delete(finalPath);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
const finalRoute = prefix ? { ...route, path: finalPath } : route;
|
|
1813
|
+
addRouteWithSource(finalRoute, source);
|
|
1814
|
+
});
|
|
1815
|
+
},
|
|
1816
|
+
onRouteRemoved: (filePath, removedRoutes) => {
|
|
1817
|
+
console.log(
|
|
1818
|
+
`File removed from ${directory}: ${filePath} with ${removedRoutes.length} route(s):`,
|
|
1819
|
+
removedRoutes.map((r) => r.path)
|
|
1820
|
+
);
|
|
1821
|
+
removedRoutes.forEach((route) => {
|
|
1822
|
+
const finalPath = prefix ? `${prefix}${route.path}` : route.path;
|
|
1823
|
+
const index = routes.findIndex((r) => r.path === finalPath);
|
|
1824
|
+
if (index >= 0) {
|
|
1825
|
+
routes.splice(index, 1);
|
|
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
|
+
});
|
|
1835
|
+
},
|
|
1836
|
+
onError: (error) => {
|
|
1837
|
+
console.error(`Route watcher error for ${directory}:`, error);
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1783
1840
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
if (
|
|
1791
|
-
|
|
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
|
+
});
|
|
1847
|
+
if (!_watchers) {
|
|
1848
|
+
_watchers = /* @__PURE__ */ new Map();
|
|
1792
1849
|
}
|
|
1850
|
+
_watchers.set(directory, watcher);
|
|
1851
|
+
return watcher;
|
|
1793
1852
|
}
|
|
1794
|
-
function
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
onError(plugin, phase, error);
|
|
1798
|
-
} else {
|
|
1799
|
-
console.error(errorMessage, error);
|
|
1800
|
-
}
|
|
1801
|
-
if (!continueOnError) {
|
|
1802
|
-
throw new Error(errorMessage);
|
|
1853
|
+
function setupWatcherForAllDirectories() {
|
|
1854
|
+
for (const directory of routeDirectories) {
|
|
1855
|
+
setupWatcherForDirectory(directory, directory);
|
|
1803
1856
|
}
|
|
1804
1857
|
}
|
|
1858
|
+
initialize().catch((error) => {
|
|
1859
|
+
console.error("Failed to initialize router on creation:", error);
|
|
1860
|
+
});
|
|
1805
1861
|
return {
|
|
1806
1862
|
/**
|
|
1807
|
-
*
|
|
1863
|
+
* Handle an incoming request
|
|
1808
1864
|
*/
|
|
1809
|
-
async
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1865
|
+
async handleRequest(ctx) {
|
|
1866
|
+
if (!initialized) {
|
|
1867
|
+
await initialize();
|
|
1868
|
+
}
|
|
1869
|
+
const { method, path: path5 } = ctx.request;
|
|
1870
|
+
const match = matcher.match(path5, method);
|
|
1871
|
+
if (!match) {
|
|
1872
|
+
ctx.response.status(404).json({ error: "Not Found" });
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
if (match.methodNotAllowed) {
|
|
1876
|
+
ctx.response.status(405).json({
|
|
1877
|
+
error: "Method Not Allowed",
|
|
1878
|
+
allowed: match.allowedMethods
|
|
1879
|
+
});
|
|
1880
|
+
if (match.allowedMethods && match.allowedMethods.length > 0) {
|
|
1881
|
+
ctx.response.header("Allow", match.allowedMethods.join(", "));
|
|
1819
1882
|
}
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
ctx.request.params = match.params;
|
|
1886
|
+
try {
|
|
1887
|
+
await executeHandler(ctx, match.route, match.params);
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
handleRouteError(ctx, error, {
|
|
1890
|
+
detailed: process.env.NODE_ENV !== "production",
|
|
1891
|
+
log: true
|
|
1892
|
+
});
|
|
1820
1893
|
}
|
|
1821
|
-
log(`Initialized ${server.plugins.length} plugins`);
|
|
1822
1894
|
},
|
|
1823
1895
|
/**
|
|
1824
|
-
*
|
|
1896
|
+
* Get all registered routes
|
|
1825
1897
|
*/
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const pluginsToTerminate = [...server.plugins].reverse();
|
|
1829
|
-
for (const plugin of pluginsToTerminate) {
|
|
1830
|
-
if (plugin.terminate) {
|
|
1831
|
-
try {
|
|
1832
|
-
log(`Terminating plugin: ${plugin.name}`);
|
|
1833
|
-
await plugin.terminate(server);
|
|
1834
|
-
} catch (error) {
|
|
1835
|
-
handleError(plugin, "terminate", error);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
log(`Terminated ${pluginsToTerminate.length} plugins`);
|
|
1898
|
+
getRoutes() {
|
|
1899
|
+
return [...routes];
|
|
1840
1900
|
},
|
|
1841
1901
|
/**
|
|
1842
|
-
*
|
|
1902
|
+
* Add a route programmatically
|
|
1843
1903
|
*/
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1904
|
+
addRoute(route) {
|
|
1905
|
+
addRouteInternal(route);
|
|
1906
|
+
},
|
|
1907
|
+
/**
|
|
1908
|
+
* Add a route directory (for plugins)
|
|
1909
|
+
*/
|
|
1910
|
+
async addRouteDirectory(directory, options2 = {}) {
|
|
1911
|
+
if (routeDirectories.has(directory)) {
|
|
1912
|
+
console.warn(`Route directory ${directory} already registered`);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
routeDirectories.add(directory);
|
|
1916
|
+
if (initialized) {
|
|
1917
|
+
await loadRoutesFromDirectory(directory, directory, options2.prefix);
|
|
1918
|
+
if (routerOptions.watchMode) {
|
|
1919
|
+
setupWatcherForDirectory(directory, directory, options2.prefix);
|
|
1854
1920
|
}
|
|
1855
1921
|
}
|
|
1856
1922
|
},
|
|
1857
1923
|
/**
|
|
1858
|
-
*
|
|
1924
|
+
* Get route conflicts
|
|
1859
1925
|
*/
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
try {
|
|
1866
|
-
log(`Notifying plugin of server stop: ${plugin.name}`);
|
|
1867
|
-
await plugin.onServerStop(httpServer);
|
|
1868
|
-
} catch (error) {
|
|
1869
|
-
handleError(plugin, "onServerStop", error);
|
|
1870
|
-
}
|
|
1926
|
+
getRouteConflicts() {
|
|
1927
|
+
const conflicts = [];
|
|
1928
|
+
for (const [path5, sources] of routeSources.entries()) {
|
|
1929
|
+
if (sources.length > 1) {
|
|
1930
|
+
conflicts.push({ path: path5, sources });
|
|
1871
1931
|
}
|
|
1872
1932
|
}
|
|
1933
|
+
return conflicts;
|
|
1873
1934
|
}
|
|
1874
1935
|
};
|
|
1875
1936
|
}
|
|
1876
1937
|
|
|
1877
|
-
// src/plugins/errors.ts
|
|
1878
|
-
var PluginValidationError = class extends Error {
|
|
1879
|
-
constructor(pluginName, message) {
|
|
1880
|
-
super(`Plugin validation error${pluginName ? ` for "${pluginName}"` : ""}: ${message}`);
|
|
1881
|
-
this.pluginName = pluginName;
|
|
1882
|
-
this.name = "PluginValidationError";
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
|
|
1886
|
-
// src/plugins/validation.ts
|
|
1887
|
-
var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
1888
|
-
"core",
|
|
1889
|
-
"server",
|
|
1890
|
-
"router",
|
|
1891
|
-
"middleware",
|
|
1892
|
-
"context",
|
|
1893
|
-
"blaize",
|
|
1894
|
-
"blaizejs"
|
|
1895
|
-
]);
|
|
1896
|
-
var VALID_NAME_PATTERN = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
|
|
1897
|
-
var VALID_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9-.]+)?(?:\+[a-zA-Z0-9-.]+)?$/;
|
|
1898
|
-
function validatePlugin(plugin, options = {}) {
|
|
1899
|
-
const { requireVersion = true, validateNameFormat = true, checkReservedNames = true } = options;
|
|
1900
|
-
if (!plugin || typeof plugin !== "object") {
|
|
1901
|
-
throw new PluginValidationError("", "Plugin must be an object");
|
|
1902
|
-
}
|
|
1903
|
-
const p = plugin;
|
|
1904
|
-
if (!p.name || typeof p.name !== "string") {
|
|
1905
|
-
throw new PluginValidationError("", "Plugin must have a name (string)");
|
|
1906
|
-
}
|
|
1907
|
-
if (validateNameFormat && !VALID_NAME_PATTERN.test(p.name)) {
|
|
1908
|
-
throw new PluginValidationError(
|
|
1909
|
-
p.name,
|
|
1910
|
-
"Plugin name must be lowercase letters, numbers, and hyphens only"
|
|
1911
|
-
);
|
|
1912
|
-
}
|
|
1913
|
-
if (checkReservedNames && RESERVED_NAMES.has(p.name.toLowerCase())) {
|
|
1914
|
-
throw new PluginValidationError(p.name, `Plugin name "${p.name}" is reserved`);
|
|
1915
|
-
}
|
|
1916
|
-
if (requireVersion) {
|
|
1917
|
-
if (!p.version || typeof p.version !== "string") {
|
|
1918
|
-
throw new PluginValidationError(p.name, "Plugin must have a version (string)");
|
|
1919
|
-
}
|
|
1920
|
-
if (!VALID_VERSION_PATTERN.test(p.version)) {
|
|
1921
|
-
throw new PluginValidationError(
|
|
1922
|
-
p.name,
|
|
1923
|
-
'Plugin version must follow semantic versioning (e.g., "1.0.0")'
|
|
1924
|
-
);
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
if (!p.register || typeof p.register !== "function") {
|
|
1928
|
-
throw new PluginValidationError(p.name, "Plugin must have a register method (function)");
|
|
1929
|
-
}
|
|
1930
|
-
const lifecycleMethods = ["initialize", "terminate", "onServerStart", "onServerStop"];
|
|
1931
|
-
for (const method of lifecycleMethods) {
|
|
1932
|
-
if (p[method] && typeof p[method] !== "function") {
|
|
1933
|
-
throw new PluginValidationError(p.name, `Plugin ${method} must be a function if provided`);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
1938
|
// src/server/create.ts
|
|
1939
1939
|
var DEFAULT_OPTIONS = {
|
|
1940
1940
|
port: 3e3,
|