typespec-hono 0.14.0 → 0.15.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.
Files changed (2) hide show
  1. package/dist/src/app.js +59 -2
  2. package/package.json +2 -2
package/dist/src/app.js CHANGED
@@ -375,11 +375,68 @@ securityFor) {
375
375
  : validated === undefined
376
376
  ? negotiated
377
377
  : `${validated} & ${negotiated}`;
378
- const output = names.response === undefined ? "void" : `z.infer<typeof ${names.response}>`;
378
+ /**
379
+ * **The RETURN type drops index signatures; the input type keeps them.**
380
+ *
381
+ * A handler receives whatever the validator let through, and an open model's validator really
382
+ * does pass unknown keys along - so the input saying `[key: string]: unknown` describes the
383
+ * value in hand. Returning is the opposite direction: the handler supplies a value the
384
+ * application already holds, and an index signature there is an obligation rather than a
385
+ * description. TypeScript gives an interface no implicit index signature, so a domain type
386
+ * could not satisfy it without a spread at every level of the tree.
387
+ *
388
+ * `typespec-http-zod@0.17.0` took the catchall off the contract types for this reason. It did
389
+ * not reach here, because this signature is derived from `z.infer` rather than from those
390
+ * types - which is exactly the half-fix `test/openmodel/` now guards against.
391
+ */
392
+ const output = names.response === undefined ? "void" : `Declared<z.infer<typeof ${names.response}>>`;
379
393
  const signature = input === undefined ? "ctx: Ctx" : `ctx: Ctx, input: ${input}`;
380
394
  const doc = route.summary === undefined ? "" : `\t/** ${route.summary} */\n`;
381
395
  return `${doc}\t${route.operationId}(${signature}): Awaitable<Result<${output}>>;`;
382
396
  });
397
+ /**
398
+ * Emitted only when an operation actually returns something, because a generated file has to pass
399
+ * `noUnusedLocals` like any other - the lint that has already failed this emitter twice over an
400
+ * import written for a construct the service did not use.
401
+ *
402
+ * **Decided from the routes, not by searching the rendered text.** Asking whether the output
403
+ * mentions a name is how this package lost the `byContentType` import: the call gained an argument
404
+ * and the substring stopped matching, so a module referenced a function it no longer imported.
405
+ */
406
+ const returnsAnything = entries.some((entry) => entry.names.response !== undefined);
407
+ const declaredHelper = returnsAnything
408
+ ? `/**
409
+ * A shape with its index signatures removed, at every depth.
410
+ *
411
+ * An open model - one declared with \`...Record<T>\` - infers \`[key: string]: unknown\`, because its
412
+ * validator really does pass unknown keys through. That is true of what a handler RECEIVES, so the
413
+ * input types above keep it. It is not true of what a handler must SUPPLY: TypeScript gives an
414
+ * interface no implicit index signature, so a domain type could not be returned without spreading
415
+ * every level of the tree, which on one real service meant a structural deep copy per response.
416
+ *
417
+ * Returning extra properties still works - excess-property checks apply to object literals, not to
418
+ * a value the application already holds.
419
+ */
420
+ type Declared<T> = T extends (...args: never[]) => unknown
421
+ ? T
422
+ : T extends readonly (infer Element)[]
423
+ ? T extends Element[]
424
+ ? Declared<Element>[]
425
+ : readonly Declared<Element>[]
426
+ : T extends object
427
+ ? {
428
+ [K in keyof T as string extends K
429
+ ? never
430
+ : number extends K
431
+ ? never
432
+ : symbol extends K
433
+ ? never
434
+ : K]: Declared<T[K]>;
435
+ }
436
+ : T;
437
+
438
+ `
439
+ : "";
383
440
  const aliases = entries.map((entry) => `export type ${capitaliseId(entry.route.operationId)}Handler = Operations[${JSON.stringify(entry.route.operationId)}];`);
384
441
  /**
385
442
  * **Which resources get a sub-app, and which routes stay on the root.**
@@ -808,7 +865,7 @@ ${imports}
808
865
  * There is no cast anywhere in this file, and no dynamic lookup: the generated call sites name the
809
866
  * method, so an implementation whose input or output does not match the contract fails to compile.
810
867
  */
811
- export interface Operations {
868
+ ${declaredHelper}export interface Operations {
812
869
  ${methods.join("\n")}
813
870
  }
814
871
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-hono",
3
- "version": "0.14.0",
3
+ "version": "0.15.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.16.0"
47
+ "typespec-http-zod": "^0.17.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@hono/zod-openapi": "^1.4.0",