bxo 0.0.5-dev.7 → 0.0.5-dev.70

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/src/index.ts ADDED
@@ -0,0 +1,644 @@
1
+ import { z } from "zod";
2
+
3
+
4
+ type Method =
5
+ | "GET"
6
+ | "POST"
7
+ | "PUT"
8
+ | "PATCH"
9
+ | "DELETE"
10
+ | "OPTIONS"
11
+ | "HEAD";
12
+
13
+ type PickWildcardName<S extends string> = S extends "" ? "wildcard" : S;
14
+
15
+ type PathParams<S extends string> =
16
+ S extends `${string}*${infer W}`
17
+ ? { [K in PickWildcardName<W>]: string }
18
+ : S extends `${string}:${infer P}/${infer R}`
19
+ ? ({ [K in P]: string } & PathParams<`/${R}`>)
20
+ : S extends `${string}:${infer P}`
21
+ ? { [K in P]: string }
22
+ : {};
23
+
24
+ type InferOr<T, Fallback> = T extends z.ZodTypeAny ? z.infer<T> : Fallback;
25
+
26
+ type ResponseSchemas = Record<number, z.ZodTypeAny>;
27
+
28
+ type InferResponse<S extends RouteSchema | undefined, Status extends number> =
29
+ S extends RouteSchema
30
+ ? S["response"] extends Record<Status, z.ZodTypeAny>
31
+ ? z.infer<S["response"][Status]>
32
+ : unknown
33
+ : unknown;
34
+
35
+ export type RouteSchema = {
36
+ headers?: z.ZodTypeAny;
37
+ query?: z.ZodTypeAny;
38
+ cookies?: z.ZodTypeAny;
39
+ body?: z.ZodTypeAny;
40
+ response?: ResponseSchemas;
41
+ detail?: {
42
+ defaultContentType?: "multipart/form-data" | "application/json" | "application/x-www-form-urlencoded" | "text/plain" | "application/octet-stream";
43
+ description?: string;
44
+ hidden?: boolean;
45
+ summary?: string;
46
+ tags?: string[];
47
+ [k: string]: any;
48
+ }
49
+ };
50
+
51
+ type QueryObject = Record<string, string | string[]>;
52
+ type CookieObject = Record<string, string>;
53
+ type HeaderObject = Record<string, string>;
54
+
55
+ // Lifecycle hook types
56
+ export type BeforeRequestHook = (req: Request, ctx?: Partial<Context<any, any>>) => Request | Response | Promise<Request | Response | void>;
57
+ export type AfterRequestHook = (req: Request, res: Response, ctx?: Partial<Context<any, any>>) => Response | Promise<Response | void>;
58
+ export type BeforeResponseHook = (res: Response, ctx?: Partial<Context<any, any>>) => Response | Promise<Response | void>;
59
+ export type OnErrorHook = (error: Error, req: Request, ctx?: Partial<Context<any, any>>) => Response | Promise<Response | void>;
60
+
61
+ export type Context<P extends string = string, S extends RouteSchema | undefined = undefined> = {
62
+ request: Request;
63
+ params: PathParams<P>;
64
+ query: S extends RouteSchema ? InferOr<S["query"], QueryObject> : QueryObject;
65
+ headers: S extends RouteSchema ? InferOr<S["headers"], HeaderObject> : HeaderObject;
66
+ cookies: S extends RouteSchema ? InferOr<S["cookies"], CookieObject> : CookieObject;
67
+ body: S extends RouteSchema ? InferOr<S["body"], unknown> : unknown;
68
+ set: { headers: Record<string, string> };
69
+ json: <T>(data: T, status?: number) => Response;
70
+ text: (data: string, status?: number) => Response;
71
+ status: <T extends number>(status: T, data: InferResponse<S, T>) => Response;
72
+ };
73
+
74
+ type AnyHandler = (ctx: Context<any, any>, app: BXO) => Response | string | Promise<Response | string>;
75
+ type Handler<P extends string, S extends RouteSchema | undefined = undefined> = (
76
+ ctx: Context<P, S>,
77
+ app: BXO
78
+ ) => Response | string | Promise<Response | string>;
79
+
80
+ type InternalRoute = {
81
+ method: Method | "DEFAULT";
82
+ path: string;
83
+ matcher: RegExp | null;
84
+ paramNames: string[];
85
+ schema?: RouteSchema;
86
+ handler: AnyHandler;
87
+ };
88
+
89
+ type ServeOptions = Partial<Parameters<typeof Bun.serve>[0]>;
90
+
91
+ function toHeaderObject(headers: Headers): HeaderObject {
92
+ const obj: HeaderObject = {};
93
+ headers.forEach((value, key) => {
94
+ obj[key.toLowerCase()] = value;
95
+ });
96
+ return obj;
97
+ }
98
+
99
+ function parseCookies(cookieHeader: string | null): CookieObject {
100
+ const out: CookieObject = {};
101
+ if (!cookieHeader) return out;
102
+ const parts = cookieHeader.split(";");
103
+ for (const part of parts) {
104
+ const [k, ...rest] = part.trim().split("=");
105
+ if (!k) continue;
106
+ out[k] = decodeURIComponent(rest.join("="));
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function parseQuery(searchParams: URLSearchParams): QueryObject;
112
+ function parseQuery<T extends z.ZodTypeAny>(searchParams: URLSearchParams, schema: T): z.infer<T>;
113
+ function parseQuery(searchParams: URLSearchParams, schema?: z.ZodTypeAny): any {
114
+ const out: QueryObject = {};
115
+ for (const [k, v] of searchParams.entries()) {
116
+ if (k in out) {
117
+ const existing = out[k];
118
+ if (Array.isArray(existing)) out[k] = [...existing, v];
119
+ else out[k] = [existing as string, v];
120
+ } else out[k] = v;
121
+ }
122
+ if (schema) {
123
+ return (schema as any).parse ? (schema as any).parse(out) : out;
124
+ }
125
+ return out;
126
+ }
127
+
128
+ function formDataToObject(fd: FormData): Record<string, any> {
129
+ const obj: Record<string, any> = {};
130
+ for (const [key, value] of fd.entries()) {
131
+ if (key in obj) {
132
+ const existing = obj[key];
133
+ if (Array.isArray(existing)) obj[key] = [...existing, value];
134
+ else obj[key] = [existing, value];
135
+ } else obj[key] = value;
136
+ }
137
+ return obj;
138
+ }
139
+
140
+ function buildMatcher(path: string): { regex: RegExp | null; names: string[] } {
141
+ if (!path.includes(":") && !path.includes("*")) return { regex: null, names: [] };
142
+ const names: string[] = [];
143
+ const pattern = path
144
+ .split("/")
145
+ .map((seg, idx, arr) => {
146
+ if (!seg) return "";
147
+ if (seg.startsWith(":")) {
148
+ names.push(seg.slice(1));
149
+ return "([^/]+)";
150
+ }
151
+ if (seg.startsWith("*")) {
152
+ const name = seg.slice(1) || "wildcard";
153
+ names.push(name);
154
+ // wildcard must be terminal
155
+ return idx === arr.length - 1 ? "(.*)" : "(.*)";
156
+ }
157
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
158
+ })
159
+ .join("/");
160
+ const regex = new RegExp(`^${pattern}$`);
161
+ return { regex, names };
162
+ }
163
+
164
+ function mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): Headers {
165
+ const h = new Headers(base);
166
+ for (const [k, v] of Object.entries(extra)) h.set(k, v);
167
+ return h;
168
+ }
169
+
170
+ function toResponse(body: unknown, init?: ResponseInit): Response {
171
+ if (body instanceof Response) return new Response(body.body, { headers: mergeHeaders(body.headers, {}), status: body.status, statusText: body.statusText });
172
+ if (
173
+ typeof body === "string" ||
174
+ body instanceof Uint8Array ||
175
+ body instanceof ArrayBuffer ||
176
+ body instanceof Blob ||
177
+ body instanceof ReadableStream
178
+ ) {
179
+ return new Response(body as BodyInit, init);
180
+ }
181
+ // Fallback: stringify unknown values
182
+ return new Response(String(body), init);
183
+ }
184
+
185
+ export default class BXO {
186
+ private routes: InternalRoute[] = [];
187
+ private serveOptions: ServeOptions;
188
+ public server?: ReturnType<typeof Bun.serve>;
189
+
190
+ // Lifecycle hooks
191
+ protected beforeRequestHooks: BeforeRequestHook[] = [];
192
+ protected afterRequestHooks: AfterRequestHook[] = [];
193
+ protected beforeResponseHooks: BeforeResponseHook[] = [];
194
+ protected onErrorHooks: OnErrorHook[] = [];
195
+
196
+ constructor(options?: { serve?: ServeOptions }) {
197
+ this.serveOptions = options?.serve ?? {};
198
+ }
199
+
200
+ getRoutes(): InternalRoute[] {
201
+ return this.routes;
202
+ }
203
+
204
+ getBXO(): this {
205
+ return this;
206
+ }
207
+
208
+ use(plugin: BXO): this {
209
+ // Merge routes from another BXO instance
210
+ this.routes.push(...plugin.routes);
211
+
212
+ // Merge lifecycle hooks from plugin
213
+ this.beforeRequestHooks.push(...plugin.beforeRequestHooks);
214
+ this.afterRequestHooks.push(...plugin.afterRequestHooks);
215
+ this.beforeResponseHooks.push(...plugin.beforeResponseHooks);
216
+ this.onErrorHooks.push(...plugin.onErrorHooks);
217
+
218
+ return this;
219
+ }
220
+
221
+ get<P extends string>(path: P, handler: Handler<P, undefined>): this;
222
+ get<P extends string, S extends RouteSchema>(path: P, handler: Handler<P, S>, schema: S): this;
223
+ get<P extends string, S extends RouteSchema | undefined>(path: P, handler: Handler<P, S>, schema?: S): this {
224
+ return this.add("GET", path, handler as AnyHandler, schema as RouteSchema | undefined);
225
+ }
226
+
227
+ post<P extends string>(path: P, handler: Handler<P, undefined>): this;
228
+ post<P extends string, S extends RouteSchema>(path: P, handler: Handler<P, S>, schema: S): this;
229
+ post<P extends string, S extends RouteSchema | undefined>(path: P, handler: Handler<P, S>, schema?: S): this {
230
+ return this.add("POST", path, handler as AnyHandler, schema as RouteSchema | undefined);
231
+ }
232
+
233
+ put<P extends string>(path: P, handler: Handler<P, undefined>): this;
234
+ put<P extends string, S extends RouteSchema>(path: P, handler: Handler<P, S>, schema: S): this;
235
+ put<P extends string, S extends RouteSchema | undefined>(path: P, handler: Handler<P, S>, schema?: S): this {
236
+ return this.add("PUT", path, handler as AnyHandler, schema as RouteSchema | undefined);
237
+ }
238
+
239
+ patch<P extends string>(path: P, handler: Handler<P, undefined>): this;
240
+ patch<P extends string, S extends RouteSchema>(path: P, handler: Handler<P, S>, schema: S): this;
241
+ patch<P extends string, S extends RouteSchema | undefined>(path: P, handler: Handler<P, S>, schema?: S): this {
242
+ return this.add("PATCH", path, handler as AnyHandler, schema as RouteSchema | undefined);
243
+ }
244
+
245
+ delete<P extends string>(path: P, handler: Handler<P, undefined>): this;
246
+ delete<P extends string, S extends RouteSchema>(path: P, handler: Handler<P, S>, schema: S): this;
247
+ delete<P extends string, S extends RouteSchema | undefined>(path: P, handler: Handler<P, S>, schema?: S): this {
248
+ return this.add("DELETE", path, handler as AnyHandler, schema as RouteSchema | undefined);
249
+ }
250
+
251
+ // default can accept a handler OR static content (including Bun HTML bundle)
252
+ default<P extends string>(path: P, handler: Handler<P, undefined>): this;
253
+ default<P extends string, S extends RouteSchema>(path: P, handler: (req: Request) => Response, schema: S): this;
254
+ default<P extends string>(path: P, content: Bun.HTMLBundle): this;
255
+ default<P extends string, S extends RouteSchema | undefined>(path: P, arg2: unknown, schema?: S): this {
256
+ return this.add("DEFAULT", path, arg2 as AnyHandler, schema as RouteSchema | undefined);
257
+ }
258
+
259
+ start(): void {
260
+ // Build a basic routes map for Bun's native routes (exact paths only)
261
+ const nativeRoutes: Record<string, Record<string, (req: Request) => Promise<Response> | Response>> = {};
262
+
263
+ for (const r of this.routes) {
264
+ switch (r.method) {
265
+ case "DEFAULT":
266
+ nativeRoutes[r.path] = r.handler as any;
267
+ break;
268
+ default:
269
+ nativeRoutes[r.path] ||= {} as Record<string, (req: Request) => Promise<Response> | Response>;
270
+ nativeRoutes[r.path][r.method] = (req: Request) => this.dispatch(r, req);
271
+ break;
272
+ }
273
+ }
274
+
275
+ this.server = Bun.serve({
276
+ ...this.serveOptions,
277
+ routes: nativeRoutes as any,
278
+ fetch: (req: Request) => this.dispatchAny(req, nativeRoutes)
279
+ });
280
+ const port = (this.server as any).port ?? this.serveOptions.port ?? 3000;
281
+ }
282
+
283
+ // Lifecycle hook methods
284
+ beforeRequest(hook: BeforeRequestHook): this {
285
+ this.beforeRequestHooks.push(hook);
286
+ return this;
287
+ }
288
+
289
+ afterRequest(hook: AfterRequestHook): this {
290
+ this.afterRequestHooks.push(hook);
291
+ return this;
292
+ }
293
+
294
+ beforeResponse(hook: BeforeResponseHook): this {
295
+ this.beforeResponseHooks.push(hook);
296
+ return this;
297
+ }
298
+
299
+ onError(hook: OnErrorHook): this {
300
+ this.onErrorHooks.push(hook);
301
+ return this;
302
+ }
303
+
304
+ // Internal
305
+ private add(method: Method | "DEFAULT", path: string, handler: AnyHandler, schema?: RouteSchema): this {
306
+ const { regex, names } = buildMatcher(path);
307
+ this.routes.push({ method, path, handler, matcher: regex, paramNames: names, schema });
308
+ return this;
309
+ }
310
+
311
+ private async dispatch(route: InternalRoute, req: Request): Promise<Response> {
312
+ // Run beforeRequest hooks
313
+ for (const hook of this.beforeRequestHooks) {
314
+ try {
315
+ const result = await hook(req);
316
+ if (result instanceof Response) {
317
+ return result;
318
+ }
319
+ if (result instanceof Request) {
320
+ req = result;
321
+ }
322
+ } catch (error) {
323
+ // Run error hooks
324
+ for (const errorHook of this.onErrorHooks) {
325
+ try {
326
+ const errorResponse = await errorHook(error as Error, req);
327
+ if (errorResponse instanceof Response) {
328
+ return errorResponse;
329
+ }
330
+ } catch {
331
+ // Ignore errors in error hooks
332
+ }
333
+ }
334
+ return new Response("Internal Server Error", { status: 500 });
335
+ }
336
+ }
337
+
338
+ const url = new URL(req.url);
339
+ const params = this.extractParams(route, url.pathname);
340
+ let queryObj: any;
341
+ let bodyObj: any = undefined;
342
+ const cookieObj = parseCookies(req.headers.get("cookie"));
343
+ const headerObj = toHeaderObject(req.headers);
344
+
345
+ // Parse query using schema if provided
346
+ try {
347
+ queryObj = route.schema?.query ? parseQuery(url.searchParams, route.schema.query as any) : parseQuery(url.searchParams);
348
+ } catch (err: any) {
349
+ const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error" };
350
+ return new Response(JSON.stringify(payload), { status: 400, headers: { "Content-Type": "application/json" } });
351
+ }
352
+
353
+ // Parse body (best-effort) and validate with schema if provided
354
+ try {
355
+ const contentType = req.headers.get("content-type") || "";
356
+ if (contentType.includes("application/json")) {
357
+ const raw = await req.json();
358
+ bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
359
+ } else if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
360
+ const fd = await req.formData();
361
+ const raw = formDataToObject(fd);
362
+ bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
363
+ } else if (contentType.includes("text/")) {
364
+ const raw = await req.text();
365
+ bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
366
+ } else if (contentType) {
367
+ // Unknown content-type: provide ArrayBuffer
368
+ const raw = await req.arrayBuffer();
369
+ bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
370
+ } else {
371
+ // No content-type: try JSON then text, otherwise undefined
372
+ try {
373
+ const raw = await req.json();
374
+ bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
375
+ } catch {
376
+ try {
377
+ const raw = await req.text();
378
+ bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
379
+ } catch {
380
+ bodyObj = undefined;
381
+ }
382
+ }
383
+ }
384
+ if (!bodyObj && route.schema?.body) {
385
+ return new Response(JSON.stringify({ error: "Body is required" }), { status: 400, headers: { "Content-Type": "application/json" } });
386
+ }
387
+ } catch (err: any) {
388
+ const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error" };
389
+ return new Response(JSON.stringify(payload), { status: 400, headers: { "Content-Type": "application/json" } });
390
+ }
391
+
392
+
393
+
394
+ // Prepare ctx with lazy helpers and header merging
395
+ const ctx: Context<any, any> = {
396
+ request: req,
397
+ params,
398
+ query: queryObj,
399
+ headers: headerObj,
400
+ cookies: cookieObj,
401
+ body: bodyObj,
402
+ set: { headers: {} },
403
+ json: (data, status = 200) => {
404
+ // Response validation if declared
405
+ if (route.schema?.response?.[status]) {
406
+ const sch = route.schema.response[status]!;
407
+ const res = (sch as any).safeParse ? (sch as any).safeParse(data) : { success: true };
408
+ if (!res.success) {
409
+ return new Response(JSON.stringify({ error: "Invalid response", issues: res.error?.issues ?? [] }), { status: 500, headers: { "Content-Type": "application/json" } });
410
+ }
411
+ }
412
+ const headers = mergeHeaders({ "Content-Type": "application/json" }, ctx.set.headers);
413
+ return new Response(JSON.stringify(data), { status, headers });
414
+ },
415
+ text: (data, status = 200) => {
416
+ if (route.schema?.response?.[status]) {
417
+ const sch = route.schema.response[status]!;
418
+ const res = (sch as any).safeParse ? (sch as any).safeParse(data) : { success: true };
419
+ if (!res.success) {
420
+ return new Response(JSON.stringify({ error: "Invalid response", issues: res.error?.issues ?? [] }), { status: 500, headers: { "Content-Type": "application/json" } });
421
+ }
422
+ }
423
+ const headers = mergeHeaders({ "Content-Type": "text/plain" }, ctx.set.headers);
424
+ return new Response(String(data), { status, headers });
425
+ },
426
+ status: (status, data) => {
427
+ // Response validation if declared
428
+ if (route.schema?.response?.[status]) {
429
+ const sch = route.schema.response[status]!;
430
+ const res = (sch as any).safeParse ? (sch as any).safeParse(data) : { success: true };
431
+ if (!res.success) {
432
+ return new Response(JSON.stringify({ error: "Invalid response", issues: res.error?.issues ?? [] }), { status: 500, headers: { "Content-Type": "application/json" } });
433
+ }
434
+ }
435
+
436
+ const resp = toResponse(data, { status });
437
+ // Merge ctx.set.headers into final response
438
+ const merged = new Response(resp.body, {
439
+ status: resp.status,
440
+ statusText: resp.statusText,
441
+ headers: mergeHeaders(resp.headers, ctx.set.headers)
442
+ });
443
+ return merged;
444
+ }
445
+ };
446
+
447
+ // Validation
448
+ if (route.schema) {
449
+ try {
450
+ if (route.schema.headers) {
451
+ (route.schema.headers as any).parse(headerObj);
452
+ }
453
+ if (route.schema.cookies) {
454
+ (route.schema.cookies as any).parse(cookieObj);
455
+ }
456
+ } catch (err: any) {
457
+ const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error" };
458
+ return new Response(JSON.stringify(payload), { status: 400, headers: { "Content-Type": "application/json" } });
459
+ }
460
+ }
461
+
462
+ const result = await route.handler(ctx, this);
463
+ const resp = toResponse(result);
464
+ // Merge ctx.set.headers into final response
465
+ let merged = new Response(resp.body, {
466
+ status: resp.status,
467
+ statusText: resp.statusText,
468
+ headers: mergeHeaders(resp.headers, ctx.set.headers)
469
+ });
470
+
471
+ // Run beforeResponse hooks
472
+ for (const hook of this.beforeResponseHooks) {
473
+ try {
474
+ const result = await hook(merged, ctx);
475
+ if (result instanceof Response) {
476
+ merged = result;
477
+ }
478
+ } catch (error) {
479
+ // Run error hooks
480
+ for (const errorHook of this.onErrorHooks) {
481
+ try {
482
+ const errorResponse = await errorHook(error as Error, req, ctx);
483
+ if (errorResponse instanceof Response) {
484
+ return errorResponse;
485
+ }
486
+ } catch {
487
+ // Ignore errors in error hooks
488
+ }
489
+ }
490
+ return new Response("Internal Server Error", { status: 500 });
491
+ }
492
+ }
493
+
494
+ // Run afterRequest hooks
495
+ for (const hook of this.afterRequestHooks) {
496
+ try {
497
+ const result = await hook(req, merged, ctx);
498
+ if (result instanceof Response) {
499
+ merged = result;
500
+ }
501
+ } catch (error) {
502
+ // Run error hooks
503
+ for (const errorHook of this.onErrorHooks) {
504
+ try {
505
+ const errorResponse = await errorHook(error as Error, req, ctx);
506
+ if (errorResponse instanceof Response) {
507
+ return errorResponse;
508
+ }
509
+ } catch {
510
+ // Ignore errors in error hooks
511
+ }
512
+ }
513
+ return new Response("Internal Server Error", { status: 500 });
514
+ }
515
+ }
516
+
517
+ return merged;
518
+ }
519
+
520
+ private async dispatchAny(req: Request, nativeRoutes: Record<string, Record<string, (req: Request) => Promise<Response> | Response>>): Promise<Response> {
521
+ // Run beforeRequest hooks for unmatched routes
522
+ for (const hook of this.beforeRequestHooks) {
523
+ try {
524
+ const result = await hook(req);
525
+ if (result instanceof Response) {
526
+ return result;
527
+ }
528
+ if (result instanceof Request) {
529
+ req = result;
530
+ }
531
+ } catch (error) {
532
+ // Run error hooks
533
+ for (const errorHook of this.onErrorHooks) {
534
+ try {
535
+ const errorResponse = await errorHook(error as Error, req);
536
+ if (errorResponse instanceof Response) {
537
+ return errorResponse;
538
+ }
539
+ } catch {
540
+ // Ignore errors in error hooks
541
+ }
542
+ }
543
+ return new Response("Internal Server Error", { status: 500 });
544
+ }
545
+ }
546
+
547
+ const url = new URL(req.url);
548
+ const method = req.method.toUpperCase() as Method;
549
+
550
+ // 1) If native routes contain exact match, prefer that
551
+ const exact = nativeRoutes[url.pathname];
552
+ if (exact) {
553
+ const h = exact[method] || exact["DEFAULT"];
554
+ if (h) return await h(req);
555
+ }
556
+
557
+ // 2) Fallback to our matcher list
558
+ for (const r of this.routes) {
559
+ if (r.matcher === null) continue; // exact paths handled above
560
+ if (r.method !== method && r.method !== "DEFAULT") continue;
561
+ const m = url.pathname.match(r.matcher);
562
+ if (m) return this.dispatch(r, req);
563
+ }
564
+
565
+ // Create 404 response
566
+ let notFoundResponse = new Response("Not Found", { status: 404 });
567
+
568
+ // Run beforeResponse hooks for 404
569
+ for (const hook of this.beforeResponseHooks) {
570
+ try {
571
+ const result = await hook(notFoundResponse);
572
+ if (result instanceof Response) {
573
+ notFoundResponse = result;
574
+ }
575
+ } catch (error) {
576
+ // Run error hooks
577
+ for (const errorHook of this.onErrorHooks) {
578
+ try {
579
+ const errorResponse = await errorHook(error as Error, req);
580
+ if (errorResponse instanceof Response) {
581
+ return errorResponse;
582
+ }
583
+ } catch {
584
+ // Ignore errors in error hooks
585
+ }
586
+ }
587
+ return new Response("Internal Server Error", { status: 500 });
588
+ }
589
+ }
590
+
591
+ // Run afterRequest hooks for 404
592
+ for (const hook of this.afterRequestHooks) {
593
+ try {
594
+ const result = await hook(req, notFoundResponse);
595
+ if (result instanceof Response) {
596
+ notFoundResponse = result;
597
+ }
598
+ } catch (error) {
599
+ // Run error hooks
600
+ for (const errorHook of this.onErrorHooks) {
601
+ try {
602
+ const errorResponse = await errorHook(error as Error, req);
603
+ if (errorResponse instanceof Response) {
604
+ return errorResponse;
605
+ }
606
+ } catch {
607
+ // Ignore errors in error hooks
608
+ }
609
+ }
610
+ return new Response("Internal Server Error", { status: 500 });
611
+ }
612
+ }
613
+
614
+ return notFoundResponse;
615
+ }
616
+
617
+ private extractParams(route: InternalRoute, pathname: string): Record<string, string> {
618
+ if (!route.matcher) return {};
619
+ const match = pathname.match(route.matcher);
620
+ if (!match) return {};
621
+ const params: Record<string, string> = {};
622
+ for (let i = 0; i < route.paramNames.length; i++) {
623
+ const name = route.paramNames[i];
624
+ const value = match[i + 1] ?? "";
625
+ params[name] = decodeURIComponent(value);
626
+ }
627
+ return params;
628
+ }
629
+ }
630
+
631
+ export { z }
632
+
633
+ export function createRoute<P extends string, S extends RouteSchema | undefined>(
634
+ handler: Handler<P, S>,
635
+ schema?: S
636
+ ): {
637
+ handler: Handler<P, S>;
638
+ schema: S | undefined;
639
+ } {
640
+ return {
641
+ handler,
642
+ schema
643
+ };
644
+ }
package/tsconfig.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "compilerOptions": {
3
- // Environment setup & latest features
4
- "lib": ["ESNext"],
3
+ // Enable latest features
4
+ "lib": ["ESNext", "DOM"],
5
5
  "target": "ESNext",
6
- "module": "Preserve",
6
+ "module": "ESNext",
7
7
  "moduleDetection": "force",
8
8
  "jsx": "react-jsx",
9
9
  "allowJs": true,
@@ -18,8 +18,6 @@
18
18
  "strict": true,
19
19
  "skipLibCheck": true,
20
20
  "noFallthroughCasesInSwitch": true,
21
- "noUncheckedIndexedAccess": true,
22
- "noImplicitOverride": true,
23
21
 
24
22
  // Some stricter flags (disabled by default)
25
23
  "noUnusedLocals": false,