@buenojs/bueno 0.8.0
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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Routes Implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides Next.js-style API routes:
|
|
5
|
+
* - pages/api/ directory for API endpoints
|
|
6
|
+
* - HTTP method handlers (GET, POST, PUT, PATCH, DELETE, etc.)
|
|
7
|
+
* - Request context with params, query, body
|
|
8
|
+
* - Middleware support via _middleware.ts
|
|
9
|
+
* - Type-safe response helpers
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createLogger, type Logger } from "../logger/index.js";
|
|
13
|
+
import type {
|
|
14
|
+
APIRouteConfig,
|
|
15
|
+
PartialAPIRouteConfig,
|
|
16
|
+
APIRouteDefinition,
|
|
17
|
+
APIRouteHandler,
|
|
18
|
+
APIContext,
|
|
19
|
+
APIResponse,
|
|
20
|
+
APIMiddleware,
|
|
21
|
+
APIRouteModule,
|
|
22
|
+
HTTPMethod,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
// ============= Constants =============
|
|
26
|
+
|
|
27
|
+
const DEFAULT_API_DIR = "pages/api";
|
|
28
|
+
const SUPPORTED_EXTENSIONS = [".ts", ".js"];
|
|
29
|
+
const SUPPORTED_METHODS: HTTPMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
30
|
+
|
|
31
|
+
// ============= API Route Manager Class =============
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* API Route Manager handles API endpoint registration and routing
|
|
35
|
+
*
|
|
36
|
+
* Features:
|
|
37
|
+
* - pages/api/ directory scanning
|
|
38
|
+
* - HTTP method handlers
|
|
39
|
+
* - Request context with params, query, body
|
|
40
|
+
* - Middleware support
|
|
41
|
+
* - Type-safe response helpers
|
|
42
|
+
*/
|
|
43
|
+
export class APIRouteManager {
|
|
44
|
+
private config: APIRouteConfig;
|
|
45
|
+
private logger: Logger;
|
|
46
|
+
private routes: Map<string, APIRouteDefinition> = new Map();
|
|
47
|
+
private middlewares: Map<string, APIMiddleware[]> = new Map();
|
|
48
|
+
private modules: Map<string, APIRouteModule> = new Map();
|
|
49
|
+
|
|
50
|
+
constructor(config: PartialAPIRouteConfig = {}) {
|
|
51
|
+
this.config = this.normalizeConfig(config);
|
|
52
|
+
this.logger = createLogger({
|
|
53
|
+
level: "debug",
|
|
54
|
+
pretty: true,
|
|
55
|
+
context: { component: "APIRouteManager" },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize partial config to full config with defaults
|
|
61
|
+
*/
|
|
62
|
+
private normalizeConfig(config: PartialAPIRouteConfig): APIRouteConfig {
|
|
63
|
+
return {
|
|
64
|
+
apiDir: config.apiDir ?? DEFAULT_API_DIR,
|
|
65
|
+
rootDir: config.rootDir ?? process.cwd(),
|
|
66
|
+
extensions: config.extensions ?? SUPPORTED_EXTENSIONS,
|
|
67
|
+
bodyLimit: config.bodyLimit ?? 1024 * 1024, // 1MB default
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize the API route manager by scanning for API files
|
|
73
|
+
*/
|
|
74
|
+
async init(): Promise<void> {
|
|
75
|
+
this.logger.info(`Initializing API routes from: ${this.config.apiDir}`);
|
|
76
|
+
await this.scanAPIDirectory();
|
|
77
|
+
this.logger.info(`Loaded ${this.routes.size} API routes`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scan the API directory for route files
|
|
82
|
+
*/
|
|
83
|
+
private async scanAPIDirectory(): Promise<void> {
|
|
84
|
+
const apiPath = this.config.apiDir;
|
|
85
|
+
const glob = new Bun.Glob(`**/*{${this.config.extensions.join(",")}}`);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
for await (const file of glob.scan(apiPath)) {
|
|
89
|
+
// Skip middleware files
|
|
90
|
+
if (file.includes("_middleware")) continue;
|
|
91
|
+
|
|
92
|
+
await this.processAPIFile(file, apiPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Load middlewares after routes
|
|
96
|
+
await this.loadMiddlewares(apiPath);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.logger.error(`Failed to scan API directory: ${apiPath}`, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Process a single API file
|
|
104
|
+
*/
|
|
105
|
+
private async processAPIFile(filePath: string, basePath: string): Promise<void> {
|
|
106
|
+
const fullPath = `${basePath}/${filePath}`;
|
|
107
|
+
const routePath = this.filePathToRoute(filePath);
|
|
108
|
+
|
|
109
|
+
// Load the module to check for handlers
|
|
110
|
+
const module = await this.loadModule(fullPath);
|
|
111
|
+
if (!module) return;
|
|
112
|
+
|
|
113
|
+
// Check which HTTP methods are exported
|
|
114
|
+
const methods: HTTPMethod[] = [];
|
|
115
|
+
for (const method of SUPPORTED_METHODS) {
|
|
116
|
+
if (typeof module[method] === "function") {
|
|
117
|
+
methods.push(method);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (methods.length === 0) {
|
|
122
|
+
this.logger.warn(`No HTTP handlers found in: ${filePath}`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse route parameters
|
|
127
|
+
const params = this.parseRouteParams(routePath);
|
|
128
|
+
|
|
129
|
+
const route: APIRouteDefinition = {
|
|
130
|
+
id: this.generateRouteId(filePath),
|
|
131
|
+
path: routePath,
|
|
132
|
+
filePath: fullPath,
|
|
133
|
+
methods,
|
|
134
|
+
params,
|
|
135
|
+
regex: this.routeToRegex(routePath),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
this.routes.set(routePath, route);
|
|
139
|
+
this.logger.debug(`Processed API route: ${routePath} [${methods.join(", ")}]`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Load middlewares from _middleware.ts files
|
|
144
|
+
*/
|
|
145
|
+
private async loadMiddlewares(basePath: string): Promise<void> {
|
|
146
|
+
const glob = new Bun.Glob(`**/_middleware{${this.config.extensions.join(",")}}`);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
for await (const file of glob.scan(basePath)) {
|
|
150
|
+
const fullPath = `${basePath}/${file}`;
|
|
151
|
+
const segment = this.getMiddlewareSegment(file);
|
|
152
|
+
|
|
153
|
+
const module = await import(fullPath);
|
|
154
|
+
const middleware = module.default || module.middleware;
|
|
155
|
+
|
|
156
|
+
if (middleware) {
|
|
157
|
+
if (Array.isArray(middleware)) {
|
|
158
|
+
this.middlewares.set(segment, middleware);
|
|
159
|
+
} else {
|
|
160
|
+
this.middlewares.set(segment, [middleware]);
|
|
161
|
+
}
|
|
162
|
+
this.logger.debug(`Loaded middleware for: ${segment}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.logger.error("Failed to load middlewares", error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get middleware segment from file path
|
|
172
|
+
*/
|
|
173
|
+
private getMiddlewareSegment(filePath: string): string {
|
|
174
|
+
const segment = filePath.replace(/\/_middleware\.(ts|js)$/, "");
|
|
175
|
+
return segment === "" ? "/" : `/${segment}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert file path to API route path
|
|
180
|
+
*/
|
|
181
|
+
private filePathToRoute(filePath: string): string {
|
|
182
|
+
// Remove file extension
|
|
183
|
+
let route = filePath.replace(/\.(ts|js)$/, "");
|
|
184
|
+
|
|
185
|
+
// Convert index to root
|
|
186
|
+
if (route === "index") {
|
|
187
|
+
return "/api";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle nested index files
|
|
191
|
+
if (route.endsWith("/index")) {
|
|
192
|
+
route = route.replace(/\/index$/, "");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Ensure leading /api
|
|
196
|
+
if (!route.startsWith("api/")) {
|
|
197
|
+
route = `api/${route}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return `/${route}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse route parameters from path
|
|
205
|
+
*/
|
|
206
|
+
private parseRouteParams(path: string): string[] {
|
|
207
|
+
const params: string[] = [];
|
|
208
|
+
const regex = /\[([^\]]+)\]/g;
|
|
209
|
+
let match;
|
|
210
|
+
|
|
211
|
+
while ((match = regex.exec(path)) !== null) {
|
|
212
|
+
params.push(match[1]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return params;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Convert route path to regex
|
|
220
|
+
*/
|
|
221
|
+
private routeToRegex(routePath: string): RegExp {
|
|
222
|
+
let regex = routePath;
|
|
223
|
+
|
|
224
|
+
// Escape special regex characters
|
|
225
|
+
regex = regex.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
226
|
+
|
|
227
|
+
// Replace catch-all params [...param] with capture group
|
|
228
|
+
regex = regex.replace(/\\\[\\\.\.\.([^\]]+)\\\]/g, "(.*)");
|
|
229
|
+
|
|
230
|
+
// Replace single params [param] with capture group
|
|
231
|
+
regex = regex.replace(/\\\[([^\]]+)\\\]/g, "([^/]+)");
|
|
232
|
+
|
|
233
|
+
return new RegExp(`^${regex}$`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generate unique route ID
|
|
238
|
+
*/
|
|
239
|
+
private generateRouteId(filePath: string): string {
|
|
240
|
+
return `api-${filePath.replace(/[\/\\.]/g, "-")}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Load API module
|
|
245
|
+
*/
|
|
246
|
+
private async loadModule(filePath: string): Promise<APIRouteModule | null> {
|
|
247
|
+
if (this.modules.has(filePath)) {
|
|
248
|
+
return this.modules.get(filePath)!;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const module = await import(filePath);
|
|
253
|
+
this.modules.set(filePath, module);
|
|
254
|
+
return module;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
this.logger.error(`Failed to load API module: ${filePath}`, error);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Match a request to an API route
|
|
263
|
+
*/
|
|
264
|
+
match(method: string, pathname: string): { route: APIRouteDefinition; params: Record<string, string> } | null {
|
|
265
|
+
for (const route of this.routes.values()) {
|
|
266
|
+
const match = pathname.match(route.regex);
|
|
267
|
+
if (match) {
|
|
268
|
+
// Check if method is supported
|
|
269
|
+
if (!route.methods.includes(method as HTTPMethod)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Extract params
|
|
274
|
+
const params: Record<string, string> = {};
|
|
275
|
+
for (let i = 0; i < route.params.length; i++) {
|
|
276
|
+
params[route.params[i]] = match[i + 1];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { route, params };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Handle incoming API request
|
|
288
|
+
*/
|
|
289
|
+
async handle(request: Request): Promise<Response> {
|
|
290
|
+
const url = new URL(request.url);
|
|
291
|
+
const pathname = url.pathname;
|
|
292
|
+
const method = request.method as HTTPMethod;
|
|
293
|
+
|
|
294
|
+
const match = this.match(method, pathname);
|
|
295
|
+
|
|
296
|
+
if (!match) {
|
|
297
|
+
// Check if route exists but method not allowed
|
|
298
|
+
const routeExists = this.routeExists(pathname);
|
|
299
|
+
if (routeExists) {
|
|
300
|
+
return this.jsonResponse({ error: "Method Not Allowed" }, 405);
|
|
301
|
+
}
|
|
302
|
+
return this.jsonResponse({ error: "Not Found" }, 404);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const { route, params } = match;
|
|
306
|
+
|
|
307
|
+
// Create context
|
|
308
|
+
const context = await this.createContext(request, params);
|
|
309
|
+
|
|
310
|
+
// Get middlewares
|
|
311
|
+
const middlewares = this.getMiddlewaresForPath(pathname);
|
|
312
|
+
|
|
313
|
+
// Run middlewares and handler
|
|
314
|
+
try {
|
|
315
|
+
return await this.runWithMiddleware(context, middlewares, route, method);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this.logger.error(`API error: ${pathname}`, error);
|
|
318
|
+
return this.jsonResponse(
|
|
319
|
+
{ error: "Internal Server Error", message: error instanceof Error ? error.message : "Unknown error" },
|
|
320
|
+
500
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if a route exists (any method)
|
|
327
|
+
*/
|
|
328
|
+
private routeExists(pathname: string): boolean {
|
|
329
|
+
for (const route of this.routes.values()) {
|
|
330
|
+
if (pathname.match(route.regex)) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create API context from request
|
|
339
|
+
*/
|
|
340
|
+
private async createContext(
|
|
341
|
+
request: Request,
|
|
342
|
+
params: Record<string, string>
|
|
343
|
+
): Promise<APIContext> {
|
|
344
|
+
const url = new URL(request.url);
|
|
345
|
+
|
|
346
|
+
// Parse body based on content type
|
|
347
|
+
let body: unknown = null;
|
|
348
|
+
const contentType = request.headers.get("content-type") || "";
|
|
349
|
+
|
|
350
|
+
if (request.body) {
|
|
351
|
+
try {
|
|
352
|
+
if (contentType.includes("application/json")) {
|
|
353
|
+
body = await request.json();
|
|
354
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
355
|
+
const formData = await request.formData();
|
|
356
|
+
body = Object.fromEntries(formData);
|
|
357
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
358
|
+
const formData = await request.formData();
|
|
359
|
+
body = formData;
|
|
360
|
+
} else {
|
|
361
|
+
body = await request.text();
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
this.logger.warn("Failed to parse request body", error as Record<string, unknown>);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
request,
|
|
370
|
+
url: request.url,
|
|
371
|
+
pathname: url.pathname,
|
|
372
|
+
query: url.searchParams,
|
|
373
|
+
params,
|
|
374
|
+
body,
|
|
375
|
+
headers: request.headers,
|
|
376
|
+
method: request.method as HTTPMethod,
|
|
377
|
+
cookies: this.parseCookies(request.headers.get("cookie") || ""),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Parse cookies from header
|
|
383
|
+
*/
|
|
384
|
+
private parseCookies(cookieHeader: string): Record<string, string> {
|
|
385
|
+
const cookies: Record<string, string> = {};
|
|
386
|
+
|
|
387
|
+
if (!cookieHeader) return cookies;
|
|
388
|
+
|
|
389
|
+
for (const cookie of cookieHeader.split(";")) {
|
|
390
|
+
const [name, value] = cookie.trim().split("=");
|
|
391
|
+
if (name && value) {
|
|
392
|
+
cookies[name] = decodeURIComponent(value);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return cookies;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get middlewares for a path
|
|
401
|
+
*/
|
|
402
|
+
private getMiddlewaresForPath(pathname: string): APIMiddleware[] {
|
|
403
|
+
const middlewares: APIMiddleware[] = [];
|
|
404
|
+
|
|
405
|
+
// Check each middleware segment
|
|
406
|
+
for (const [segment, segmentMiddlewares] of this.middlewares) {
|
|
407
|
+
if (segment === "/" || pathname.startsWith(segment)) {
|
|
408
|
+
middlewares.push(...segmentMiddlewares);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return middlewares;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Run request through middlewares and handler
|
|
417
|
+
*/
|
|
418
|
+
private async runWithMiddleware(
|
|
419
|
+
context: APIContext,
|
|
420
|
+
middlewares: APIMiddleware[],
|
|
421
|
+
route: APIRouteDefinition,
|
|
422
|
+
method: HTTPMethod
|
|
423
|
+
): Promise<Response> {
|
|
424
|
+
let index = 0;
|
|
425
|
+
|
|
426
|
+
const next = async (): Promise<Response> => {
|
|
427
|
+
if (index < middlewares.length) {
|
|
428
|
+
const middleware = middlewares[index++];
|
|
429
|
+
return middleware(context, next);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Run the actual handler
|
|
433
|
+
const module = await this.loadModule(route.filePath);
|
|
434
|
+
if (!module || !module[method]) {
|
|
435
|
+
return this.jsonResponse({ error: "Handler Not Found" }, 404);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const result = await module[method](context);
|
|
439
|
+
return this.normalizeResponse(result);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return next();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Normalize response to Response object
|
|
447
|
+
*/
|
|
448
|
+
private normalizeResponse(result: APIResponse | Response): Response {
|
|
449
|
+
if (result instanceof Response) {
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (typeof result === "string") {
|
|
454
|
+
return new Response(result, {
|
|
455
|
+
headers: { "Content-Type": "text/plain" },
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return this.jsonResponse(result);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create JSON response
|
|
464
|
+
*/
|
|
465
|
+
private jsonResponse(data: unknown, status = 200): Response {
|
|
466
|
+
return new Response(JSON.stringify(data), {
|
|
467
|
+
status,
|
|
468
|
+
headers: {
|
|
469
|
+
"Content-Type": "application/json",
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get all routes
|
|
476
|
+
*/
|
|
477
|
+
getRoutes(): APIRouteDefinition[] {
|
|
478
|
+
return Array.from(this.routes.values());
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get route by path
|
|
483
|
+
*/
|
|
484
|
+
getRoute(path: string): APIRouteDefinition | undefined {
|
|
485
|
+
return this.routes.get(path);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Reload routes (for hot reload)
|
|
490
|
+
*/
|
|
491
|
+
async reload(): Promise<void> {
|
|
492
|
+
this.logger.info("Reloading API routes...");
|
|
493
|
+
this.routes.clear();
|
|
494
|
+
this.middlewares.clear();
|
|
495
|
+
this.modules.clear();
|
|
496
|
+
await this.init();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get configuration
|
|
501
|
+
*/
|
|
502
|
+
getConfig(): APIRouteConfig {
|
|
503
|
+
return { ...this.config };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ============= Factory Function =============
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create an API route manager
|
|
511
|
+
*/
|
|
512
|
+
export function createAPIRouteManager(config: PartialAPIRouteConfig = {}): APIRouteManager {
|
|
513
|
+
return new APIRouteManager(config);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ============= Response Helpers =============
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Create a JSON response
|
|
520
|
+
*/
|
|
521
|
+
export function json(data: unknown, status = 200, headers?: Record<string, string>): Response {
|
|
522
|
+
return new Response(JSON.stringify(data), {
|
|
523
|
+
status,
|
|
524
|
+
headers: {
|
|
525
|
+
"Content-Type": "application/json",
|
|
526
|
+
...headers,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Create a text response
|
|
533
|
+
*/
|
|
534
|
+
export function text(data: string, status = 200, headers?: Record<string, string>): Response {
|
|
535
|
+
return new Response(data, {
|
|
536
|
+
status,
|
|
537
|
+
headers: {
|
|
538
|
+
"Content-Type": "text/plain",
|
|
539
|
+
...headers,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Create an HTML response
|
|
546
|
+
*/
|
|
547
|
+
export function html(data: string, status = 200, headers?: Record<string, string>): Response {
|
|
548
|
+
return new Response(data, {
|
|
549
|
+
status,
|
|
550
|
+
headers: {
|
|
551
|
+
"Content-Type": "text/html",
|
|
552
|
+
...headers,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Create a redirect response
|
|
559
|
+
*/
|
|
560
|
+
export function redirect(url: string, status = 302): Response {
|
|
561
|
+
return new Response(null, {
|
|
562
|
+
status,
|
|
563
|
+
headers: {
|
|
564
|
+
Location: url,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Create an error response
|
|
571
|
+
*/
|
|
572
|
+
export function error(message: string, status = 500): Response {
|
|
573
|
+
return json({ error: message }, status);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Create a 404 Not Found response
|
|
578
|
+
*/
|
|
579
|
+
export function notFound(message = "Not Found"): Response {
|
|
580
|
+
return json({ error: message }, 404);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Create a 401 Unauthorized response
|
|
585
|
+
*/
|
|
586
|
+
export function unauthorized(message = "Unauthorized"): Response {
|
|
587
|
+
return json({ error: message }, 401);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Create a 403 Forbidden response
|
|
592
|
+
*/
|
|
593
|
+
export function forbidden(message = "Forbidden"): Response {
|
|
594
|
+
return json({ error: message }, 403);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Create a 400 Bad Request response
|
|
599
|
+
*/
|
|
600
|
+
export function badRequest(message = "Bad Request"): Response {
|
|
601
|
+
return json({ error: message }, 400);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Create a 201 Created response
|
|
606
|
+
*/
|
|
607
|
+
export function created(data: unknown): Response {
|
|
608
|
+
return json(data, 201);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Create a 204 No Content response
|
|
613
|
+
*/
|
|
614
|
+
export function noContent(): Response {
|
|
615
|
+
return new Response(null, { status: 204 });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ============= Utility Functions =============
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Check if a file is an API route file
|
|
622
|
+
*/
|
|
623
|
+
export function isAPIRouteFile(filename: string): boolean {
|
|
624
|
+
return SUPPORTED_EXTENSIONS.some(ext => filename.endsWith(ext)) &&
|
|
625
|
+
!filename.includes("_middleware");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Check if a file is a middleware file
|
|
630
|
+
*/
|
|
631
|
+
export function isMiddlewareFile(filename: string): boolean {
|
|
632
|
+
return filename.includes("_middleware");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get HTTP methods from module
|
|
637
|
+
*/
|
|
638
|
+
export function getModuleMethods(module: APIRouteModule): HTTPMethod[] {
|
|
639
|
+
return SUPPORTED_METHODS.filter(method => typeof module[method] === "function");
|
|
640
|
+
}
|