@basaltkit/hono 1.1.0 → 1.2.1

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/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ <p align="center">
2
+ <a href="https://basaltkit-docs.pages.dev">
3
+ <img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440">
4
+ </a>
5
+ </p>
6
+
1
7
  # @basaltkit/hono
2
8
 
3
9
  The Basalt adapter for [Hono](https://hono.dev): the same typed routes, enrichers, and guards you'd use with Fastify or Express, running on Hono — on Node.js, Bun, Deno, or *edge* platforms (Cloudflare Workers, Vercel Edge, …). You need this when you want to take your Basalt API outside classic Node, or when you're already using Hono.
package/dist/index.d.ts CHANGED
@@ -1,15 +1,12 @@
1
- import * as _basaltkit_core from '@basaltkit/core';
2
1
  import { Container } from '@basaltkit/core';
3
- import * as hono_types from 'hono/types';
4
- import { BasaltRoute, RequestEnricher, RouteGuard } from '@basaltkit/http';
2
+ import { type BasaltRoute, type RequestEnricher, type RouteGuard } from '@basaltkit/http';
5
3
  import { Hono } from 'hono';
6
-
7
- declare const HONO: _basaltkit_core.Token<Hono<any, hono_types.BlankSchema, "/">>;
4
+ export declare const HONO: import("@basaltkit/core").Token<Hono<any, import("hono/types").BlankSchema, "/">>;
8
5
  /** Default maximum request body size (1 MiB) — override via honoPlugin({ bodyLimit }). */
9
- declare const DEFAULT_BODY_LIMIT = 1048576;
6
+ export declare const DEFAULT_BODY_LIMIT = 1048576;
10
7
  /** Mounts Basalt routes on a Hono app (usable without the plugin). */
11
- declare function registerRoutes(app: Hono<any>, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[]): void;
12
- interface HonoPluginOptions {
8
+ export declare function registerRoutes(app: Hono<any>, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[]): void;
9
+ export interface HonoPluginOptions {
13
10
  routes?: BasaltRoute[];
14
11
  /** Bring your own Hono app; otherwise a fresh one is created. */
15
12
  app?: Hono<any>;
@@ -25,6 +22,4 @@ interface HonoPluginOptions {
25
22
  * guards you register for Fastify work unchanged — resolve `HONO` for the app
26
23
  * to serve (e.g. `@hono/node-server` or an edge runtime's `fetch` export).
27
24
  */
28
- declare function honoPlugin(options?: HonoPluginOptions): _basaltkit_core.BasaltPlugin<unknown>;
29
-
30
- export { DEFAULT_BODY_LIMIT, HONO, type HonoPluginOptions, honoPlugin, registerRoutes };
25
+ export declare function honoPlugin(options?: HonoPluginOptions): import("@basaltkit/core").BasaltPlugin<unknown>;
package/dist/index.js CHANGED
@@ -1,176 +1,206 @@
1
- // src/index.ts
2
- import { createToken, definePlugin, ensureMetadata } from "@basaltkit/core";
3
- import {
4
- HttpServerCollector,
5
- HTTP_SERVER,
6
- runRoute,
7
- toErrorResponse
8
- } from "@basaltkit/http";
9
- import { Hono } from "hono";
10
- var HONO = createToken("hono");
11
- var DEFAULT_BODY_LIMIT = 1048576;
1
+ import { Container, createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
2
+ import { HttpServerCollector, HTTP_SERVER, runRoute, toErrorResponse, isSseResponse, sseProducerOf, driveSse, SSE_HEADERS, } from '@basaltkit/http';
3
+ import { Hono } from 'hono';
4
+ export const HONO = createToken('hono');
5
+ /** Default maximum request body size (1 MiB) — override via honoPlugin({ bodyLimit }). */
6
+ export const DEFAULT_BODY_LIMIT = 1_048_576;
12
7
  async function parseBody(context) {
13
- const method = context.req.method;
14
- if (method === "GET" || method === "HEAD") return void 0;
15
- const contentType = context.req.header("content-type") ?? "";
16
- try {
17
- if (contentType.includes("application/json")) return await context.req.json();
18
- if (contentType.includes("form")) return await context.req.parseBody();
19
- const text = await context.req.text();
20
- return text || void 0;
21
- } catch {
22
- return void 0;
23
- }
8
+ const method = context.req.method;
9
+ if (method === 'GET' || method === 'HEAD')
10
+ return undefined;
11
+ const contentType = context.req.header('content-type') ?? '';
12
+ try {
13
+ if (contentType.includes('application/json'))
14
+ return await context.req.json();
15
+ if (contentType.includes('form'))
16
+ return await context.req.parseBody();
17
+ const text = await context.req.text();
18
+ return text || undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
24
23
  }
25
24
  async function toNeutralRequest(context) {
26
- return {
27
- method: context.req.method,
28
- url: context.req.url,
29
- headers: Object.fromEntries(context.req.raw.headers.entries()),
30
- params: context.req.param(),
31
- query: context.req.query(),
32
- body: await parseBody(context),
33
- ...context.req.routePath ? { routePattern: context.req.routePath } : {},
34
- raw: context
35
- };
25
+ return {
26
+ method: context.req.method,
27
+ url: context.req.url,
28
+ headers: Object.fromEntries(context.req.raw.headers.entries()),
29
+ params: context.req.param(),
30
+ query: context.req.query(),
31
+ body: await parseBody(context),
32
+ ...(context.req.routePath ? { routePattern: context.req.routePath } : {}),
33
+ raw: context,
34
+ };
36
35
  }
37
- var HonoReply = class {
38
- constructor(context) {
39
- this.context = context;
40
- }
41
- context;
42
- _status = 200;
43
- _sent = false;
44
- _payload;
45
- get sent() {
46
- return this._sent;
47
- }
48
- get statusCode() {
49
- return this._status;
50
- }
51
- get payload() {
52
- return this._payload;
53
- }
54
- get raw() {
55
- return this.context;
56
- }
57
- code(status) {
58
- this._status = status;
59
- return this;
60
- }
61
- header(name, value) {
62
- this.context.header(name, value);
63
- return this;
64
- }
65
- send(payload) {
66
- this._sent = true;
67
- this._payload = payload;
68
- return this;
69
- }
70
- };
71
- function toResponse(reply, payload) {
72
- const headers = new Headers(reply.context.res?.headers);
73
- let body;
74
- if (payload === void 0 || payload === null) {
75
- body = null;
76
- } else if (typeof payload === "string") {
77
- body = payload;
78
- if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
79
- } else {
80
- body = JSON.stringify(payload);
81
- headers.set("content-type", "application/json");
82
- }
83
- return new Response(body, { status: reply.statusCode, headers });
36
+ /** Streams an SSE producer as a Response backed by a ReadableStream (Web streams). */
37
+ function sseResponse(context, producer) {
38
+ const encoder = new TextEncoder();
39
+ const stream = new ReadableStream({
40
+ start(controller) {
41
+ void driveSse(producer, {
42
+ write: (frame) => controller.enqueue(encoder.encode(frame)),
43
+ end: () => {
44
+ try {
45
+ controller.close();
46
+ }
47
+ catch {
48
+ /* already closed */
49
+ }
50
+ },
51
+ onClose: (listener) => context.req.raw.signal.addEventListener('abort', listener),
52
+ });
53
+ },
54
+ });
55
+ const { connection: _connection, ...headers } = SSE_HEADERS;
56
+ return new Response(stream, { headers });
84
57
  }
85
- function handlerFor(definition, container, enrichers, guards) {
86
- return async (context) => {
87
- const reply = new HonoReply(context);
88
- try {
89
- const result = await runRoute(definition, await toNeutralRequest(context), reply, {
90
- ...container ? { container } : {},
91
- enrichers,
92
- guards
93
- });
94
- return toResponse(reply, reply.sent ? reply.payload : result);
95
- } catch (error) {
96
- const { status, body } = toErrorResponse(error);
97
- return new Response(JSON.stringify(body), {
98
- status,
99
- headers: { "content-type": "application/json" }
100
- });
58
+ /** Neutral reply that buffers the response; the handler emits a native Response. */
59
+ class HonoReply {
60
+ context;
61
+ _status = 200;
62
+ _sent = false;
63
+ _payload;
64
+ constructor(context) {
65
+ this.context = context;
66
+ }
67
+ get sent() {
68
+ return this._sent;
69
+ }
70
+ get statusCode() {
71
+ return this._status;
72
+ }
73
+ get payload() {
74
+ return this._payload;
75
+ }
76
+ get raw() {
77
+ return this.context;
78
+ }
79
+ code(status) {
80
+ this._status = status;
81
+ return this;
82
+ }
83
+ header(name, value) {
84
+ // Accumulate on the Hono context so headers set in a pre-hook survive to
85
+ // the final response (a separate reply instance builds it).
86
+ this.context.header(name, value);
87
+ return this;
88
+ }
89
+ send(payload) {
90
+ this._sent = true;
91
+ this._payload = payload;
92
+ return this;
101
93
  }
102
- };
103
94
  }
104
- function registerRoutes(app, routes, container, enrichers = [], guards = []) {
105
- for (const definition of routes) {
106
- app.on(definition.method, definition.url, handlerFor(definition, container, enrichers, guards));
107
- }
95
+ function toResponse(reply, payload) {
96
+ const headers = new Headers(reply.context.res?.headers);
97
+ let body;
98
+ if (payload === undefined || payload === null) {
99
+ body = null;
100
+ }
101
+ else if (typeof payload === 'string') {
102
+ body = payload;
103
+ if (!headers.has('content-type'))
104
+ headers.set('content-type', 'text/plain; charset=utf-8');
105
+ }
106
+ else {
107
+ body = JSON.stringify(payload);
108
+ headers.set('content-type', 'application/json');
109
+ }
110
+ return new Response(body, { status: reply.statusCode, headers });
108
111
  }
109
- function honoPlugin(options = {}) {
110
- const collector = new HttpServerCollector();
111
- return definePlugin({
112
- name: "basalt:hono",
113
- register({ container }) {
114
- container.singleton(HONO, () => options.app ?? new Hono());
115
- container.singleton(HTTP_SERVER, () => collector);
116
- },
117
- boot({ container, hooks }) {
118
- const app = container.get(HONO);
119
- const routes = options.routes ?? [];
120
- const metadata = ensureMetadata(container);
121
- const enrichers = metadata.get("http:enrichers");
122
- const guards = metadata.get("http:guards");
123
- const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
124
- hooks.on("app:booted", () => {
125
- app.use(async (context, next) => {
126
- const declared = Number(context.req.header("content-length") ?? "");
127
- if (Number.isFinite(declared) && declared > bodyLimit) {
128
- return context.json(
129
- { code: "PAYLOAD_TOO_LARGE", message: `Request body exceeds the ${bodyLimit}-byte limit.` },
130
- 413
131
- );
132
- }
133
- return next();
134
- });
135
- if (collector.afterHooks.length) {
136
- app.use(async (context, next) => {
137
- const start = Date.now();
138
- await next();
139
- await collector.runAfter(await toNeutralRequest(context), new HonoReply(context), context.res.status, Date.now() - start);
140
- });
141
- }
142
- app.use(async (context, next) => {
143
- const reply = new HonoReply(context);
144
- if (await collector.runPre(await toNeutralRequest(context), reply)) return toResponse(reply, reply.payload);
145
- await next();
146
- return void 0;
147
- });
148
- registerRoutes(app, routes, container, enrichers, guards);
149
- for (const { method, url, handler } of collector.extraRoutes) {
150
- app.on(method, url, async (context) => {
151
- const reply = new HonoReply(context);
152
- const result = await handler({ request: await toNeutralRequest(context), reply });
112
+ function handlerFor(definition, container, enrichers, guards) {
113
+ return async (context) => {
114
+ const reply = new HonoReply(context);
115
+ try {
116
+ const result = await runRoute(definition, await toNeutralRequest(context), reply, {
117
+ ...(container ? { container } : {}),
118
+ enrichers,
119
+ guards,
120
+ });
121
+ if (isSseResponse(result))
122
+ return sseResponse(context, sseProducerOf(result));
153
123
  return toResponse(reply, reply.sent ? reply.payload : result);
154
- });
155
124
  }
156
- });
157
- for (const definition of routes) {
158
- metadata.add("http:routes", {
159
- method: definition.method,
160
- url: definition.url,
161
- meta: definition.meta ?? {},
162
- body: definition.body,
163
- query: definition.query,
164
- params: definition.params,
165
- response: definition.response
166
- });
167
- }
125
+ catch (error) {
126
+ const { status, body } = toErrorResponse(error);
127
+ return new Response(JSON.stringify(body), {
128
+ status,
129
+ headers: { 'content-type': 'application/json' },
130
+ });
131
+ }
132
+ };
133
+ }
134
+ /** Mounts Basalt routes on a Hono app (usable without the plugin). */
135
+ export function registerRoutes(app, routes, container, enrichers = [], guards = []) {
136
+ for (const definition of routes) {
137
+ app.on(definition.method, definition.url, handlerFor(definition, container, enrichers, guards));
168
138
  }
169
- });
170
139
  }
171
- export {
172
- DEFAULT_BODY_LIMIT,
173
- HONO,
174
- honoPlugin,
175
- registerRoutes
176
- };
140
+ /**
141
+ * Runs Basalt on Hono (Node, Bun, Deno, edge). The same routes, enrichers and
142
+ * guards you register for Fastify work unchanged — resolve `HONO` for the app
143
+ * to serve (e.g. `@hono/node-server` or an edge runtime's `fetch` export).
144
+ */
145
+ export function honoPlugin(options = {}) {
146
+ const collector = new HttpServerCollector();
147
+ return definePlugin({
148
+ name: 'basalt:hono',
149
+ register({ container }) {
150
+ container.singleton(HONO, () => options.app ?? new Hono());
151
+ container.singleton(HTTP_SERVER, () => collector);
152
+ },
153
+ boot({ container, hooks }) {
154
+ const app = container.get(HONO);
155
+ const routes = options.routes ?? [];
156
+ const metadata = ensureMetadata(container);
157
+ const enrichers = metadata.get('http:enrichers');
158
+ const guards = metadata.get('http:guards');
159
+ // Mount once edge plugins have registered their hooks/routes.
160
+ const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
161
+ hooks.on('app:booted', () => {
162
+ // Reject oversized bodies up front (Hono/edge has no default cap).
163
+ app.use(async (context, next) => {
164
+ const declared = Number(context.req.header('content-length') ?? '');
165
+ if (Number.isFinite(declared) && declared > bodyLimit) {
166
+ return context.json({ code: 'PAYLOAD_TOO_LARGE', message: `Request body exceeds the ${bodyLimit}-byte limit.` }, 413);
167
+ }
168
+ return next();
169
+ });
170
+ if (collector.afterHooks.length) {
171
+ app.use(async (context, next) => {
172
+ const start = Date.now();
173
+ await next();
174
+ await collector.runAfter(await toNeutralRequest(context), new HonoReply(context), context.res.status, Date.now() - start);
175
+ });
176
+ }
177
+ app.use(async (context, next) => {
178
+ const reply = new HonoReply(context);
179
+ if (await collector.runPre(await toNeutralRequest(context), reply))
180
+ return toResponse(reply, reply.payload);
181
+ await next();
182
+ return undefined;
183
+ });
184
+ registerRoutes(app, routes, container, enrichers, guards);
185
+ for (const { method, url, handler } of collector.extraRoutes) {
186
+ app.on(method, url, async (context) => {
187
+ const reply = new HonoReply(context);
188
+ const result = await handler({ request: await toNeutralRequest(context), reply });
189
+ return toResponse(reply, reply.sent ? reply.payload : result);
190
+ });
191
+ }
192
+ });
193
+ for (const definition of routes) {
194
+ metadata.add('http:routes', {
195
+ method: definition.method,
196
+ url: definition.url,
197
+ meta: definition.meta ?? {},
198
+ body: definition.body,
199
+ query: definition.query,
200
+ params: definition.params,
201
+ response: definition.response,
202
+ });
203
+ }
204
+ },
205
+ });
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/hono",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Hono adapter for Basalt: run the same typed routes, enrichers and guards on Hono (Node, Bun, Deno, edge) as on Fastify or Express.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,19 +14,18 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "@basaltkit/core": "^1.0.0",
18
- "@basaltkit/http": "^1.3.0"
17
+ "@basaltkit/core": "^1.1.2",
18
+ "@basaltkit/http": "^1.9.1"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "hono": "^4.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^22.15.0",
25
- "hono": "^4.6.0",
26
- "tsup": "^8.4.0",
27
- "typescript": "^5.8.0",
28
- "vitest": "^3.1.0",
29
- "zod": "^3.24.0",
24
+ "@types/node": "^26.3.0",
25
+ "hono": "^4.13.4",
26
+ "typescript": "^7.0.2",
27
+ "vitest": "^4.1.11",
28
+ "zod": "^3.24.0 || ^4.0.0",
30
29
  "@basaltkit/tsconfig": "^0.24.0"
31
30
  },
32
31
  "publishConfig": {
@@ -34,11 +33,11 @@
34
33
  },
35
34
  "repository": {
36
35
  "type": "git",
37
- "url": "git+https://github.com/Zebedeu/basalt.git",
36
+ "url": "git+https://github.com/basaltkit/basalt.git",
38
37
  "directory": "packages/hono"
39
38
  },
40
- "homepage": "https://github.com/Zebedeu/basalt/tree/main/packages/hono#readme",
41
- "bugs": "https://github.com/Zebedeu/basalt/issues",
39
+ "homepage": "https://github.com/basaltkit/basalt/tree/main/packages/hono#readme",
40
+ "bugs": "https://github.com/basaltkit/basalt/issues",
42
41
  "keywords": [
43
42
  "basalt",
44
43
  "typescript",
@@ -48,7 +47,7 @@
48
47
  "edge"
49
48
  ],
50
49
  "scripts": {
51
- "build": "tsup src/index.ts --format esm --dts --clean",
50
+ "build": "tsc -p tsconfig.build.json",
52
51
  "test": "vitest run",
53
52
  "typecheck": "tsc --noEmit"
54
53
  }