@basaltkit/hono 1.2.0 → 1.3.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/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,18 +1,22 @@
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>;
13
+ /**
14
+ * Serve the neutral JSON body (`NOT_FOUND_RESPONSE` from @basaltkit/http)
15
+ * for unmatched routes, identical across all adapters, instead of Hono's
16
+ * text default. Default: true. An app calling `hono.notFound(…)` later
17
+ * still wins (Hono keeps the last handler); pass false to opt out entirely.
18
+ */
19
+ notFound?: boolean;
16
20
  /**
17
21
  * Maximum request body size in bytes. A request whose `Content-Length`
18
22
  * exceeds this is rejected with 413 before the body is read — Hono/edge has
@@ -25,6 +29,4 @@ interface HonoPluginOptions {
25
29
  * guards you register for Fastify work unchanged — resolve `HONO` for the app
26
30
  * to serve (e.g. `@hono/node-server` or an edge runtime's `fetch` export).
27
31
  */
28
- declare function honoPlugin(options?: HonoPluginOptions): _basaltkit_core.BasaltPlugin<unknown>;
29
-
30
- export { DEFAULT_BODY_LIMIT, HONO, type HonoPluginOptions, honoPlugin, registerRoutes };
32
+ export declare function honoPlugin(options?: HonoPluginOptions): import("@basaltkit/core").BasaltPlugin<unknown>;
package/dist/index.js CHANGED
@@ -1,200 +1,210 @@
1
- // src/index.ts
2
- import { createToken, definePlugin, ensureMetadata } from "@basaltkit/core";
3
- import {
4
- HttpServerCollector,
5
- HTTP_SERVER,
6
- runRoute,
7
- toErrorResponse,
8
- isSseResponse,
9
- sseProducerOf,
10
- driveSse,
11
- SSE_HEADERS
12
- } from "@basaltkit/http";
13
- import { Hono } from "hono";
14
- var HONO = createToken("hono");
15
- var DEFAULT_BODY_LIMIT = 1048576;
1
+ import { Container, createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
2
+ import { NOT_FOUND_RESPONSE, 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;
16
7
  async function parseBody(context) {
17
- const method = context.req.method;
18
- if (method === "GET" || method === "HEAD") return void 0;
19
- const contentType = context.req.header("content-type") ?? "";
20
- try {
21
- if (contentType.includes("application/json")) return await context.req.json();
22
- if (contentType.includes("form")) return await context.req.parseBody();
23
- const text = await context.req.text();
24
- return text || void 0;
25
- } catch {
26
- return void 0;
27
- }
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
+ }
28
23
  }
29
24
  async function toNeutralRequest(context) {
30
- return {
31
- method: context.req.method,
32
- url: context.req.url,
33
- headers: Object.fromEntries(context.req.raw.headers.entries()),
34
- params: context.req.param(),
35
- query: context.req.query(),
36
- body: await parseBody(context),
37
- ...context.req.routePath ? { routePattern: context.req.routePath } : {},
38
- raw: context
39
- };
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
+ };
40
35
  }
36
+ /** Streams an SSE producer as a Response backed by a ReadableStream (Web streams). */
41
37
  function sseResponse(context, producer) {
42
- const encoder = new TextEncoder();
43
- const stream = new ReadableStream({
44
- start(controller) {
45
- void driveSse(producer, {
46
- write: (frame) => controller.enqueue(encoder.encode(frame)),
47
- end: () => {
48
- try {
49
- controller.close();
50
- } catch {
51
- }
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
+ });
52
53
  },
53
- onClose: (listener) => context.req.raw.signal.addEventListener("abort", listener)
54
- });
54
+ });
55
+ const { connection: _connection, ...headers } = SSE_HEADERS;
56
+ return new Response(stream, { headers });
57
+ }
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;
55
93
  }
56
- });
57
- const { connection: _connection, ...headers } = SSE_HEADERS;
58
- return new Response(stream, { headers });
59
94
  }
60
- var HonoReply = class {
61
- constructor(context) {
62
- this.context = context;
63
- }
64
- context;
65
- _status = 200;
66
- _sent = false;
67
- _payload;
68
- get sent() {
69
- return this._sent;
70
- }
71
- get statusCode() {
72
- return this._status;
73
- }
74
- get payload() {
75
- return this._payload;
76
- }
77
- get raw() {
78
- return this.context;
79
- }
80
- code(status) {
81
- this._status = status;
82
- return this;
83
- }
84
- header(name, value) {
85
- this.context.header(name, value);
86
- return this;
87
- }
88
- send(payload) {
89
- this._sent = true;
90
- this._payload = payload;
91
- return this;
92
- }
93
- };
94
95
  function toResponse(reply, payload) {
95
- const headers = new Headers(reply.context.res?.headers);
96
- let body;
97
- if (payload === void 0 || payload === null) {
98
- body = null;
99
- } else if (typeof payload === "string") {
100
- body = payload;
101
- if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
102
- } else {
103
- body = JSON.stringify(payload);
104
- headers.set("content-type", "application/json");
105
- }
106
- return new Response(body, { status: reply.statusCode, headers });
107
- }
108
- function handlerFor(definition, container, enrichers, guards) {
109
- return async (context) => {
110
- const reply = new HonoReply(context);
111
- try {
112
- const result = await runRoute(definition, await toNeutralRequest(context), reply, {
113
- ...container ? { container } : {},
114
- enrichers,
115
- guards
116
- });
117
- if (isSseResponse(result)) return sseResponse(context, sseProducerOf(result));
118
- return toResponse(reply, reply.sent ? reply.payload : result);
119
- } catch (error) {
120
- const { status, body } = toErrorResponse(error);
121
- return new Response(JSON.stringify(body), {
122
- status,
123
- headers: { "content-type": "application/json" }
124
- });
96
+ const headers = new Headers(reply.context.res?.headers);
97
+ let body;
98
+ if (payload === undefined || payload === null) {
99
+ body = null;
125
100
  }
126
- };
127
- }
128
- function registerRoutes(app, routes, container, enrichers = [], guards = []) {
129
- for (const definition of routes) {
130
- app.on(definition.method, definition.url, handlerFor(definition, container, enrichers, guards));
131
- }
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 });
132
111
  }
133
- function honoPlugin(options = {}) {
134
- const collector = new HttpServerCollector();
135
- return definePlugin({
136
- name: "basalt:hono",
137
- register({ container }) {
138
- container.singleton(HONO, () => options.app ?? new Hono());
139
- container.singleton(HTTP_SERVER, () => collector);
140
- },
141
- boot({ container, hooks }) {
142
- const app = container.get(HONO);
143
- const routes = options.routes ?? [];
144
- const metadata = ensureMetadata(container);
145
- const enrichers = metadata.get("http:enrichers");
146
- const guards = metadata.get("http:guards");
147
- const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
148
- hooks.on("app:booted", () => {
149
- app.use(async (context, next) => {
150
- const declared = Number(context.req.header("content-length") ?? "");
151
- if (Number.isFinite(declared) && declared > bodyLimit) {
152
- return context.json(
153
- { code: "PAYLOAD_TOO_LARGE", message: `Request body exceeds the ${bodyLimit}-byte limit.` },
154
- 413
155
- );
156
- }
157
- return next();
158
- });
159
- if (collector.afterHooks.length) {
160
- app.use(async (context, next) => {
161
- const start = Date.now();
162
- await next();
163
- await collector.runAfter(await toNeutralRequest(context), new HonoReply(context), context.res.status, Date.now() - start);
164
- });
165
- }
166
- app.use(async (context, next) => {
167
- const reply = new HonoReply(context);
168
- if (await collector.runPre(await toNeutralRequest(context), reply)) return toResponse(reply, reply.payload);
169
- await next();
170
- return void 0;
171
- });
172
- registerRoutes(app, routes, container, enrichers, guards);
173
- for (const { method, url, handler } of collector.extraRoutes) {
174
- app.on(method, url, async (context) => {
175
- const reply = new HonoReply(context);
176
- 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));
177
123
  return toResponse(reply, reply.sent ? reply.payload : result);
178
- });
179
124
  }
180
- });
181
- for (const definition of routes) {
182
- metadata.add("http:routes", {
183
- method: definition.method,
184
- url: definition.url,
185
- meta: definition.meta ?? {},
186
- body: definition.body,
187
- query: definition.query,
188
- params: definition.params,
189
- response: definition.response
190
- });
191
- }
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));
192
138
  }
193
- });
194
139
  }
195
- export {
196
- DEFAULT_BODY_LIMIT,
197
- HONO,
198
- honoPlugin,
199
- registerRoutes
200
- };
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
+ // Neutral JSON 404 (an app's own later `notFound` call replaces it).
186
+ if (options.notFound !== false) {
187
+ app.notFound((context) => context.json(NOT_FOUND_RESPONSE, 404));
188
+ }
189
+ for (const { method, url, handler } of collector.extraRoutes) {
190
+ app.on(method, url, async (context) => {
191
+ const reply = new HonoReply(context);
192
+ const result = await handler({ request: await toNeutralRequest(context), reply });
193
+ return toResponse(reply, reply.sent ? reply.payload : result);
194
+ });
195
+ }
196
+ });
197
+ for (const definition of routes) {
198
+ metadata.add('http:routes', {
199
+ method: definition.method,
200
+ url: definition.url,
201
+ meta: definition.meta ?? {},
202
+ body: definition.body,
203
+ query: definition.query,
204
+ params: definition.params,
205
+ response: definition.response,
206
+ });
207
+ }
208
+ },
209
+ });
210
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/hono",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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.1.0",
18
- "@basaltkit/http": "^1.7.0"
17
+ "@basaltkit/core": "^1.1.2",
18
+ "@basaltkit/http": "^1.10.0"
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
  }