@typokit/server-hono 0.1.4

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.
@@ -0,0 +1,27 @@
1
+ import type { SerializerMap, TypoKitResponse, ValidatorMap, ValidationFieldError } from "@typokit/types";
2
+ import type { ServerAdapter } from "@typokit/core";
3
+ declare function validationErrorResponse(message: string, fields: ValidationFieldError[]): TypoKitResponse;
4
+ declare function runValidators(routeHandler: {
5
+ validators?: {
6
+ params?: string;
7
+ query?: string;
8
+ body?: string;
9
+ };
10
+ }, validatorMap: ValidatorMap | null, params: Record<string, string>, query: Record<string, string | string[] | undefined>, body: unknown): TypoKitResponse | undefined;
11
+ declare function serializeResponse(response: TypoKitResponse, serializerRef: string | undefined, serializerMap: SerializerMap | null): TypoKitResponse;
12
+ /**
13
+ * Create a Hono server adapter for TypoKit.
14
+ *
15
+ * ```ts
16
+ * import { honoServer } from "@typokit/server-hono";
17
+ * const adapter = honoServer();
18
+ * adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
19
+ * const handle = await adapter.listen(3000);
20
+ * ```
21
+ */
22
+ export declare function honoServer(options?: {
23
+ basePath?: string;
24
+ }): ServerAdapter;
25
+ export { serializeResponse, runValidators, validationErrorResponse };
26
+ export { type ServerAdapter } from "@typokit/core";
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAOV,aAAa,EAGb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,eAAe,CAAC;AA4DpE,iBAAS,uBAAuB,CAC9B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,oBAAoB,EAAE,GAC7B,eAAe,CAajB;AAED,iBAAS,aAAa,CACpB,YAAY,EAAE;IACZ,UAAU,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE,EACD,YAAY,EAAE,YAAY,GAAG,IAAI,EACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACpD,IAAI,EAAE,OAAO,GACZ,eAAe,GAAG,SAAS,CA4D7B;AAID,iBAAS,iBAAiB,CACxB,QAAQ,EAAE,eAAe,EACzB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,aAAa,EAAE,aAAa,GAAG,IAAI,GAClC,eAAe,CA8BjB;AAYD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAgPzE;AAGD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,uBAAuB,EAAE,CAAC;AACrE,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,345 @@
1
+ // @typokit/server-hono — Hono Server Adapter
2
+ //
3
+ // Translates TypoKit's compiled route table into Hono-native route
4
+ // registrations. Runs on any platform Hono supports via @hono/node-server
5
+ // for the listen() method.
6
+ import { Hono } from "hono";
7
+ import { serve } from "@hono/node-server";
8
+ import { createRequestContext, executeMiddlewareChain } from "@typokit/core";
9
+ /**
10
+ * Recursively walk the compiled radix tree and collect all registered routes
11
+ * as flat entries with their full paths reconstructed.
12
+ */
13
+ function collectRoutes(node, prefix, entries) {
14
+ if (node.handlers) {
15
+ for (const [method, handler] of Object.entries(node.handlers)) {
16
+ if (handler) {
17
+ entries.push({
18
+ method: method,
19
+ path: prefix || "/",
20
+ handlerRef: handler.ref,
21
+ validators: handler.validators,
22
+ serializer: handler.serializer,
23
+ middleware: handler.middleware,
24
+ });
25
+ }
26
+ }
27
+ }
28
+ // Static children
29
+ if (node.children) {
30
+ for (const [segment, child] of Object.entries(node.children)) {
31
+ collectRoutes(child, `${prefix}/${segment}`, entries);
32
+ }
33
+ }
34
+ // Param child (:id) — Hono uses :param syntax same as TypoKit
35
+ if (node.paramChild) {
36
+ const paramNode = node.paramChild;
37
+ collectRoutes(paramNode, `${prefix}/:${paramNode.paramName}`, entries);
38
+ }
39
+ // Wildcard child (*path)
40
+ if (node.wildcardChild) {
41
+ const wildcardNode = node.wildcardChild;
42
+ collectRoutes(wildcardNode, `${prefix}/*`, entries);
43
+ }
44
+ }
45
+ // ─── Request Validation Pipeline ─────────────────────────────
46
+ function validationErrorResponse(message, fields) {
47
+ const body = {
48
+ error: {
49
+ code: "VALIDATION_ERROR",
50
+ message,
51
+ details: { fields },
52
+ },
53
+ };
54
+ return {
55
+ status: 400,
56
+ headers: { "content-type": "application/json" },
57
+ body,
58
+ };
59
+ }
60
+ function runValidators(routeHandler, validatorMap, params, query, body) {
61
+ if (!validatorMap || !routeHandler.validators) {
62
+ return undefined;
63
+ }
64
+ const allErrors = [];
65
+ if (routeHandler.validators.params) {
66
+ const validator = validatorMap[routeHandler.validators.params];
67
+ if (validator) {
68
+ const result = validator(params);
69
+ if (!result.success && result.errors) {
70
+ for (const e of result.errors) {
71
+ allErrors.push({
72
+ path: `params.${e.path}`,
73
+ expected: e.expected,
74
+ actual: e.actual,
75
+ });
76
+ }
77
+ }
78
+ }
79
+ }
80
+ if (routeHandler.validators.query) {
81
+ const validator = validatorMap[routeHandler.validators.query];
82
+ if (validator) {
83
+ const result = validator(query);
84
+ if (!result.success && result.errors) {
85
+ for (const e of result.errors) {
86
+ allErrors.push({
87
+ path: `query.${e.path}`,
88
+ expected: e.expected,
89
+ actual: e.actual,
90
+ });
91
+ }
92
+ }
93
+ }
94
+ }
95
+ if (routeHandler.validators.body) {
96
+ const validator = validatorMap[routeHandler.validators.body];
97
+ if (validator) {
98
+ const result = validator(body);
99
+ if (!result.success && result.errors) {
100
+ for (const e of result.errors) {
101
+ allErrors.push({
102
+ path: `body.${e.path}`,
103
+ expected: e.expected,
104
+ actual: e.actual,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ }
110
+ if (allErrors.length > 0) {
111
+ return validationErrorResponse("Request validation failed", allErrors);
112
+ }
113
+ return undefined;
114
+ }
115
+ // ─── Response Serialization Pipeline ──────────────────────────
116
+ function serializeResponse(response, serializerRef, serializerMap) {
117
+ if (response.body === null ||
118
+ response.body === undefined ||
119
+ typeof response.body === "string") {
120
+ return response;
121
+ }
122
+ const headers = { ...response.headers };
123
+ if (!headers["content-type"]) {
124
+ headers["content-type"] = "application/json";
125
+ }
126
+ if (serializerRef && serializerMap) {
127
+ const serializer = serializerMap[serializerRef];
128
+ if (serializer) {
129
+ return {
130
+ ...response,
131
+ headers,
132
+ body: serializer(response.body),
133
+ };
134
+ }
135
+ }
136
+ return {
137
+ ...response,
138
+ headers,
139
+ body: JSON.stringify(response.body),
140
+ };
141
+ }
142
+ /**
143
+ * Create a Hono server adapter for TypoKit.
144
+ *
145
+ * ```ts
146
+ * import { honoServer } from "@typokit/server-hono";
147
+ * const adapter = honoServer();
148
+ * adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
149
+ * const handle = await adapter.listen(3000);
150
+ * ```
151
+ */
152
+ export function honoServer(options) {
153
+ const app = new Hono();
154
+ const state = {
155
+ routeTable: null,
156
+ handlerMap: null,
157
+ middlewareChain: null,
158
+ validatorMap: null,
159
+ serializerMap: null,
160
+ };
161
+ const _basePath = options?.basePath;
162
+ /** Convert Hono context to TypoKitRequest */
163
+ function normalizeRequest(raw) {
164
+ const c = raw;
165
+ const req = c.req;
166
+ const headers = {};
167
+ req.raw.headers.forEach((value, key) => {
168
+ headers[key] = value;
169
+ });
170
+ // Parse query parameters from URL
171
+ const url = new URL(req.url);
172
+ const query = {};
173
+ url.searchParams.forEach((value, key) => {
174
+ const existing = query[key];
175
+ if (existing !== undefined) {
176
+ if (Array.isArray(existing)) {
177
+ existing.push(value);
178
+ }
179
+ else {
180
+ query[key] = [existing, value];
181
+ }
182
+ }
183
+ else {
184
+ query[key] = value;
185
+ }
186
+ });
187
+ return {
188
+ method: req.method.toUpperCase(),
189
+ path: url.pathname,
190
+ headers,
191
+ body: c._typoBody,
192
+ query,
193
+ params: c.req.param(),
194
+ };
195
+ }
196
+ /** Write TypoKitResponse to Hono context — returns a Response */
197
+ function writeResponse(raw, response) {
198
+ // In Hono, responses are returned, not written imperatively.
199
+ // We store the response on the context for the route handler to return.
200
+ const c = raw;
201
+ c._typoResponse = response;
202
+ }
203
+ function buildHonoResponse(response) {
204
+ const responseBody = response.body === null || response.body === undefined
205
+ ? ""
206
+ : typeof response.body === "string"
207
+ ? response.body
208
+ : JSON.stringify(response.body);
209
+ const headers = new Headers();
210
+ for (const [key, value] of Object.entries(response.headers)) {
211
+ if (value !== undefined) {
212
+ if (Array.isArray(value)) {
213
+ for (const v of value) {
214
+ headers.append(key, v);
215
+ }
216
+ }
217
+ else {
218
+ headers.set(key, value);
219
+ }
220
+ }
221
+ }
222
+ return new Response(responseBody, {
223
+ status: response.status,
224
+ headers,
225
+ });
226
+ }
227
+ const adapter = {
228
+ name: "hono",
229
+ registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap, serializerMap) {
230
+ state.routeTable = routeTable;
231
+ state.handlerMap = handlerMap;
232
+ state.middlewareChain = middlewareChain;
233
+ state.validatorMap = validatorMap ?? null;
234
+ state.serializerMap = serializerMap ?? null;
235
+ // Collect all routes from the compiled radix tree
236
+ const routes = [];
237
+ collectRoutes(routeTable, "", routes);
238
+ // Register each route as a Hono-native route
239
+ for (const route of routes) {
240
+ const method = route.method.toUpperCase();
241
+ app.on(method, route.path, async (c) => {
242
+ // Parse body for methods that have one
243
+ let body = undefined;
244
+ if (route.method === "POST" ||
245
+ route.method === "PUT" ||
246
+ route.method === "PATCH") {
247
+ try {
248
+ body = await c.req.json();
249
+ }
250
+ catch {
251
+ body = undefined;
252
+ }
253
+ }
254
+ // Stash body on context for normalizeRequest
255
+ c._typoBody = body;
256
+ const typoReq = normalizeRequest(c);
257
+ // Run request validation pipeline
258
+ const validationError = runValidators({ validators: route.validators }, state.validatorMap, typoReq.params, typoReq.query, typoReq.body);
259
+ if (validationError) {
260
+ return buildHonoResponse(validationError);
261
+ }
262
+ const handlerFn = state.handlerMap[route.handlerRef];
263
+ if (!handlerFn) {
264
+ const errorResp = {
265
+ status: 500,
266
+ headers: { "content-type": "application/json" },
267
+ body: JSON.stringify({
268
+ error: "Internal Server Error",
269
+ message: `Handler not found: ${route.handlerRef}`,
270
+ }),
271
+ };
272
+ return buildHonoResponse(errorResp);
273
+ }
274
+ // Create request context and execute middleware chain
275
+ let ctx = createRequestContext();
276
+ if (state.middlewareChain &&
277
+ state.middlewareChain.entries.length > 0) {
278
+ const entries = state.middlewareChain.entries.map((e) => ({
279
+ name: e.name,
280
+ middleware: {
281
+ handler: async (input) => {
282
+ const mwReq = {
283
+ method: typoReq.method,
284
+ path: typoReq.path,
285
+ headers: input.headers,
286
+ body: input.body,
287
+ query: input.query,
288
+ params: input.params,
289
+ };
290
+ const response = await e.handler(mwReq, input.ctx, async () => {
291
+ return { status: 200, headers: {}, body: null };
292
+ });
293
+ return response;
294
+ },
295
+ },
296
+ }));
297
+ ctx = await executeMiddlewareChain(typoReq, ctx, entries);
298
+ }
299
+ // Call the handler
300
+ const response = await handlerFn(typoReq, ctx);
301
+ // Response serialization pipeline
302
+ const serialized = serializeResponse(response, route.serializer, state.serializerMap);
303
+ return buildHonoResponse(serialized);
304
+ });
305
+ }
306
+ },
307
+ async listen(port) {
308
+ const server = serve({
309
+ fetch: app.fetch,
310
+ port,
311
+ hostname: "0.0.0.0",
312
+ });
313
+ // Wait briefly for server to bind
314
+ await new Promise((resolve) => {
315
+ server.once("listening", () => resolve());
316
+ // If already listening, resolve immediately
317
+ if (server.listening)
318
+ resolve();
319
+ });
320
+ return {
321
+ async close() {
322
+ await new Promise((resolve, reject) => {
323
+ server.close((err) => {
324
+ if (err)
325
+ reject(err);
326
+ else
327
+ resolve();
328
+ });
329
+ });
330
+ },
331
+ // Expose server for port retrieval in tests
332
+ _server: server,
333
+ };
334
+ },
335
+ normalizeRequest,
336
+ writeResponse,
337
+ getNativeServer() {
338
+ return app;
339
+ },
340
+ };
341
+ return adapter;
342
+ }
343
+ // Re-export for convenience
344
+ export { serializeResponse, runValidators, validationErrorResponse };
345
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,EAAE;AACF,mEAAmE;AACnE,0EAA0E;AAC1E,2BAA2B;AAE3B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAgB1C,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAa7E;;;GAGG;AACH,SAAS,aAAa,CACpB,IAAmB,EACnB,MAAc,EACd,OAAqB;IAErB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9D,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,CAAC,IAAI,CAAC;oBACX,MAAM,EAAE,MAAoB;oBAC5B,IAAI,EAAE,MAAM,IAAI,GAAG;oBACnB,UAAU,EAAE,OAAO,CAAC,GAAG;oBACvB,UAAU,EAAE,OAAO,CAAC,UAAU;oBAC9B,UAAU,EAAE,OAAO,CAAC,UAAU;oBAC9B,UAAU,EAAE,OAAO,CAAC,UAAU;iBAC/B,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,kBAAkB;IAClB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7D,aAAa,CAAC,KAAK,EAAE,GAAG,MAAM,IAAI,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,8DAA8D;IAC9D,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,aAAa,CAAC,SAAS,EAAE,GAAG,MAAM,KAAK,SAAS,CAAC,SAAS,EAAE,EAAE,OAAO,CAAC,CAAC;IACzE,CAAC;IAED,yBAAyB;IACzB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC;QACxC,aAAa,CAAC,YAAY,EAAE,GAAG,MAAM,IAAI,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,gEAAgE;AAEhE,SAAS,uBAAuB,CAC9B,OAAe,EACf,MAA8B;IAE9B,MAAM,IAAI,GAAkB;QAC1B,KAAK,EAAE;YACL,IAAI,EAAE,kBAAkB;YACxB,OAAO;YACP,OAAO,EAAE,EAAE,MAAM,EAAE;SACpB;KACF,CAAC;IACF,OAAO;QACL,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI;KACL,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CACpB,YAEC,EACD,YAAiC,EACjC,MAA8B,EAC9B,KAAoD,EACpD,IAAa;IAEb,IAAI,CAAC,YAAY,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,SAAS,GAA2B,EAAE,CAAC;IAE7C,IAAI,YAAY,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,YAAY,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC/D,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBACrC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;oBAC9B,SAAS,CAAC,IAAI,CAAC;wBACb,IAAI,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE;wBACxB,QAAQ,EAAE,CAAC,CAAC,QAAQ;wBACpB,MAAM,EAAE,CAAC,CAAC,MAAM;qBACjB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,YAAY,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,YAAY,CAAC,YAAY,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAC9D,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBACrC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;oBAC9B,SAAS,CAAC,IAAI,CAAC;wBACb,IAAI,EAAE,SAAS,CAAC,CAAC,IAAI,EAAE;wBACvB,QAAQ,EAAE,CAAC,CAAC,QAAQ;wBACpB,MAAM,EAAE,CAAC,CAAC,MAAM;qBACjB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QACjC,MAAM,SAAS,GAAG,YAAY,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBACrC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;oBAC9B,SAAS,CAAC,IAAI,CAAC;wBACb,IAAI,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE;wBACtB,QAAQ,EAAE,CAAC,CAAC,QAAQ;wBACpB,MAAM,EAAE,CAAC,CAAC,MAAM;qBACjB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,uBAAuB,CAAC,2BAA2B,EAAE,SAAS,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,iEAAiE;AAEjE,SAAS,iBAAiB,CACxB,QAAyB,EACzB,aAAiC,EACjC,aAAmC;IAEnC,IACE,QAAQ,CAAC,IAAI,KAAK,IAAI;QACtB,QAAQ,CAAC,IAAI,KAAK,SAAS;QAC3B,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,EACjC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;IACxC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAC;IAC/C,CAAC;IAED,IAAI,aAAa,IAAI,aAAa,EAAE,CAAC;QACnC,MAAM,UAAU,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;QAChD,IAAI,UAAU,EAAE,CAAC;YACf,OAAO;gBACL,GAAG,QAAQ;gBACX,OAAO;gBACP,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC;aAChC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,QAAQ;QACX,OAAO;QACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;KACpC,CAAC;AACJ,CAAC;AAYD;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,OAA+B;IACxD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,MAAM,KAAK,GAAoB;QAC7B,UAAU,EAAE,IAAI;QAChB,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,IAAI;QACrB,YAAY,EAAE,IAAI;QAClB,aAAa,EAAE,IAAI;KACpB,CAAC;IAEF,MAAM,SAAS,GAAG,OAAO,EAAE,QAAQ,CAAC;IAEpC,6CAA6C;IAC7C,SAAS,gBAAgB,CAAC,GAAY;QACpC,MAAM,CAAC,GAAG,GAAkB,CAAC;QAC7B,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QAElB,MAAM,OAAO,GAAkD,EAAE,CAAC;QAClE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAa,EAAE,GAAW,EAAE,EAAE;YACrD,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,kCAAkC;QAClC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAkD,EAAE,CAAC;QAChE,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YACtC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAC5B,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACvB,CAAC;qBAAM,CAAC;oBACN,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACrB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO;YACL,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,WAAW,EAAgB;YAC9C,IAAI,EAAE,GAAG,CAAC,QAAQ;YAClB,OAAO;YACP,IAAI,EAAG,CAAwC,CAAC,SAAS;YACzD,KAAK;YACL,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,EAA4B;SAChD,CAAC;IACJ,CAAC;IAED,iEAAiE;IACjE,SAAS,aAAa,CAAC,GAAY,EAAE,QAAyB;QAC5D,6DAA6D;QAC7D,wEAAwE;QACxE,MAAM,CAAC,GAAG,GAAkB,CAAC;QAC5B,CAAwC,CAAC,aAAa,GAAG,QAAQ,CAAC;IACrE,CAAC;IAED,SAAS,iBAAiB,CAAC,QAAyB;QAClD,MAAM,YAAY,GAChB,QAAQ,CAAC,IAAI,KAAK,IAAI,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS;YACnD,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ;gBACjC,CAAC,CAAC,QAAQ,CAAC,IAAI;gBACf,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAEtC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAC9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBACzB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;wBACtB,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;oBACzB,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,IAAI,QAAQ,CAAC,YAAY,EAAE;YAChC,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAkB;QAC7B,IAAI,EAAE,MAAM;QAEZ,cAAc,CACZ,UAA8B,EAC9B,UAAsB,EACtB,eAAgC,EAChC,YAA2B,EAC3B,aAA6B;YAE7B,KAAK,CAAC,UAAU,GAAG,UAAU,CAAC;YAC9B,KAAK,CAAC,UAAU,GAAG,UAAU,CAAC;YAC9B,KAAK,CAAC,eAAe,GAAG,eAAe,CAAC;YACxC,KAAK,CAAC,YAAY,GAAG,YAAY,IAAI,IAAI,CAAC;YAC1C,KAAK,CAAC,aAAa,GAAG,aAAa,IAAI,IAAI,CAAC;YAE5C,kDAAkD;YAClD,MAAM,MAAM,GAAiB,EAAE,CAAC;YAChC,aAAa,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;YAEtC,6CAA6C;YAC7C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;gBAE1C,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAc,EAAE,EAAE;oBAClD,uCAAuC;oBACvC,IAAI,IAAI,GAAY,SAAS,CAAC;oBAC9B,IACE,KAAK,CAAC,MAAM,KAAK,MAAM;wBACvB,KAAK,CAAC,MAAM,KAAK,KAAK;wBACtB,KAAK,CAAC,MAAM,KAAK,OAAO,EACxB,CAAC;wBACD,IAAI,CAAC;4BACH,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;wBAC5B,CAAC;wBAAC,MAAM,CAAC;4BACP,IAAI,GAAG,SAAS,CAAC;wBACnB,CAAC;oBACH,CAAC;oBAED,6CAA6C;oBAC5C,CAAwC,CAAC,SAAS,GAAG,IAAI,CAAC;oBAE3D,MAAM,OAAO,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;oBAEpC,kCAAkC;oBAClC,MAAM,eAAe,GAAG,aAAa,CACnC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,EAChC,KAAK,CAAC,YAAY,EAClB,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,KAAK,EACb,OAAO,CAAC,IAAI,CACb,CAAC;oBACF,IAAI,eAAe,EAAE,CAAC;wBACpB,OAAO,iBAAiB,CAAC,eAAe,CAAC,CAAC;oBAC5C,CAAC;oBAED,MAAM,SAAS,GAAG,KAAK,CAAC,UAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;oBACtD,IAAI,CAAC,SAAS,EAAE,CAAC;wBACf,MAAM,SAAS,GAAoB;4BACjC,MAAM,EAAE,GAAG;4BACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;4BAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gCACnB,KAAK,EAAE,uBAAuB;gCAC9B,OAAO,EAAE,sBAAsB,KAAK,CAAC,UAAU,EAAE;6BAClD,CAAC;yBACH,CAAC;wBACF,OAAO,iBAAiB,CAAC,SAAS,CAAC,CAAC;oBACtC,CAAC;oBAED,sDAAsD;oBACtD,IAAI,GAAG,GAAG,oBAAoB,EAAE,CAAC;oBAEjC,IACE,KAAK,CAAC,eAAe;wBACrB,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EACxC,CAAC;wBACD,MAAM,OAAO,GACX,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;4BACxC,IAAI,EAAE,CAAC,CAAC,IAAI;4BACZ,UAAU,EAAE;gCACV,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;oCACvB,MAAM,KAAK,GAAmB;wCAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;wCACtB,IAAI,EAAE,OAAO,CAAC,IAAI;wCAClB,OAAO,EAAE,KAAK,CAAC,OAAO;wCACtB,IAAI,EAAE,KAAK,CAAC,IAAI;wCAChB,KAAK,EAAE,KAAK,CAAC,KAAK;wCAClB,MAAM,EAAE,KAAK,CAAC,MAAM;qCACrB,CAAC;oCACF,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,OAAO,CAC9B,KAAK,EACL,KAAK,CAAC,GAAG,EACT,KAAK,IAAI,EAAE;wCACT,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;oCAClD,CAAC,CACF,CAAC;oCACF,OAAO,QAA8C,CAAC;gCACxD,CAAC;6BACF;yBACF,CAAC,CAAC,CAAC;wBAEN,GAAG,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;oBAC5D,CAAC;oBAED,mBAAmB;oBACnB,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;oBAE/C,kCAAkC;oBAClC,MAAM,UAAU,GAAG,iBAAiB,CAClC,QAAQ,EACR,KAAK,CAAC,UAAU,EAChB,KAAK,CAAC,aAAa,CACpB,CAAC;oBAEF,OAAO,iBAAiB,CAAC,UAAU,CAAC,CAAC;gBACvC,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,IAAY;YACvB,MAAM,MAAM,GAAG,KAAK,CAAC;gBACnB,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,IAAI;gBACJ,QAAQ,EAAE,SAAS;aACpB,CAAC,CAAC;YAEH,kCAAkC;YAClC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBAClC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC1C,4CAA4C;gBAC5C,IAAI,MAAM,CAAC,SAAS;oBAAE,OAAO,EAAE,CAAC;YAClC,CAAC,CAAC,CAAC;YAEH,OAAO;gBACL,KAAK,CAAC,KAAK;oBACT,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;wBAC1C,MAAM,CAAC,KAAK,CAAC,CAAC,GAAW,EAAE,EAAE;4BAC3B,IAAI,GAAG;gCAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;gCAChB,OAAO,EAAE,CAAC;wBACjB,CAAC,CAAC,CAAC;oBACL,CAAC,CAAC,CAAC;gBACL,CAAC;gBACD,4CAA4C;gBAC5C,OAAO,EAAE,MAAM;aAC6B,CAAC;QACjD,CAAC;QAED,gBAAgB;QAChB,aAAa;QAEb,eAAe;YACb,OAAO,GAAG,CAAC;QACb,CAAC;KACF,CAAC;IAEF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,4BAA4B;AAC5B,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,uBAAuB,EAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@typokit/server-hono",
3
+ "exports": {
4
+ ".": {
5
+ "import": "./dist/index.js",
6
+ "types": "./dist/index.d.ts"
7
+ }
8
+ },
9
+ "version": "0.1.4",
10
+ "type": "module",
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "dependencies": {
18
+ "@hono/node-server": "^1.19.9",
19
+ "hono": "^4.12.3",
20
+ "@typokit/core": "0.1.4",
21
+ "@typokit/types": "0.1.4"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/KyleBastien/typokit",
29
+ "directory": "packages/server-hono"
30
+ },
31
+ "scripts": {
32
+ "test": "rstest run --passWithNoTests"
33
+ }
34
+ }
@@ -0,0 +1,449 @@
1
+ // @typokit/server-hono — Integration Tests
2
+
3
+ import { describe, it, expect } from "@rstest/core";
4
+ import { honoServer } from "./index.js";
5
+ import type {
6
+ CompiledRouteTable,
7
+ HandlerMap,
8
+ MiddlewareChain,
9
+ TypoKitRequest,
10
+ ValidatorMap,
11
+ RequestContext,
12
+ } from "@typokit/types";
13
+ import type { Server } from "node:http";
14
+
15
+ // ─── Helpers ─────────────────────────────────────────────────
16
+
17
+ function makeRouteTable(
18
+ overrides?: Partial<CompiledRouteTable>,
19
+ ): CompiledRouteTable {
20
+ return {
21
+ segment: "",
22
+ children: {
23
+ users: {
24
+ segment: "users",
25
+ handlers: {
26
+ GET: { ref: "users#list", middleware: [] },
27
+ POST: { ref: "users#create", middleware: [] },
28
+ },
29
+ paramChild: {
30
+ segment: ":id",
31
+ paramName: "id",
32
+ handlers: {
33
+ GET: { ref: "users#get", middleware: [] },
34
+ },
35
+ },
36
+ },
37
+ health: {
38
+ segment: "health",
39
+ handlers: {
40
+ GET: { ref: "health#check", middleware: [] },
41
+ },
42
+ },
43
+ },
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ function makeHandlerMap(): HandlerMap {
49
+ return {
50
+ "users#list": async () => ({
51
+ status: 200,
52
+ headers: {},
53
+ body: [
54
+ { id: 1, name: "Alice" },
55
+ { id: 2, name: "Bob" },
56
+ ],
57
+ }),
58
+ "users#create": async (req: TypoKitRequest) => ({
59
+ status: 201,
60
+ headers: {},
61
+ body: {
62
+ id: 3,
63
+ name: (req.body as Record<string, unknown>)?.name ?? "Unknown",
64
+ },
65
+ }),
66
+ "users#get": async (req: TypoKitRequest) => ({
67
+ status: 200,
68
+ headers: {},
69
+ body: { id: req.params.id, name: "User " + req.params.id },
70
+ }),
71
+ "health#check": async () => ({
72
+ status: 200,
73
+ headers: {},
74
+ body: { status: "ok" },
75
+ }),
76
+ };
77
+ }
78
+
79
+ function emptyMiddlewareChain(): MiddlewareChain {
80
+ return { entries: [] };
81
+ }
82
+
83
+ function getPort(handle: { _server: Server }): number {
84
+ const addr = handle._server.address();
85
+ if (addr && typeof addr === "object") {
86
+ return addr.port;
87
+ }
88
+ throw new Error("Could not get port from server handle");
89
+ }
90
+
91
+ async function fetchJson(
92
+ port: number,
93
+ path: string,
94
+ options?: RequestInit,
95
+ ): Promise<{ status: number; body: unknown }> {
96
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
97
+ ...options,
98
+ headers: {
99
+ "content-type": "application/json",
100
+ ...(options?.headers as Record<string, string> | undefined),
101
+ },
102
+ });
103
+ const text = await res.text();
104
+ let body: unknown;
105
+ try {
106
+ body = JSON.parse(text);
107
+ } catch {
108
+ body = text;
109
+ }
110
+ return { status: res.status, body };
111
+ }
112
+
113
+ // ─── Tests ───────────────────────────────────────────────────
114
+
115
+ describe("honoServer", () => {
116
+ it("implements ServerAdapter interface with correct name", () => {
117
+ const adapter = honoServer();
118
+ expect(adapter.name).toBe("hono");
119
+ expect(typeof adapter.registerRoutes).toBe("function");
120
+ expect(typeof adapter.listen).toBe("function");
121
+ expect(typeof adapter.normalizeRequest).toBe("function");
122
+ expect(typeof adapter.writeResponse).toBe("function");
123
+ expect(typeof adapter.getNativeServer).toBe("function");
124
+ });
125
+
126
+ it("getNativeServer returns the Hono instance", () => {
127
+ const adapter = honoServer();
128
+ const native = adapter.getNativeServer!();
129
+ expect(native).toBeDefined();
130
+ // Hono instances have a .fetch method
131
+ expect(typeof (native as Record<string, unknown>).fetch).toBe("function");
132
+ });
133
+
134
+ it("routes GET /health correctly", async () => {
135
+ const adapter = honoServer();
136
+ adapter.registerRoutes(
137
+ makeRouteTable(),
138
+ makeHandlerMap(),
139
+ emptyMiddlewareChain(),
140
+ );
141
+ const handle = (await adapter.listen(0)) as unknown as {
142
+ close(): Promise<void>;
143
+ _server: Server;
144
+ };
145
+ try {
146
+ const port = getPort(handle);
147
+
148
+ const { status, body } = await fetchJson(port, "/health");
149
+ expect(status).toBe(200);
150
+ expect((body as Record<string, unknown>).status).toBe("ok");
151
+ } finally {
152
+ await handle.close();
153
+ }
154
+ });
155
+
156
+ it("routes GET /users and returns list", async () => {
157
+ const adapter = honoServer();
158
+ adapter.registerRoutes(
159
+ makeRouteTable(),
160
+ makeHandlerMap(),
161
+ emptyMiddlewareChain(),
162
+ );
163
+ const handle = (await adapter.listen(0)) as unknown as {
164
+ close(): Promise<void>;
165
+ _server: Server;
166
+ };
167
+ try {
168
+ const port = getPort(handle);
169
+
170
+ const { status, body } = await fetchJson(port, "/users");
171
+ expect(status).toBe(200);
172
+ expect(Array.isArray(body)).toBe(true);
173
+ expect((body as Array<unknown>).length).toBe(2);
174
+ } finally {
175
+ await handle.close();
176
+ }
177
+ });
178
+
179
+ it("routes POST /users with body", async () => {
180
+ const adapter = honoServer();
181
+ adapter.registerRoutes(
182
+ makeRouteTable(),
183
+ makeHandlerMap(),
184
+ emptyMiddlewareChain(),
185
+ );
186
+ const handle = (await adapter.listen(0)) as unknown as {
187
+ close(): Promise<void>;
188
+ _server: Server;
189
+ };
190
+ try {
191
+ const port = getPort(handle);
192
+
193
+ const { status, body } = await fetchJson(port, "/users", {
194
+ method: "POST",
195
+ body: JSON.stringify({ name: "Charlie" }),
196
+ });
197
+ expect(status).toBe(201);
198
+ expect((body as Record<string, unknown>).name).toBe("Charlie");
199
+ } finally {
200
+ await handle.close();
201
+ }
202
+ });
203
+
204
+ it("routes GET /users/:id with params", async () => {
205
+ const adapter = honoServer();
206
+ adapter.registerRoutes(
207
+ makeRouteTable(),
208
+ makeHandlerMap(),
209
+ emptyMiddlewareChain(),
210
+ );
211
+ const handle = (await adapter.listen(0)) as unknown as {
212
+ close(): Promise<void>;
213
+ _server: Server;
214
+ };
215
+ try {
216
+ const port = getPort(handle);
217
+
218
+ const { status, body } = await fetchJson(port, "/users/42");
219
+ expect(status).toBe(200);
220
+ expect((body as Record<string, unknown>).id).toBe("42");
221
+ expect((body as Record<string, unknown>).name).toBe("User 42");
222
+ } finally {
223
+ await handle.close();
224
+ }
225
+ });
226
+
227
+ it("returns 404 for unknown routes", async () => {
228
+ const adapter = honoServer();
229
+ adapter.registerRoutes(
230
+ makeRouteTable(),
231
+ makeHandlerMap(),
232
+ emptyMiddlewareChain(),
233
+ );
234
+ const handle = (await adapter.listen(0)) as unknown as {
235
+ close(): Promise<void>;
236
+ _server: Server;
237
+ };
238
+ try {
239
+ const port = getPort(handle);
240
+
241
+ const res = await fetch(`http://127.0.0.1:${port}/nonexistent`);
242
+ expect(res.status).toBe(404);
243
+ } finally {
244
+ await handle.close();
245
+ }
246
+ });
247
+
248
+ it("runs request validation and returns 400 on failure", async () => {
249
+ const routeTable: CompiledRouteTable = {
250
+ segment: "",
251
+ children: {
252
+ items: {
253
+ segment: "items",
254
+ handlers: {
255
+ POST: {
256
+ ref: "items#create",
257
+ middleware: [],
258
+ validators: { body: "items#create.body" },
259
+ },
260
+ },
261
+ },
262
+ },
263
+ };
264
+
265
+ const handlerMap: HandlerMap = {
266
+ "items#create": async (req: TypoKitRequest) => ({
267
+ status: 201,
268
+ headers: {},
269
+ body: req.body,
270
+ }),
271
+ };
272
+
273
+ const validatorMap: ValidatorMap = {
274
+ "items#create.body": (input: unknown) => {
275
+ const data = input as Record<string, unknown> | null;
276
+ if (!data || typeof data.title !== "string") {
277
+ return {
278
+ success: false,
279
+ errors: [
280
+ { path: "title", expected: "string", actual: typeof data?.title },
281
+ ],
282
+ };
283
+ }
284
+ return { success: true, data };
285
+ },
286
+ };
287
+
288
+ const adapter = honoServer();
289
+ adapter.registerRoutes(
290
+ routeTable,
291
+ handlerMap,
292
+ emptyMiddlewareChain(),
293
+ validatorMap,
294
+ );
295
+ const handle = (await adapter.listen(0)) as unknown as {
296
+ close(): Promise<void>;
297
+ _server: Server;
298
+ };
299
+ try {
300
+ const port = getPort(handle);
301
+
302
+ const { status, body } = await fetchJson(port, "/items", {
303
+ method: "POST",
304
+ body: JSON.stringify({ invalid: true }),
305
+ });
306
+ expect(status).toBe(400);
307
+ expect((body as Record<string, unknown>).error).toBeDefined();
308
+ } finally {
309
+ await handle.close();
310
+ }
311
+ });
312
+
313
+ it("runs response serialization with custom serializer", async () => {
314
+ const routeTable: CompiledRouteTable = {
315
+ segment: "",
316
+ children: {
317
+ data: {
318
+ segment: "data",
319
+ handlers: {
320
+ GET: {
321
+ ref: "data#get",
322
+ middleware: [],
323
+ serializer: "data#get.response",
324
+ },
325
+ },
326
+ },
327
+ },
328
+ };
329
+
330
+ const serializerCalls: unknown[] = [];
331
+ const handlerMap: HandlerMap = {
332
+ "data#get": async () => ({
333
+ status: 200,
334
+ headers: {},
335
+ body: { value: 42 },
336
+ }),
337
+ };
338
+
339
+ const adapter = honoServer();
340
+ adapter.registerRoutes(
341
+ routeTable,
342
+ handlerMap,
343
+ emptyMiddlewareChain(),
344
+ undefined,
345
+ {
346
+ "data#get.response": (input: unknown) => {
347
+ serializerCalls.push(input);
348
+ return JSON.stringify(input);
349
+ },
350
+ },
351
+ );
352
+ const handle = (await adapter.listen(0)) as unknown as {
353
+ close(): Promise<void>;
354
+ _server: Server;
355
+ };
356
+ try {
357
+ const port = getPort(handle);
358
+
359
+ const { status, body } = await fetchJson(port, "/data");
360
+ expect(status).toBe(200);
361
+ expect((body as Record<string, unknown>).value).toBe(42);
362
+ expect(serializerCalls.length).toBe(1);
363
+ } finally {
364
+ await handle.close();
365
+ }
366
+ });
367
+
368
+ it("middleware chain runs before handler", async () => {
369
+ const callOrder: string[] = [];
370
+
371
+ const routeTable: CompiledRouteTable = {
372
+ segment: "",
373
+ children: {
374
+ test: {
375
+ segment: "test",
376
+ handlers: {
377
+ GET: { ref: "test#handler", middleware: ["mw1"] },
378
+ },
379
+ },
380
+ },
381
+ };
382
+
383
+ const handlerMap: HandlerMap = {
384
+ "test#handler": async (_req: TypoKitRequest, ctx: RequestContext) => {
385
+ callOrder.push("handler");
386
+ return {
387
+ status: 200,
388
+ headers: {},
389
+ body: { requestId: ctx.requestId },
390
+ };
391
+ },
392
+ };
393
+
394
+ const middlewareChain: MiddlewareChain = {
395
+ entries: [
396
+ {
397
+ name: "mw1",
398
+ handler: async (_req, _ctx, next) => {
399
+ callOrder.push("middleware");
400
+ return next();
401
+ },
402
+ },
403
+ ],
404
+ };
405
+
406
+ const adapter = honoServer();
407
+ adapter.registerRoutes(routeTable, handlerMap, middlewareChain);
408
+ const handle = (await adapter.listen(0)) as unknown as {
409
+ close(): Promise<void>;
410
+ _server: Server;
411
+ };
412
+ try {
413
+ const port = getPort(handle);
414
+
415
+ const { status } = await fetchJson(port, "/test");
416
+ expect(status).toBe(200);
417
+ expect(callOrder[0]).toBe("middleware");
418
+ expect(callOrder[1]).toBe("handler");
419
+ } finally {
420
+ await handle.close();
421
+ }
422
+ });
423
+
424
+ it("listen on port 0 assigns auto port", async () => {
425
+ const adapter = honoServer();
426
+ adapter.registerRoutes(
427
+ makeRouteTable(),
428
+ makeHandlerMap(),
429
+ emptyMiddlewareChain(),
430
+ );
431
+ const handle = (await adapter.listen(0)) as unknown as {
432
+ close(): Promise<void>;
433
+ _server: Server;
434
+ };
435
+ try {
436
+ const port = getPort(handle);
437
+ expect(port).toBeGreaterThan(0);
438
+ } finally {
439
+ await handle.close();
440
+ }
441
+ });
442
+
443
+ it("honoServer accepts options", () => {
444
+ const adapter = honoServer({ basePath: "/api" });
445
+ const native = adapter.getNativeServer!();
446
+ expect(native).toBeDefined();
447
+ expect(typeof (native as Record<string, unknown>).fetch).toBe("function");
448
+ });
449
+ });
package/src/index.ts ADDED
@@ -0,0 +1,474 @@
1
+ // @typokit/server-hono — Hono Server Adapter
2
+ //
3
+ // Translates TypoKit's compiled route table into Hono-native route
4
+ // registrations. Runs on any platform Hono supports via @hono/node-server
5
+ // for the listen() method.
6
+
7
+ import { Hono } from "hono";
8
+ import type { Context as HonoContext } from "hono";
9
+ import { serve } from "@hono/node-server";
10
+ import type {
11
+ CompiledRoute,
12
+ CompiledRouteTable,
13
+ ErrorResponse,
14
+ HandlerMap,
15
+ HttpMethod,
16
+ MiddlewareChain,
17
+ SerializerMap,
18
+ ServerHandle,
19
+ TypoKitRequest,
20
+ TypoKitResponse,
21
+ ValidatorMap,
22
+ ValidationFieldError,
23
+ } from "@typokit/types";
24
+ import type { ServerAdapter, MiddlewareEntry } from "@typokit/core";
25
+ import { createRequestContext, executeMiddlewareChain } from "@typokit/core";
26
+
27
+ // ─── Route Traversal ─────────────────────────────────────────
28
+
29
+ interface RouteEntry {
30
+ method: HttpMethod;
31
+ path: string;
32
+ handlerRef: string;
33
+ validators?: { params?: string; query?: string; body?: string };
34
+ serializer?: string;
35
+ middleware: string[];
36
+ }
37
+
38
+ /**
39
+ * Recursively walk the compiled radix tree and collect all registered routes
40
+ * as flat entries with their full paths reconstructed.
41
+ */
42
+ function collectRoutes(
43
+ node: CompiledRoute,
44
+ prefix: string,
45
+ entries: RouteEntry[],
46
+ ): void {
47
+ if (node.handlers) {
48
+ for (const [method, handler] of Object.entries(node.handlers)) {
49
+ if (handler) {
50
+ entries.push({
51
+ method: method as HttpMethod,
52
+ path: prefix || "/",
53
+ handlerRef: handler.ref,
54
+ validators: handler.validators,
55
+ serializer: handler.serializer,
56
+ middleware: handler.middleware,
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ // Static children
63
+ if (node.children) {
64
+ for (const [segment, child] of Object.entries(node.children)) {
65
+ collectRoutes(child, `${prefix}/${segment}`, entries);
66
+ }
67
+ }
68
+
69
+ // Param child (:id) — Hono uses :param syntax same as TypoKit
70
+ if (node.paramChild) {
71
+ const paramNode = node.paramChild;
72
+ collectRoutes(paramNode, `${prefix}/:${paramNode.paramName}`, entries);
73
+ }
74
+
75
+ // Wildcard child (*path)
76
+ if (node.wildcardChild) {
77
+ const wildcardNode = node.wildcardChild;
78
+ collectRoutes(wildcardNode, `${prefix}/*`, entries);
79
+ }
80
+ }
81
+
82
+ // ─── Request Validation Pipeline ─────────────────────────────
83
+
84
+ function validationErrorResponse(
85
+ message: string,
86
+ fields: ValidationFieldError[],
87
+ ): TypoKitResponse {
88
+ const body: ErrorResponse = {
89
+ error: {
90
+ code: "VALIDATION_ERROR",
91
+ message,
92
+ details: { fields },
93
+ },
94
+ };
95
+ return {
96
+ status: 400,
97
+ headers: { "content-type": "application/json" },
98
+ body,
99
+ };
100
+ }
101
+
102
+ function runValidators(
103
+ routeHandler: {
104
+ validators?: { params?: string; query?: string; body?: string };
105
+ },
106
+ validatorMap: ValidatorMap | null,
107
+ params: Record<string, string>,
108
+ query: Record<string, string | string[] | undefined>,
109
+ body: unknown,
110
+ ): TypoKitResponse | undefined {
111
+ if (!validatorMap || !routeHandler.validators) {
112
+ return undefined;
113
+ }
114
+
115
+ const allErrors: ValidationFieldError[] = [];
116
+
117
+ if (routeHandler.validators.params) {
118
+ const validator = validatorMap[routeHandler.validators.params];
119
+ if (validator) {
120
+ const result = validator(params);
121
+ if (!result.success && result.errors) {
122
+ for (const e of result.errors) {
123
+ allErrors.push({
124
+ path: `params.${e.path}`,
125
+ expected: e.expected,
126
+ actual: e.actual,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ if (routeHandler.validators.query) {
134
+ const validator = validatorMap[routeHandler.validators.query];
135
+ if (validator) {
136
+ const result = validator(query);
137
+ if (!result.success && result.errors) {
138
+ for (const e of result.errors) {
139
+ allErrors.push({
140
+ path: `query.${e.path}`,
141
+ expected: e.expected,
142
+ actual: e.actual,
143
+ });
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ if (routeHandler.validators.body) {
150
+ const validator = validatorMap[routeHandler.validators.body];
151
+ if (validator) {
152
+ const result = validator(body);
153
+ if (!result.success && result.errors) {
154
+ for (const e of result.errors) {
155
+ allErrors.push({
156
+ path: `body.${e.path}`,
157
+ expected: e.expected,
158
+ actual: e.actual,
159
+ });
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ if (allErrors.length > 0) {
166
+ return validationErrorResponse("Request validation failed", allErrors);
167
+ }
168
+
169
+ return undefined;
170
+ }
171
+
172
+ // ─── Response Serialization Pipeline ──────────────────────────
173
+
174
+ function serializeResponse(
175
+ response: TypoKitResponse,
176
+ serializerRef: string | undefined,
177
+ serializerMap: SerializerMap | null,
178
+ ): TypoKitResponse {
179
+ if (
180
+ response.body === null ||
181
+ response.body === undefined ||
182
+ typeof response.body === "string"
183
+ ) {
184
+ return response;
185
+ }
186
+
187
+ const headers = { ...response.headers };
188
+ if (!headers["content-type"]) {
189
+ headers["content-type"] = "application/json";
190
+ }
191
+
192
+ if (serializerRef && serializerMap) {
193
+ const serializer = serializerMap[serializerRef];
194
+ if (serializer) {
195
+ return {
196
+ ...response,
197
+ headers,
198
+ body: serializer(response.body),
199
+ };
200
+ }
201
+ }
202
+
203
+ return {
204
+ ...response,
205
+ headers,
206
+ body: JSON.stringify(response.body),
207
+ };
208
+ }
209
+
210
+ // ─── Hono Server Adapter ─────────────────────────────────────
211
+
212
+ interface HonoServerState {
213
+ routeTable: CompiledRouteTable | null;
214
+ handlerMap: HandlerMap | null;
215
+ middlewareChain: MiddlewareChain | null;
216
+ validatorMap: ValidatorMap | null;
217
+ serializerMap: SerializerMap | null;
218
+ }
219
+
220
+ /**
221
+ * Create a Hono server adapter for TypoKit.
222
+ *
223
+ * ```ts
224
+ * import { honoServer } from "@typokit/server-hono";
225
+ * const adapter = honoServer();
226
+ * adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
227
+ * const handle = await adapter.listen(3000);
228
+ * ```
229
+ */
230
+ export function honoServer(options?: { basePath?: string }): ServerAdapter {
231
+ const app = new Hono();
232
+
233
+ const state: HonoServerState = {
234
+ routeTable: null,
235
+ handlerMap: null,
236
+ middlewareChain: null,
237
+ validatorMap: null,
238
+ serializerMap: null,
239
+ };
240
+
241
+ const _basePath = options?.basePath;
242
+
243
+ /** Convert Hono context to TypoKitRequest */
244
+ function normalizeRequest(raw: unknown): TypoKitRequest {
245
+ const c = raw as HonoContext;
246
+ const req = c.req;
247
+
248
+ const headers: Record<string, string | string[] | undefined> = {};
249
+ req.raw.headers.forEach((value: string, key: string) => {
250
+ headers[key] = value;
251
+ });
252
+
253
+ // Parse query parameters from URL
254
+ const url = new URL(req.url);
255
+ const query: Record<string, string | string[] | undefined> = {};
256
+ url.searchParams.forEach((value, key) => {
257
+ const existing = query[key];
258
+ if (existing !== undefined) {
259
+ if (Array.isArray(existing)) {
260
+ existing.push(value);
261
+ } else {
262
+ query[key] = [existing, value];
263
+ }
264
+ } else {
265
+ query[key] = value;
266
+ }
267
+ });
268
+
269
+ return {
270
+ method: req.method.toUpperCase() as HttpMethod,
271
+ path: url.pathname,
272
+ headers,
273
+ body: (c as unknown as Record<string, unknown>)._typoBody,
274
+ query,
275
+ params: c.req.param() as Record<string, string>,
276
+ };
277
+ }
278
+
279
+ /** Write TypoKitResponse to Hono context — returns a Response */
280
+ function writeResponse(raw: unknown, response: TypoKitResponse): void {
281
+ // In Hono, responses are returned, not written imperatively.
282
+ // We store the response on the context for the route handler to return.
283
+ const c = raw as HonoContext;
284
+ (c as unknown as Record<string, unknown>)._typoResponse = response;
285
+ }
286
+
287
+ function buildHonoResponse(response: TypoKitResponse): Response {
288
+ const responseBody =
289
+ response.body === null || response.body === undefined
290
+ ? ""
291
+ : typeof response.body === "string"
292
+ ? response.body
293
+ : JSON.stringify(response.body);
294
+
295
+ const headers = new Headers();
296
+ for (const [key, value] of Object.entries(response.headers)) {
297
+ if (value !== undefined) {
298
+ if (Array.isArray(value)) {
299
+ for (const v of value) {
300
+ headers.append(key, v);
301
+ }
302
+ } else {
303
+ headers.set(key, value);
304
+ }
305
+ }
306
+ }
307
+
308
+ return new Response(responseBody, {
309
+ status: response.status,
310
+ headers,
311
+ });
312
+ }
313
+
314
+ const adapter: ServerAdapter = {
315
+ name: "hono",
316
+
317
+ registerRoutes(
318
+ routeTable: CompiledRouteTable,
319
+ handlerMap: HandlerMap,
320
+ middlewareChain: MiddlewareChain,
321
+ validatorMap?: ValidatorMap,
322
+ serializerMap?: SerializerMap,
323
+ ): void {
324
+ state.routeTable = routeTable;
325
+ state.handlerMap = handlerMap;
326
+ state.middlewareChain = middlewareChain;
327
+ state.validatorMap = validatorMap ?? null;
328
+ state.serializerMap = serializerMap ?? null;
329
+
330
+ // Collect all routes from the compiled radix tree
331
+ const routes: RouteEntry[] = [];
332
+ collectRoutes(routeTable, "", routes);
333
+
334
+ // Register each route as a Hono-native route
335
+ for (const route of routes) {
336
+ const method = route.method.toUpperCase();
337
+
338
+ app.on(method, route.path, async (c: HonoContext) => {
339
+ // Parse body for methods that have one
340
+ let body: unknown = undefined;
341
+ if (
342
+ route.method === "POST" ||
343
+ route.method === "PUT" ||
344
+ route.method === "PATCH"
345
+ ) {
346
+ try {
347
+ body = await c.req.json();
348
+ } catch {
349
+ body = undefined;
350
+ }
351
+ }
352
+
353
+ // Stash body on context for normalizeRequest
354
+ (c as unknown as Record<string, unknown>)._typoBody = body;
355
+
356
+ const typoReq = normalizeRequest(c);
357
+
358
+ // Run request validation pipeline
359
+ const validationError = runValidators(
360
+ { validators: route.validators },
361
+ state.validatorMap,
362
+ typoReq.params,
363
+ typoReq.query,
364
+ typoReq.body,
365
+ );
366
+ if (validationError) {
367
+ return buildHonoResponse(validationError);
368
+ }
369
+
370
+ const handlerFn = state.handlerMap![route.handlerRef];
371
+ if (!handlerFn) {
372
+ const errorResp: TypoKitResponse = {
373
+ status: 500,
374
+ headers: { "content-type": "application/json" },
375
+ body: JSON.stringify({
376
+ error: "Internal Server Error",
377
+ message: `Handler not found: ${route.handlerRef}`,
378
+ }),
379
+ };
380
+ return buildHonoResponse(errorResp);
381
+ }
382
+
383
+ // Create request context and execute middleware chain
384
+ let ctx = createRequestContext();
385
+
386
+ if (
387
+ state.middlewareChain &&
388
+ state.middlewareChain.entries.length > 0
389
+ ) {
390
+ const entries: MiddlewareEntry[] =
391
+ state.middlewareChain.entries.map((e) => ({
392
+ name: e.name,
393
+ middleware: {
394
+ handler: async (input) => {
395
+ const mwReq: TypoKitRequest = {
396
+ method: typoReq.method,
397
+ path: typoReq.path,
398
+ headers: input.headers,
399
+ body: input.body,
400
+ query: input.query,
401
+ params: input.params,
402
+ };
403
+ const response = await e.handler(
404
+ mwReq,
405
+ input.ctx,
406
+ async () => {
407
+ return { status: 200, headers: {}, body: null };
408
+ },
409
+ );
410
+ return response as unknown as Record<string, unknown>;
411
+ },
412
+ },
413
+ }));
414
+
415
+ ctx = await executeMiddlewareChain(typoReq, ctx, entries);
416
+ }
417
+
418
+ // Call the handler
419
+ const response = await handlerFn(typoReq, ctx);
420
+
421
+ // Response serialization pipeline
422
+ const serialized = serializeResponse(
423
+ response,
424
+ route.serializer,
425
+ state.serializerMap,
426
+ );
427
+
428
+ return buildHonoResponse(serialized);
429
+ });
430
+ }
431
+ },
432
+
433
+ async listen(port: number): Promise<ServerHandle> {
434
+ const server = serve({
435
+ fetch: app.fetch,
436
+ port,
437
+ hostname: "0.0.0.0",
438
+ });
439
+
440
+ // Wait briefly for server to bind
441
+ await new Promise<void>((resolve) => {
442
+ server.once("listening", () => resolve());
443
+ // If already listening, resolve immediately
444
+ if (server.listening) resolve();
445
+ });
446
+
447
+ return {
448
+ async close(): Promise<void> {
449
+ await new Promise<void>((resolve, reject) => {
450
+ server.close((err?: Error) => {
451
+ if (err) reject(err);
452
+ else resolve();
453
+ });
454
+ });
455
+ },
456
+ // Expose server for port retrieval in tests
457
+ _server: server,
458
+ } as ServerHandle & { _server: typeof server };
459
+ },
460
+
461
+ normalizeRequest,
462
+ writeResponse,
463
+
464
+ getNativeServer(): unknown {
465
+ return app;
466
+ },
467
+ };
468
+
469
+ return adapter;
470
+ }
471
+
472
+ // Re-export for convenience
473
+ export { serializeResponse, runValidators, validationErrorResponse };
474
+ export { type ServerAdapter } from "@typokit/core";