@aklinker1/zeta 1.3.3 → 2.1.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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@aklinker1/zeta",
3
3
  "description": "Composable, testable, OpenAPI-first backend framework with validation built-in",
4
- "version": "1.3.3",
4
+ "version": "2.1.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "packageManager": "bun@1.3.2",
7
+ "packageManager": "bun@1.3.5",
8
8
  "module": "src/index.ts",
9
9
  "types": "src/index.ts",
10
10
  "exports": {
@@ -41,23 +41,31 @@
41
41
  "bench": "bun run src/__tests__/bench.ts",
42
42
  "example": "bun --watch run example.ts",
43
43
  "example:prod": "NODE_ENV=production bun run example",
44
- "docs:dev": "vitepress dev docs",
44
+ "docs:dev": "zola -r docs serve",
45
45
  "docs:build": "vitepress build docs",
46
46
  "docs:preview": "vitepress preview docs"
47
47
  },
48
48
  "dependencies": {
49
49
  "@standard-schema/spec": "^1.0.0",
50
50
  "openapi-types": "^12.1.3",
51
- "rou3": "^0.7.1"
51
+ "rou3": "^0.7.12",
52
+ "scule": "^1.3.0"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@aklinker1/check": "^2.2.0",
55
- "@typescript/native-preview": "^7.0.0-dev.20251114.1",
56
+ "@opentelemetry/api": "^1.9.0",
57
+ "@opentelemetry/resources": "^2.5.0",
58
+ "@opentelemetry/sdk-node": "^0.211.0",
59
+ "@opentelemetry/sdk-trace-node": "^2.5.0",
60
+ "@opentelemetry/semantic-conventions": "^1.39.0",
56
61
  "@types/bun": "latest",
62
+ "@typescript/native-preview": "^7.0.0-dev.20251114.1",
57
63
  "changelogen": "^0.6.2",
58
- "elysia": "^1.3.5",
64
+ "cookie": "^1.1.1",
65
+ "dedent": "^1.7.1",
66
+ "elysia": "^1.4.19",
59
67
  "expect-type": "^1.2.1",
60
- "hono": "^4.8.3",
68
+ "hono": "^4.11.2",
61
69
  "jsr": "^0.13.5",
62
70
  "mermaid": "^11.12.0",
63
71
  "oxlint": "^1.2.0",
@@ -2,11 +2,9 @@
2
2
  * Contains a schema adapter for app's using [`zod`](https://npmjs.com/package/zod).
3
3
  * @module
4
4
  */
5
+ import { z } from "zod";
5
6
  import type { SchemaAdapter } from "../types";
6
7
 
7
- const zod = "zod";
8
- const { z } = await import(zod);
9
-
10
8
  /**
11
9
  * Usage:
12
10
  *
@@ -25,21 +23,6 @@ export const zodSchemaAdapter: SchemaAdapter = {
25
23
  delete res.$schema;
26
24
  return res;
27
25
  },
28
- parseParamsRecord: (params: any) => {
29
- if (params?._zod?.def?.type !== "object")
30
- throw Error(
31
- "Query, params, and header schemas must be simple Zode objects defined with z.object({ ... })",
32
- );
33
-
34
- return Object.entries(params._zod.def.shape).map(
35
- ([key, schema]: [string, any]) => ({
36
- name: key,
37
- schema,
38
- optional: schema.safeParse(undefined).success,
39
- ...z.globalRegistry.get(schema),
40
- }),
41
- );
42
- },
43
26
  getMeta: (schema: any) => {
44
27
  return z.globalRegistry.get(schema);
45
28
  },
package/src/app.ts CHANGED
@@ -1,28 +1,23 @@
1
- import { callHandler } from "./internal/call-handler";
2
- import { HttpError } from "./errors";
3
- import { HttpStatus } from "./status";
1
+ import type { OpenAPI, OpenAPIV3_1 } from "openapi-types";
2
+ import { addRoute, createRouter } from "rou3";
3
+ import { compileRouter } from "rou3/compiler";
4
+ import { compileFetchFunction } from "./internal/compile-fetch-function";
5
+ import { compileRouteHandler } from "./internal/compile-route-handler";
6
+ import { detectTransport } from "./internal/utils";
7
+ import { buildOpenApiDocs, buildScalarHtml } from "./open-api";
4
8
  import type {
5
9
  App,
6
- RouterData,
7
- RouteDef,
8
10
  BasePath,
9
- ServerSideFetch,
10
- OnGlobalRequestContext,
11
- LifeCycleHooks,
12
- LifeCycleHook,
13
- DefaultAppData,
14
11
  BasePrefix,
12
+ DefaultAppData,
13
+ LifeCycleHook,
14
+ LifeCycleHookName,
15
+ RouteDef,
16
+ RouterData,
15
17
  SchemaAdapter,
18
+ ServerSideFetch,
16
19
  Transport,
17
20
  } from "./types";
18
- import { addRoute, createRouter, findRoute } from "rou3";
19
- import {
20
- callCtxModifierHooks,
21
- detectTransport,
22
- serializeErrorResponse,
23
- } from "./internal/utils";
24
- import type { OpenAPIV3_1 } from "openapi-types";
25
- import { buildOpenApiDocs, buildScalarHtml } from "./open-api";
26
21
 
27
22
  let appIdInc = 0;
28
23
  const nextAppId = () => `app-${appIdInc++}`;
@@ -68,15 +63,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
68
63
  const appId = nextAppId();
69
64
 
70
65
  const { origin = "http://localhost", prefix = "" } = options ?? {};
71
- const hooks: App["~zeta"]["hooks"] = {
72
- onAfterHandle: [],
73
- onGlobalAfterResponse: [],
74
- onBeforeHandle: [],
75
- onMapResponse: [],
76
- onGlobalError: [],
77
- onGlobalRequest: [],
78
- onTransform: [],
79
- };
66
+ const hooks: App["~zeta"]["hooks"] = {};
80
67
  const routes: App["~zeta"]["routes"] = {};
81
68
 
82
69
  const addRoutesEntry = (method: string, route: string, data: RouterData) => {
@@ -87,15 +74,13 @@ export function createApp<TPrefix extends BasePrefix = "">(
87
74
  routes[method][route] = data;
88
75
  };
89
76
 
90
- const cloneHooks = () => ({
91
- onAfterHandle: [...hooks.onAfterHandle],
92
- onGlobalAfterResponse: [...hooks.onGlobalAfterResponse],
93
- onBeforeHandle: [...hooks.onBeforeHandle],
94
- onMapResponse: [...hooks.onMapResponse],
95
- onGlobalError: [...hooks.onGlobalError],
96
- onGlobalRequest: [...hooks.onGlobalRequest],
97
- onTransform: [...hooks.onTransform],
98
- });
77
+ const cloneHooks = (): App["~zeta"]["hooks"] => {
78
+ const cloned: App["~zeta"]["hooks"] = {};
79
+ for (const key of Object.keys(hooks) as LifeCycleHookName[]) {
80
+ if (hooks[key]) cloned[key] = [...hooks[key]] as any;
81
+ }
82
+ return cloned;
83
+ };
99
84
 
100
85
  const app: App<DefaultAppData> = {
101
86
  // @ts-expect-error
@@ -146,77 +131,8 @@ export function createApp<TPrefix extends BasePrefix = "">(
146
131
  }
147
132
  }
148
133
 
149
- // const getRoute = compileRouter(router);
150
- const getRoute = (method: string, path: string) =>
151
- findRoute(router, method, path);
152
-
153
- return async (request) => {
154
- const url = new URL(request.url, origin);
155
- const ctx: any = {
156
- path: url.pathname,
157
- url,
158
- request,
159
- method: request.method,
160
- set: {
161
- status: HttpStatus.Ok,
162
- headers: {},
163
- },
164
- } satisfies OnGlobalRequestContext;
165
-
166
- try {
167
- const onGlobalRequestResponse = await callCtxModifierHooks(
168
- ctx,
169
- hooks.onGlobalRequest,
170
- );
171
- if (onGlobalRequestResponse) {
172
- ctx.response = onGlobalRequestResponse;
173
- return onGlobalRequestResponse;
174
- }
175
-
176
- const response = await callHandler(
177
- ctx,
178
- getRoute,
179
- options?.schemaAdapter,
180
- );
181
- ctx.response = response;
182
-
183
- return response;
184
- } catch (err) {
185
- ctx.error = err;
186
-
187
- for (const hook of hooks.onGlobalError) {
188
- const res = hook.callback(ctx);
189
- res instanceof Promise ? await res : res;
190
- }
191
-
192
- const status =
193
- err instanceof HttpError
194
- ? err.status
195
- : HttpStatus.InternalServerError;
196
- const res = Response.json(serializeErrorResponse(err), { status });
197
- ctx.response = res;
198
- return res;
199
- } finally {
200
- // Defer calls to the `onGlobalAfterResponse` hooks until after the response is sent
201
- if (hooks.onGlobalAfterResponse.length > 0) {
202
- setTimeout(async () => {
203
- try {
204
- for (const hook of hooks.onGlobalAfterResponse) {
205
- let res = hook.callback(ctx);
206
- if (res instanceof Promise) await res;
207
- }
208
- } catch (err) {
209
- ctx.error = err;
210
-
211
- for (const hook of hooks.onGlobalError) {
212
- const res = hook.callback(ctx);
213
- res instanceof Promise ? await res : res;
214
- }
215
- }
216
- }, 0);
217
- }
218
- }
219
- };
134
+ const getRoute = compileRouter(router);
135
+ return compileFetchFunction({ getRoute, hooks, origin });
220
136
  },
221
137
 
222
138
  getOpenApiSpec: () => {
@@ -241,6 +157,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
241
157
  const obj: Record<string, any> =
242
158
  args.length === 2 ? { [args[0]]: args[1] } : args[0];
243
159
 
160
+ hooks.onTransform ??= [];
244
161
  hooks.onTransform.push({
245
162
  id: nextHookId(appId),
246
163
  applyTo: "local",
@@ -251,6 +168,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
251
168
  },
252
169
 
253
170
  onGlobalRequest(callback: any) {
171
+ hooks.onGlobalRequest ??= [];
254
172
  hooks.onGlobalRequest.push({
255
173
  id: nextHookId(appId),
256
174
  applyTo: "global",
@@ -259,6 +177,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
259
177
  return app;
260
178
  },
261
179
  onTransform(callback: any) {
180
+ hooks.onTransform ??= [];
262
181
  hooks.onTransform.push({
263
182
  id: nextHookId(appId),
264
183
  applyTo: "local",
@@ -267,6 +186,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
267
186
  return app;
268
187
  },
269
188
  onBeforeHandle(callback: any) {
189
+ hooks.onBeforeHandle ??= [];
270
190
  hooks.onBeforeHandle.push({
271
191
  id: nextHookId(appId),
272
192
  applyTo: "local",
@@ -275,6 +195,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
275
195
  return app;
276
196
  },
277
197
  onAfterHandle(callback: any) {
198
+ hooks.onAfterHandle ??= [];
278
199
  hooks.onAfterHandle.push({
279
200
  id: nextHookId(appId),
280
201
  applyTo: "local",
@@ -283,6 +204,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
283
204
  return app;
284
205
  },
285
206
  onMapResponse(callback: any) {
207
+ hooks.onMapResponse ??= [];
286
208
  hooks.onMapResponse.push({
287
209
  id: nextHookId(appId),
288
210
  applyTo: "local",
@@ -291,6 +213,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
291
213
  return app;
292
214
  },
293
215
  onGlobalError(callback: any) {
216
+ hooks.onGlobalError ??= [];
294
217
  hooks.onGlobalError.push({
295
218
  id: nextHookId(appId),
296
219
  applyTo: "global",
@@ -299,6 +222,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
299
222
  return app;
300
223
  },
301
224
  onGlobalAfterResponse(callback: any) {
225
+ hooks.onGlobalAfterResponse ??= [];
302
226
  hooks.onGlobalAfterResponse.push({
303
227
  id: nextHookId(appId),
304
228
  applyTo: "global",
@@ -319,21 +243,36 @@ export function createApp<TPrefix extends BasePrefix = "">(
319
243
  app.method.apply(app, [Method.Any, ...args] as any) as any,
320
244
 
321
245
  method(method: string, path: BasePath, ...args: any[]) {
322
- const def: RouteDef = args.length === 2 ? args[0] : undefined;
246
+ const routeDef: RouteDef | undefined =
247
+ args.length === 2 ? args[0] : undefined;
323
248
  const handler = args[1] ?? args[0];
324
249
  const route = `${prefix}${path}`;
325
- addRoutesEntry(method, route, {
250
+ const hooks = cloneHooks();
251
+
252
+ // Merge app-level tags and security into route definition
253
+ const def: RouteDef | undefined = mergeAppDefaults(routeDef, options);
254
+
255
+ const compiledHandler = compileRouteHandler({
256
+ schemaAdapter: options?.schemaAdapter,
326
257
  def,
258
+ hooks,
259
+ method,
260
+ route,
327
261
  handler,
262
+ });
263
+ addRoutesEntry(method, route, {
264
+ def,
328
265
  route,
329
- hooks: cloneHooks(),
266
+ hooks,
267
+ compiledHandler,
268
+ handler,
330
269
  });
331
270
  return app;
332
271
  },
333
272
 
334
273
  mount(...args: any[]) {
335
274
  let path = "";
336
- let def = {};
275
+ let routeDef: RouteDef = {};
337
276
  let fetch: ServerSideFetch;
338
277
 
339
278
  if (args.length === 1) {
@@ -343,16 +282,31 @@ export function createApp<TPrefix extends BasePrefix = "">(
343
282
  fetch = args[1];
344
283
  } else {
345
284
  path = args[0];
346
- def = args[1];
285
+ routeDef = args[1];
347
286
  fetch = args[2];
348
287
  }
349
288
 
350
289
  const route = `${prefix}${path}/**`;
290
+ const hooks = cloneHooks();
291
+
292
+ // Merge app-level tags and security into route definition
293
+ const def = mergeAppDefaults(routeDef, options);
294
+
295
+ const compiledHandler = compileRouteHandler({
296
+ schemaAdapter: options?.schemaAdapter,
297
+ hooks,
298
+ method: "ANY",
299
+ route,
300
+ fetch,
301
+ def,
302
+ });
303
+
351
304
  addRoutesEntry(Method.Any, route, {
352
305
  def,
353
306
  fetch,
354
307
  route,
355
- hooks: cloneHooks(),
308
+ hooks,
309
+ compiledHandler,
356
310
  });
357
311
 
358
312
  return app as any;
@@ -369,20 +323,23 @@ export function createApp<TPrefix extends BasePrefix = "">(
369
323
  }
370
324
  }
371
325
 
372
- // Add global hooks to parent app's hooks
373
- for (const _name of Object.keys(hooks)) {
374
- const name = _name as keyof LifeCycleHooks;
375
- for (const hook of childApp["~zeta"].hooks[name]) {
376
- if (hook.applyTo === "global" || childApp["~zeta"].exported) {
377
- hooks[name].push(hook as LifeCycleHook<any>);
326
+ // Add the child's global hooks to parent
327
+ for (const hookName of Object.keys(
328
+ childApp["~zeta"].hooks,
329
+ ) as LifeCycleHookName[]) {
330
+ for (const hook of childApp["~zeta"].hooks[
331
+ hookName
332
+ ]! as LifeCycleHook<any>[]) {
333
+ if (hook.applyTo !== "global" && !childApp["~zeta"].exported)
334
+ continue;
335
+
336
+ if (hooks[hookName]) {
337
+ // Don't add a hook if it's already there
338
+ if (!hooks[hookName].includes(hook)) hooks[hookName].push(hook);
339
+ } else {
340
+ hooks[hookName] = [hook];
378
341
  }
379
342
  }
380
- let seen = new Set<string>();
381
- hooks[name] = hooks[name].filter((hook) => {
382
- if (seen.has(hook.id)) return false;
383
- seen.add(hook.id);
384
- return true;
385
- }) as LifeCycleHook<any>[];
386
343
  }
387
344
 
388
345
  return app as any;
@@ -455,6 +412,38 @@ export type CreateAppOptions<TPrefix extends BasePrefix = ""> = {
455
412
 
456
413
  /** Configure how your application's OpenAPI docs are generated. */
457
414
  openApi?: Partial<OpenAPIV3_1.Document> & {};
415
+
416
+ /**
417
+ * OpenAPI tags to apply to all routes in this app. Route-level tags will
418
+ * override these app-level tags.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * const usersApp = createApp({
423
+ * prefix: "/users",
424
+ * tags: ["Users"],
425
+ * })
426
+ * .get("/", {}, () => [...]) // Will have ["Users"] tag
427
+ * .get("/:id", { tags: ["Admin"] }, () => {...}) // Will have ["Admin"] tag (overrides app-level)
428
+ * ```
429
+ */
430
+ tags?: string[];
431
+
432
+ /**
433
+ * OpenAPI security requirements to apply to all routes in this app.
434
+ * Route-level security will override these app-level security requirements.
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * const authApp = createApp({
439
+ * prefix: "/auth",
440
+ * security: [{ bearerAuth: [] }],
441
+ * })
442
+ * .get("/profile", {}, () => {...}) // Will require bearerAuth
443
+ * .get("/admin", { security: [{ adminKey: [] }] }, () => {...}) // Will require adminKey (overrides app-level)
444
+ * ```
445
+ */
446
+ security?: OpenAPI.Document["security"];
458
447
  /**
459
448
  * Configure [Scalar](https://scalar.com/) UI docs.
460
449
  * @see https://github.com/scalar/scalar/blob/main/documentation/configuration.md#list-of-all-attributes
@@ -462,6 +451,25 @@ export type CreateAppOptions<TPrefix extends BasePrefix = ""> = {
462
451
  scalar?: any;
463
452
  };
464
453
 
454
+ /**
455
+ * Apply app-level defaults (tags, security) to a route definition.
456
+ * Route-level values override app-level defaults.
457
+ */
458
+ function mergeAppDefaults(
459
+ routeDef: RouteDef | undefined,
460
+ options: CreateAppOptions<any> | undefined,
461
+ ): RouteDef | undefined {
462
+ if (!options?.tags?.length && !options?.security?.length) {
463
+ return routeDef;
464
+ }
465
+
466
+ return {
467
+ tags: options.tags,
468
+ security: options.security,
469
+ ...routeDef,
470
+ };
471
+ }
472
+
465
473
  enum Method {
466
474
  Get = "GET",
467
475
  Post = "POST",
package/src/client.ts CHANGED
@@ -99,11 +99,12 @@ export function createAppClient<TApp extends App>(
99
99
  headers: { ...headers } as Record<string, string>,
100
100
  } satisfies RequestInit;
101
101
 
102
- const body =
102
+ const serializedBody =
103
103
  inputs.body == null ? undefined : smartSerialize(inputs.body);
104
- if (body) {
105
- init.body = body.serialized;
106
- if (body.contentType) init.headers["Content-Type"] = body.contentType;
104
+ if (serializedBody) {
105
+ init.body = serializedBody.value;
106
+ if (serializedBody.contentType)
107
+ init.headers["Content-Type"] = serializedBody.contentType;
107
108
  }
108
109
 
109
110
  try {
package/src/errors.ts CHANGED
@@ -1,19 +1,5 @@
1
- import type { StandardSchemaV1 } from "@standard-schema/spec";
2
1
  import { HttpStatus } from "./status";
3
2
 
4
- /**
5
- * Thrown when validation fails. Refer to `.issues` for details on what went wrong.
6
- */
7
- export class ValidationGlobalError extends Error {
8
- constructor(
9
- readonly input: any,
10
- readonly issues: ReadonlyArray<StandardSchemaV1.Issue>,
11
- ) {
12
- super("Validation error");
13
- this.name = "ValidationGlobalError";
14
- }
15
- }
16
-
17
3
  /**
18
4
  * Base class of all HTTP errors. You can throw this error or throw any of the
19
5
  * subclasses. Zeta will automatically detect and handle these errors, setting
@@ -425,7 +411,7 @@ export class NotImplementedHttpError extends HttpError {
425
411
  options?: ErrorOptions,
426
412
  ) {
427
413
  super(HttpStatus.NotImplemented, message, additionalInfo, options);
428
- this.name = "NotImplementedError";
414
+ this.name = "NotImplementedHttpError";
429
415
  }
430
416
  }
431
417
 
@@ -0,0 +1,166 @@
1
+ import type { MatchedRoute } from "rou3";
2
+ import { HttpError, NotFoundHttpError } from "../errors";
3
+ import { HttpStatus } from "../status";
4
+ import type { LifeCycleHooks, RouterData, ServerSideFetch } from "../types";
5
+ import { Context } from "./context";
6
+ import {
7
+ cleanupCompiledWhitespace,
8
+ getRawPathname,
9
+ serializeErrorResponse,
10
+ } from "./utils";
11
+
12
+ export function compileFetchFunction(options: CompileOptions): ServerSideFetch {
13
+ const onGlobalRequestCount = options.hooks.onGlobalRequest?.length;
14
+ const onGlobalAfterResponseCount =
15
+ options.hooks.onGlobalAfterResponse?.length;
16
+ const onGlobalErrorCount = options.hooks.onGlobalError?.length;
17
+
18
+ const js = `
19
+ return (request) => {
20
+ const path = utils.getRawPathname(request);
21
+ const ctx = new utils.Context(request, path, utils.origin);
22
+ ${onGlobalAfterResponseCount ? "let handlerReturnedPromise = false;" : ""}
23
+
24
+ try {
25
+ ${onGlobalRequestCount ? compileOnGlobalRequestHook(onGlobalRequestCount) : ""}
26
+
27
+ const matchedRoute = utils.getRoute(request.method, path);
28
+ if (matchedRoute == null) {
29
+ throw new utils.NotFoundHttpError(undefined, {
30
+ method: request.method,
31
+ path,
32
+ });
33
+ } else {
34
+ ctx.matchedRoute = matchedRoute;
35
+ }
36
+
37
+ ctx.response = matchedRoute.data.compiledHandler(request, ctx);
38
+ if (typeof ctx.response.then !== utils.FUNCTION) return ctx.response;
39
+
40
+ ${onGlobalAfterResponseCount ? "handlerReturnedPromise = true;" : ""}
41
+ return ctx.response.catch(error => {
42
+ ${onGlobalErrorCount ? compileOnGlobalErrorHook(onGlobalErrorCount, 3) : ""}
43
+
44
+ ${compileErrorResponse(3)}
45
+ })${onGlobalAfterResponseCount ? compileOnGlobalAfterResponsePromiseFinally(onGlobalAfterResponseCount, 2) : ""};
46
+ } catch (error) {
47
+ ${onGlobalErrorCount ? compileOnGlobalErrorHook(onGlobalErrorCount, 2) : ""}
48
+
49
+ ${compileErrorResponse(2)}
50
+ } ${onGlobalAfterResponseCount ? compileOnGlobalAfterResponseFinally(onGlobalAfterResponseCount, 1) : ""}
51
+ }
52
+ //#sourceURL=zeta-jit-generated://zeta-fetch-fn.js
53
+ `;
54
+ return new Function("utils", cleanupCompiledWhitespace(js))({
55
+ FUNCTION: "function",
56
+ getRawPathname,
57
+ hooks: options.hooks,
58
+ Context,
59
+ getRoute: options.getRoute,
60
+ NotFoundHttpError,
61
+ origin: options.origin,
62
+ HttpError,
63
+ HttpStatus,
64
+ serializeErrorResponse,
65
+ });
66
+ }
67
+
68
+ function compileOnGlobalRequestHook(hookCount: number): string {
69
+ const lines: string[] = [];
70
+
71
+ for (let i = 0; i < hookCount; i++) {
72
+ const resultVar = `onGlobalRequestRes${i}`;
73
+ lines.push(
74
+ ` const ${resultVar} = utils.hooks.onGlobalRequest[${i}].callback(ctx);`,
75
+ ...(process.env.NODE_ENV !== "production"
76
+ ? [
77
+ ` if (${resultVar} instanceof Promise)`,
78
+ ` console.warn("Warning: Promise returned from onGlobalRequest hook. Promises returned from onGlobalRequest are not awaited, ignoring the return value.");`,
79
+ ]
80
+ : []),
81
+ ` if (${resultVar})`,
82
+ ` if (typeof ${resultVar}.body?.bytes === utils.FUNCTION)`,
83
+ ` return ${resultVar};`,
84
+ ` else`,
85
+ ` for (const key of Object.keys(${resultVar}))`,
86
+ ` ctx[key] = ${resultVar}[key];`,
87
+ );
88
+ }
89
+
90
+ return lines.join("\n");
91
+ }
92
+
93
+ function compileOnGlobalErrorHook(hookCount: number, tabs: number): string {
94
+ const indent = " ".repeat(tabs);
95
+ const lines: string[] = [`${indent}ctx.error = error;`];
96
+
97
+ for (let i = 0; i < hookCount; i++) {
98
+ lines.push(`${indent}utils.hooks.onGlobalError[${i}].callback(ctx);`);
99
+ }
100
+
101
+ return lines.join("\n");
102
+ }
103
+
104
+ function compileOnGlobalAfterResponseFinally(
105
+ hookCount: number,
106
+ tabs: number,
107
+ ): string {
108
+ const indent = " ".repeat(tabs);
109
+ return `finally {
110
+ ${indent} if (!handlerReturnedPromise) {
111
+ ${compileOnGlobalAfterResponseHook(hookCount, tabs + 2)}
112
+ ${indent} }
113
+ ${indent}}
114
+ `;
115
+ }
116
+
117
+ function compileOnGlobalAfterResponsePromiseFinally(
118
+ hookCount: number,
119
+ tabs: number,
120
+ ): string {
121
+ const indent = " ".repeat(tabs);
122
+ return `.finally(() => {
123
+ ${compileOnGlobalAfterResponseHook(hookCount, tabs + 1)}
124
+ ${indent}})`;
125
+ }
126
+
127
+ function compileOnGlobalAfterResponseHook(
128
+ hookCount: number,
129
+ tabs: number,
130
+ ): string {
131
+ const indent = " ".repeat(tabs);
132
+ const lines: string[] = [`${indent}setTimeout(() => {`];
133
+
134
+ for (let i = 0; i < hookCount; i++) {
135
+ lines.push(
136
+ `${indent} utils.hooks.onGlobalAfterResponse[${i}].callback(ctx);`,
137
+ );
138
+ }
139
+
140
+ lines.push(`${indent}})`);
141
+
142
+ return lines.join("\n");
143
+ }
144
+
145
+ function compileErrorResponse(tabs: number): string {
146
+ const indent = " ".repeat(tabs);
147
+ return `${indent}const status =
148
+ ${indent} error instanceof utils.HttpError
149
+ ${indent} ? error.status
150
+ ${indent} : utils.HttpStatus.InternalServerError;
151
+ ${indent}return (
152
+ ${indent} ctx.response = Response.json(
153
+ ${indent} utils.serializeErrorResponse(error),
154
+ ${indent} { status, headers: ctx.set.headers },
155
+ ${indent} )
156
+ ${indent});`;
157
+ }
158
+
159
+ type CompileOptions = {
160
+ hooks: LifeCycleHooks;
161
+ getRoute: (
162
+ method: string,
163
+ path: string,
164
+ ) => MatchedRoute<RouterData> | undefined;
165
+ origin: string;
166
+ };