@dotpress/adapter-hono 0.0.0-stage → 2.0.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Renaud Guyon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
- # Temporary Holding Version
1
+ # @dotpress/adapter-hono
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Hono](https://hono.dev) adapter for [Dotpress](https://dotpress.dev), on Node.js and AWS Lambda. Supports Hono 4 (4.13 or later); Hono 5 is tested against its release candidate.
4
+
5
+ > 🚧 Dotpress V2 is in alpha (`2.0.0-alpha`): the API may still change before 2.0.0.
6
+
7
+ ## On Node.js
8
+
9
+ ```sh
10
+ pnpm add @dotpress/core @dotpress/adapter-hono hono @hono/node-server zod
11
+ ```
12
+
13
+ ```ts
14
+ import { createApp, defineRoute } from '@dotpress/core'
15
+ import { honoAdapter } from '@dotpress/adapter-hono/node'
16
+ import { secureHeaders } from 'hono/secure-headers'
17
+
18
+ defineRoute({ path: '/hello', handler: () => ({ message: 'Hello' }) })
19
+
20
+ const app = await createApp({
21
+ adapter: honoAdapter({
22
+ // Native Hono middlewares, before the Dotpress routes
23
+ beforeRoutes: (app) => {
24
+ app.use('*', secureHeaders())
25
+ },
26
+ }),
27
+ })
28
+
29
+ await app.listen(3000)
30
+ ```
31
+
32
+ `@dotpress/adapter-hono/node` serves the app with [`@hono/node-server`](https://github.com/honojs/node-server). `close()` waits for pending requests and closes the responses being streamed (Server-Sent Events); `adapter.server()` returns the Node.js HTTP server started by `listen()`.
33
+
34
+ ## On AWS Lambda
35
+
36
+ ```sh
37
+ pnpm add @dotpress/core @dotpress/adapter-hono hono @hono/aws-lambda zod
38
+ ```
39
+
40
+ ```ts
41
+ import { createApp } from '@dotpress/core'
42
+ import { honoAdapter } from '@dotpress/adapter-hono'
43
+ import { createLambdaHandler } from '@dotpress/adapter-hono/lambda'
44
+
45
+ export const handler = createLambdaHandler(() =>
46
+ createApp({ adapter: honoAdapter(), plugins: [/* ... */] })
47
+ )
48
+ ```
49
+
50
+ `createLambdaHandler()` (based on [`@hono/aws-lambda`](https://github.com/honojs/middleware), which requires Hono 4.13.9 or later) handles the events of API Gateway REST APIs (payload v1), HTTP APIs and Function URLs (payload v2), and Application Load Balancers. Enable the multi-value headers of the target group to receive repeated query keys and send several cookies through a load balancer.
51
+
52
+ - **Lazy creation.** The factory is called at the first invocation (cold start), and the app is reused by the next invocations of the execution environment. Invocations received meanwhile wait for the same app. The handler export stays synchronous: no top-level `await`.
53
+ - **Recoverable failures.** When the factory fails, the error is logged (`logger` option, console by default), the invocation gets the standard `500` error, and the next invocation calls the factory again.
54
+ - **Buffered responses.** Responses are sent once complete: `stream()` works for finite streams, but Server-Sent Events (`sse()`) are not supported, and a stream that fails makes the invocation fail (`502` from API Gateway) rather than sending a truncated response. Lambda response streaming is not supported yet.
55
+ - **Binary responses** are sent in base64: every content type except text, JSON and XML by default, or as decided by the `isContentTypeBinary(contentType)` option.
56
+ - **`ctx.req.ip`** is the source IP of API Gateway, or the last `X-Forwarded-For` entry behind a load balancer (appended by the load balancer). Behind CloudFront or another proxy, use the `getIp(c)` option of `honoAdapter()`.
57
+
58
+ ## Other runtimes
59
+
60
+ The main entry, `@dotpress/adapter-hono`, only relies on Hono and Web-standard APIs: no Node.js server. `adapter.fetch` handles a Web `Request`, like `app.fetch` in Hono, for runtimes that take a `fetch` handler:
61
+
62
+ ```ts
63
+ import { honoAdapter } from '@dotpress/adapter-hono'
64
+
65
+ const adapter = honoAdapter()
66
+ await createApp({ adapter })
67
+
68
+ export default { fetch: adapter.fetch }
69
+ ```
70
+
71
+ Outside Node.js, `ctx.req.ip` comes from the `getIp(c)` option. Dotpress V2.0 is only tested on Node.js: other runtimes are not officially supported yet.
72
+
73
+ ## Options
74
+
75
+ | Option | Description |
76
+ | --- | --- |
77
+ | `app` | Existing Hono application. Defaults to `new Hono()`. |
78
+ | `beforeRoutes(app)` | Registers native middlewares and routes before the Dotpress routes. |
79
+ | `afterRoutes(app)` | Registers native middlewares and routes after the Dotpress routes, before the not-found handler. |
80
+ | `bodyLimit` | Maximum request body size in bytes. Defaults to 1 MiB. |
81
+ | `getIp(c)` | Client IP address (`ctx.req.ip`). Defaults to the address given by the runtime: the socket address on Node.js, see below for AWS Lambda. |
82
+
83
+ ## Behavior
84
+
85
+ - Routes are matched by Dotpress, so they behave the same on every adapter (case-sensitive, static segments before parameters, Hono options such as `strict` do not apply). Use the `basePath` option of `createApp()` rather than `app.basePath()`.
86
+ - Request bodies are read by the adapter: JSON, URL-encoded forms, text and multipart forms are parsed, other content types are exposed as `Uint8Array`. Bodies already read by a native middleware through Hono (`c.req.json()`, `c.req.parseBody()`…) are taken from its cache. Routes declared with `rawBody: true` need the exact bytes: the adapter must read the body, or a native middleware must read it with `c.req.arrayBuffer()` or `c.req.blob()`.
87
+ - `HTTPException` client errors (4xx) thrown by native middlewares use the Dotpress error format and keep their status, message and headers (`WWW-Authenticate` of `bearerAuth()`…). Other errors are unexpected errors (`500`). The adapter takes over `app.notFound()` and `app.onError()`.
88
+ - `ctx.req.ip` does not read proxy headers such as `X-Forwarded-For`: behind a proxy, read them in `getIp(c)`.
89
+ - `ctx.raw` is the Hono context.
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,212 @@
1
+ // src/index.ts
2
+ import {
3
+ DEFAULT_BODY_LIMIT,
4
+ HttpError,
5
+ createRouteMatcher,
6
+ fromWebRequest,
7
+ parseRequestBody,
8
+ payloadTooLargeError,
9
+ readRequestBody,
10
+ toWebResponse
11
+ } from "@dotpress/core";
12
+ import { Hono } from "hono";
13
+ var runtimeIp = (c) => {
14
+ const env = c.env;
15
+ const socket = env?.incoming?.socket?.remoteAddress;
16
+ if (typeof socket === "string") return socket;
17
+ const context = env?.requestContext;
18
+ const sourceIp = context?.identity?.sourceIp ?? context?.http?.sourceIp;
19
+ if (typeof sourceIp === "string") return sourceIp;
20
+ if (context?.elb !== void 0) {
21
+ const forwarded = c.req.header("x-forwarded-for")?.split(",").at(-1);
22
+ return forwarded?.trim() || void 0;
23
+ }
24
+ return void 0;
25
+ };
26
+ var CLIENT_ERRORS = {
27
+ 400: ["BAD_REQUEST", "Bad Request"],
28
+ 401: ["UNAUTHORIZED", "Unauthorized"],
29
+ 403: ["FORBIDDEN", "Forbidden"],
30
+ 404: ["NOT_FOUND", "Not Found"],
31
+ 405: ["METHOD_NOT_ALLOWED", "Method Not Allowed"],
32
+ 408: ["REQUEST_TIMEOUT", "Request Timeout"],
33
+ 409: ["CONFLICT", "Conflict"],
34
+ 413: ["PAYLOAD_TOO_LARGE", "Payload Too Large"],
35
+ 415: ["UNSUPPORTED_MEDIA_TYPE", "Unsupported Media Type"],
36
+ 422: ["UNPROCESSABLE_ENTITY", "Unprocessable Entity"],
37
+ 429: ["TOO_MANY_REQUESTS", "Too Many Requests"]
38
+ };
39
+ var isHttpException = (error) => error instanceof Error && typeof error.status === "number" && typeof error.getResponse === "function";
40
+ var fromNativeError = (error) => {
41
+ if (!isHttpException(error)) return error;
42
+ const { status, message } = error;
43
+ if (!Number.isInteger(status) || status < 400 || status > 499) return error;
44
+ const [code, defaultMessage] = CLIENT_ERRORS[status] ?? [
45
+ "CLIENT_ERROR",
46
+ "Client Error"
47
+ ];
48
+ return new HttpError(status, code, message || defaultMessage, void 0, {
49
+ cause: error
50
+ });
51
+ };
52
+ var addNativeHeaders = (response, error) => {
53
+ if (!error.res) return;
54
+ for (const [name, value] of error.res.headers) {
55
+ if (name.startsWith("content-")) continue;
56
+ if (name === "set-cookie") continue;
57
+ response.headers[name] = value;
58
+ }
59
+ const cookies = error.res.headers.getSetCookie();
60
+ if (cookies.length > 0) {
61
+ const current = response.headers["set-cookie"] ?? [];
62
+ response.headers["set-cookie"] = [
63
+ ...Array.isArray(current) ? current : [current],
64
+ ...cookies
65
+ ];
66
+ }
67
+ };
68
+ var encoder = new TextEncoder();
69
+ var cachedBody = async (c, limit) => {
70
+ const cache = c.req.bodyCache;
71
+ const contentType = c.req.header("content-type");
72
+ let bytes;
73
+ let exact = false;
74
+ let type = contentType;
75
+ if (cache.arrayBuffer) {
76
+ bytes = new Uint8Array(await cache.arrayBuffer);
77
+ exact = true;
78
+ } else if (cache.blob) {
79
+ bytes = new Uint8Array(await (await cache.blob).arrayBuffer());
80
+ exact = true;
81
+ } else if (cache.text) {
82
+ bytes = encoder.encode(await cache.text);
83
+ } else if (cache.formData) {
84
+ const response = new Response(await cache.formData);
85
+ bytes = new Uint8Array(await response.arrayBuffer());
86
+ type = response.headers.get("content-type") ?? void 0;
87
+ } else {
88
+ throw new Error(
89
+ "The request body was read by a native middleware outside the cache of Hono (c.req.raw): read it with c.req.json(), c.req.text() or c.req.arrayBuffer()."
90
+ );
91
+ }
92
+ if (bytes.byteLength > limit) throw payloadTooLargeError();
93
+ const parsed = await parseRequestBody(bytes, type);
94
+ return { ...parsed, rawBody: exact ? bytes : void 0 };
95
+ };
96
+ var readBody = (c, limit) => c.req.raw.bodyUsed ? cachedBody(c, limit) : readRequestBody(c.req.raw, limit);
97
+ var track = (body, controller, streams) => {
98
+ const reader = body.getReader();
99
+ let stop = () => {
100
+ };
101
+ return new ReadableStream({
102
+ start() {
103
+ stop = () => {
104
+ streams.delete(stop);
105
+ controller.abort();
106
+ reader.cancel().catch(() => {
107
+ });
108
+ };
109
+ streams.add(stop);
110
+ },
111
+ async pull(stream) {
112
+ try {
113
+ const { done, value } = await reader.read();
114
+ if (done) {
115
+ streams.delete(stop);
116
+ stream.close();
117
+ } else {
118
+ stream.enqueue(value);
119
+ }
120
+ } catch (error) {
121
+ streams.delete(stop);
122
+ stream.error(error);
123
+ }
124
+ },
125
+ cancel(reason) {
126
+ streams.delete(stop);
127
+ controller.abort(reason);
128
+ return reader.cancel(reason);
129
+ }
130
+ });
131
+ };
132
+ function honoAdapter(options = {}) {
133
+ const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
134
+ if (!Number.isInteger(bodyLimit) || bodyLimit < 0) {
135
+ throw new RangeError(
136
+ "Invalid bodyLimit: expected a non-negative integer (bytes)."
137
+ );
138
+ }
139
+ const application = options.app ?? new Hono();
140
+ const app = application;
141
+ const getIp = options.getIp ?? runtimeIp;
142
+ let mounted = false;
143
+ const streams = /* @__PURE__ */ new Set();
144
+ const normalize = (c, signal) => fromWebRequest(c.req.raw, { ip: getIp(c), signal, raw: c });
145
+ const send = (c, response, controller) => {
146
+ const { body } = response;
147
+ if (controller && body instanceof ReadableStream) {
148
+ return toWebResponse(
149
+ { ...response, body: track(body, controller, streams) },
150
+ c.req.method
151
+ );
152
+ }
153
+ return toWebResponse(response, c.req.method);
154
+ };
155
+ const handleRoute = async (c, match, runtime) => {
156
+ const controller = new AbortController();
157
+ const signal = AbortSignal.any([c.req.raw.signal, controller.signal]);
158
+ const request = normalize(c, signal);
159
+ try {
160
+ const { body, files, rawBody } = await readBody(
161
+ c,
162
+ match.route.definition.bodyLimit ?? bodyLimit
163
+ );
164
+ request.body = body;
165
+ request.files = files;
166
+ request.rawBody = rawBody;
167
+ } catch (error) {
168
+ return send(c, await runtime.handleError(error, request));
169
+ }
170
+ request.params = match.params;
171
+ return send(c, await match.route.execute(request), controller);
172
+ };
173
+ return {
174
+ name: "hono",
175
+ mount: async (routes, runtime) => {
176
+ if (mounted) throw new Error("The Hono adapter is already mounted.");
177
+ mounted = true;
178
+ const matcher = createRouteMatcher(routes);
179
+ await options.beforeRoutes?.(application);
180
+ app.use("*", async (c, next) => {
181
+ const match = matcher.match(c.req.method, new URL(c.req.url).pathname);
182
+ if (!match) {
183
+ await next();
184
+ return;
185
+ }
186
+ return handleRoute(c, match, runtime);
187
+ });
188
+ await options.afterRoutes?.(application);
189
+ app.notFound(
190
+ async (c) => send(c, await runtime.handleNotFound(normalize(c)))
191
+ );
192
+ app.onError(async (error, c) => {
193
+ const converted = fromNativeError(error);
194
+ const response = await runtime.handleError(converted, normalize(c));
195
+ if (converted !== error) {
196
+ addNativeHeaders(response, error);
197
+ }
198
+ return send(c, response);
199
+ });
200
+ },
201
+ fetch: async (request, env, executionContext) => app.fetch(request, env, executionContext),
202
+ close: async () => {
203
+ for (const stop of [...streams]) stop();
204
+ },
205
+ native: () => application
206
+ };
207
+ }
208
+
209
+ export {
210
+ honoAdapter
211
+ };
212
+ //# sourceMappingURL=chunk-4EA3O2OL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n DEFAULT_BODY_LIMIT,\n HttpError,\n createRouteMatcher,\n fromWebRequest,\n parseRequestBody,\n payloadTooLargeError,\n readRequestBody,\n toWebResponse,\n type CompiledRoute,\n type DotpressRuntime,\n type HttpAdapter,\n type NormalizedRequest,\n type NormalizedResponse,\n type RequestBody,\n type RouteMatch,\n} from '@dotpress/core'\nimport { Hono, type Context } from 'hono'\n\n/**\n * Minimal shape of a Hono application (Hono 4 or 5). The types of Hono 5\n * differ from those of Hono 4, hence the loose signatures.\n */\nexport interface HonoApplication {\n use: (...args: never[]) => unknown\n notFound: (...args: never[]) => unknown\n onError: (...args: never[]) => unknown\n fetch: (\n request: Request,\n env?: never,\n executionContext?: never\n ) => Response | Promise<Response>\n}\n\n/** Members of the Hono application used by the adapter. */\ninterface MountableApp {\n use(\n path: string,\n handler: (c: Context, next: () => Promise<void>) => Promise<Response | void>\n ): unknown\n notFound(handler: (c: Context) => Promise<Response>): unknown\n onError(handler: (error: Error, c: Context) => Promise<Response>): unknown\n fetch(\n request: Request,\n env?: unknown,\n executionContext?: unknown\n ): Response | Promise<Response>\n}\n\nexport interface HonoAdapterOptions<TApp extends HonoApplication> {\n /** Hono application to mount the routes on. Defaults to `new Hono()`. */\n app?: TApp\n /** Registers native middlewares before the Dotpress routes. */\n beforeRoutes?: (app: TApp) => void | Promise<void>\n /**\n * Registers native middlewares and routes after the Dotpress routes,\n * before the not-found handler.\n */\n afterRoutes?: (app: TApp) => void | Promise<void>\n /**\n * Maximum request body size, in bytes. Defaults to 1 MiB. Routes can\n * override it with their own `bodyLimit`.\n */\n bodyLimit?: number\n /**\n * Client IP address of a request (`ctx.req.ip`). Defaults to the address\n * given by the runtime: the socket on Node.js, the source IP of API\n * Gateway, the last `X-Forwarded-For` entry behind an Application Load\n * Balancer (added by the load balancer). Proxy headers are not read\n * otherwise: behind another proxy (CloudFront…), read them here.\n */\n getIp?: (c: Context) => string | undefined\n}\n\nexport interface HonoAdapter<\n TApp extends HonoApplication = Hono,\n> extends HttpAdapter<TApp> {\n /**\n * Handles a Web request, like `app.native().fetch()`: for runtimes that\n * take a `fetch` handler, and for tests.\n */\n fetch(\n request: Request,\n env?: unknown,\n executionContext?: unknown\n ): Promise<Response>\n /** Closes the responses being streamed (their `req.signal` is aborted). */\n close(): Promise<void>\n}\n\n/** Environment given by `@hono/node-server` and `@hono/aws-lambda`. */\ninterface RuntimeEnv {\n readonly incoming?: { readonly socket?: { readonly remoteAddress?: unknown } }\n readonly requestContext?: {\n readonly identity?: { readonly sourceIp?: unknown }\n readonly http?: { readonly sourceIp?: unknown }\n readonly elb?: unknown\n }\n}\n\n/**\n * Client address given by the runtime: the socket on Node.js, the source\n * IP of API Gateway (REST and HTTP APIs, Function URLs), or the address\n * appended to `X-Forwarded-For` by an Application Load Balancer.\n */\nconst runtimeIp = (c: Context): string | undefined => {\n const env = c.env as RuntimeEnv | undefined\n\n const socket = env?.incoming?.socket?.remoteAddress\n if (typeof socket === 'string') return socket\n\n const context = env?.requestContext\n const sourceIp = context?.identity?.sourceIp ?? context?.http?.sourceIp\n if (typeof sourceIp === 'string') return sourceIp\n\n if (context?.elb !== undefined) {\n const forwarded = c.req.header('x-forwarded-for')?.split(',').at(-1)\n return forwarded?.trim() || undefined\n }\n return undefined\n}\n\n/** Code and default message of client errors. */\nconst CLIENT_ERRORS: Record<number, readonly [string, string]> = {\n 400: ['BAD_REQUEST', 'Bad Request'],\n 401: ['UNAUTHORIZED', 'Unauthorized'],\n 403: ['FORBIDDEN', 'Forbidden'],\n 404: ['NOT_FOUND', 'Not Found'],\n 405: ['METHOD_NOT_ALLOWED', 'Method Not Allowed'],\n 408: ['REQUEST_TIMEOUT', 'Request Timeout'],\n 409: ['CONFLICT', 'Conflict'],\n 413: ['PAYLOAD_TOO_LARGE', 'Payload Too Large'],\n 415: ['UNSUPPORTED_MEDIA_TYPE', 'Unsupported Media Type'],\n 422: ['UNPROCESSABLE_ENTITY', 'Unprocessable Entity'],\n 429: ['TOO_MANY_REQUESTS', 'Too Many Requests'],\n}\n\ninterface HttpExceptionLike {\n readonly status: number\n readonly message: string\n readonly res?: Response\n getResponse(): Response\n}\n\n/** `HTTPException` of Hono 4 or 5, recognized by its shape. */\nconst isHttpException = (error: unknown): error is HttpExceptionLike =>\n error instanceof Error &&\n typeof (error as Partial<HttpExceptionLike>).status === 'number' &&\n typeof (error as Partial<HttpExceptionLike>).getResponse === 'function'\n\n/**\n * Converts the client errors (4xx) raised by native middlewares\n * (`HTTPException`) into Dotpress errors, with the same status. Anything\n * else is an unexpected error.\n */\nconst fromNativeError = (error: unknown): unknown => {\n if (!isHttpException(error)) return error\n\n const { status, message } = error\n if (!Number.isInteger(status) || status < 400 || status > 499) return error\n\n const [code, defaultMessage] = CLIENT_ERRORS[status] ?? [\n 'CLIENT_ERROR',\n 'Client Error',\n ]\n return new HttpError(status, code, message || defaultMessage, undefined, {\n cause: error,\n })\n}\n\n/**\n * Adds the headers of the custom response of an `HTTPException`\n * (`WWW-Authenticate`…) to the Dotpress error response.\n */\nconst addNativeHeaders = (\n response: NormalizedResponse,\n error: HttpExceptionLike\n) => {\n if (!error.res) return\n for (const [name, value] of error.res.headers) {\n if (name.startsWith('content-')) continue\n if (name === 'set-cookie') continue\n response.headers[name] = value\n }\n const cookies = error.res.headers.getSetCookie()\n if (cookies.length > 0) {\n const current = response.headers['set-cookie'] ?? []\n response.headers['set-cookie'] = [\n ...(Array.isArray(current) ? current : [current]),\n ...cookies,\n ]\n }\n}\n\nconst encoder = new TextEncoder()\n\n/**\n * Body already read by a native middleware, from the cache of Hono\n * (`c.req.json()`, `c.req.parseBody()`…). Only an `ArrayBuffer` or a\n * `Blob` keeps the exact bytes (`rawBody`).\n */\nconst cachedBody = async (c: Context, limit: number): Promise<RequestBody> => {\n const cache = c.req.bodyCache as Partial<\n Record<'arrayBuffer' | 'blob' | 'text' | 'formData', Promise<unknown>>\n >\n const contentType = c.req.header('content-type')\n\n let bytes: Uint8Array\n let exact = false\n let type = contentType\n\n if (cache.arrayBuffer) {\n bytes = new Uint8Array((await cache.arrayBuffer) as ArrayBuffer)\n exact = true\n } else if (cache.blob) {\n bytes = new Uint8Array(await ((await cache.blob) as Blob).arrayBuffer())\n exact = true\n } else if (cache.text) {\n bytes = encoder.encode((await cache.text) as string)\n } else if (cache.formData) {\n // Serialized again, with a new boundary.\n const response = new Response((await cache.formData) as FormData)\n bytes = new Uint8Array(await response.arrayBuffer())\n type = response.headers.get('content-type') ?? undefined\n } else {\n throw new Error(\n 'The request body was read by a native middleware outside the cache of Hono (c.req.raw): read it with c.req.json(), c.req.text() or c.req.arrayBuffer().'\n )\n }\n\n if (bytes.byteLength > limit) throw payloadTooLargeError()\n const parsed = await parseRequestBody(bytes, type)\n return { ...parsed, rawBody: exact ? bytes : undefined }\n}\n\n// Reading the body through Hono consumes the request.\nconst readBody = (c: Context, limit: number): Promise<RequestBody> =>\n c.req.raw.bodyUsed ? cachedBody(c, limit) : readRequestBody(c.req.raw, limit)\n\n/**\n * Closes the streamed body when the adapter is closed, and aborts the\n * request signal when the client cancels it.\n */\nconst track = (\n body: ReadableStream<Uint8Array>,\n controller: AbortController,\n streams: Set<() => void>\n): ReadableStream<Uint8Array> => {\n const reader = body.getReader()\n let stop: () => void = () => {}\n\n return new ReadableStream<Uint8Array>({\n start() {\n // The cancelled reader ends the stream (see `pull()`).\n stop = () => {\n streams.delete(stop)\n controller.abort()\n reader.cancel().catch(() => {})\n }\n streams.add(stop)\n },\n async pull(stream) {\n try {\n const { done, value } = await reader.read()\n if (done) {\n streams.delete(stop)\n stream.close()\n } else {\n stream.enqueue(value)\n }\n } catch (error) {\n streams.delete(stop)\n stream.error(error)\n }\n },\n cancel(reason) {\n streams.delete(stop)\n controller.abort(reason)\n return reader.cancel(reason)\n },\n })\n}\n\n/**\n * Creates the Hono adapter. The main entry is runtime-agnostic: serve\n * `adapter.fetch` with the runtime, or use `honoAdapter()` of\n * `@dotpress/adapter-hono/node` to listen on Node.js.\n *\n * @example\n * const adapter = honoAdapter()\n * await createApp({ adapter })\n * export default { fetch: adapter.fetch }\n */\nexport function honoAdapter(options?: HonoAdapterOptions<Hono>): HonoAdapter\nexport function honoAdapter<TApp extends HonoApplication>(\n options: HonoAdapterOptions<TApp> & { app: TApp }\n): HonoAdapter<TApp>\nexport function honoAdapter<TApp extends HonoApplication>(\n options: HonoAdapterOptions<TApp> = {}\n): HonoAdapter<TApp> {\n const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT\n\n if (!Number.isInteger(bodyLimit) || bodyLimit < 0) {\n throw new RangeError(\n 'Invalid bodyLimit: expected a non-negative integer (bytes).'\n )\n }\n\n const application = options.app ?? (new Hono() as unknown as TApp)\n const app = application as unknown as MountableApp\n const getIp = options.getIp ?? runtimeIp\n let mounted = false\n /** Closes the responses being streamed, so that `close()` can complete. */\n const streams = new Set<() => void>()\n\n const normalize = (c: Context, signal?: AbortSignal): NormalizedRequest =>\n fromWebRequest(c.req.raw, { ip: getIp(c), signal, raw: c })\n\n const send = (\n c: Context,\n response: NormalizedResponse,\n controller?: AbortController\n ): Promise<Response> => {\n const { body } = response\n if (controller && body instanceof ReadableStream) {\n return toWebResponse(\n { ...response, body: track(body, controller, streams) },\n c.req.method\n )\n }\n return toWebResponse(response, c.req.method)\n }\n\n const handleRoute = async (\n c: Context,\n match: RouteMatch,\n runtime: DotpressRuntime\n ): Promise<Response> => {\n const controller = new AbortController()\n const signal = AbortSignal.any([c.req.raw.signal, controller.signal])\n const request = normalize(c, signal)\n\n try {\n const { body, files, rawBody } = await readBody(\n c,\n match.route.definition.bodyLimit ?? bodyLimit\n )\n request.body = body\n request.files = files\n request.rawBody = rawBody\n } catch (error) {\n return send(c, await runtime.handleError(error, request))\n }\n\n request.params = match.params\n return send(c, await match.route.execute(request), controller)\n }\n\n return {\n name: 'hono',\n\n mount: async (\n routes: readonly CompiledRoute[],\n runtime: DotpressRuntime\n ) => {\n if (mounted) throw new Error('The Hono adapter is already mounted.')\n mounted = true\n\n const matcher = createRouteMatcher(routes)\n\n await options.beforeRoutes?.(application)\n\n app.use('*', async (c, next) => {\n const match = matcher.match(c.req.method, new URL(c.req.url).pathname)\n if (!match) {\n await next()\n return\n }\n return handleRoute(c, match, runtime)\n })\n\n await options.afterRoutes?.(application)\n\n app.notFound(async (c) =>\n send(c, await runtime.handleNotFound(normalize(c)))\n )\n\n app.onError(async (error, c) => {\n const converted = fromNativeError(error)\n const response = await runtime.handleError(converted, normalize(c))\n if (converted !== error) {\n addNativeHeaders(response, error as unknown as HttpExceptionLike)\n }\n return send(c, response)\n })\n },\n\n fetch: async (request, env, executionContext) =>\n app.fetch(request, env, executionContext),\n\n close: async () => {\n for (const stop of [...streams]) stop()\n },\n\n native: () => application,\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,63 @@
1
+ import { HttpAdapter } from '@dotpress/core';
2
+ import { Hono, Context } from 'hono';
3
+
4
+ /**
5
+ * Minimal shape of a Hono application (Hono 4 or 5). The types of Hono 5
6
+ * differ from those of Hono 4, hence the loose signatures.
7
+ */
8
+ interface HonoApplication {
9
+ use: (...args: never[]) => unknown;
10
+ notFound: (...args: never[]) => unknown;
11
+ onError: (...args: never[]) => unknown;
12
+ fetch: (request: Request, env?: never, executionContext?: never) => Response | Promise<Response>;
13
+ }
14
+ interface HonoAdapterOptions<TApp extends HonoApplication> {
15
+ /** Hono application to mount the routes on. Defaults to `new Hono()`. */
16
+ app?: TApp;
17
+ /** Registers native middlewares before the Dotpress routes. */
18
+ beforeRoutes?: (app: TApp) => void | Promise<void>;
19
+ /**
20
+ * Registers native middlewares and routes after the Dotpress routes,
21
+ * before the not-found handler.
22
+ */
23
+ afterRoutes?: (app: TApp) => void | Promise<void>;
24
+ /**
25
+ * Maximum request body size, in bytes. Defaults to 1 MiB. Routes can
26
+ * override it with their own `bodyLimit`.
27
+ */
28
+ bodyLimit?: number;
29
+ /**
30
+ * Client IP address of a request (`ctx.req.ip`). Defaults to the address
31
+ * given by the runtime: the socket on Node.js, the source IP of API
32
+ * Gateway, the last `X-Forwarded-For` entry behind an Application Load
33
+ * Balancer (added by the load balancer). Proxy headers are not read
34
+ * otherwise: behind another proxy (CloudFront…), read them here.
35
+ */
36
+ getIp?: (c: Context) => string | undefined;
37
+ }
38
+ interface HonoAdapter<TApp extends HonoApplication = Hono> extends HttpAdapter<TApp> {
39
+ /**
40
+ * Handles a Web request, like `app.native().fetch()`: for runtimes that
41
+ * take a `fetch` handler, and for tests.
42
+ */
43
+ fetch(request: Request, env?: unknown, executionContext?: unknown): Promise<Response>;
44
+ /** Closes the responses being streamed (their `req.signal` is aborted). */
45
+ close(): Promise<void>;
46
+ }
47
+ /**
48
+ * Creates the Hono adapter. The main entry is runtime-agnostic: serve
49
+ * `adapter.fetch` with the runtime, or use `honoAdapter()` of
50
+ * `@dotpress/adapter-hono/node` to listen on Node.js.
51
+ *
52
+ * @example
53
+ * const adapter = honoAdapter()
54
+ * await createApp({ adapter })
55
+ * export default { fetch: adapter.fetch }
56
+ */
57
+ declare function honoAdapter(options?: HonoAdapterOptions<Hono>): HonoAdapter;
58
+ declare function honoAdapter<TApp extends HonoApplication>(options: HonoAdapterOptions<TApp> & {
59
+ app: TApp;
60
+ }): HonoAdapter<TApp>;
61
+
62
+ export { honoAdapter };
63
+ export type { HonoAdapter, HonoAdapterOptions, HonoApplication };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ honoAdapter
3
+ } from "./chunk-4EA3O2OL.js";
4
+ export {
5
+ honoAdapter
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,43 @@
1
+ import { App, Logger } from '@dotpress/core';
2
+ import { LambdaEvent, LambdaContext, APIGatewayProxyResult } from '@hono/aws-lambda';
3
+ export { APIGatewayProxyResult, LambdaContext, LambdaEvent } from '@hono/aws-lambda';
4
+ import { HonoApplication } from './index.js';
5
+ import 'hono';
6
+
7
+ interface LambdaHandlerOptions {
8
+ /** Logs the failures to create the app. Defaults to a console logger. */
9
+ logger?: Logger;
10
+ /**
11
+ * Whether a response content type is binary (sent in base64). Defaults to
12
+ * the rule of `@hono/aws-lambda`: everything except text, JSON and XML.
13
+ */
14
+ isContentTypeBinary?: (contentType: string) => boolean;
15
+ }
16
+ /**
17
+ * AWS Lambda handler: API Gateway REST (payload v1) and HTTP APIs (payload
18
+ * v2), Function URLs, Application Load Balancer.
19
+ */
20
+ type LambdaHandler = (event: LambdaEvent, context?: LambdaContext) => Promise<APIGatewayProxyResult>;
21
+ /** App of the factory, created with `honoAdapter()`. */
22
+ type LambdaApp = Pick<App<HonoApplication>, 'native'>;
23
+ /**
24
+ * Creates the AWS Lambda handler of an app (`@hono/aws-lambda`).
25
+ *
26
+ * The app is created by the factory at the first invocation (cold start),
27
+ * then reused by the next invocations of the execution environment.
28
+ * Invocations received meanwhile wait for the same app. When the factory
29
+ * fails, the error is logged, the invocation gets a `500` error, and the
30
+ * next invocation calls the factory again.
31
+ *
32
+ * Responses are sent once complete: streamed responses (`stream()`) are
33
+ * buffered, and Server-Sent Events are not supported.
34
+ *
35
+ * @example
36
+ * export const handler = createLambdaHandler(() =>
37
+ * createApp({ adapter: honoAdapter(), plugins: [authPlugin(auth)] })
38
+ * )
39
+ */
40
+ declare const createLambdaHandler: (factory: () => LambdaApp | Promise<LambdaApp>, options?: LambdaHandlerOptions) => LambdaHandler;
41
+
42
+ export { createLambdaHandler };
43
+ export type { LambdaApp, LambdaHandler, LambdaHandlerOptions };
package/dist/lambda.js ADDED
@@ -0,0 +1,55 @@
1
+ // src/lambda.ts
2
+ import {
3
+ createConsoleLogger,
4
+ internalError,
5
+ toErrorBody
6
+ } from "@dotpress/core";
7
+ import {
8
+ handle
9
+ } from "@hono/aws-lambda";
10
+ import { Hono } from "hono";
11
+ var failureApp = () => new Hono().all(
12
+ "*",
13
+ () => new Response(JSON.stringify(toErrorBody(internalError())), {
14
+ status: 500,
15
+ headers: { "content-type": "application/json; charset=utf-8" }
16
+ })
17
+ );
18
+ var createLambdaHandler = (factory, options = {}) => {
19
+ const logger = options.logger ?? createConsoleLogger();
20
+ const toHandler = (app) => handle(app, {
21
+ isContentTypeBinary: options.isContentTypeBinary
22
+ });
23
+ let ready;
24
+ let failure;
25
+ const init = async () => {
26
+ const app = await factory();
27
+ const native = app?.native?.();
28
+ if (typeof native?.fetch !== "function") {
29
+ throw new TypeError(
30
+ "createLambdaHandler() expects an app created with honoAdapter() of @dotpress/adapter-hono."
31
+ );
32
+ }
33
+ return toHandler(native);
34
+ };
35
+ return async (event, context) => {
36
+ const current = ready ??= init();
37
+ let handler;
38
+ try {
39
+ handler = await current;
40
+ } catch (error) {
41
+ if (ready === current) ready = void 0;
42
+ logger.error(
43
+ error instanceof Error ? error : { error },
44
+ "Failed to create the app"
45
+ );
46
+ failure ??= toHandler(failureApp());
47
+ return failure(event, context);
48
+ }
49
+ return handler(event, context);
50
+ };
51
+ };
52
+ export {
53
+ createLambdaHandler
54
+ };
55
+ //# sourceMappingURL=lambda.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lambda.ts"],"sourcesContent":["import {\n createConsoleLogger,\n internalError,\n toErrorBody,\n type App,\n type Logger,\n} from '@dotpress/core'\nimport {\n handle,\n type APIGatewayProxyResult,\n type LambdaContext,\n type LambdaEvent,\n} from '@hono/aws-lambda'\nimport { Hono } from 'hono'\nimport type { HonoApplication } from './index.js'\n\nexport type { APIGatewayProxyResult, LambdaContext, LambdaEvent }\n\nexport interface LambdaHandlerOptions {\n /** Logs the failures to create the app. Defaults to a console logger. */\n logger?: Logger\n /**\n * Whether a response content type is binary (sent in base64). Defaults to\n * the rule of `@hono/aws-lambda`: everything except text, JSON and XML.\n */\n isContentTypeBinary?: (contentType: string) => boolean\n}\n\n/**\n * AWS Lambda handler: API Gateway REST (payload v1) and HTTP APIs (payload\n * v2), Function URLs, Application Load Balancer.\n */\nexport type LambdaHandler = (\n event: LambdaEvent,\n context?: LambdaContext\n) => Promise<APIGatewayProxyResult>\n\n/** App of the factory, created with `honoAdapter()`. */\nexport type LambdaApp = Pick<App<HonoApplication>, 'native'>\n\n/** Answers `500 INTERNAL_ERROR` when the app cannot be created. */\nconst failureApp = () =>\n new Hono().all(\n '*',\n () =>\n new Response(JSON.stringify(toErrorBody(internalError())), {\n status: 500,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n })\n )\n\n/**\n * Creates the AWS Lambda handler of an app (`@hono/aws-lambda`).\n *\n * The app is created by the factory at the first invocation (cold start),\n * then reused by the next invocations of the execution environment.\n * Invocations received meanwhile wait for the same app. When the factory\n * fails, the error is logged, the invocation gets a `500` error, and the\n * next invocation calls the factory again.\n *\n * Responses are sent once complete: streamed responses (`stream()`) are\n * buffered, and Server-Sent Events are not supported.\n *\n * @example\n * export const handler = createLambdaHandler(() =>\n * createApp({ adapter: honoAdapter(), plugins: [authPlugin(auth)] })\n * )\n */\nexport const createLambdaHandler = (\n factory: () => LambdaApp | Promise<LambdaApp>,\n options: LambdaHandlerOptions = {}\n): LambdaHandler => {\n const logger = options.logger ?? createConsoleLogger()\n const toHandler = (app: HonoApplication) =>\n handle(app as unknown as Hono, {\n isContentTypeBinary: options.isContentTypeBinary,\n }) as LambdaHandler\n\n let ready: Promise<LambdaHandler> | undefined\n let failure: LambdaHandler | undefined\n\n const init = async (): Promise<LambdaHandler> => {\n const app = await factory()\n const native: unknown = app?.native?.()\n if (typeof (native as Partial<HonoApplication>)?.fetch !== 'function') {\n throw new TypeError(\n 'createLambdaHandler() expects an app created with honoAdapter() of @dotpress/adapter-hono.'\n )\n }\n return toHandler(native as HonoApplication)\n }\n\n return async (event, context) => {\n const current = (ready ??= init())\n let handler: LambdaHandler\n try {\n handler = await current\n } catch (error) {\n // Retried at the next invocation.\n if (ready === current) ready = undefined\n logger.error(\n error instanceof Error ? error : { error },\n 'Failed to create the app'\n )\n failure ??= toHandler(failureApp() as unknown as HonoApplication)\n return failure(event, context)\n }\n return handler(event, context)\n }\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,OAIK;AACP,SAAS,YAAY;AA4BrB,IAAM,aAAa,MACjB,IAAI,KAAK,EAAE;AAAA,EACT;AAAA,EACA,MACE,IAAI,SAAS,KAAK,UAAU,YAAY,cAAc,CAAC,CAAC,GAAG;AAAA,IACzD,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,kCAAkC;AAAA,EAC/D,CAAC;AACL;AAmBK,IAAM,sBAAsB,CACjC,SACA,UAAgC,CAAC,MACf;AAClB,QAAM,SAAS,QAAQ,UAAU,oBAAoB;AACrD,QAAM,YAAY,CAAC,QACjB,OAAO,KAAwB;AAAA,IAC7B,qBAAqB,QAAQ;AAAA,EAC/B,CAAC;AAEH,MAAI;AACJ,MAAI;AAEJ,QAAM,OAAO,YAAoC;AAC/C,UAAM,MAAM,MAAM,QAAQ;AAC1B,UAAM,SAAkB,KAAK,SAAS;AACtC,QAAI,OAAQ,QAAqC,UAAU,YAAY;AACrE,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,WAAO,UAAU,MAAyB;AAAA,EAC5C;AAEA,SAAO,OAAO,OAAO,YAAY;AAC/B,UAAM,UAAW,UAAU,KAAK;AAChC,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM;AAAA,IAClB,SAAS,OAAO;AAEd,UAAI,UAAU,QAAS,SAAQ;AAC/B,aAAO;AAAA,QACL,iBAAiB,QAAQ,QAAQ,EAAE,MAAM;AAAA,QACzC;AAAA,MACF;AACA,kBAAY,UAAU,WAAW,CAA+B;AAChE,aAAO,QAAQ,OAAO,OAAO;AAAA,IAC/B;AACA,WAAO,QAAQ,OAAO,OAAO;AAAA,EAC/B;AACF;","names":[]}
package/dist/node.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { Server } from 'node:http';
2
+ import { Hono } from 'hono';
3
+ import { HonoApplication, HonoAdapter, HonoAdapterOptions } from './index.js';
4
+ import '@dotpress/core';
5
+
6
+ interface NodeHonoAdapter<TApp extends HonoApplication = Hono> extends HonoAdapter<TApp> {
7
+ /** Starts an HTTP server (`@hono/node-server`). */
8
+ listen(port: number, host?: string): Promise<void>;
9
+ /**
10
+ * Stops the HTTP server: waits for pending requests, and closes the
11
+ * responses being streamed (their `req.signal` is aborted).
12
+ */
13
+ close(): Promise<void>;
14
+ /** HTTP server started by `listen()`, if any. */
15
+ server(): Server | undefined;
16
+ }
17
+ /**
18
+ * Creates the Hono adapter for Node.js: the adapter of
19
+ * `@dotpress/adapter-hono`, with an HTTP server (`@hono/node-server`).
20
+ *
21
+ * @example
22
+ * const app = await createApp({ adapter: honoAdapter() })
23
+ * await app.listen(3000)
24
+ */
25
+ declare function honoAdapter(options?: HonoAdapterOptions<Hono>): NodeHonoAdapter;
26
+ declare function honoAdapter<TApp extends HonoApplication>(options: HonoAdapterOptions<TApp> & {
27
+ app: TApp;
28
+ }): NodeHonoAdapter<TApp>;
29
+
30
+ export { HonoAdapter, HonoAdapterOptions, HonoApplication, honoAdapter };
31
+ export type { NodeHonoAdapter };
package/dist/node.js ADDED
@@ -0,0 +1,70 @@
1
+ import {
2
+ honoAdapter
3
+ } from "./chunk-4EA3O2OL.js";
4
+
5
+ // src/node.ts
6
+ import { createAdaptorServer } from "@hono/node-server";
7
+ var trackConnections = (server) => {
8
+ const sockets = /* @__PURE__ */ new Set();
9
+ let closing = false;
10
+ server.on("connection", (socket) => {
11
+ sockets.add(socket);
12
+ socket.once("close", () => sockets.delete(socket));
13
+ });
14
+ server.on(
15
+ "request",
16
+ (_request, response) => {
17
+ response.once("finish", () => {
18
+ if (closing) server.closeIdleConnections();
19
+ });
20
+ }
21
+ );
22
+ return () => {
23
+ closing = true;
24
+ for (const socket of sockets) {
25
+ if (socket.bytesRead === 0) socket.destroy();
26
+ }
27
+ };
28
+ };
29
+ function honoAdapter2(options = {}) {
30
+ const adapter = honoAdapter(
31
+ options
32
+ );
33
+ let server;
34
+ let closeConnections = () => {
35
+ };
36
+ return {
37
+ ...adapter,
38
+ listen: async (port, host) => {
39
+ if (server) throw new Error("The Hono adapter is already listening.");
40
+ const created = createAdaptorServer({ fetch: adapter.fetch });
41
+ await new Promise((resolve, reject) => {
42
+ created.once("error", reject);
43
+ created.listen(port, host, () => {
44
+ created.off("error", reject);
45
+ resolve();
46
+ });
47
+ });
48
+ server = created;
49
+ closeConnections = trackConnections(created);
50
+ },
51
+ close: async () => {
52
+ const current = server;
53
+ server = void 0;
54
+ if (!current) {
55
+ await adapter.close();
56
+ return;
57
+ }
58
+ await new Promise((resolve, reject) => {
59
+ current.close((error) => error ? reject(error) : resolve());
60
+ closeConnections();
61
+ void adapter.close();
62
+ });
63
+ },
64
+ server: () => server
65
+ };
66
+ }
67
+ export {
68
+ honoAdapter2 as honoAdapter
69
+ };
70
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/node.ts"],"sourcesContent":["import type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { Socket } from 'node:net'\nimport { createAdaptorServer } from '@hono/node-server'\nimport type { Hono } from 'hono'\nimport {\n honoAdapter as createHonoAdapter,\n type HonoAdapter,\n type HonoAdapterOptions,\n type HonoApplication,\n} from './index.js'\n\nexport * from './index.js'\n\nexport interface NodeHonoAdapter<\n TApp extends HonoApplication = Hono,\n> extends HonoAdapter<TApp> {\n /** Starts an HTTP server (`@hono/node-server`). */\n listen(port: number, host?: string): Promise<void>\n /**\n * Stops the HTTP server: waits for pending requests, and closes the\n * responses being streamed (their `req.signal` is aborted).\n */\n close(): Promise<void>\n /** HTTP server started by `listen()`, if any. */\n server(): Server | undefined\n}\n\n/**\n * Lets `close()` complete as soon as the pending requests are served, and\n * returns the function to call on close. Before Node.js 26, `server.close()`\n * waits for the connections that have sent nothing yet (a browser\n * preconnect…) until `headersTimeout`; and, whatever the version, the\n * connections of the pending requests stay open once served, until\n * `keepAliveTimeout`.\n */\nconst trackConnections = (server: Server) => {\n const sockets = new Set<Socket>()\n let closing = false\n server.on('connection', (socket: Socket) => {\n sockets.add(socket)\n socket.once('close', () => sockets.delete(socket))\n })\n server.on(\n 'request',\n (_request: IncomingMessage, response: ServerResponse) => {\n response.once('finish', () => {\n if (closing) server.closeIdleConnections()\n })\n }\n )\n return () => {\n closing = true\n for (const socket of sockets) {\n if (socket.bytesRead === 0) socket.destroy()\n }\n }\n}\n\n/**\n * Creates the Hono adapter for Node.js: the adapter of\n * `@dotpress/adapter-hono`, with an HTTP server (`@hono/node-server`).\n *\n * @example\n * const app = await createApp({ adapter: honoAdapter() })\n * await app.listen(3000)\n */\nexport function honoAdapter(options?: HonoAdapterOptions<Hono>): NodeHonoAdapter\nexport function honoAdapter<TApp extends HonoApplication>(\n options: HonoAdapterOptions<TApp> & { app: TApp }\n): NodeHonoAdapter<TApp>\nexport function honoAdapter<TApp extends HonoApplication>(\n options: HonoAdapterOptions<TApp> = {}\n): NodeHonoAdapter<TApp> {\n const adapter = createHonoAdapter(\n options as HonoAdapterOptions<TApp> & { app: TApp }\n )\n let server: Server | undefined\n let closeConnections = () => {}\n\n return {\n ...adapter,\n\n listen: async (port: number, host?: string) => {\n if (server) throw new Error('The Hono adapter is already listening.')\n\n const created = createAdaptorServer({ fetch: adapter.fetch }) as Server\n await new Promise<void>((resolve, reject) => {\n created.once('error', reject)\n created.listen(port, host, () => {\n created.off('error', reject)\n resolve()\n })\n })\n server = created\n closeConnections = trackConnections(created)\n },\n\n close: async () => {\n const current = server\n server = undefined\n if (!current) {\n await adapter.close()\n return\n }\n\n await new Promise<void>((resolve, reject) => {\n // Also closes the idle connections.\n current.close((error) => (error ? reject(error) : resolve()))\n closeConnections()\n // Streams never end by themselves: close them.\n void adapter.close()\n })\n },\n\n server: () => server,\n }\n}\n"],"mappings":";;;;;AAEA,SAAS,2BAA2B;AAiCpC,IAAM,mBAAmB,CAAC,WAAmB;AAC3C,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,UAAU;AACd,SAAO,GAAG,cAAc,CAAC,WAAmB;AAC1C,YAAQ,IAAI,MAAM;AAClB,WAAO,KAAK,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC;AAAA,EACnD,CAAC;AACD,SAAO;AAAA,IACL;AAAA,IACA,CAAC,UAA2B,aAA6B;AACvD,eAAS,KAAK,UAAU,MAAM;AAC5B,YAAI,QAAS,QAAO,qBAAqB;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO,MAAM;AACX,cAAU;AACV,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,cAAc,EAAG,QAAO,QAAQ;AAAA,IAC7C;AAAA,EACF;AACF;AAcO,SAASA,aACd,UAAoC,CAAC,GACd;AACvB,QAAM,UAAU;AAAA,IACd;AAAA,EACF;AACA,MAAI;AACJ,MAAI,mBAAmB,MAAM;AAAA,EAAC;AAE9B,SAAO;AAAA,IACL,GAAG;AAAA,IAEH,QAAQ,OAAO,MAAc,SAAkB;AAC7C,UAAI,OAAQ,OAAM,IAAI,MAAM,wCAAwC;AAEpE,YAAM,UAAU,oBAAoB,EAAE,OAAO,QAAQ,MAAM,CAAC;AAC5D,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,gBAAQ,KAAK,SAAS,MAAM;AAC5B,gBAAQ,OAAO,MAAM,MAAM,MAAM;AAC/B,kBAAQ,IAAI,SAAS,MAAM;AAC3B,kBAAQ;AAAA,QACV,CAAC;AAAA,MACH,CAAC;AACD,eAAS;AACT,yBAAmB,iBAAiB,OAAO;AAAA,IAC7C;AAAA,IAEA,OAAO,YAAY;AACjB,YAAM,UAAU;AAChB,eAAS;AACT,UAAI,CAAC,SAAS;AACZ,cAAM,QAAQ,MAAM;AACpB;AAAA,MACF;AAEA,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAE3C,gBAAQ,MAAM,CAAC,UAAW,QAAQ,OAAO,KAAK,IAAI,QAAQ,CAAE;AAC5D,yBAAiB;AAEjB,aAAK,QAAQ,MAAM;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,MAAM;AAAA,EAChB;AACF;","names":["honoAdapter"]}
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@dotpress/adapter-hono",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.0.0-alpha.1",
4
+ "description": "Hono adapter for Dotpress, on Node.js and AWS Lambda",
5
+ "author": "Renaud Guyon",
6
+ "license": "MIT",
7
+ "homepage": "https://dotpress.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/dotpressjs/dotpress.git",
11
+ "directory": "packages/adapter-hono"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/dotpressjs/dotpress/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./node": {
29
+ "types": "./dist/node.d.ts",
30
+ "default": "./dist/node.js"
31
+ },
32
+ "./lambda": {
33
+ "types": "./dist/lambda.d.ts",
34
+ "default": "./dist/lambda.js"
35
+ }
36
+ },
37
+ "engines": {
38
+ "node": ">=22.12"
39
+ },
40
+ "peerDependencies": {
41
+ "@dotpress/core": "^2.0.0-alpha.1",
42
+ "@hono/aws-lambda": "^1.0.0",
43
+ "@hono/node-server": "^2.1.0",
44
+ "hono": "^4.13.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@hono/aws-lambda": {
48
+ "optional": true
49
+ },
50
+ "@hono/node-server": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@dotpress/core": "2.0.0-alpha.1",
56
+ "@dotpress/testing": "2.0.0-alpha.1",
57
+ "@hono/aws-lambda": "^1.0.1",
58
+ "@hono/node-server": "^2.1.6",
59
+ "hono": "^4.13.17",
60
+ "hono-5": "npm:hono@5.0.0-rc.1",
61
+ "zod": "^4.6.7"
62
+ },
63
+ "keywords": [
64
+ "dotpress",
65
+ "hono",
66
+ "adapter",
67
+ "aws-lambda",
68
+ "typescript",
69
+ "zod"
70
+ ],
71
+ "scripts": {
72
+ "build": "tsup",
73
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.test.json",
74
+ "lint": "eslint src test",
75
+ "test": "vitest run",
76
+ "test:watch": "vitest watch"
77
+ }
6
78
  }