typespec-hono 0.16.0 → 0.18.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 +52 -27
  2. package/package.json +2 -2
package/dist/src/app.js CHANGED
@@ -359,6 +359,23 @@ securityFor) {
359
359
  const slot = `${entry.route.verb} ${entry.route.path}`;
360
360
  grouped.set(slot, [...(grouped.get(slot) ?? []), entry]);
361
361
  }
362
+ /**
363
+ * The input type of an operation that declares no input.
364
+ *
365
+ * **Every operation takes `(ctx, input)`, including this one.** A surface written once against
366
+ * `Operations` - an RPC entrypoint, a proxy, any uniform dispatch - is typed `(ctx, input)`, and
367
+ * TypeScript refuses a function with MORE parameters than the signature it is assigned to. So a
368
+ * single parameterless operation emitting `(ctx)` made the whole surface unassignable, measured as
369
+ * `TS2322: Target signature provides too few arguments. Expected 2 or more, but got 1.`
370
+ *
371
+ * **Adding the parameter breaks nothing**, which is what makes this the right shape rather than a
372
+ * trade: FEWER parameters is always assignable, so a handler already written `(ctx) => ...` keeps
373
+ * compiling untouched. `test/openmodel/treaty.test.ts` holds both halves.
374
+ *
375
+ * `Record<string, never>` rather than `{}`, because `{}` in TypeScript means "anything but null",
376
+ * which is the opposite of the claim being made.
377
+ */
378
+ const EMPTY_INPUT = "Record<string, never>";
362
379
  const methods = entries.map((entry) => {
363
380
  const { route, names } = entry;
364
381
  /**
@@ -376,21 +393,20 @@ securityFor) {
376
393
  ? negotiated
377
394
  : `${validated} & ${negotiated}`;
378
395
  /**
379
- * **The RETURN type drops index signatures; the input type keeps them.**
396
+ * **The RETURN type is the producer's view; the input type is left exactly as it arrives.**
380
397
  *
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.
398
+ * A handler receives whatever the validator let through, so an input carrying
399
+ * `[key: string]: unknown`, `T | undefined` on an optional, and mutable arrays is an honest
400
+ * description of the value in hand. Returning is the opposite direction: the handler supplies
401
+ * something the application already holds, and each of those becomes an obligation rather than
402
+ * a description. See `Produced` below for what that cost, measured.
387
403
  *
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.
404
+ * `typespec-http-zod` fixes the same three things on its contract types. None of it reaches
405
+ * here on its own, because this signature is derived from `z.infer` rather than from those
406
+ * types - which is exactly the half-fix `test/openmodel/` exists to catch.
391
407
  */
392
- const output = names.response === undefined ? "void" : `Declared<z.infer<typeof ${names.response}>>`;
393
- const signature = input === undefined ? "ctx: Ctx" : `ctx: Ctx, input: ${input}`;
408
+ const output = names.response === undefined ? "void" : `Produced<z.infer<typeof ${names.response}>>`;
409
+ const signature = `ctx: Ctx, input: ${input ?? EMPTY_INPUT}`;
394
410
  const doc = route.summary === undefined ? "" : `\t/** ${route.summary} */\n`;
395
411
  return `${doc}\t${route.operationId}(${signature}): Awaitable<Result<${output}>>;`;
396
412
  });
@@ -406,32 +422,40 @@ securityFor) {
406
422
  const returnsAnything = entries.some((entry) => entry.names.response !== undefined);
407
423
  const declaredHelper = returnsAnything
408
424
  ? `/**
409
- * A shape with its index signatures removed, at every depth.
425
+ * What a handler must SUPPLY, as opposed to what it receives.
426
+ *
427
+ * **Three things that are true of an inferred type are not obligations on a producer**, and each one
428
+ * made a value the application already held unreturnable:
429
+ *
430
+ * - an index signature, which an open model infers because its validator really does pass unknown
431
+ * keys through. TypeScript gives an interface no implicit index signature, so returning one meant
432
+ * spreading every level of the tree - a structural deep copy per response on one real service;
433
+ * - \`readonly\`, which a codebase commonly puts on the views its layers hand back. \`readonly T[]\`
434
+ * and \`T[]\` serialise to identical bytes, so mutability says nothing about a payload.
435
+ *
436
+ * The input types above keep both, deliberately: they describe the value in hand.
410
437
  *
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.
438
+ * **An explicit \`undefined\` on an optional property is NOT removed, and that was tried.** It looks
439
+ * like the same class and is the opposite: dropping an index signature or adding \`readonly\` makes
440
+ * MORE values assignable, and removing \`| undefined\` makes fewer. Measured, it broke
441
+ * \`(ctx, input) => ok(input)\` - return what you were given - because what arrives carries it.
416
442
  *
417
- * Returning extra properties still works - excess-property checks apply to object literals, not to
418
- * a value the application already holds.
443
+ * Returning extra properties still works - excess-property checks apply to object literals, not to a
444
+ * value the application already holds.
419
445
  */
420
- type Declared<T> = T extends (...args: never[]) => unknown
446
+ type Produced<T> = T extends (...args: never[]) => unknown
421
447
  ? T
422
448
  : T extends readonly (infer Element)[]
423
- ? T extends Element[]
424
- ? Declared<Element>[]
425
- : readonly Declared<Element>[]
449
+ ? readonly Produced<Element>[]
426
450
  : T extends object
427
451
  ? {
428
- [K in keyof T as string extends K
452
+ readonly [K in keyof T as string extends K
429
453
  ? never
430
454
  : number extends K
431
455
  ? never
432
456
  : symbol extends K
433
457
  ? never
434
- : K]: Declared<T[K]>;
458
+ : K]: Produced<T[K]>;
435
459
  }
436
460
  : T;
437
461
 
@@ -643,7 +667,8 @@ type Declared<T> = T extends (...args: never[]) => unknown
643
667
  * properties at six.
644
668
  */
645
669
  const call = input.length === 0
646
- ? `handlersFor(c).${member.route.operationId}(ctx)`
670
+ ? // An empty object, because every operation takes `(ctx, input)`. See `EMPTY_INPUT`.
671
+ `handlersFor(c).${member.route.operationId}(ctx, {})`
647
672
  : [
648
673
  `handlersFor(c).${member.route.operationId}(ctx, {`,
649
674
  ...input.map((piece) => `\t\t\t\t\t\t${piece},`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-hono",
3
- "version": "0.16.0",
3
+ "version": "0.18.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.17.0"
47
+ "typespec-http-zod": "^0.19.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@hono/zod-openapi": "^1.4.0",