typespec-hono 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,44 +1,22 @@
1
1
  # typespec-hono
2
2
 
3
- Generate a [Hono](https://hono.dev) server and the Zod validators it enforces — from a TypeSpec HTTP
4
- service, agreeing with the OpenAPI document [`@typespec/openapi3`](https://typespec.io) publishes from
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
- This package runs the whole of
8
- [`typespec-http-zod`](https://github.com/bison-digital/typespec-http-zod) and adds one file. **A
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
- ## Why one emitter and not two
12
-
13
- The server and the validators share a naming contract: `app.gen.ts` imports `readWidgetPath` and
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
- npm install typespec-hono
16
+ pnpm add -D typespec-hono
26
17
  ```
27
18
 
28
- ⚠️ **A dependency, not a devDependency, and this is not a style preference.** `typespec-hono/runtime`
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
- ## What it emits
54
-
55
- `app.gen.ts`, plus everything `typespec-http-zod` emits — see its README for the other four.
31
+ ## Quick start
56
32
 
57
- Routes are grouped into a **sub-app per resource** and mounted with `app.route()`, which is what
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
- ### Hono RPC (`hc`) works, and the return value is why
85
-
86
- `registerRoutes` **chains** its registrations and hands back the result, so Hono's RPC client gets a
87
- fully typed surface derived from the same document:
88
-
89
- ```ts
90
- import { hc } from "hono/client";
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
- ⚠️ **Use the RETURNED value, not the instance you passed in.** `hc` reads the `Schema` type Hono
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
- `@server("/api/v1")` reaches OpenAPI as `servers: [{ url: "/api/v1" }]`, and **an OpenAPI path is
110
- relative to its server** — so the document publishes `/api/v1/accounts`, not `/accounts`. The
111
- generated routes are mounted under that prefix with a nested `app.route()`, so the server and the
112
- document agree and a client generated from the document reaches it.
49
+ ```tsp
50
+ import "@typespec/http";
51
+ using Http;
113
52
 
114
- Where the document is ambiguous — several servers with disagreeing paths — routes are mounted at the
115
- root and `ambiguous-server-path` says so. A templated server such as `@server("{endpoint}")` means the
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
- ### Authentication carries the scheme, not just "someone is here"
56
+ model Widget {
57
+ id: string;
58
+ name: string;
59
+ }
119
60
 
120
- `@useAuth(BearerAuth)` publishes `security: [{ "BearerAuth": [] }]`, and `deps.authorize` receives
121
- exactly that:
122
-
123
- ```ts
124
- deps.authorize([{ BearerAuth: [] }]) // one scheme, no scopes
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
- Satisfying **any one** requirement authorises the caller; every scheme **within** one must be
130
- satisfied together. That is what an array of OpenAPI `security` objects means, and a flat list of
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
- All three scopes are reachable and each is asserted by a real request in
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
- **Recommendation for a generated app: `RegExpRouter`.**
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 { RegExpRouter } from "hono/router/reg-exp-router";
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
- | | raw | gzip | share of the 3 MB free limit |
200
- | -------------- | -------- | ------------- | ---------------------------- |
201
- | 20 operations | 642 KiB | 100.8 KiB | 3.3% |
202
- | 580 operations | 1008 KiB | **112.6 KiB** | **3.7%** |
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
- 560 extra operations cost ~12 KiB gzipped — the baseline is Hono and Zod, not your API. Router choice
205
- moves it by under 1 KiB, so do not choose a router for size. Limits are 3 MB gzipped on the free plan
206
- and 10 MB on paid.
87
+ export default registerRoutes(new Hono(), handlersFor, deps);
88
+ ```
207
89
 
208
- `registerRoutes` runs at module scope in 1.1–6.1 ms, comfortably inside the **1 second** startup
209
- budget, despite the docs' warning that "generating or consuming a large schema at the top level is a
210
- common cause of exceeding this limit".
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
- Nothing here needs `nodejs_compat`: the generated server runs on `workerd` with no Node built-ins.
94
+ ### `src/deps.ts`
213
95
 
214
- ## Streaming
96
+ Six hooks, each answering something the spec does not contain:
215
97
 
216
- A generated operation returns a **value**, not a `Response`, so a handler cannot hand back a
217
- `ReadableStream`. Streaming happens in `deps.respond`, which may return any `Response`:
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
- const deps: RouteDeps = {
221
- // ...
222
- respond: (c, arms, result) =>
223
- streamSSE(c, async (stream) => {
224
- for await (const item of pageThrough(result)) {
225
- await stream.writeSSE({ data: JSON.stringify(item) });
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
- The validators are middleware, so they still run **before** anything is streamed a request the
232
- document forbids is refused with an ordinary response and never opens a stream. Both properties are
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
- Nothing here is Sentry-specific, and this package deliberately ships no instrumentation — but two
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
- Every option `typespec-http-zod` accepts, forwarded — the schema is **derived** from that package's,
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
- `runtime-module` is the one you are most likely to set: point it at a module of your own that
259
- re-declares `Result`, `Ctx`, `AppEnv` and `RouteDeps`, and every generated signature carries your
260
- types instead of the identity defaults.
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
- ## What it refuses, and why
138
+ The last four are `typespec-http-zod`'s; see its README for what they contain.
263
139
 
264
- Both refusals are about the target framework. `typespec-http-zod` emits correct validators for these
265
- operations; only a Hono server cannot serve them.
140
+ Your own code imports from `runtime.gen.ts`:
266
141
 
267
- | code | why |
268
- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
269
- | `unroutable-verb` | ⚠️ **A `@head` operation.** Hono rewrites every HEAD request to GET _before_ matching — `hono-base.js` does it unconditionally at the top of `#dispatch` — so a route registered under HEAD is never reached: 404 where the path has no GET, dead code where it has one. Measured on Hono 4.13.1; `on("PURGE", …)` and `on("OPTIONS", …)` both work, so this is HEAD specifically. **The remedy is in your spec:** declare `@get`, and Hono answers HEAD from it with the body stripped, which is what RFC 9110 requires anyway. Hono's own [best-practices guide](https://hono.dev/docs/guides/best-practices) states the same rule. |
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
- ### Refusals are warnings, and you choose whether they fail the build
146
+ Setting `runtime-module` replaces it with a module of your own, and it is then not written.
274
147
 
275
- A refusal is reported as a **warning**, so a compile that contains one still succeeds and still emits
276
- everything else — including `@typespec/openapi3`'s document, if you run it.
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
- ⚠️ **That last part is why.** A TypeSpec `error` sets `program.hasError()`, and openapi3 declines to
279
- write anything when the program has errors _including errors that are not its own_. With these
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
- It is also what the first-party rule says. openapi3 uses `warning` for exactly this shape —
284
- `streams-not-supported`, `unsupported-auth` meaning _"the spec is valid and this emitter cannot
285
- express it"_, and reserves `error` for a spec that is wrong for any emitter. A `@head` operation is
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
- **Nothing is lost.** The operation is still excluded from the server, still named, still carries its
289
- remedy. If you want a refusal to fail your build, that is the compiler's switch and always was:
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
- ```yaml
292
- # tspconfig.yaml
293
- warn-as-error: true
166
+ export default routes;
294
167
  ```
295
168
 
296
- ```
297
- default -> exit 0, document written, operation excluded
298
- warn-as-error -> exit 1, build fails, refusal reported as an error
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
- ## Known limits
302
-
303
- - **Fifteen of the seventeen `@head` operations in `@typespec/http-specs` are refused**, and that is
304
- the honest count rather than a defect: they have no sibling `GET`, so Hono cannot route them at all.
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
- * ⚠️ **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
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. Hono reads a parameter up to the next
12
- * `/`, so an RFC 6570 modifier would survive into the name and `*` would become Hono's wildcard — a
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
- * ⚠️ **This runs at RENDER time, not during collection.** It used to run inside `collectRoutes`, which
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 validators included over a template no router could mount. What a request body must
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 two things a Hono server cannot express, handed back rather than thrown. */
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 unroutableVerb: (route: EmittedRoute) => void;
30
+ readonly unvalidatableMediaType: (route: EmittedRoute, types: readonly string[]) => void;
25
31
  }
26
32
  /**
27
33
  * The generated Hono server.
28
34
  *
29
- * ⚠️ **This replaces a data table that a hand-written loop interpreted at run time.** The emitter knew,
30
- * per operation, which validators applied and what the call looked like and then flattened all of it
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
- * ⚠️ **Plain `Hono` and `@hono/zod-validator`, deliberately not `@hono/zod-openapi`.** The latter is
35
- * the same validation plus a document generated FROM the code spec-last, and a second source of
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
- * ⚠️ **It declares no schema of its own.** Every validator this file names was declared by
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 and it is why a consumer who
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
- * The path the DOCUMENT says this service is served under, when it says one unambiguously.
52
+ * Every path the DOCUMENT says this service is served under. All of them are mounted.
47
53
  *
48
- * ⚠️ **An OpenAPI path is relative to its server**, so `@server("/api/v1")` plus `/accounts` means
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
- basePath?: string,
58
+ basePaths?: readonly string[],
53
59
  /**
54
60
  * What the DOCUMENT says a caller must satisfy, per operation id.
55
61
  *
56
- * ⚠️ **Resolved by the caller rather than read off `EmittedRoute`**, because which schemes an
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.