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