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 +3 -0
- package/dist/src/api.js +53 -8
- package/dist/src/lib.d.ts +10 -0
- package/dist/src/lib.js +2 -0
- package/package.json +1 -1
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
|
-
|
|
64
|
-
|
|
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}
|
|
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 = [
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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.
|
|
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",
|