typespec-http-zod 0.11.0 → 0.12.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/api.d.ts CHANGED
@@ -43,6 +43,7 @@ export { EmitterOptionsSchema, type EmitterOptions } from "./lib.js";
43
43
  * independently.
44
44
  */
45
45
  export { objectKey } from "./zod.js";
46
+ export declare function beginBanner(hint: string | undefined): void;
46
47
  /**
47
48
  * The status a row registers: the first non-error status the operation declares.
48
49
  *
@@ -328,6 +329,8 @@ export interface ResolvedServiceOptions {
328
329
  readonly sealObjectSchemas: boolean;
329
330
  readonly keyVocabularies: readonly string[];
330
331
  readonly runtimeModule: string;
332
+ /** The command that regenerates, for the banner. `undefined` keeps the generic line. */
333
+ readonly regenerateHint: string | undefined;
331
334
  }
332
335
  /** What the generated files import their runtime contract from when nothing says otherwise. */
333
336
  export declare const DEFAULT_RUNTIME_MODULE = "typespec-http-zod/runtime";
package/dist/src/api.js CHANGED
@@ -60,9 +60,30 @@ const PACKAGE_NAME = "typespec-http-zod";
60
60
  * The package name stays, because that IS the answer to "what wrote this". The instruction becomes
61
61
  * the one thing true for every consumer: recompile the source, do not edit the output.
62
62
  */
63
- const GENERATED_BANNER = `// GENERATED by ${PACKAGE_NAME} from the TypeSpec service definition. DO NOT EDIT.
64
- // Recompile the spec that produced it; edits here are overwritten on the next run.
65
- `;
63
+ /**
64
+ * How to regenerate, for the banner. Set per emission; `undefined` keeps the generic line.
65
+ *
66
+ * Module-scoped rather than threaded through four render functions, matching how the contracts
67
+ * package and the stream module are carried. The emission loop awaits each service to completion
68
+ * before the next begins, so nothing interleaves.
69
+ */
70
+ let regenerateHint;
71
+ export function beginBanner(hint) {
72
+ regenerateHint = hint;
73
+ }
74
+ /**
75
+ * The header every emitted file carries.
76
+ *
77
+ * **`DO NOT EDIT` says what not to do and not what to do instead**, and only the project knows the
78
+ * command that regenerates. Where it states one, the banner carries it; otherwise the generic line
79
+ * stands.
80
+ */
81
+ function generatedBanner() {
82
+ const second = regenerateHint === undefined
83
+ ? "// Recompile the spec that produced it; edits here are overwritten on the next run."
84
+ : `// Regenerate with: ${regenerateHint}`;
85
+ return `// GENERATED by ${PACKAGE_NAME} from the TypeSpec service definition. DO NOT EDIT.\n${second}\n`;
86
+ }
66
87
  /**
67
88
  * The status a row registers: the first non-error status the operation declares.
68
89
  *
@@ -1110,7 +1131,7 @@ function renderSchemas(registry, externals, routeDeclarations, runtimeModule) {
1110
1131
  * with no cycle emits byte-identical output to before.
1111
1132
  */
1112
1133
  if (d.structural === undefined) {
1113
- return `\nexport const ${d.identifier} = ${d.source};\nexport type ${d.typeName} = z.infer<typeof ${d.identifier}>;\n`;
1134
+ return `\nexport const ${d.identifier} = ${d.source};\nexport type ${d.typeName} = Exact<z.infer<typeof ${d.identifier}>>;\n`;
1114
1135
  }
1115
1136
  const declared = d.structural.startsWith("{")
1116
1137
  ? `export interface ${d.typeName} ${d.structural}`
@@ -1134,8 +1155,28 @@ function renderSchemas(registry, externals, routeDeclarations, runtimeModule) {
1134
1155
  * inside `zValidator` or missed beside a bracket. `renderExternalImports` below reads the same
1135
1156
  * rendered content for the same reason.
1136
1157
  */
1158
+ /**
1159
+ * **`Exact` narrows an inferred optional to what the wire can carry.**
1160
+ *
1161
+ * `z.infer` of `.optional()` gives `p?: T | undefined`, which under `exactOptionalPropertyTypes`
1162
+ * admits an explicitly-undefined property. **JSON has no `undefined`**, and the document says the
1163
+ * same thing by leaving the property out of `required`, so the inferred type was wider than both
1164
+ * the contract and the wire and a consumer had to strip it at the boundary.
1165
+ *
1166
+ * Derived from the same schema rather than hand-written beside it, so there is still one source of
1167
+ * truth: a mapped type cannot drift from the thing it maps.
1168
+ *
1169
+ * Emitted only where a declaration names it, like the imports beside it.
1170
+ */
1171
+ const EXACT_HELPER = `
1172
+ /** An inferred type narrowed to what JSON can carry: an optional property is absent or a value. */
1173
+ type Exact<T> = { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } & {
1174
+ [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
1175
+ };
1176
+ `;
1177
+ const usesExact = declarations.some((d) => d.includes("Exact<"));
1137
1178
  const usesZod = new Set([...declarations, ...routeDeclarations].join("").match(/[A-Za-z_$][\w$]*/g) ?? []).has("z");
1138
- const parts = [GENERATED_BANNER];
1179
+ const parts = [generatedBanner()];
1139
1180
  if (usesZod)
1140
1181
  parts.push('\nimport { z } from "zod";\n');
1141
1182
  /**
@@ -1150,6 +1191,8 @@ function renderSchemas(registry, externals, routeDeclarations, runtimeModule) {
1150
1191
  parts.push(`import type { ResponseArm } from ${JSON.stringify(runtimeModule)};\n`);
1151
1192
  }
1152
1193
  parts.push(renderExternalImports(externals, [...declarations, ...routeDeclarations].join("")));
1194
+ if (usesExact)
1195
+ parts.push(EXACT_HELPER);
1153
1196
  parts.push(declarations.join(""));
1154
1197
  if (routeDeclarations.length > 0)
1155
1198
  parts.push(`\n${routeDeclarations.join("\n")}\n`);
@@ -1468,7 +1511,7 @@ function renderRequestTypes(entries, responses, registry) {
1468
1511
  const lookup = entries
1469
1512
  .map((entry) => `\t${entry.operationId}: ${upperFirst(entry.operationId)}Input;`)
1470
1513
  .join("\n");
1471
- return `${GENERATED_BANNER}
1514
+ return `${generatedBanner()}
1472
1515
  /**
1473
1516
  * Flattens an intersection into a single object type.
1474
1517
  *
@@ -1549,7 +1592,7 @@ function renderWireAssertions(schemas, types, entries, contractsPackage) {
1549
1592
  if (paired.length === 0)
1550
1593
  return undefined;
1551
1594
  const assertions = paired.map((pair) => `export type Assert${pair.name} = MustHold<Identical<z.infer<typeof ${pair.identifier}>, Contracts.${pair.name}>>;`);
1552
- return `${GENERATED_BANNER}
1595
+ return `${generatedBanner()}
1553
1596
  import type { z } from "zod";
1554
1597
  import type * as Contracts from "${contractsPackage}";
1555
1598
  import type {
@@ -1587,7 +1630,7 @@ function renderVocabularies(vocabularies) {
1587
1630
  const entries = [...vocabularies.entries()]
1588
1631
  .toSorted(([a], [b]) => a.localeCompare(b))
1589
1632
  .map(([name, values]) => `\t${name}: [${values.map((v) => JSON.stringify(v)).join(", ")}],`);
1590
- return `${GENERATED_BANNER}
1633
+ return `${generatedBanner()}
1591
1634
  /**
1592
1635
  * The spec's vocabularies - every enum the surface reaches, and what each one contains.
1593
1636
  *
@@ -1671,7 +1714,9 @@ export async function emitHttpZod(context, wrapper = {}) {
1671
1714
  sealObjectSchemas: perService?.["seal-object-schemas"] ?? options["seal-object-schemas"] ?? false,
1672
1715
  keyVocabularies: perService?.["key-vocabularies"] ?? options["key-vocabularies"] ?? [],
1673
1716
  runtimeModule: perService?.["runtime-module"] ?? options["runtime-module"] ?? defaultRuntimeModule,
1717
+ regenerateHint: perService?.["regenerate-hint"] ?? options["regenerate-hint"],
1674
1718
  };
1719
+ beginBanner(resolved.regenerateHint);
1675
1720
  const registry = new SchemaRegistry(context.program);
1676
1721
  const { result: { routes }, imports: externals, vocabularies, } = collectExternalImports(() => withContractsPackage(resolved.contractsPackage, () => withSealedObjects(resolved.sealObjectSchemas, () => {
1677
1722
  // Declarations first: a vocabulary is a fact about the contract, so it is collected from
package/dist/src/lib.d.ts CHANGED
@@ -78,6 +78,15 @@ export interface EmitterOptions {
78
78
  * package's own tests, where the package name does not resolve.
79
79
  */
80
80
  "runtime-module"?: string;
81
+ /**
82
+ * The command that regenerates these files, written into every banner.
83
+ *
84
+ * **`DO NOT EDIT` says what not to do and not what to do instead.** Only the project knows whether
85
+ * that is `pnpm generate`, `npm run api` or a `tsp compile` with three flags, so no emitter can
86
+ * supply it, and the generic line standing in its place lost the one thing a reader opening a
87
+ * generated file needs. Omitted, the generic line is kept.
88
+ */
89
+ "regenerate-hint"?: string;
81
90
  /**
82
91
  * Per-`@service` overrides, keyed by the service namespace name.
83
92
  *
@@ -93,6 +102,7 @@ export interface EmitterOptions {
93
102
  "seal-object-schemas"?: boolean;
94
103
  "key-vocabularies"?: string[];
95
104
  "runtime-module"?: string;
105
+ "regenerate-hint"?: string;
96
106
  }>;
97
107
  }
98
108
  /**
package/dist/src/lib.js CHANGED
@@ -14,6 +14,7 @@ export const EmitterOptionsSchema = {
14
14
  "seal-object-schemas": { type: "boolean", nullable: true },
15
15
  "key-vocabularies": { type: "array", items: { type: "string" }, nullable: true },
16
16
  "runtime-module": { type: "string", nullable: true },
17
+ "regenerate-hint": { type: "string", nullable: true },
17
18
  services: {
18
19
  type: "object",
19
20
  nullable: true,
@@ -27,6 +28,7 @@ export const EmitterOptionsSchema = {
27
28
  "seal-object-schemas": { type: "boolean", nullable: true },
28
29
  "key-vocabularies": { type: "array", items: { type: "string" }, nullable: true },
29
30
  "runtime-module": { type: "string", nullable: true },
31
+ "regenerate-hint": { type: "string", nullable: true },
30
32
  },
31
33
  required: [],
32
34
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-zod",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "TypeSpec emitter and library: turn an HTTP service definition into Zod validators and framework-free contract types that agree, keyword for keyword, with the OpenAPI document @typespec/openapi3 publishes from the same source.",
5
5
  "keywords": [
6
6
  "codegen",