typespec-hono 0.6.0 → 0.8.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/dist/src/app.d.ts CHANGED
@@ -23,7 +23,20 @@ import { type EmittedRoute, type EmittedService } from "typespec-http-zod";
23
23
  * operation (validators included) over a template no router could mount. What a request body must
24
24
  * look like does not depend on that.
25
25
  */
26
- export declare function toHonoPath(template: string, refuse: (template: string, name: string) => void): string;
26
+ export declare function toHonoPath(template: string, refuse: (template: string, name: string) => void,
27
+ /**
28
+ * Wire names the document says carry RFC 6570 reserved expansion, so their value may contain `/`.
29
+ *
30
+ * **A hierarchical identifier is ONE value, not several segments.** An Obsidian note is
31
+ * `areas/health.md`; an S3 key and a GitHub file path are the same shape. A router that stops at
32
+ * the first `/` binds `areas` and 404s the rest. Hono spells the greedy form `:name{.+}`.
33
+ *
34
+ * Read from `EmittedRoute.reservedPathParameters`, which the library resolves from `allowReserved`
35
+ * on the parameter. **Never from the template**: the operator does not survive to `route.path`,
36
+ * `@typespec/http` strips it, and it can also be set with no operator in the template at all, so
37
+ * the template is a derived artefact rather than the source of truth.
38
+ */
39
+ reserved?: ReadonlySet<string>): string;
27
40
  /** The one thing a Hono server cannot express, handed back rather than thrown. */
28
41
  export interface RenderRefusals {
29
42
  readonly unsupportedPathTemplate: (route: EmittedRoute, template: string, name: string) => void;
package/dist/src/app.js CHANGED
@@ -29,12 +29,32 @@ const PLAIN_PATH_PARAMETER = /^[A-Za-z0-9_.~-]+$/;
29
29
  * operation (validators included) over a template no router could mount. What a request body must
30
30
  * look like does not depend on that.
31
31
  */
32
- export function toHonoPath(template, refuse) {
32
+ export function toHonoPath(template, refuse,
33
+ /**
34
+ * Wire names the document says carry RFC 6570 reserved expansion, so their value may contain `/`.
35
+ *
36
+ * **A hierarchical identifier is ONE value, not several segments.** An Obsidian note is
37
+ * `areas/health.md`; an S3 key and a GitHub file path are the same shape. A router that stops at
38
+ * the first `/` binds `areas` and 404s the rest. Hono spells the greedy form `:name{.+}`.
39
+ *
40
+ * Read from `EmittedRoute.reservedPathParameters`, which the library resolves from `allowReserved`
41
+ * on the parameter. **Never from the template**: the operator does not survive to `route.path`,
42
+ * `@typespec/http` strips it, and it can also be set with no operator in the template at all, so
43
+ * the template is a derived artefact rather than the source of truth.
44
+ */
45
+ reserved = new Set()) {
33
46
  return template.replace(/\{([^}]+)\}/g, (match, name) => {
34
- if (PLAIN_PATH_PARAMETER.test(name))
35
- return `:${name}`;
36
- refuse(template, name);
37
- return match;
47
+ /**
48
+ * **The name check comes FIRST and is unconditional.** A name Hono cannot carry is refused
49
+ * whether or not it is reserved: greedy matching does not make a space or a `+` in a parameter
50
+ * name expressible, and letting one through because another flag was set would mount a route
51
+ * matching the wrong requests rather than one that fails.
52
+ */
53
+ if (!PLAIN_PATH_PARAMETER.test(name)) {
54
+ refuse(template, name);
55
+ return match;
56
+ }
57
+ return reserved.has(name) ? `:${name}{.+}` : `:${name}`;
38
58
  });
39
59
  }
40
60
  /**
@@ -248,7 +268,7 @@ basePaths = [],
248
268
  * audience is wider.
249
269
  */
250
270
  securityFor) {
251
- const entries = emitted.routes.flatMap((route) => {
271
+ const mounted = emitted.routes.flatMap((route) => {
252
272
  let dispatched;
253
273
  const names = emitted.schemaNames.get(route.operationId);
254
274
  // The library declares a `Responses` const for every operation, so a missing entry is a bug in
@@ -291,6 +311,7 @@ securityFor) {
291
311
  }
292
312
  return [{ route, names, validators, dispatched }];
293
313
  });
314
+ const entries = mounted;
294
315
  /**
295
316
  * **One registration per verb+path, not per operation.**
296
317
  *
@@ -385,7 +406,7 @@ securityFor) {
385
406
  /** A HEAD with no GET beside it: registered under GET, and guarded so only a HEAD reaches it. */
386
407
  const headOnly = plainGroups.length === 0 && headGroups.length > 0;
387
408
  const method = HONO_METHOD[registrationVerbOf(route.verb)] ?? "on";
388
- const path = toHonoPath(route.path, (template, name) => refuse.unsupportedPathTemplate(route, template, name));
409
+ const path = toHonoPath(route.path, (template, name) => refuse.unsupportedPathTemplate(route, template, name), new Set(route.reservedPathParameters));
389
410
  /**
390
411
  * A route inside a sub-app is registered RELATIVE to the prefix it is mounted at.
391
412
  *
@@ -703,6 +724,27 @@ securityFor) {
703
724
  * check cannot be wrong in the direction that breaks a build.
704
725
  */
705
726
  const dispatchesBody = entries.some((entry) => entry.dispatched !== undefined);
727
+ /**
728
+ * **Imported only where a route actually validates something, like every other value import here.**
729
+ *
730
+ * This one was unconditional while the three around it were not, and the comment above them stated
731
+ * the rule it was breaking. A service whose operations declare no parameters at all -- two bare
732
+ * `GET`s, which is where a health check starts and therefore where a new consumer starts -- emitted
733
+ * an import nothing used, and `tsp compile` reported success:
734
+ * `TS6133: 'zValidator' is declared but its value is never read`.
735
+ *
736
+ * Counted from the same filtered list the middleware is rendered from, so it cannot disagree with
737
+ * what was emitted: a dispatched body's validator is `byContentType`, not a `zValidator`.
738
+ */
739
+ /**
740
+ * **`z` is only ever reached through `z.infer`**, which appears where an operation has an input
741
+ * type or a response body. A service whose every operation takes nothing and returns `void` names
742
+ * neither, so the import was written and never used:
743
+ * `TS6133: 'z' is declared but its value is never read`. Same shape as the `zValidator` one above,
744
+ * one step further along.
745
+ */
746
+ const usesZod = entries.some((entry) => inputTypeOf(entry) !== undefined || entry.names.response !== undefined);
747
+ const validates = entries.some((entry) => entry.validators.filter(([target]) => entry.dispatched === undefined || target !== VALIDATOR_TARGET.body).length > 0);
706
748
  const runtimeModule = JSON.stringify(emitted.options.runtimeModule);
707
749
  /**
708
750
  * **One base sub-app, mounted with `app.route()`. Hono's own nesting, not a rewritten path on
@@ -714,10 +756,8 @@ securityFor) {
714
756
  const usesBasePath = basePaths.length > 0;
715
757
  const needsHonoValue = subApps.size > 0 || usesBasePath;
716
758
  return `${GENERATED_BANNER}
717
- import { zValidator } from "@hono/zod-validator";
718
- ${needsHonoValue ? 'import { Hono } from "hono";\nimport type { Context, Input } from "hono";' : 'import type { Context, Hono, Input } from "hono";'}
719
- import { z } from "zod";
720
- import type { AppEnv, Awaitable, Ctx, Result, RouteDeps } from ${runtimeModule};${negotiates ? `\nimport { selectContentType } from ${runtimeModule};` : ""}${guardsHead ? `\nimport { headOnly } from ${runtimeModule};` : ""}${dispatchesBody ? `\nimport { byContentType } from ${runtimeModule};` : ""}
759
+ ${validates ? 'import { zValidator } from "@hono/zod-validator";\n' : ""}${needsHonoValue ? 'import { Hono } from "hono";\nimport type { Context, Input } from "hono";' : 'import type { Context, Hono, Input } from "hono";'}
760
+ ${usesZod ? 'import { z } from "zod";\n' : ""}import type { AppEnv, Awaitable, Ctx, Result, RouteDeps } from ${runtimeModule};${negotiates ? `\nimport { selectContentType } from ${runtimeModule};` : ""}${guardsHead ? `\nimport { headOnly } from ${runtimeModule};` : ""}${dispatchesBody ? `\nimport { byContentType } from ${runtimeModule};` : ""}
721
761
  ${imports}
722
762
  /**
723
763
  * One method per operation, each concretely typed from the schemas it validates against.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-hono",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "TypeSpec emitter: generate a Hono server, and the Zod validators it enforces, from an HTTP service definition, agreeing with the OpenAPI document @typespec/openapi3 publishes from the same source.",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -44,7 +44,7 @@
44
44
  "provenance": true
45
45
  },
46
46
  "dependencies": {
47
- "typespec-http-zod": "^0.6.0"
47
+ "typespec-http-zod": "^0.10.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@hono/zod-openapi": "^1.4.0",