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