@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 +6 -0
- package/dist/index.d.ts +6 -11
- package/dist/index.js +194 -164
- package/package.json +12 -13
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
|
|
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):
|
|
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
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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.
|
|
18
|
-
"@basaltkit/http": "^1.
|
|
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": "^
|
|
25
|
-
"hono": "^4.
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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/
|
|
36
|
+
"url": "git+https://github.com/basaltkit/basalt.git",
|
|
38
37
|
"directory": "packages/hono"
|
|
39
38
|
},
|
|
40
|
-
"homepage": "https://github.com/
|
|
41
|
-
"bugs": "https://github.com/
|
|
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": "
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
51
|
"test": "vitest run",
|
|
53
52
|
"typecheck": "tsc --noEmit"
|
|
54
53
|
}
|