Goal
Hono's runtime-agnostic value proposition is that the same (req: Request) => Promise<Response> code runs unchanged on Workers, Bun, Deno, Node, Lambda. We want Perry to be one of those runtimes — same Hono app, swap only the adapter import. Today the contract breaks at multiple layers; this tracker pins down the precise set of issues to close and the acceptance test for "done".
Current state, mapped
Layer
Status
Tracking
Hono compiles via compilePackages: ["hono"]
✅ done
#421 (closed v0.5.613)
new Hono(), app.get/app.post/etc. route registration
✅ done
(covered by #421 )
app.fetch(req) dispatches into route handlers
✅ partial — runs, but throws on first header access
#1649
new Request(url, { headers }) — req.headers is a Headers instance
❌ returns numeric handle
#1649
new Response(body) — res.text() / res.arrayBuffer()
✅ works for string bodies
—
new Response(body) — res.body is ReadableStream | null
❌ returns numeric handle
#1650
hono/jsx — <Component/> evaluates to a JSXNode; c.html(<Comp/>) renders
❌ evaluates to undefined
#1653
hono/jsx/server, hono/jsx/streaming module imports
❌ throw undefined
(covered by #1653 )
HTTP listener path A — @hono/node-server works unchanged
❌ blocked on node:http2 exports + node:http link gap
#1651 , #1652
HTTP listener path B — @hono/perry-server adapter exists
❌ not shipped (deferred from #487 )
#1654
HTTP listener path C — perry-ext-fastify direct
✅ works
(already shipped)
Streaming via hono/streaming
❌ depends on Response.body fix
(covered by #1650 )
WebSocket upgrade via Server.on('upgrade', …)
✅ shipped per #577 Phase 4; not exercised by Hono
—
Decision: which HTTP listener does the architecture commit to?
Two viable end-states; tracker leaves both open and recommends shipping both .
Path A — @hono/node-server on Perry's node:http(s) / http2. Identical user code to Node. Blocked on node:http2 module is missing named exports (Http2ServerRequest, Http2ServerResponse, constants) #1651 + node:http.createServer FFI symbols undefined in default Perry binary (follow-up to #589) #1652 . Once landed, Perry users do exactly what Node users do.
Path B — @hono/perry-server, Perry-org-owned package. Wraps perry-ext-fastify in ~100 LOC. Blocked on Web Fetch: Request.headers is a numeric handle, not a Headers instance — breaks every Hono adapter #1649 (and benefits from Web Fetch: Response.body is a numeric handle, not a ReadableStream | null #1650 ). Once landed, Perry users do import { serve } from '@hono/perry-server' — same shape as @hono/bun, @hono/deno, etc., and a place to absorb perry-ext-fastify quirks centrally.
Path B is the closer-in deliverable (smaller scope, Perry-org owns it end-to-end). Path A is the longer-term "no Perry-specific import" win.
Out of scope (explicitly)
Dynamic imports (await import(...)) — call-site workarounds exist; not on the critical path for the runtime-agnostic Hono contract.
Full Express compatibility shim — explicitly out per Compile hono end-to-end via perry.compilePackages #421 ; Hono + Fastify cover the modern TS backend space, full Express is a tarpit.
Acceptance for "Hono on Perry is done"
Single 30-line program serves real HTTP traffic, identical to Node:
```typescript
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { serve } from '@hono/perry-server' // ← only Perry-specific line
const Layout = (props: { title: string; children: any }) => (
<title>{props.title}</title>{props.children}
)
const app = new Hono()
app.use('*', logger())
app.get('/', (c) => c.html(
hello ))
app.get('/api/echo', (c) => c.json({ q: Object.fromEntries(new URL(c.req.url).searchParams) }))
app.post('/api/echo', async (c) => c.json({ body: await c.req.json() }))
serve({ fetch: app.fetch, port: 3000 }, ({ port }) => {
console.log(`listening :${port}`)
})
```
```bash
perry compile app.tsx -o app
./app &
curl -sS http://127.0.0.1:3000/ # → real HTML
curl -sS http://127.0.0.1:3000/api/echo?k=v # → {"q":{"k":"v"}}
curl -sS -X POST -H 'content-type: application/json' -d '{"x":1}'
http://127.0.0.1:3000/api/echo # → {"body":{"x":1}}
```
The same source file, with import { serve } from '@hono/node-server' instead, must produce byte-for-byte identical responses under node --import tsx app.tsx. That's the acceptance bar — runtime-agnostic in fact, not in marketing.
Sub-issues
Related (already closed)
Goal
Hono's runtime-agnostic value proposition is that the same
(req: Request) => Promise<Response>code runs unchanged on Workers, Bun, Deno, Node, Lambda. We want Perry to be one of those runtimes — same Hono app, swap only the adapter import. Today the contract breaks at multiple layers; this tracker pins down the precise set of issues to close and the acceptance test for "done".Current state, mapped
compilePackages: ["hono"]new Hono(),app.get/app.post/etc. route registrationapp.fetch(req)dispatches into route handlersnew Request(url, { headers })—req.headersis a Headers instancenew Response(body)—res.text() / res.arrayBuffer()new Response(body)—res.bodyisReadableStream | nullhono/jsx—<Component/>evaluates to a JSXNode;c.html(<Comp/>)rendersundefinedhono/jsx/server,hono/jsx/streamingmodule importsundefined@hono/node-serverworks unchangednode:http2exports +node:httplink gap@hono/perry-serveradapter existsperry-ext-fastifydirecthono/streamingServer.on('upgrade', …)Decision: which HTTP listener does the architecture commit to?
Two viable end-states; tracker leaves both open and recommends shipping both.
@hono/node-serveron Perry'snode:http(s) / http2. Identical user code to Node. Blocked on node:http2 module is missing named exports (Http2ServerRequest, Http2ServerResponse, constants) #1651 + node:http.createServer FFI symbols undefined in default Perry binary (follow-up to #589) #1652. Once landed, Perry users do exactly what Node users do.@hono/perry-server, Perry-org-owned package. Wrapsperry-ext-fastifyin ~100 LOC. Blocked on Web Fetch: Request.headers is a numeric handle, not a Headers instance — breaks every Hono adapter #1649 (and benefits from Web Fetch: Response.body is a numeric handle, not a ReadableStream | null #1650). Once landed, Perry users doimport { serve } from '@hono/perry-server'— same shape as@hono/bun,@hono/deno, etc., and a place to absorbperry-ext-fastifyquirks centrally.Path B is the closer-in deliverable (smaller scope, Perry-org owns it end-to-end). Path A is the longer-term "no Perry-specific import" win.
Out of scope (explicitly)
await import(...)) — call-site workarounds exist; not on the critical path for the runtime-agnostic Hono contract.honoend-to-end viaperry.compilePackages#421; Hono + Fastify cover the modern TS backend space, full Express is a tarpit.Acceptance for "Hono on Perry is done"
Single 30-line program serves real HTTP traffic, identical to Node:
```typescript
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { serve } from '@hono/perry-server' // ← only Perry-specific line
const Layout = (props: { title: string; children: any }) => (
<title>{props.title}</title>{props.children} )const app = new Hono()
app.use('*', logger())
app.get('/', (c) => c.html(
hello
))app.get('/api/echo', (c) => c.json({ q: Object.fromEntries(new URL(c.req.url).searchParams) }))
app.post('/api/echo', async (c) => c.json({ body: await c.req.json() }))
serve({ fetch: app.fetch, port: 3000 }, ({ port }) => {
console.log(`listening :${port}`)
})
```
```bash
perry compile app.tsx -o app
./app &
curl -sS http://127.0.0.1:3000/ # → real HTML
curl -sS http://127.0.0.1:3000/api/echo?k=v # → {"q":{"k":"v"}}
curl -sS -X POST -H 'content-type: application/json' -d '{"x":1}'
http://127.0.0.1:3000/api/echo # → {"body":{"x":1}}
```
The same source file, with
import { serve } from '@hono/node-server'instead, must produce byte-for-byte identical responses undernode --import tsx app.tsx. That's the acceptance bar — runtime-agnostic in fact, not in marketing.Sub-issues
Request.headersis a numeric handle, not aHeadersinstance (CRITICAL: blocks every adapter)Response.bodyis a numeric handle, notReadableStream | null(blocks streaming; .text/.arrayBuffer already work)node:http2missing named exports (Http2ServerRequest,Http2ServerResponse,constants) — blocks Path Anode:http.createServerFFI symbols undefined in default build (Link gap: hono + node:http combination drops Web Fetch FFIs (js_headers_new / js_response_new) #589 follow-up) — blocks Path Ahono/jsx<Component/>evaluates toundefined;hono/jsx/server/hono/jsx/streamingimports throw — blocks JSX templating@hono/perry-serveradapter package — Path B deliverableRelated (already closed)
honoend-to-end viaperry.compilePackages#421 — Compile hono end-to-end viaperry.compilePackages(closed v0.5.613)@hono/perry-serveradapter (closed "as a side effect" of Implement node:http / node:https / node:http2 + WebSocket upgrade for npm-ecosystem HTTP server compat #577; in practice still needed → Ship @hono/perry-server adapter package (#487 follow-through) #1654)node:http/node:https/node:http2+ WS upgrade (closed v0.5.691; named-export and link surfaces still gappy → node:http2 module is missing named exports (Http2ServerRequest, Http2ServerResponse, constants) #1651, node:http.createServer FFI symbols undefined in default Perry binary (follow-up to #589) #1652)