typespec-hono 0.1.0 → 0.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 +113 -265
- package/dist/src/app.d.ts +25 -19
- package/dist/src/app.js +238 -113
- package/dist/src/base-path.d.ts +18 -9
- package/dist/src/base-path.js +20 -18
- package/dist/src/emitter.d.ts +9 -18
- package/dist/src/emitter.js +51 -26
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/lib.d.ts +13 -16
- package/dist/src/lib.js +28 -68
- package/dist/src/runtime.d.ts +102 -46
- package/dist/src/runtime.js +63 -17
- package/dist/src/security.d.ts +4 -4
- package/dist/src/security.js +2 -2
- package/dist/src/tsp-index.d.ts +1 -1
- package/dist/src/tsp-index.js +1 -1
- package/lib/main.tsp +1 -1
- package/package.json +6 -5
- package/src/runtime.ts +299 -0
package/README.md
CHANGED
|
@@ -1,44 +1,22 @@
|
|
|
1
1
|
# typespec-hono
|
|
2
2
|
|
|
3
|
-
Generate a [Hono](https://hono.dev) server
|
|
4
|
-
|
|
5
|
-
the same source.
|
|
3
|
+
Generate a [Hono](https://hono.dev) server from a TypeSpec API definition. Routing, request
|
|
4
|
+
validation and handler types all come from the spec.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
consumer lists one emitter and gets five artefacts.**
|
|
6
|
+
Add [`@typespec/openapi3`](https://typespec.io) to the same config and it writes the OpenAPI document
|
|
7
|
+
from that same definition, so your server and your docs cannot drift apart.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`readWidgetResponses` from `schemas.gen.js` **by name**. Two separate TypeSpec emitters would each get
|
|
15
|
-
their own `$onEmit` and their own registry, and would have to arrive at identical identifiers by
|
|
16
|
-
coincidence. Running the library from inside this one means it mints the names, writes them, and hands
|
|
17
|
-
them back — so the agreement is structural.
|
|
18
|
-
|
|
19
|
-
If you want the validators without a server, install `typespec-http-zod` alone. Nothing here is
|
|
20
|
-
required for that.
|
|
9
|
+
Validation and types come from
|
|
10
|
+
[`typespec-http-zod`](https://github.com/bison-digital/typespec-http-zod), which this runs for you, so
|
|
11
|
+
your config lists one emitter.
|
|
21
12
|
|
|
22
13
|
## Install
|
|
23
14
|
|
|
24
15
|
```bash
|
|
25
|
-
|
|
16
|
+
pnpm add -D typespec-hono
|
|
26
17
|
```
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
exports **runtime values** — `armFor`, and `selectContentType` where a route negotiates — and both the
|
|
30
|
-
generated server and your own `deps` import them. Under `--save-dev` everything looks fine: install,
|
|
31
|
-
typecheck and `wrangler dev` all pass. It fails at **deploy**, when the production install drops it:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
✘ [ERROR] Could not resolve "typespec-hono/runtime"
|
|
35
|
-
import { armFor, type RouteDeps } from "typespec-hono/runtime";
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Measured: `pnpm install --prod` then `wrangler deploy --dry-run` → exit 1. The emitter half is
|
|
39
|
-
build-time, but the runtime half ships.
|
|
40
|
-
|
|
41
|
-
Peers: `hono`, `@hono/zod-validator`, `zod`, `@typespec/compiler`.
|
|
19
|
+
Peer dependencies: `hono`, `@hono/zod-validator`, `zod`, `@typespec/compiler`.
|
|
42
20
|
|
|
43
21
|
```yaml
|
|
44
22
|
# tspconfig.yaml
|
|
@@ -50,283 +28,153 @@ options:
|
|
|
50
28
|
seal-object-schemas: true
|
|
51
29
|
```
|
|
52
30
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
`app.gen.ts`, plus everything `typespec-http-zod` emits — see its README for the other four.
|
|
31
|
+
## Quick start
|
|
56
32
|
|
|
57
|
-
|
|
58
|
-
[Hono's best-practices guide](https://hono.dev/docs/guides/best-practices) recommends for building a
|
|
59
|
-
larger application. Its other recommendation is honoured at the same time and matters more: handlers
|
|
60
|
-
are written **directly after the path definitions**, never lifted into Rails-style controllers, because
|
|
61
|
-
a handler in a separate file cannot infer its path parameters. A resource with one route gets no
|
|
62
|
-
sub-app — that is ceremony around a single line, and not what an author writes.
|
|
63
|
-
|
|
64
|
-
Measured before relying on it: `app.route(prefix, sub)` composes paths exactly, including a parameter
|
|
65
|
-
in the prefix, and the parent's `app.routes` reports the fully composed path — so every arm that
|
|
66
|
-
counts routes still counts them.
|
|
67
|
-
|
|
68
|
-
The server is **plain `Hono` and `@hono/zod-validator`**, deliberately not `@hono/zod-openapi`. That
|
|
69
|
-
package is the same validation plus a document generated FROM the code — spec-last, and a second
|
|
70
|
-
source of truth competing with the one openapi3 publishes from the spec. We want its validation, not
|
|
71
|
-
its documentation.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import { Hono } from "hono";
|
|
75
|
-
import { registerRoutes } from "./generated/app.gen.js";
|
|
76
|
-
|
|
77
|
-
// ⚠️ Unannotated on purpose — annotating widens the value and disables the exhaustiveness check.
|
|
78
|
-
const handlersFor = (c) => backendFor(c.env);
|
|
79
|
-
const routes = registerRoutes(new Hono<AppEnv>(), handlersFor, deps);
|
|
33
|
+
You write four files. Everything under `src/generated/` is produced by the compiler and never edited:
|
|
80
34
|
|
|
81
|
-
export default routes;
|
|
82
35
|
```
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const client = hc<typeof routes>("https://api.example.com");
|
|
93
|
-
const response = await client.widgets[":widget-id"].$get({
|
|
94
|
-
param: { "widget-id": "w-1" },
|
|
95
|
-
header: { "x-request-id": "r-1" },
|
|
96
|
-
});
|
|
36
|
+
main.tsp your API definition
|
|
37
|
+
tspconfig.yaml which emitters to run
|
|
38
|
+
src/
|
|
39
|
+
generated/ written by `tsp compile`, never edited by hand
|
|
40
|
+
app.gen.ts
|
|
41
|
+
runtime.gen.ts
|
|
42
|
+
schemas.gen.ts
|
|
43
|
+
deps.ts your application's answers
|
|
44
|
+
index.ts your handlers, and the app
|
|
97
45
|
```
|
|
98
46
|
|
|
99
|
-
|
|
100
|
-
accumulates through the chain; the bare `new Hono()` still carries nothing. Measured before this
|
|
101
|
-
worked: `hc<typeof app>` resolved to `unknown` — not an empty client, an unusable one.
|
|
102
|
-
|
|
103
|
-
`handlersFor` is a **factory** rather than an object because in Workers a service binding lives on
|
|
104
|
-
`c.env` and exists only for the duration of a request — there is no module scope in which
|
|
105
|
-
`backendFor(env)` can be resolved.
|
|
106
|
-
|
|
107
|
-
### The document's base path is honoured
|
|
47
|
+
### `main.tsp`
|
|
108
48
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
document agree and a client generated from the document reaches it.
|
|
49
|
+
```tsp
|
|
50
|
+
import "@typespec/http";
|
|
51
|
+
using Http;
|
|
113
52
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
caller supplies the whole origin, so the root is already correct and nothing is reported.
|
|
53
|
+
@service(#{ title: "Widgets" })
|
|
54
|
+
namespace Widgets;
|
|
117
55
|
|
|
118
|
-
|
|
56
|
+
model Widget {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
}
|
|
119
60
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
deps.authorize([{ OAuth2Auth: ["widgets:read"] }]) // scopes, from the declared flows
|
|
126
|
-
deps.authorize([{ OAuth2Auth: [...] }, { BearerAuth: [] }]) // EITHER authorises
|
|
61
|
+
@route("/widgets")
|
|
62
|
+
interface WidgetRoutes {
|
|
63
|
+
@get list(@query limit?: int32): Widget[];
|
|
64
|
+
@get read(@path id: string): Widget;
|
|
65
|
+
}
|
|
127
66
|
```
|
|
128
67
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
scopes cannot express the difference.
|
|
132
|
-
|
|
133
|
-
⚠️ **This used to carry scopes only**, so a gate was emitted for OAuth2 and for nothing else — bearer,
|
|
134
|
-
api-key and basic carried none at all and rested entirely on `deps.context` returning null. That
|
|
135
|
-
answers "is somebody here", not "did they satisfy the scheme the contract names": an application
|
|
136
|
-
reading a cookie would have served a route the document says needs a bearer token.
|
|
137
|
-
|
|
138
|
-
Which credentials satisfy a scheme is still yours — it could not be anything else. Which schemes an
|
|
139
|
-
operation accepts is a contract fact, and is now generated.
|
|
140
|
-
|
|
141
|
-
## Middleware — register it BEFORE `registerRoutes`
|
|
142
|
-
|
|
143
|
-
⚠️ **This is the one ordering rule, and getting it wrong fails silently.** Hono middleware applies
|
|
144
|
-
only to routes registered _after_ it, and `registerRoutes` registers everything at once. Middleware
|
|
145
|
-
added afterwards does not error — it simply never runs.
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
const app = new Hono<AppEnv>();
|
|
149
|
-
|
|
150
|
-
app.use(cors()); // ✅ global
|
|
151
|
-
app.use("/widgets/*", rateLimit()); // ✅ per resource
|
|
152
|
-
app.use("/widgets/:widget-id", cache()); // ✅ per route
|
|
153
|
-
|
|
154
|
-
const routes = registerRoutes(app, handlersFor, deps); // ← everything above applies
|
|
155
|
-
|
|
156
|
-
app.use(cors()); // ❌ silently applies to nothing
|
|
68
|
+
```bash
|
|
69
|
+
pnpm exec tsp compile .
|
|
157
70
|
```
|
|
158
71
|
|
|
159
|
-
|
|
160
|
-
`test/wire/middleware.test.ts`. Per-resource works through a prefix wildcard rather than a handle on
|
|
161
|
-
the sub-app: the sub-apps are `const`s inside `registerRoutes`, and `/widgets/*` is the equivalent —
|
|
162
|
-
it works because every route of a resource is mounted under that resource's prefix.
|
|
163
|
-
|
|
164
|
-
`app.onError` and `app.notFound` are **not** subject to this: they are app-level handlers rather than
|
|
165
|
-
route middleware, and may be registered in any order.
|
|
166
|
-
|
|
167
|
-
## Cloudflare Workers — pick a router
|
|
168
|
-
|
|
169
|
-
You construct the `Hono` instance, so the router is your choice. **Make it deliberately.** Measured on
|
|
170
|
-
a generated **580-operation** service (58 resources × 10), three runs, on Hono 4.13.1:
|
|
171
|
-
|
|
172
|
-
| router | register | first request | 1000 requests |
|
|
173
|
-
| ------------------------------ | -------------- | ---------------- | ------------- |
|
|
174
|
-
| SmartRouter _(Hono's default)_ | 5.4–6.1 ms | **18.5–18.9 ms** | 24–26 ms |
|
|
175
|
-
| **RegExpRouter** | 2.6–4.4 ms | 2.2–2.6 ms | **16–18 ms** |
|
|
176
|
-
| LinearRouter | **1.1–1.2 ms** | **0.6 ms** | 66–70 ms |
|
|
177
|
-
| PatternRouter | 2.7–2.9 ms | 2.5 ms | 36–39 ms |
|
|
178
|
-
|
|
179
|
-
⚠️ **The default is the worst choice here, and the reason is a cold-start cost you pay per isolate.**
|
|
180
|
-
SmartRouter picks a router by trying one, and that build happens on the **first request** rather than
|
|
181
|
-
at registration. At this scale that is ~18.8 ms of CPU — and the Workers **free plan allows 10 ms of
|
|
182
|
-
CPU per request**, so the first request into every new isolate can be killed with `exceededCpu`.
|
|
72
|
+
### `src/index.ts`
|
|
183
73
|
|
|
184
|
-
|
|
74
|
+
`input` and the return type are both known from the spec, so a handler that does not match the
|
|
75
|
+
contract does not compile:
|
|
185
76
|
|
|
186
77
|
```ts
|
|
187
78
|
import { Hono } from "hono";
|
|
188
|
-
import {
|
|
189
|
-
|
|
190
|
-
const routes = registerRoutes(new Hono<AppEnv>({ router: new RegExpRouter() }), handlersFor, deps);
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Its reputation for slow registration does not bite here — it registered in under 5 ms and is fastest
|
|
194
|
-
in steady state. Choose `LinearRouter` instead only for a very low-traffic Worker where cold start
|
|
195
|
-
dominates: it is the cheapest to start and ~4× slower per request thereafter.
|
|
196
|
-
|
|
197
|
-
### Bundle size is not the constraint
|
|
79
|
+
import { registerRoutes } from "./generated/app.gen.js";
|
|
80
|
+
import { deps } from "./deps.js";
|
|
198
81
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
82
|
+
const handlersFor = () => ({
|
|
83
|
+
WidgetRoutes_list: (ctx, input) => widgets.slice(0, input.limit ?? 20),
|
|
84
|
+
WidgetRoutes_read: (ctx, input) => widgets.find((w) => w.id === input.id),
|
|
85
|
+
});
|
|
203
86
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
and 10 MB on paid.
|
|
87
|
+
export default registerRoutes(new Hono(), handlersFor, deps);
|
|
88
|
+
```
|
|
207
89
|
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
90
|
+
Leave `handlersFor` unannotated: annotating it widens the value and disables the check that catches a
|
|
91
|
+
handler for an operation the spec no longer declares. It is a factory rather than an object because a
|
|
92
|
+
Workers service binding lives on `c.env` and exists only for the duration of a request.
|
|
211
93
|
|
|
212
|
-
|
|
94
|
+
### `src/deps.ts`
|
|
213
95
|
|
|
214
|
-
|
|
96
|
+
Six hooks, each answering something the spec does not contain:
|
|
215
97
|
|
|
216
|
-
|
|
217
|
-
|
|
98
|
+
| hook | the spec says | you say |
|
|
99
|
+
| --------------- | ------------------------------------------- | ------------------------------------ |
|
|
100
|
+
| `authorize` | which schemes and scopes an operation needs | whether this caller satisfies them |
|
|
101
|
+
| `context` | whether a caller is required | who the caller is |
|
|
102
|
+
| `noContext` | | what to answer when there is not one |
|
|
103
|
+
| `notAcceptable` | which media types are offered | what to answer when none match |
|
|
104
|
+
| `invalid` | the schema | what a validation failure looks like |
|
|
105
|
+
| `respond` | every status arm and its schema | which arm this result is |
|
|
218
106
|
|
|
219
107
|
```ts
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
108
|
+
import type { RouteDeps } from "./generated/runtime.gen.js";
|
|
109
|
+
|
|
110
|
+
export const deps: RouteDeps = {
|
|
111
|
+
authorize: (requirements) => async (c, next) => {
|
|
112
|
+
await next();
|
|
113
|
+
},
|
|
114
|
+
context: (c) => ({ userId: c.req.header("x-user") }),
|
|
115
|
+
noContext: (c) => c.json({ error: "unauthorized" }, 401),
|
|
116
|
+
notAcceptable: (c, offered) => c.json({ error: "not_acceptable", offered }, 406),
|
|
117
|
+
invalid: (result, c) => (result.success ? undefined : c.json({ error: "invalid" }, 400)),
|
|
118
|
+
respond: (c, arms, result) => c.json(result as never, 200),
|
|
228
119
|
};
|
|
229
120
|
```
|
|
230
121
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
asserted by real requests in `test/wire/streaming.test.ts`.
|
|
234
|
-
|
|
235
|
-
Point `runtime-module` at your own module and re-declare `Result<T>` if you want the handler's return
|
|
236
|
-
type to carry the stream shape; that is the same seam the README describes for result envelopes.
|
|
237
|
-
|
|
238
|
-
## Observability
|
|
122
|
+
Routing, request validation and the handler types come from the spec. What is left is the four files
|
|
123
|
+
above.
|
|
239
124
|
|
|
240
|
-
|
|
241
|
-
properties an APM needs are asserted rather than hoped for:
|
|
242
|
-
|
|
243
|
-
- **`c.req.routePath` yields the route pattern**, `/widgets/:widget-id`, not the concrete URL. That is
|
|
244
|
-
the span name you want; the URL would be a cardinality bomb. It survives being mounted through a
|
|
245
|
-
sub-app, which is not obvious.
|
|
246
|
-
- **A handler's `throw` reaches an app-level `onError`.** Nothing in the generated file swallows it —
|
|
247
|
-
`deps.respond` is only reached on success.
|
|
248
|
-
|
|
249
|
-
Both are pinned by real requests, so a change to how routes are grouped cannot quietly remove them.
|
|
250
|
-
|
|
251
|
-
## Options
|
|
125
|
+
## What it emits
|
|
252
126
|
|
|
253
|
-
|
|
254
|
-
not restated, and a test asserts the forwarding as a class at both the top level and inside the
|
|
255
|
-
per-service overrides. See its README for `seal-object-schemas`, `contracts-output-dir`,
|
|
256
|
-
`contracts-package`, `key-vocabularies`, `runtime-module` and `services`.
|
|
127
|
+
Into your output directory:
|
|
257
128
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
129
|
+
| file | what it is |
|
|
130
|
+
| ---------------------- | ------------------------------------------------------------------------------ |
|
|
131
|
+
| `app.gen.ts` | the server: routes, validators, and the handler interface you implement |
|
|
132
|
+
| `runtime.gen.ts` | the types your `deps` implements against, and the helpers the server calls |
|
|
133
|
+
| `schemas.gen.ts` | a Zod schema for every request and response, and the status arms each declares |
|
|
134
|
+
| `vocabularies.gen.ts` | shared enums, where the spec declares them |
|
|
135
|
+
| `requests.gen.ts` | request types, when `contracts-output-dir` is set |
|
|
136
|
+
| `wire-contract.gen.ts` | assertions that the schemas and the types agree, with the same option |
|
|
261
137
|
|
|
262
|
-
|
|
138
|
+
The last four are `typespec-http-zod`'s; see its README for what they contain.
|
|
263
139
|
|
|
264
|
-
|
|
265
|
-
operations; only a Hono server cannot serve them.
|
|
140
|
+
Your own code imports from `runtime.gen.ts`:
|
|
266
141
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
| `ambiguous-server-path` | The service declares several `@server` entries whose base paths disagree, so there is no single prefix to mount under. Routes are mounted at the **root** and this says so. An OpenAPI path is relative to its server, so callers following the document will prefix one of them — mount the returned app under the prefix you actually serve, or declare a single base path. |
|
|
271
|
-
| `unsupported-path-template` | a path parameter that is not a plain name. Hono reads a parameter up to the next `/`, so an RFC 6570 operator or modifier would become part of the name — and `*` would become Hono's wildcard, mounting a route that matches the wrong requests and answers them. Refused rather than approximated, because a route that works and is wrong is worse than one that fails. |
|
|
142
|
+
```ts
|
|
143
|
+
import type { Ctx, RouteDeps } from "./generated/runtime.gen.js";
|
|
144
|
+
```
|
|
272
145
|
|
|
273
|
-
|
|
146
|
+
Setting `runtime-module` replaces it with a module of your own, and it is then not written.
|
|
274
147
|
|
|
275
|
-
|
|
276
|
-
|
|
148
|
+
Routes are grouped into a sub-app per resource and mounted with `app.route()`, following
|
|
149
|
+
[Hono's best-practices guide](https://hono.dev/docs/guides/best-practices). Handlers are written
|
|
150
|
+
directly after the path definitions rather than lifted into separate controller files, because a
|
|
151
|
+
handler in another file cannot infer its path parameters. A resource with a single route gets no
|
|
152
|
+
sub-app.
|
|
277
153
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
refusals as errors, one `@head` operation cost you your entire OpenAPI document, and whether it did
|
|
281
|
-
depended on the order emitters were listed in. Measured: `openapi/` went from one file to none.
|
|
154
|
+
The output is plain `Hono` and `@hono/zod-validator`, not `@hono/zod-openapi`. That package generates
|
|
155
|
+
a document from the code, which would compete with the one openapi3 publishes from the spec.
|
|
282
156
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
valid TypeSpec that openapi3 renders perfectly; only Hono cannot route it.
|
|
157
|
+
```ts
|
|
158
|
+
import { Hono } from "hono";
|
|
159
|
+
import { registerRoutes } from "./generated/app.gen.js";
|
|
287
160
|
|
|
288
|
-
|
|
289
|
-
|
|
161
|
+
// Leave handlersFor unannotated. Annotating it widens the value and disables the
|
|
162
|
+
// exhaustiveness check that catches a handler for an operation the spec no longer declares.
|
|
163
|
+
const handlersFor = (c) => backendFor(c.env);
|
|
164
|
+
const routes = registerRoutes(new Hono<AppEnv>(), handlersFor, deps);
|
|
290
165
|
|
|
291
|
-
|
|
292
|
-
# tspconfig.yaml
|
|
293
|
-
warn-as-error: true
|
|
166
|
+
export default routes;
|
|
294
167
|
```
|
|
295
168
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
169
|
+
`handlersFor` is a factory rather than an object because a Workers service binding lives on `c.env`
|
|
170
|
+
and exists only for the duration of a request.
|
|
171
|
+
|
|
172
|
+
## Docs
|
|
300
173
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
-
|
|
304
|
-
|
|
305
|
-
This emitter previously emitted them, and every route-counting arm called them mounted.
|
|
306
|
-
- **`app.on(method, …)` is reachable by no TypeSpec spec**, because `@typespec/http` declares six
|
|
307
|
-
verbs, five have Hono helpers, and the sixth is `@head`, which is refused. The branch is exercised
|
|
308
|
-
directly by `test/render.test.ts` rather than left untested or deleted — deleting it would make
|
|
309
|
-
`HONO_METHOD[verb]` `undefined` for any verb TypeSpec adds later, emitting `app.undefined(...)`
|
|
310
|
-
from a spec that compiles.
|
|
311
|
-
|
|
312
|
-
## How it is graded
|
|
313
|
-
|
|
314
|
-
- **Route surface over the corpus** — 61 scenarios of `@typespec/http-specs`, with counts read from
|
|
315
|
-
`app.routes` after mounting the real `registerRoutes`, never from the emitted text. **577 declared,
|
|
316
|
-
564 mounted, 13 refused**, and `mounted + refused === declared` is asserted, because
|
|
317
|
-
`mounted === declared` can be satisfied by a route nobody can reach — and was.
|
|
318
|
-
- **An application is compiled against it**, with `runtime-module` pointed at a module that
|
|
319
|
-
substitutes real types, and with no cast anywhere. A signature no application could satisfy passed
|
|
320
|
-
every other test for a long time because the suite that mounted it cast to `unknown`.
|
|
321
|
-
- **Equivalence against a hand-written Hono app** — `test/equivalence/reference-app.ts` follows the
|
|
322
|
-
pattern in Hono's own validation guide, written without reference to what this emitter produces.
|
|
323
|
-
Both apps serve the same API and answer thirteen identical exchanges identically, and both routing
|
|
324
|
-
tables are compared, because behaviour alone would miss two apps that happen to 404 together. ⚠️
|
|
325
|
-
**Its value is entirely in its independence** — adjusted to match our output, it would prove only
|
|
326
|
-
that we agree with ourselves, so when the two disagree the emitter is what changes.
|
|
327
|
-
- **Real requests through the real router** — the only thing that can see a validator-to-wire defect.
|
|
328
|
-
A flattened collection parameter once had the document saying `array`, the validator saying `array`,
|
|
329
|
-
and the server rejecting every conformant caller.
|
|
174
|
+
- [Guides](docs/guides.md): middleware, the RPC client, authentication, base paths, HEAD operations,
|
|
175
|
+
request bodies, streaming, observability
|
|
176
|
+
- [Cloudflare Workers](docs/cloudflare-workers.md): which router to pick, and what the bundle costs
|
|
177
|
+
- [Reference](docs/reference.md): every option, every diagnostic, and the known limits
|
|
330
178
|
|
|
331
179
|
## Licence
|
|
332
180
|
|
package/dist/src/app.d.ts
CHANGED
|
@@ -3,57 +3,63 @@ import { type EmittedRoute, type EmittedService } from "typespec-http-zod";
|
|
|
3
3
|
/**
|
|
4
4
|
* TypeSpec publishes `/widgets/{widget-id}`; Hono routes on `/widgets/:widget-id`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* `@path("thing-id")` produced the literal route `/things/{thing-id}
|
|
6
|
+
* **This used to match `\w+`, so any parameter carrying a hyphen was left ALONE.**
|
|
7
|
+
* `@path("thing-id")` produced the literal route `/things/{thing-id}`, mounted, counted by every arm
|
|
8
8
|
* that counts routes, and reachable by nobody. It answered 404 to the only requests it was for. Hono
|
|
9
9
|
* handles `:thing-id` and `:x.y` perfectly well; the narrow character class was ours.
|
|
10
10
|
*
|
|
11
|
-
* A name that is not plain is REFUSED rather than approximated
|
|
12
|
-
*
|
|
13
|
-
* route that matches the wrong requests and answers them, which is worse than one that fails.
|
|
11
|
+
* A name that is not plain is REFUSED rather than approximated, and the route stays at the literal
|
|
12
|
+
* template so it matches nothing rather than matching the wrong thing.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
14
|
+
* **This is about the NAME, not about RFC 6570 operators.** An earlier version of this comment
|
|
15
|
+
* claimed `{+path}` or `{tag*}` would survive into the name and that `*` would become Hono's
|
|
16
|
+
* wildcard. Measured, that is false: `@typespec/http` resolves the operator before this emitter sees
|
|
17
|
+
* the path, and `@typespec/openapi3` strips it from the published document too, so both artefacts say
|
|
18
|
+
* `/files{path}` and agree. What actually reaches here is a wire name from `@path("...")`, and the
|
|
19
|
+
* forms that fail are a space, `+` and `!`. `*` is rejected by `@typespec/http` before it arrives.
|
|
20
|
+
*
|
|
21
|
+
* **This runs at RENDER time, not during collection.** It used to run inside `collectRoutes`, which
|
|
16
22
|
* put one framework's spelling into the shared intermediate representation and refused the whole
|
|
17
|
-
* operation
|
|
23
|
+
* operation (validators included) over a template no router could mount. What a request body must
|
|
18
24
|
* look like does not depend on that.
|
|
19
25
|
*/
|
|
20
26
|
export declare function toHonoPath(template: string, refuse: (template: string, name: string) => void): string;
|
|
21
|
-
/** The
|
|
27
|
+
/** The one thing a Hono server cannot express, handed back rather than thrown. */
|
|
22
28
|
export interface RenderRefusals {
|
|
23
29
|
readonly unsupportedPathTemplate: (route: EmittedRoute, template: string, name: string) => void;
|
|
24
|
-
readonly
|
|
30
|
+
readonly unvalidatableMediaType: (route: EmittedRoute, types: readonly string[]) => void;
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
33
|
* The generated Hono server.
|
|
28
34
|
*
|
|
29
|
-
*
|
|
30
|
-
* per operation, which validators applied and what the call looked like
|
|
35
|
+
* **This replaces a data table that a hand-written loop interpreted at run time.** The emitter knew,
|
|
36
|
+
* per operation, which validators applied and what the call looked like, and then flattened all of it
|
|
31
37
|
* into one homogeneous array, so the consumer had to recover it dynamically and could not. Emitting
|
|
32
38
|
* the call sites keeps that knowledge, and every one of them is monomorphic and checked.
|
|
33
39
|
*
|
|
34
|
-
*
|
|
35
|
-
* the same validation plus a document generated FROM the code
|
|
40
|
+
* **Plain `Hono` and `@hono/zod-validator`, deliberately not `@hono/zod-openapi`.** The latter is
|
|
41
|
+
* the same validation plus a document generated FROM the code, spec-last, and a second source of
|
|
36
42
|
* truth competing with the one `@typespec/openapi3` publishes from the spec. We want its validation,
|
|
37
43
|
* not its documentation.
|
|
38
44
|
*
|
|
39
|
-
*
|
|
45
|
+
* **It declares no schema of its own.** Every validator this file names was declared by
|
|
40
46
|
* `typespec-http-zod` into `schemas.gen.ts` and is imported by name. That is what makes the two
|
|
41
|
-
* packages one emitter rather than two that must agree by coincidence
|
|
47
|
+
* packages one emitter rather than two that must agree by coincidence, and it is why a consumer who
|
|
42
48
|
* wants the validators without a server can simply not install this one.
|
|
43
49
|
*/
|
|
44
50
|
export declare function renderApp(emitted: EmittedService, refuse: RenderRefusals,
|
|
45
51
|
/**
|
|
46
|
-
*
|
|
52
|
+
* Every path the DOCUMENT says this service is served under. All of them are mounted.
|
|
47
53
|
*
|
|
48
|
-
*
|
|
54
|
+
* **An OpenAPI path is relative to its server**, so `@server("/api/v1")` plus `/accounts` means
|
|
49
55
|
* the document publishes `/api/v1/accounts`. Mounting at the root made every client generated from
|
|
50
56
|
* the document, and every "try it" in a rendered document, 404.
|
|
51
57
|
*/
|
|
52
|
-
|
|
58
|
+
basePaths?: readonly string[],
|
|
53
59
|
/**
|
|
54
60
|
* What the DOCUMENT says a caller must satisfy, per operation id.
|
|
55
61
|
*
|
|
56
|
-
*
|
|
62
|
+
* **Resolved by the caller rather than read off `EmittedRoute`**, because which schemes an
|
|
57
63
|
* operation accepts is a fact about the HTTP program and not part of the validator IR the library
|
|
58
64
|
* publishes. Keeping it out of that IR is what stops a Hono concern leaking into a package whose
|
|
59
65
|
* audience is wider.
|