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