blaizejs 0.1.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +746 -198
- package/dist/index.cjs +1629 -1355
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +688 -23
- package/dist/index.d.ts +688 -23
- package/dist/index.js +1629 -1355
- package/dist/index.js.map +1 -1
- package/package.json +24 -25
package/dist/index.js
CHANGED
|
@@ -1,37 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* blaizejs v0.
|
|
3
|
-
* A blazing-fast,
|
|
2
|
+
* blaizejs v0.2.2
|
|
3
|
+
* A blazing-fast, TypeScript-first Node.js framework with HTTP/2 support, file-based routing, powerful middleware system, and end-to-end type safety for building modern APIs.
|
|
4
4
|
*
|
|
5
5
|
* Copyright (c) 2025 BlaizeJS Contributors
|
|
6
6
|
* @license MIT
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
// src/middleware/create.ts
|
|
11
|
-
function create(handlerOrOptions) {
|
|
12
|
-
if (typeof handlerOrOptions === "function") {
|
|
13
|
-
return {
|
|
14
|
-
name: "anonymous",
|
|
15
|
-
// Default name for function middleware
|
|
16
|
-
execute: handlerOrOptions,
|
|
17
|
-
debug: false
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
const { name = "anonymous", handler, skip, debug = false } = handlerOrOptions;
|
|
21
|
-
const middleware = {
|
|
22
|
-
name,
|
|
23
|
-
execute: handler,
|
|
24
|
-
debug
|
|
25
|
-
};
|
|
26
|
-
if (skip !== void 0) {
|
|
27
|
-
return {
|
|
28
|
-
...middleware,
|
|
29
|
-
skip
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
return middleware;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
10
|
// src/middleware/execute.ts
|
|
36
11
|
function execute(middleware, ctx, next) {
|
|
37
12
|
if (!middleware) {
|
|
@@ -79,54 +54,80 @@ function compose(middlewareStack) {
|
|
|
79
54
|
};
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
// src/
|
|
83
|
-
function
|
|
84
|
-
|
|
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/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
121
|
-
return routeFiles;
|
|
105
|
+
};
|
|
106
|
+
return plugin;
|
|
107
|
+
};
|
|
122
108
|
}
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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 =
|
|
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/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
async function loadRouteModule(filePath, basePath) {
|
|
167
|
+
// src/router/create.ts
|
|
168
|
+
function getCallerFilePath() {
|
|
169
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
171
170
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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/
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
412
|
-
}
|
|
374
|
+
};
|
|
413
375
|
|
|
414
|
-
// src/
|
|
415
|
-
import {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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/
|
|
424
|
-
|
|
425
|
-
function
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
455
|
+
raw: req,
|
|
456
|
+
...info,
|
|
457
|
+
header: createRequestHeaderGetter(req),
|
|
458
|
+
headers: createRequestHeadersGetter(req),
|
|
459
|
+
body: void 0
|
|
479
460
|
};
|
|
480
461
|
}
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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/
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
805
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
throw new Error("Unable to determine caller file name");
|
|
801
|
+
if (certOptions.certFile) {
|
|
802
|
+
http2ServerOptions.cert = fs2.readFileSync(certOptions.certFile);
|
|
813
803
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
886
|
-
|
|
843
|
+
}
|
|
844
|
+
async function startServer(serverInstance, serverOptions) {
|
|
845
|
+
if (serverInstance.server) {
|
|
846
|
+
return;
|
|
887
847
|
}
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
907
|
-
|
|
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
|
-
|
|
910
|
-
|
|
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
|
-
|
|
913
|
-
|
|
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/
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
980
|
-
|
|
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/
|
|
985
|
-
var
|
|
986
|
-
constructor(message
|
|
987
|
-
super(message);
|
|
988
|
-
this.
|
|
1066
|
+
// src/plugins/errors.ts
|
|
1067
|
+
var PluginValidationError = class extends Error {
|
|
1068
|
+
constructor(pluginName, message) {
|
|
1069
|
+
super(`Plugin validation error${pluginName ? ` for "${pluginName}"` : ""}: ${message}`);
|
|
1070
|
+
this.pluginName = pluginName;
|
|
1071
|
+
this.name = "PluginValidationError";
|
|
989
1072
|
}
|
|
990
1073
|
};
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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/
|
|
1008
|
-
import
|
|
1009
|
-
|
|
1010
|
-
function
|
|
1011
|
-
|
|
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/
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
|
|
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
|
-
|
|
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.
|
|
1039
|
-
|
|
1208
|
+
console.error(`Failed to load route module ${filePath}:`, error);
|
|
1209
|
+
return [];
|
|
1040
1210
|
}
|
|
1041
1211
|
}
|
|
1042
|
-
function
|
|
1043
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
|
1235
|
+
return routes;
|
|
1056
1236
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
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 (
|
|
1195
|
-
|
|
1344
|
+
if (error && typeof error === "object" && "details" in error && error.details) {
|
|
1345
|
+
response.details = error.details;
|
|
1196
1346
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
responseState.sent = true;
|
|
1200
|
-
};
|
|
1347
|
+
}
|
|
1348
|
+
ctx.response.status(status).json(response);
|
|
1201
1349
|
}
|
|
1202
|
-
function
|
|
1203
|
-
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
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 (
|
|
1208
|
-
|
|
1355
|
+
if ("statusCode" in error && typeof error.statusCode === "number") {
|
|
1356
|
+
return error.statusCode;
|
|
1209
1357
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
res.end();
|
|
1223
|
-
responseState.sent = true;
|
|
1224
|
-
};
|
|
1361
|
+
}
|
|
1362
|
+
return 500;
|
|
1225
1363
|
}
|
|
1226
|
-
function
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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 (
|
|
1235
|
-
|
|
1385
|
+
if ("name" in error && typeof error.name === "string") {
|
|
1386
|
+
return error.name;
|
|
1236
1387
|
}
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1392
|
+
return "Error";
|
|
1393
|
+
}
|
|
1394
|
+
function getErrorMessage(error) {
|
|
1395
|
+
if (error instanceof Error) {
|
|
1396
|
+
return error.message;
|
|
1264
1397
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1499
|
+
};
|
|
1500
|
+
await next();
|
|
1501
|
+
};
|
|
1502
|
+
return {
|
|
1503
|
+
name: "ResponseValidator",
|
|
1504
|
+
execute: middlewareFn,
|
|
1505
|
+
debug
|
|
1506
|
+
};
|
|
1328
1507
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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/
|
|
1354
|
-
function
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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/
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
|
1545
|
+
return params;
|
|
1419
1546
|
}
|
|
1420
|
-
function
|
|
1421
|
-
|
|
1422
|
-
|
|
1547
|
+
function compilePathPattern(path5) {
|
|
1548
|
+
const paramNames = [];
|
|
1549
|
+
if (path5 === "/") {
|
|
1550
|
+
return {
|
|
1551
|
+
pattern: /^\/$/,
|
|
1552
|
+
paramNames: []
|
|
1553
|
+
};
|
|
1423
1554
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
if (options.onStopping) {
|
|
1514
|
-
await options.onStopping();
|
|
1703
|
+
async function initialize() {
|
|
1704
|
+
if (initialized || initializationPromise) {
|
|
1705
|
+
return initializationPromise;
|
|
1515
1706
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
})
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1591
|
-
|
|
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,
|