@kubb/plugin-zod 5.9.0-canary.20261111T122006 → 5.9.1
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/index.cjs +849 -816
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -4
- package/dist/index.d.ts +13 -4
- package/dist/index.js +850 -817
- package/dist/index.js.map +1 -1
- package/docs/guides/customization.md +36 -27
- package/docs/options.md +15 -8
- package/kubb-extension.json +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,143 +1,407 @@
|
|
|
1
|
-
import { Diagnostics, Resolver, addGenerator, ast,
|
|
1
|
+
import { Diagnostics, Resolver, addGenerator, ast, casing, createPrinter, createResolver, defineExtension, defineGenerator, setMacros, setResolver, text } from "@kubb/kit";
|
|
2
2
|
import { Const, File, Function, Type, jsxRenderer } from "@kubb/renderer-jsx";
|
|
3
3
|
import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
|
|
4
|
-
// ../../internals/
|
|
4
|
+
// ../../internals/utils/src/reserved.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
* JavaScript and Java reserved words.
|
|
7
|
+
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
8
|
+
*/
|
|
9
|
+
const reservedWords = /* @__PURE__ */ new Set([
|
|
10
|
+
"abstract",
|
|
11
|
+
"arguments",
|
|
12
|
+
"await",
|
|
13
|
+
"boolean",
|
|
14
|
+
"break",
|
|
15
|
+
"byte",
|
|
16
|
+
"case",
|
|
17
|
+
"catch",
|
|
18
|
+
"char",
|
|
19
|
+
"class",
|
|
20
|
+
"const",
|
|
21
|
+
"continue",
|
|
22
|
+
"debugger",
|
|
23
|
+
"default",
|
|
24
|
+
"delete",
|
|
25
|
+
"do",
|
|
26
|
+
"double",
|
|
27
|
+
"else",
|
|
28
|
+
"enum",
|
|
29
|
+
"eval",
|
|
30
|
+
"export",
|
|
31
|
+
"extends",
|
|
32
|
+
"false",
|
|
33
|
+
"final",
|
|
34
|
+
"finally",
|
|
35
|
+
"float",
|
|
36
|
+
"for",
|
|
37
|
+
"function",
|
|
38
|
+
"goto",
|
|
39
|
+
"if",
|
|
40
|
+
"implements",
|
|
41
|
+
"import",
|
|
42
|
+
"in",
|
|
43
|
+
"instanceof",
|
|
44
|
+
"int",
|
|
45
|
+
"interface",
|
|
46
|
+
"let",
|
|
47
|
+
"long",
|
|
48
|
+
"native",
|
|
49
|
+
"new",
|
|
50
|
+
"null",
|
|
51
|
+
"package",
|
|
52
|
+
"private",
|
|
53
|
+
"protected",
|
|
54
|
+
"public",
|
|
55
|
+
"return",
|
|
56
|
+
"short",
|
|
57
|
+
"static",
|
|
58
|
+
"super",
|
|
59
|
+
"switch",
|
|
60
|
+
"synchronized",
|
|
61
|
+
"this",
|
|
62
|
+
"throw",
|
|
63
|
+
"throws",
|
|
64
|
+
"transient",
|
|
65
|
+
"true",
|
|
66
|
+
"try",
|
|
67
|
+
"typeof",
|
|
68
|
+
"var",
|
|
69
|
+
"void",
|
|
70
|
+
"volatile",
|
|
71
|
+
"while",
|
|
72
|
+
"with",
|
|
73
|
+
"yield",
|
|
74
|
+
"Array",
|
|
75
|
+
"Date",
|
|
76
|
+
"hasOwnProperty",
|
|
77
|
+
"Infinity",
|
|
78
|
+
"isFinite",
|
|
79
|
+
"isNaN",
|
|
80
|
+
"isPrototypeOf",
|
|
81
|
+
"length",
|
|
82
|
+
"Math",
|
|
83
|
+
"name",
|
|
84
|
+
"NaN",
|
|
85
|
+
"Number",
|
|
86
|
+
"Object",
|
|
87
|
+
"prototype",
|
|
88
|
+
"String",
|
|
89
|
+
"toString",
|
|
90
|
+
"undefined",
|
|
91
|
+
"valueOf"
|
|
92
|
+
]);
|
|
93
|
+
/**
|
|
94
|
+
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
10
95
|
*
|
|
11
96
|
* @example
|
|
12
97
|
* ```ts
|
|
13
|
-
*
|
|
98
|
+
* isValidVarName('status') // true
|
|
99
|
+
* isValidVarName('class') // false (reserved word)
|
|
100
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
14
101
|
* ```
|
|
15
102
|
*/
|
|
16
|
-
function
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
extname,
|
|
20
|
-
tag: node.tags[0] ?? "default",
|
|
21
|
-
path: node.path
|
|
22
|
-
};
|
|
103
|
+
function isValidVarName(name) {
|
|
104
|
+
if (!name || reservedWords.has(name)) return false;
|
|
105
|
+
return isIdentifier(name);
|
|
23
106
|
}
|
|
24
107
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* operation. The plugin itself and every plugin that imports from it call this, so they all get
|
|
28
|
-
* the same path.
|
|
108
|
+
* Returns `name` when it's a syntactically valid JavaScript variable name,
|
|
109
|
+
* otherwise prefixes it with `_` so the result is a valid identifier.
|
|
29
110
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* Only the file name changes. The declarations keep their names.
|
|
111
|
+
* Useful for sanitizing OpenAPI schema names or operation IDs that start with
|
|
112
|
+
* a digit (e.g. `409`, `504AccountCancel`) before using them as exported
|
|
113
|
+
* variable, type, or function names.
|
|
34
114
|
*
|
|
35
|
-
* @example
|
|
115
|
+
* @example
|
|
36
116
|
* ```ts
|
|
37
|
-
*
|
|
117
|
+
* ensureValidVarName('409') // '_409'
|
|
118
|
+
* ensureValidVarName('504AccountCancel') // '_504AccountCancel'
|
|
119
|
+
* ensureValidVarName('Pet') // 'Pet'
|
|
120
|
+
* ensureValidVarName('class') // '_class'
|
|
38
121
|
* ```
|
|
39
122
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const location = {
|
|
44
|
-
root,
|
|
45
|
-
output,
|
|
46
|
-
group: options.group ?? void 0
|
|
47
|
-
};
|
|
48
|
-
return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => {
|
|
49
|
-
const schemaPaths = memo(`${resolver.pluginName}:schemaFilePaths`, () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.name ? [resolver.file({
|
|
50
|
-
name: schema.name,
|
|
51
|
-
extname: ".ts",
|
|
52
|
-
...location
|
|
53
|
-
}).path.toLowerCase()] : [])));
|
|
54
|
-
let file = resolver.file({
|
|
55
|
-
...operationFileEntry(node, node.operationId),
|
|
56
|
-
...location
|
|
57
|
-
});
|
|
58
|
-
for (let name = `${node.operationId}Operation`; schemaPaths.has(file.path.toLowerCase()); name += "Operation") {
|
|
59
|
-
const next = resolver.file({
|
|
60
|
-
...operationFileEntry(node, name),
|
|
61
|
-
...location
|
|
62
|
-
});
|
|
63
|
-
if (next.path === file.path) break;
|
|
64
|
-
file = next;
|
|
65
|
-
}
|
|
66
|
-
return file;
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
function baseContentType(contentType) {
|
|
70
|
-
return contentType.split(";")[0].trim().toLowerCase();
|
|
123
|
+
function ensureValidVarName(name) {
|
|
124
|
+
if (!name || isValidVarName(name)) return name;
|
|
125
|
+
return `_${name}`;
|
|
71
126
|
}
|
|
72
127
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
128
|
+
* Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
|
|
129
|
+
*
|
|
130
|
+
* Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
|
|
131
|
+
* even though they are not valid variable names, so use this (not {@link isValidVarName}) when
|
|
132
|
+
* deciding whether an object key needs quoting.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* isIdentifier('name') // true
|
|
137
|
+
* isIdentifier('x-total')// false
|
|
138
|
+
* ```
|
|
75
139
|
*/
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
return Boolean(entry.stream) && baseType !== "text/event-stream" && !baseType.startsWith("multipart/");
|
|
79
|
-
}
|
|
80
|
-
function isJsonContentType(baseType) {
|
|
81
|
-
return baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json";
|
|
140
|
+
function isIdentifier(name) {
|
|
141
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
82
142
|
}
|
|
143
|
+
// ../../internals/utils/src/codegen.ts
|
|
83
144
|
/**
|
|
84
|
-
*
|
|
145
|
+
* Assembles a multi-line object literal from already-rendered `entries`, indenting each entry one
|
|
146
|
+
* level and closing the brace at column zero. Entries that are themselves multi-line objects indent
|
|
147
|
+
* cumulatively. Each entry ends with a trailing comma to match the formatter's multi-line style.
|
|
85
148
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* buildObject(['id: z.number()', 'name: z.string()'])
|
|
152
|
+
* // '{\n id: z.number(),\n name: z.string(),\n}'
|
|
153
|
+
* ```
|
|
90
154
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (baseType === "text/event-stream" || isJsonLines(entry)) return "stream";
|
|
95
|
-
if (isJsonContentType(baseType)) return void 0;
|
|
96
|
-
if (entry.schema?.type === "blob") return "blob";
|
|
97
|
-
if (baseType.startsWith("text/")) return "text";
|
|
98
|
-
if (isBinaryContentType(baseType)) return "blob";
|
|
99
|
-
}
|
|
100
|
-
function isBinaryContentType(baseType) {
|
|
101
|
-
return baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType);
|
|
155
|
+
function buildObject(entries) {
|
|
156
|
+
if (entries.length === 0) return "{}";
|
|
157
|
+
return `{\n${entries.map((entry) => `${text.indent({ text: entry })},`).join("\n")}\n}`;
|
|
102
158
|
}
|
|
103
159
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
160
|
+
* Assembles a bracketed list (array by default) from already-rendered `items`. Keeps everything on
|
|
161
|
+
* one line when no item spans multiple lines, and otherwise puts each item on its own line, indented
|
|
162
|
+
* one level with a trailing comma and the closing bracket at column zero. Used for member lists such
|
|
163
|
+
* as `z.union([…])` and `z.array([…])`.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* buildList(['z.string()', 'z.number()'])
|
|
168
|
+
* // '[z.string(), z.number()]'
|
|
169
|
+
* ```
|
|
106
170
|
*/
|
|
107
|
-
function
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
113
|
-
if (parts.length === 0) return "Unknown";
|
|
114
|
-
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
171
|
+
function buildList(items, brackets = ["[", "]"]) {
|
|
172
|
+
const [open, close] = brackets;
|
|
173
|
+
if (items.length === 0) return `${open}${close}`;
|
|
174
|
+
if (!items.some((item) => item.includes("\n"))) return `${open}${items.join(", ")}${close}`;
|
|
175
|
+
return `${open}\n${items.map((item) => `${text.indent({ text: item })},`).join("\n")}\n${close}`;
|
|
115
176
|
}
|
|
116
177
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
178
|
+
* Emits a lazy getter for a circular-ref property position, `get name() { return body }`. The key
|
|
179
|
+
* is quoted only when it is not a valid identifier. Used by the string printers to defer evaluation
|
|
180
|
+
* of a recursive schema until first access.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* lazyGetter({ name: 'parent', body: 'z.lazy(() => Pet)' })
|
|
185
|
+
* // "get parent() { return z.lazy(() => Pet) }"
|
|
186
|
+
* ```
|
|
119
187
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
return baseName + suffix;
|
|
188
|
+
function lazyGetter({ name, body }) {
|
|
189
|
+
return `get ${objectKey(name)}() { return ${body} }`;
|
|
123
190
|
}
|
|
124
191
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
192
|
+
* An object key, quoted only when it is not identifier-shaped. Reserved words such as `class` stay bare.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* objectKey('x-total') // "'x-total'"
|
|
197
|
+
* ```
|
|
129
198
|
*/
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
199
|
+
function objectKey(name) {
|
|
200
|
+
return isIdentifier(name) ? name : text.quote(name);
|
|
201
|
+
}
|
|
202
|
+
// ../../internals/utils/src/fs.ts
|
|
203
|
+
/**
|
|
204
|
+
* Builds a nested file path from a dotted name. Splits on dots that precede a letter
|
|
205
|
+
* (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
|
|
206
|
+
* every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
|
|
207
|
+
*
|
|
208
|
+
* Empty segments are dropped before joining. They arise when the name starts with a dot
|
|
209
|
+
* followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
|
|
210
|
+
* an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
|
|
211
|
+
* absolute path, letting generated files escape the configured output directory.
|
|
212
|
+
*
|
|
213
|
+
* @example Nested path from a dotted name
|
|
214
|
+
* `toFilePath('pet.petId') // 'pet/petId'`
|
|
215
|
+
*
|
|
216
|
+
* @example PascalCase the final segment
|
|
217
|
+
* `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
|
|
218
|
+
*
|
|
219
|
+
* @example Suffix applied to the final segment only
|
|
220
|
+
* `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
|
|
221
|
+
*/
|
|
222
|
+
function toFilePath(name, caseLast = casing.camel) {
|
|
223
|
+
const parts = name.split(/\.(?=[a-zA-Z])/);
|
|
224
|
+
return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : casing.camel(part)).filter(Boolean).join("/");
|
|
225
|
+
}
|
|
226
|
+
// ../../internals/utils/src/strings.ts
|
|
227
|
+
const NEEDS_UNICODE = /(?<!\\)(?:\\\\)*\\[pPu]\{|[\u{10000}-\u{10FFFF}]/u;
|
|
228
|
+
const KEPT_ESCAPE = /[A-Za-z0-9^$\\.*+?()[\]{}|/]/;
|
|
229
|
+
/**
|
|
230
|
+
* Drops the backslash from escapes that the `u` flag rejects: an escaped punctuation character that is
|
|
231
|
+
* not a syntax character, and `\-` outside a class. Both match the same character without the backslash.
|
|
232
|
+
*/
|
|
233
|
+
function dropIdentityEscapes(pattern) {
|
|
234
|
+
let inClass = false;
|
|
235
|
+
return pattern.replace(/\\(.)|[[\]]/gs, (match, escaped) => {
|
|
236
|
+
if (escaped === void 0) {
|
|
237
|
+
inClass = match === "[";
|
|
238
|
+
return match;
|
|
239
|
+
}
|
|
240
|
+
return KEPT_ESCAPE.test(escaped) || escaped === "-" && inClass ? match : escaped;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Builds the `RegExp` for a pattern, adding the `u` flag when it uses `\p{...}`, `\P{...}` or `\u{...}`, or
|
|
245
|
+
* holds a character outside the BMP. A pattern that is not valid under `u` keeps the flags it came with.
|
|
246
|
+
*/
|
|
247
|
+
function toRegExp(pattern, flags) {
|
|
248
|
+
if (flags?.includes("u") || !NEEDS_UNICODE.test(pattern)) return new RegExp(pattern, flags);
|
|
249
|
+
try {
|
|
250
|
+
return new RegExp(dropIdentityEscapes(pattern), `${flags ?? ""}u`);
|
|
251
|
+
} catch {
|
|
252
|
+
return new RegExp(pattern, flags);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Converts a pattern string into a `new RegExp(...)` constructor call or a regex literal string.
|
|
257
|
+
* Inline flags expressed as a `^(?im)` prefix are extracted and applied to the resulting expression.
|
|
258
|
+
* Pass `null` as the second argument to emit a `/pattern/flags` literal instead.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* toRegExpString('^(?im)foo') // 'new RegExp("^foo", "im")'
|
|
263
|
+
* toRegExpString('^(?im)foo', null) // '/^foo/im'
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
function toRegExpString(pattern, func = "RegExp") {
|
|
267
|
+
const raw = text.unquote(pattern);
|
|
268
|
+
const match = raw.match(/^\^(\(\?([igmsuy]+)\))/i);
|
|
269
|
+
const replacementTarget = match?.[1] ?? "";
|
|
270
|
+
const matchedFlags = match?.[2];
|
|
271
|
+
const { source, flags } = toRegExp(raw.replace(/^\\?\//, "").replace(/\\?\/$/, "").replace(replacementTarget, ""), matchedFlags);
|
|
272
|
+
if (func === null) return `/${source}/${flags}`;
|
|
273
|
+
return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ""})`;
|
|
274
|
+
}
|
|
275
|
+
// ../../internals/shared/src/operation.ts
|
|
276
|
+
/**
|
|
277
|
+
* Builds the `ResolverFileParams` every operation generator passes to
|
|
278
|
+
* `resolver.file`: a file named `name`, tagged by the operation's first
|
|
279
|
+
* tag (or `'default'`), at the operation's path. Centralizes the entry object
|
|
280
|
+
* that was repeated at dozens of call sites across the client and query plugins.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```ts
|
|
284
|
+
* resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
function operationFileEntry(node, name, extname = ".ts") {
|
|
288
|
+
return {
|
|
289
|
+
name,
|
|
290
|
+
extname,
|
|
291
|
+
tag: node.tags[0] ?? "default",
|
|
292
|
+
path: node.path
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Resolves the file a schema-emitting plugin (`plugin-ts`, `plugin-zod`, `plugin-faker`) writes
|
|
297
|
+
* `node` to, cached in `ctx.memo` under the resolver's plugin name and the
|
|
298
|
+
* operation. The plugin itself and every plugin that imports from it call this, so they all get
|
|
299
|
+
* the same path.
|
|
300
|
+
*
|
|
301
|
+
* When that path matches the file of a component schema in `ctx.driver.inputNode`, ignoring case, the file name
|
|
302
|
+
* gets an `Operation` suffix: operation `point` goes to `PointOperation.ts` next to schema `Point.ts`.
|
|
303
|
+
* Otherwise the two merge into one file, or overwrite each other on a case-insensitive file system.
|
|
304
|
+
* Only the file name changes. The declarations keep their names.
|
|
305
|
+
*
|
|
306
|
+
* @example Resolve `plugin-ts`'s file for the current operation
|
|
307
|
+
* ```ts
|
|
308
|
+
* const fileTs = resolveOperationFile({ ctx, node, resolver: tsResolver, root, output })
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
function resolveOperationFile(options) {
|
|
312
|
+
const { ctx, node, resolver, root, output } = options;
|
|
313
|
+
const { memo } = ctx;
|
|
314
|
+
const location = {
|
|
315
|
+
root,
|
|
316
|
+
output,
|
|
317
|
+
group: options.group ?? void 0
|
|
318
|
+
};
|
|
319
|
+
return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => {
|
|
320
|
+
const schemaPaths = memo(`${resolver.pluginName}:schemaFilePaths`, () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.name ? [resolver.file({
|
|
321
|
+
name: schema.name,
|
|
322
|
+
extname: ".ts",
|
|
323
|
+
...location
|
|
324
|
+
}).path.toLowerCase()] : [])));
|
|
325
|
+
let file = resolver.file({
|
|
326
|
+
...operationFileEntry(node, node.operationId),
|
|
327
|
+
...location
|
|
328
|
+
});
|
|
329
|
+
for (let name = `${node.operationId}Operation`; schemaPaths.has(file.path.toLowerCase()); name += "Operation") {
|
|
330
|
+
const next = resolver.file({
|
|
331
|
+
...operationFileEntry(node, name),
|
|
332
|
+
...location
|
|
333
|
+
});
|
|
334
|
+
if (next.path === file.path) break;
|
|
335
|
+
file = next;
|
|
336
|
+
}
|
|
337
|
+
return file;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* How a content entry is framed as a stream: `'events'` for Server-Sent Events, `'lines'` for JSON Lines.
|
|
342
|
+
*/
|
|
343
|
+
function getStreamFraming(entry) {
|
|
344
|
+
if (ast.mediaType.getBase(entry) === "text/event-stream") return "events";
|
|
345
|
+
return entry.stream && ast.mediaType.isStream(entry) ? "lines" : void 0;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Parser for one response content entry.
|
|
349
|
+
*
|
|
350
|
+
* JSON stays unset so the runtime detects it. `text/event-stream` and JSON Lines are streams. A binary schema
|
|
351
|
+
* (`format: binary` / `contentMediaType`) is a blob even when the media type is not a well-known
|
|
352
|
+
* binary type. Other `text/*` is text, and `application/octet-stream`, `application/pdf`,
|
|
353
|
+
* `image/*`, `audio/*`, and `video/*` are blobs.
|
|
354
|
+
*/
|
|
355
|
+
function responseTypeForContent(entry) {
|
|
356
|
+
const baseType = ast.mediaType.getBase(entry);
|
|
357
|
+
if (!baseType) return void 0;
|
|
358
|
+
if (getStreamFraming(entry)) return "stream";
|
|
359
|
+
if (ast.mediaType.isJson(baseType)) return void 0;
|
|
360
|
+
if (entry.schema?.type === "blob") return "blob";
|
|
361
|
+
if (baseType.startsWith("text/")) return "text";
|
|
362
|
+
if (isBinaryContentType(baseType)) return "blob";
|
|
363
|
+
}
|
|
364
|
+
function isBinaryContentType(baseType) {
|
|
365
|
+
return baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Maps a content type to the PascalCase suffix used to name per-content-type variants
|
|
369
|
+
* (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
|
|
370
|
+
*/
|
|
371
|
+
function getContentTypeSuffix(contentType) {
|
|
372
|
+
const baseType = contentType.split(";")[0].trim();
|
|
373
|
+
if (baseType === "application/json") return "Json";
|
|
374
|
+
if (baseType === "multipart/form-data") return "FormData";
|
|
375
|
+
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
|
|
376
|
+
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
377
|
+
if (parts.length === 0) return "Unknown";
|
|
378
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
|
|
382
|
+
* (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
|
|
383
|
+
*/
|
|
384
|
+
function getPerContentTypeName(baseName, suffix) {
|
|
385
|
+
if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
|
|
386
|
+
return baseName + suffix;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Returns the entry with the schema of the value the client returns: `string` for a body read as text, such as `Pet` on
|
|
390
|
+
* `application/xml`, and `blob` for `image/*`, `audio/*`, `video/*`, PDF, and octet-stream. A binary schema elsewhere is a `Blob`
|
|
391
|
+
* only through `parseAs: 'blob'`, which needs every entry in `entries` binary. JSON, vendor (`application/vnd.*`), stream,
|
|
392
|
+
* schemaless, and wildcard entries keep their schema. `entries` is the response's full list of content types.
|
|
393
|
+
*/
|
|
394
|
+
function toResponseContent(entry, entries = [entry]) {
|
|
395
|
+
const { contentType, schema } = entry;
|
|
396
|
+
if (!schema || entry.stream) return entry;
|
|
397
|
+
const baseType = ast.mediaType.getBase(contentType);
|
|
398
|
+
const resolved = ast.ref.getSchema(schema);
|
|
399
|
+
if (isBinaryContentType(baseType)) return resolved.type === "blob" ? entry : {
|
|
400
|
+
...entry,
|
|
401
|
+
schema: ast.factory.createSchema({ type: "blob" }),
|
|
402
|
+
keysToOmit: null
|
|
403
|
+
};
|
|
404
|
+
const isPlainMediaType = Boolean(baseType) && !ast.mediaType.isJson(baseType) && !baseType.includes("*");
|
|
141
405
|
if (resolved.type === "blob") return isPlainMediaType && !entries.every((item) => item.schema?.type === "blob") ? {
|
|
142
406
|
...entry,
|
|
143
407
|
schema: ast.factory.createSchema({ type: "string" }),
|
|
@@ -180,50 +444,6 @@ function resolveContentTypeVariants(entries, baseName) {
|
|
|
180
444
|
};
|
|
181
445
|
});
|
|
182
446
|
}
|
|
183
|
-
const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
|
|
184
|
-
/**
|
|
185
|
-
* An OpenAPI 3.2 `querystring` parameter is the whole query string, so a form-encoded object becomes one query parameter
|
|
186
|
-
* per property, typed and serialized like any other.
|
|
187
|
-
* Another content type, such as JSON, stays one parameter named after it, and the runtime sends its value as the whole query string.
|
|
188
|
-
*/
|
|
189
|
-
function spreadQuerystring(param) {
|
|
190
|
-
const schema = param.schema.type === "ref" ? param.schema.schema : param.schema;
|
|
191
|
-
const formEncoded = !param.contentType || param.contentType.split(";")[0].trim().toLowerCase() === "application/x-www-form-urlencoded";
|
|
192
|
-
if (schema?.type !== "object" || !formEncoded) return [param];
|
|
193
|
-
return schema.properties.map((property) => ast.factory.createParameter({
|
|
194
|
-
name: property.name,
|
|
195
|
-
in: "query",
|
|
196
|
-
schema: property.schema,
|
|
197
|
-
required: param.required && property.required
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
|
|
202
|
-
* group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
|
|
203
|
-
* (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
|
|
204
|
-
* same parameters once per plugin.
|
|
205
|
-
*/
|
|
206
|
-
function getOperationParameters(node) {
|
|
207
|
-
const cached = operationParameterGroupsByNode.get(node);
|
|
208
|
-
if (cached) return cached;
|
|
209
|
-
const dedupe = (params) => {
|
|
210
|
-
const seen = /* @__PURE__ */ new Set();
|
|
211
|
-
return params.filter((param) => {
|
|
212
|
-
if (seen.has(param.name)) return false;
|
|
213
|
-
seen.add(param.name);
|
|
214
|
-
return true;
|
|
215
|
-
});
|
|
216
|
-
};
|
|
217
|
-
const group = (location) => dedupe(node.parameters.filter((param) => param.in === location));
|
|
218
|
-
const groups = {
|
|
219
|
-
path: group("path"),
|
|
220
|
-
query: dedupe([...group("query"), ...group("querystring").flatMap(spreadQuerystring)]),
|
|
221
|
-
header: group("header"),
|
|
222
|
-
cookie: group("cookie")
|
|
223
|
-
};
|
|
224
|
-
operationParameterGroupsByNode.set(node, groups);
|
|
225
|
-
return groups;
|
|
226
|
-
}
|
|
227
447
|
/**
|
|
228
448
|
* Whether the operation declares more than one request media type with a schema, so a caller must pick one on `contentType`.
|
|
229
449
|
*/
|
|
@@ -239,8 +459,8 @@ function hasRequestContentTypeChoice(node) {
|
|
|
239
459
|
* than a record.
|
|
240
460
|
*/
|
|
241
461
|
function buildOptionsSchema(node, resolver, { pairContentTypes = false } = {}) {
|
|
242
|
-
const { path, query, header, cookie } =
|
|
243
|
-
const hasBody = Boolean(
|
|
462
|
+
const { path, query, header, cookie } = ast.http.groupParameters(node);
|
|
463
|
+
const hasBody = Boolean(ast.http.requestBody.getSchema(node));
|
|
244
464
|
const createNever = () => ast.factory.createSchema({
|
|
245
465
|
type: "never",
|
|
246
466
|
primitive: void 0,
|
|
@@ -331,9 +551,10 @@ function buildOptionsSchema(node, resolver, { pairContentTypes = false } = {}) {
|
|
|
331
551
|
* The schema a status occupies in the `<Name>Responses` record. A status that documents several
|
|
332
552
|
* content types becomes a `{ contentType; data }` union so the runtime can surface the negotiated type
|
|
333
553
|
* on `result.parsed`, while the standalone `<Name>StatusNNN` alias stays the plain body union that the
|
|
334
|
-
* query hooks and `result.data` use.
|
|
554
|
+
* query hooks and `result.data` use. With `streamData`, the stream variant of a 2xx status holds that schema, since the
|
|
555
|
+
* client returns its body unread.
|
|
335
556
|
*/
|
|
336
|
-
function buildResponseRecordEntry(node, res, resolver) {
|
|
557
|
+
function buildResponseRecordEntry(node, res, resolver, streamData) {
|
|
337
558
|
const statusName = resolver.response.status(node, res.statusCode);
|
|
338
559
|
const variants = (res.content ?? []).filter((entry) => entry.schema);
|
|
339
560
|
if (variants.length <= 1) return ast.factory.createSchema({
|
|
@@ -350,12 +571,12 @@ function buildResponseRecordEntry(node, res, resolver) {
|
|
|
350
571
|
required: true,
|
|
351
572
|
schema: ast.factory.createSchema({
|
|
352
573
|
type: "enum",
|
|
353
|
-
enumValues: [
|
|
574
|
+
enumValues: [ast.mediaType.getBase(variant)]
|
|
354
575
|
})
|
|
355
576
|
}), ast.factory.createProperty({
|
|
356
577
|
name: "data",
|
|
357
578
|
required: true,
|
|
358
|
-
schema: ast.factory.createSchema({
|
|
579
|
+
schema: streamData && ast.http.status.isSuccess(res) && ast.mediaType.isStream(variant) ? streamData : ast.factory.createSchema({
|
|
359
580
|
type: "ref",
|
|
360
581
|
name: variant.name
|
|
361
582
|
})
|
|
@@ -371,29 +592,20 @@ function buildResponseRecordEntry(node, res, resolver) {
|
|
|
371
592
|
* Always emits the keyed record, even when an operation declares no responses. An operation with no
|
|
372
593
|
* responses renders as an empty object, which keeps every consumer's import (for example the axios
|
|
373
594
|
* SDK's `RequestResult<XResponses>`) resolvable instead of pointing at a missing export.
|
|
595
|
+
*
|
|
596
|
+
* `streamData` is the type of a stream variant's `data`, such as a `ReadableStream<Uint8Array>` reference.
|
|
374
597
|
*/
|
|
375
|
-
function buildResponses(node, resolver) {
|
|
598
|
+
function buildResponses(node, resolver, { streamData } = {}) {
|
|
376
599
|
return ast.factory.createSchema({
|
|
377
600
|
type: "object",
|
|
378
601
|
primitive: "object",
|
|
379
602
|
properties: node.responses.map((res) => ast.factory.createProperty({
|
|
380
603
|
name: String(res.statusCode),
|
|
381
604
|
required: true,
|
|
382
|
-
schema: buildResponseRecordEntry(node, res, resolver)
|
|
605
|
+
schema: buildResponseRecordEntry(node, res, resolver, streamData)
|
|
383
606
|
}))
|
|
384
607
|
});
|
|
385
608
|
}
|
|
386
|
-
function getStatusCodeNumber(statusCode) {
|
|
387
|
-
const code = Number(statusCode);
|
|
388
|
-
return Number.isNaN(code) ? null : code;
|
|
389
|
-
}
|
|
390
|
-
function isSuccessStatusCode(statusCode) {
|
|
391
|
-
const code = getStatusCodeNumber(statusCode);
|
|
392
|
-
return code !== null && code >= 200 && code < 300;
|
|
393
|
-
}
|
|
394
|
-
function getSuccessResponses(responses) {
|
|
395
|
-
return responses.filter((response) => isSuccessStatusCode(response.statusCode));
|
|
396
|
-
}
|
|
397
609
|
// ../../internals/shared/src/adapter.ts
|
|
398
610
|
/**
|
|
399
611
|
* Narrows the generic `Adapter` from a generator context to the OpenAPI adapter,
|
|
@@ -407,344 +619,15 @@ function getSuccessResponses(responses) {
|
|
|
407
619
|
* const { dateType } = getOasAdapter(ctx.adapter).options
|
|
408
620
|
* ```
|
|
409
621
|
*/
|
|
410
|
-
function getOasAdapter(adapter) {
|
|
411
|
-
if (adapter.name !== "oas") throw new Diagnostics.Error({
|
|
412
|
-
code: Diagnostics.code.adapterRequired,
|
|
413
|
-
severity: "error",
|
|
414
|
-
message: `Expected the OpenAPI adapter (adapterOas), but received "${adapter.name}".`,
|
|
415
|
-
help: "Set `adapter: adapterOas()` in kubb.config.ts.",
|
|
416
|
-
location: { kind: "config" }
|
|
417
|
-
});
|
|
418
|
-
return adapter;
|
|
419
|
-
}
|
|
420
|
-
// ../../internals/utils/src/reserved.ts
|
|
421
|
-
/**
|
|
422
|
-
* JavaScript and Java reserved words.
|
|
423
|
-
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
424
|
-
*/
|
|
425
|
-
const reservedWords = /* @__PURE__ */ new Set([
|
|
426
|
-
"abstract",
|
|
427
|
-
"arguments",
|
|
428
|
-
"await",
|
|
429
|
-
"boolean",
|
|
430
|
-
"break",
|
|
431
|
-
"byte",
|
|
432
|
-
"case",
|
|
433
|
-
"catch",
|
|
434
|
-
"char",
|
|
435
|
-
"class",
|
|
436
|
-
"const",
|
|
437
|
-
"continue",
|
|
438
|
-
"debugger",
|
|
439
|
-
"default",
|
|
440
|
-
"delete",
|
|
441
|
-
"do",
|
|
442
|
-
"double",
|
|
443
|
-
"else",
|
|
444
|
-
"enum",
|
|
445
|
-
"eval",
|
|
446
|
-
"export",
|
|
447
|
-
"extends",
|
|
448
|
-
"false",
|
|
449
|
-
"final",
|
|
450
|
-
"finally",
|
|
451
|
-
"float",
|
|
452
|
-
"for",
|
|
453
|
-
"function",
|
|
454
|
-
"goto",
|
|
455
|
-
"if",
|
|
456
|
-
"implements",
|
|
457
|
-
"import",
|
|
458
|
-
"in",
|
|
459
|
-
"instanceof",
|
|
460
|
-
"int",
|
|
461
|
-
"interface",
|
|
462
|
-
"let",
|
|
463
|
-
"long",
|
|
464
|
-
"native",
|
|
465
|
-
"new",
|
|
466
|
-
"null",
|
|
467
|
-
"package",
|
|
468
|
-
"private",
|
|
469
|
-
"protected",
|
|
470
|
-
"public",
|
|
471
|
-
"return",
|
|
472
|
-
"short",
|
|
473
|
-
"static",
|
|
474
|
-
"super",
|
|
475
|
-
"switch",
|
|
476
|
-
"synchronized",
|
|
477
|
-
"this",
|
|
478
|
-
"throw",
|
|
479
|
-
"throws",
|
|
480
|
-
"transient",
|
|
481
|
-
"true",
|
|
482
|
-
"try",
|
|
483
|
-
"typeof",
|
|
484
|
-
"var",
|
|
485
|
-
"void",
|
|
486
|
-
"volatile",
|
|
487
|
-
"while",
|
|
488
|
-
"with",
|
|
489
|
-
"yield",
|
|
490
|
-
"Array",
|
|
491
|
-
"Date",
|
|
492
|
-
"hasOwnProperty",
|
|
493
|
-
"Infinity",
|
|
494
|
-
"isFinite",
|
|
495
|
-
"isNaN",
|
|
496
|
-
"isPrototypeOf",
|
|
497
|
-
"length",
|
|
498
|
-
"Math",
|
|
499
|
-
"name",
|
|
500
|
-
"NaN",
|
|
501
|
-
"Number",
|
|
502
|
-
"Object",
|
|
503
|
-
"prototype",
|
|
504
|
-
"String",
|
|
505
|
-
"toString",
|
|
506
|
-
"undefined",
|
|
507
|
-
"valueOf"
|
|
508
|
-
]);
|
|
509
|
-
/**
|
|
510
|
-
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
511
|
-
*
|
|
512
|
-
* @example
|
|
513
|
-
* ```ts
|
|
514
|
-
* isValidVarName('status') // true
|
|
515
|
-
* isValidVarName('class') // false (reserved word)
|
|
516
|
-
* isValidVarName('42foo') // false (starts with digit)
|
|
517
|
-
* ```
|
|
518
|
-
*/
|
|
519
|
-
function isValidVarName(name) {
|
|
520
|
-
if (!name || reservedWords.has(name)) return false;
|
|
521
|
-
return isIdentifier(name);
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* Returns `name` when it's a syntactically valid JavaScript variable name,
|
|
525
|
-
* otherwise prefixes it with `_` so the result is a valid identifier.
|
|
526
|
-
*
|
|
527
|
-
* Useful for sanitizing OpenAPI schema names or operation IDs that start with
|
|
528
|
-
* a digit (e.g. `409`, `504AccountCancel`) before using them as exported
|
|
529
|
-
* variable, type, or function names.
|
|
530
|
-
*
|
|
531
|
-
* @example
|
|
532
|
-
* ```ts
|
|
533
|
-
* ensureValidVarName('409') // '_409'
|
|
534
|
-
* ensureValidVarName('504AccountCancel') // '_504AccountCancel'
|
|
535
|
-
* ensureValidVarName('Pet') // 'Pet'
|
|
536
|
-
* ensureValidVarName('class') // '_class'
|
|
537
|
-
* ```
|
|
538
|
-
*/
|
|
539
|
-
function ensureValidVarName(name) {
|
|
540
|
-
if (!name || isValidVarName(name)) return name;
|
|
541
|
-
return `_${name}`;
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
|
|
545
|
-
*
|
|
546
|
-
* Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
|
|
547
|
-
* even though they are not valid variable names, so use this (not {@link isValidVarName}) when
|
|
548
|
-
* deciding whether an object key needs quoting.
|
|
549
|
-
*
|
|
550
|
-
* @example
|
|
551
|
-
* ```ts
|
|
552
|
-
* isIdentifier('name') // true
|
|
553
|
-
* isIdentifier('x-total')// false
|
|
554
|
-
* ```
|
|
555
|
-
*/
|
|
556
|
-
function isIdentifier(name) {
|
|
557
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
558
|
-
}
|
|
559
|
-
// ../../internals/utils/src/strings.ts
|
|
560
|
-
/**
|
|
561
|
-
* Wraps a value in single quotes for emitting a single-quoted JavaScript string literal, escaping
|
|
562
|
-
* any backslash or single quote in the content.
|
|
563
|
-
*
|
|
564
|
-
* @example
|
|
565
|
-
* ```ts
|
|
566
|
-
* singleQuote('foo') // "'foo'"
|
|
567
|
-
* singleQuote("o'clock") // "'o\\'clock'"
|
|
568
|
-
* ```
|
|
569
|
-
*/
|
|
570
|
-
function singleQuote(value) {
|
|
571
|
-
if (value === void 0 || value === null) return "''";
|
|
572
|
-
return `'${String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
|
|
576
|
-
* Returns the string unchanged when no balanced quote pair is found.
|
|
577
|
-
*
|
|
578
|
-
* @example
|
|
579
|
-
* ```ts
|
|
580
|
-
* trimQuotes('"hello"') // 'hello'
|
|
581
|
-
* trimQuotes('hello') // 'hello'
|
|
582
|
-
* ```
|
|
583
|
-
*/
|
|
584
|
-
function trimQuotes(text) {
|
|
585
|
-
if (text.length >= 2) {
|
|
586
|
-
const first = text[0];
|
|
587
|
-
const last = text[text.length - 1];
|
|
588
|
-
if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`") return text.slice(1, -1);
|
|
589
|
-
}
|
|
590
|
-
return text;
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Serializes a primitive to a single-quoted string literal, stripping any surrounding quotes first.
|
|
594
|
-
*
|
|
595
|
-
* Escaping runs through `JSON.stringify`, then the result switches to single quotes so the generated
|
|
596
|
-
* code matches the repo style without a formatter.
|
|
597
|
-
*
|
|
598
|
-
* @example
|
|
599
|
-
* ```ts
|
|
600
|
-
* stringify('hello') // "'hello'"
|
|
601
|
-
* stringify('"hello"') // "'hello'"
|
|
602
|
-
* ```
|
|
603
|
-
*/
|
|
604
|
-
function stringify(value) {
|
|
605
|
-
if (value === void 0 || value === null) return "''";
|
|
606
|
-
return `'${JSON.stringify(trimQuotes(value.toString())).slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
|
|
607
|
-
}
|
|
608
|
-
const NEEDS_UNICODE = /(?<!\\)(?:\\\\)*\\[pPu]\{|[\u{10000}-\u{10FFFF}]/u;
|
|
609
|
-
const KEPT_ESCAPE = /[A-Za-z0-9^$\\.*+?()[\]{}|/]/;
|
|
610
|
-
/**
|
|
611
|
-
* Drops the backslash from escapes that the `u` flag rejects: an escaped punctuation character that is
|
|
612
|
-
* not a syntax character, and `\-` outside a class. Both match the same character without the backslash.
|
|
613
|
-
*/
|
|
614
|
-
function dropIdentityEscapes(pattern) {
|
|
615
|
-
let inClass = false;
|
|
616
|
-
return pattern.replace(/\\(.)|[[\]]/gs, (match, escaped) => {
|
|
617
|
-
if (escaped === void 0) {
|
|
618
|
-
inClass = match === "[";
|
|
619
|
-
return match;
|
|
620
|
-
}
|
|
621
|
-
return KEPT_ESCAPE.test(escaped) || escaped === "-" && inClass ? match : escaped;
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Builds the `RegExp` for a pattern, adding the `u` flag when it uses `\p{...}`, `\P{...}` or `\u{...}`, or
|
|
626
|
-
* holds a character outside the BMP. A pattern that is not valid under `u` keeps the flags it came with.
|
|
627
|
-
*/
|
|
628
|
-
function toRegExp(pattern, flags) {
|
|
629
|
-
if (flags?.includes("u") || !NEEDS_UNICODE.test(pattern)) return new RegExp(pattern, flags);
|
|
630
|
-
try {
|
|
631
|
-
return new RegExp(dropIdentityEscapes(pattern), `${flags ?? ""}u`);
|
|
632
|
-
} catch {
|
|
633
|
-
return new RegExp(pattern, flags);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
/**
|
|
637
|
-
* Converts a pattern string into a `new RegExp(...)` constructor call or a regex literal string.
|
|
638
|
-
* Inline flags expressed as a `^(?im)` prefix are extracted and applied to the resulting expression.
|
|
639
|
-
* Pass `null` as the second argument to emit a `/pattern/flags` literal instead.
|
|
640
|
-
*
|
|
641
|
-
* @example
|
|
642
|
-
* ```ts
|
|
643
|
-
* toRegExpString('^(?im)foo') // 'new RegExp("^foo", "im")'
|
|
644
|
-
* toRegExpString('^(?im)foo', null) // '/^foo/im'
|
|
645
|
-
* ```
|
|
646
|
-
*/
|
|
647
|
-
function toRegExpString(text, func = "RegExp") {
|
|
648
|
-
const raw = trimQuotes(text);
|
|
649
|
-
const match = raw.match(/^\^(\(\?([igmsuy]+)\))/i);
|
|
650
|
-
const replacementTarget = match?.[1] ?? "";
|
|
651
|
-
const matchedFlags = match?.[2];
|
|
652
|
-
const { source, flags } = toRegExp(raw.replace(/^\\?\//, "").replace(/\\?\/$/, "").replace(replacementTarget, ""), matchedFlags);
|
|
653
|
-
if (func === null) return `/${source}/${flags}`;
|
|
654
|
-
return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ""})`;
|
|
655
|
-
}
|
|
656
|
-
// ../../internals/utils/src/codegen.ts
|
|
657
|
-
const INDENT = " ";
|
|
658
|
-
/**
|
|
659
|
-
* Indents every non-empty line of `text` by one indent level, leaving blank lines empty.
|
|
660
|
-
*/
|
|
661
|
-
function indentLines(text) {
|
|
662
|
-
if (!text) return "";
|
|
663
|
-
return text.split("\n").map((line) => line.trim() ? `${INDENT}${line}` : "").join("\n");
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Renders an object key, quoting it with single quotes only when it is not a valid identifier.
|
|
667
|
-
* Reserved words and globals (`name`, `class`, …) are valid bare keys and stay unquoted.
|
|
668
|
-
*
|
|
669
|
-
* @example
|
|
670
|
-
* ```ts
|
|
671
|
-
* objectKey('name') // 'name'
|
|
672
|
-
* objectKey('x-total') // "'x-total'"
|
|
673
|
-
* ```
|
|
674
|
-
*/
|
|
675
|
-
function objectKey(name) {
|
|
676
|
-
return isIdentifier(name) ? name : singleQuote(name);
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Assembles a multi-line object literal from already-rendered `entries`, indenting each entry one
|
|
680
|
-
* level and closing the brace at column zero. Entries that are themselves multi-line objects indent
|
|
681
|
-
* cumulatively. Each entry ends with a trailing comma to match the formatter's multi-line style.
|
|
682
|
-
*
|
|
683
|
-
* @example
|
|
684
|
-
* ```ts
|
|
685
|
-
* buildObject(['id: z.number()', 'name: z.string()'])
|
|
686
|
-
* // '{\n id: z.number(),\n name: z.string(),\n}'
|
|
687
|
-
* ```
|
|
688
|
-
*/
|
|
689
|
-
function buildObject(entries) {
|
|
690
|
-
if (entries.length === 0) return "{}";
|
|
691
|
-
return `{\n${entries.map((entry) => `${indentLines(entry)},`).join("\n")}\n}`;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Assembles a bracketed list (array by default) from already-rendered `items`. Keeps everything on
|
|
695
|
-
* one line when no item spans multiple lines, and otherwise puts each item on its own line, indented
|
|
696
|
-
* one level with a trailing comma and the closing bracket at column zero. Used for member lists such
|
|
697
|
-
* as `z.union([…])` and `z.array([…])`.
|
|
698
|
-
*
|
|
699
|
-
* @example
|
|
700
|
-
* ```ts
|
|
701
|
-
* buildList(['z.string()', 'z.number()'])
|
|
702
|
-
* // '[z.string(), z.number()]'
|
|
703
|
-
* ```
|
|
704
|
-
*/
|
|
705
|
-
function buildList(items, brackets = ["[", "]"]) {
|
|
706
|
-
const [open, close] = brackets;
|
|
707
|
-
if (items.length === 0) return `${open}${close}`;
|
|
708
|
-
if (!items.some((item) => item.includes("\n"))) return `${open}${items.join(", ")}${close}`;
|
|
709
|
-
return `${open}\n${items.map((item) => `${indentLines(item)},`).join("\n")}\n${close}`;
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Emits a lazy getter for a circular-ref property position, `get name() { return body }`. The key
|
|
713
|
-
* is quoted only when it is not a valid identifier. Used by the string printers to defer evaluation
|
|
714
|
-
* of a recursive schema until first access.
|
|
715
|
-
*
|
|
716
|
-
* @example
|
|
717
|
-
* ```ts
|
|
718
|
-
* lazyGetter({ name: 'parent', body: 'z.lazy(() => Pet)' })
|
|
719
|
-
* // "get parent() { return z.lazy(() => Pet) }"
|
|
720
|
-
* ```
|
|
721
|
-
*/
|
|
722
|
-
function lazyGetter({ name, body }) {
|
|
723
|
-
return `get ${objectKey(name)}() { return ${body} }`;
|
|
724
|
-
}
|
|
725
|
-
// ../../internals/utils/src/fs.ts
|
|
726
|
-
/**
|
|
727
|
-
* Builds a nested file path from a dotted name. Splits on dots that precede a letter
|
|
728
|
-
* (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
|
|
729
|
-
* every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
|
|
730
|
-
*
|
|
731
|
-
* Empty segments are dropped before joining. They arise when the name starts with a dot
|
|
732
|
-
* followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
|
|
733
|
-
* an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
|
|
734
|
-
* absolute path, letting generated files escape the configured output directory.
|
|
735
|
-
*
|
|
736
|
-
* @example Nested path from a dotted name
|
|
737
|
-
* `toFilePath('pet.petId') // 'pet/petId'`
|
|
738
|
-
*
|
|
739
|
-
* @example PascalCase the final segment
|
|
740
|
-
* `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
|
|
741
|
-
*
|
|
742
|
-
* @example Suffix applied to the final segment only
|
|
743
|
-
* `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
|
|
744
|
-
*/
|
|
745
|
-
function toFilePath(name, caseLast = camelCase) {
|
|
746
|
-
const parts = name.split(/\.(?=[a-zA-Z])/);
|
|
747
|
-
return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
|
|
622
|
+
function getOasAdapter(adapter) {
|
|
623
|
+
if (adapter.name !== "oas") throw new Diagnostics.Error({
|
|
624
|
+
code: Diagnostics.code.adapterRequired,
|
|
625
|
+
severity: "error",
|
|
626
|
+
message: `Expected the OpenAPI adapter (adapterOas), but received "${adapter.name}".`,
|
|
627
|
+
help: "Set `adapter: adapterOas()` in kubb.config.ts.",
|
|
628
|
+
location: { kind: "config" }
|
|
629
|
+
});
|
|
630
|
+
return adapter;
|
|
748
631
|
}
|
|
749
632
|
// ../../internals/shared/src/resolver.ts
|
|
750
633
|
/**
|
|
@@ -834,6 +717,13 @@ function createOperationResponseResolver() {
|
|
|
834
717
|
name
|
|
835
718
|
});
|
|
836
719
|
},
|
|
720
|
+
headers(node, statusCode) {
|
|
721
|
+
const name = this.name(`${node.operationId} Status ${statusCode} Headers`);
|
|
722
|
+
return this.resolveName({
|
|
723
|
+
key: operationKey(node, "response", "headers", statusCode),
|
|
724
|
+
name
|
|
725
|
+
});
|
|
726
|
+
},
|
|
837
727
|
body(node) {
|
|
838
728
|
const name = this.name(`${node.operationId} Body`);
|
|
839
729
|
return this.resolveName({
|
|
@@ -875,147 +765,7 @@ function createCasedFile(caseLast) {
|
|
|
875
765
|
* Names of the schemas in a circular reference chain, computed once per build.
|
|
876
766
|
*/
|
|
877
767
|
function getCircularNames(ctx) {
|
|
878
|
-
return ctx.memo("circularNames:set", () => ast.
|
|
879
|
-
}
|
|
880
|
-
// ../../internals/shared/src/group.ts
|
|
881
|
-
/**
|
|
882
|
-
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
|
|
883
|
-
* shared default naming so every plugin groups output consistently:
|
|
884
|
-
*
|
|
885
|
-
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
886
|
-
* - other groups use the camelCased group (`pet store` → `petStore`).
|
|
887
|
-
*
|
|
888
|
-
* A user-provided `group.name` always wins over the default namer, so callers stay in
|
|
889
|
-
* control of their output folders. Returns `null` when grouping is disabled, matching the
|
|
890
|
-
* per-plugin convention.
|
|
891
|
-
*
|
|
892
|
-
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
893
|
-
*
|
|
894
|
-
* @example
|
|
895
|
-
* ```ts
|
|
896
|
-
* createGroupConfig(group) // shared across every plugin
|
|
897
|
-
* ```
|
|
898
|
-
*/
|
|
899
|
-
function createGroupConfig(group) {
|
|
900
|
-
if (!group) return null;
|
|
901
|
-
const defaultName = (ctx) => {
|
|
902
|
-
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
903
|
-
return camelCase(ctx.group);
|
|
904
|
-
};
|
|
905
|
-
return {
|
|
906
|
-
...group,
|
|
907
|
-
name: group.name ? group.name : defaultName
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
// ../../internals/shared/src/schemaTraversal.ts
|
|
911
|
-
/**
|
|
912
|
-
* Maps each property of an object schema to its transformed output. Pairs every result with the
|
|
913
|
-
* original property so the printer keeps full control over modifiers, getters, and key syntax.
|
|
914
|
-
*
|
|
915
|
-
* @example
|
|
916
|
-
* ```ts
|
|
917
|
-
* const entries = mapSchemaProperties(node, (schema) => this.transform(schema))
|
|
918
|
-
* // entries: [{ name: 'id', property, output: 'z.number()' }, ...]
|
|
919
|
-
* ```
|
|
920
|
-
*/
|
|
921
|
-
function mapSchemaProperties(node, transform) {
|
|
922
|
-
return node.properties.map((property) => ({
|
|
923
|
-
name: property.name,
|
|
924
|
-
property,
|
|
925
|
-
output: transform(property.schema)
|
|
926
|
-
}));
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Maps each member of a union or intersection schema to its transformed output, pairing every
|
|
930
|
-
* result with the original member.
|
|
931
|
-
*/
|
|
932
|
-
function mapSchemaMembers(node, transform) {
|
|
933
|
-
return (node.members ?? []).map((schema) => ({
|
|
934
|
-
schema,
|
|
935
|
-
output: transform(schema)
|
|
936
|
-
}));
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Maps each item of an array or tuple schema to its transformed output, pairing every result with
|
|
940
|
-
* the original item.
|
|
941
|
-
*/
|
|
942
|
-
function mapSchemaItems(node, transform) {
|
|
943
|
-
return (node.items ?? []).map((schema) => ({
|
|
944
|
-
schema,
|
|
945
|
-
output: transform(schema)
|
|
946
|
-
}));
|
|
947
|
-
}
|
|
948
|
-
// src/components/Zod.tsx
|
|
949
|
-
function Zod({ name, node, printer, inferTypeName, inferKind = "infer", typeGuards, isName, assertName, mini, cyclic, compile }) {
|
|
950
|
-
const output = printer.print(node);
|
|
951
|
-
const printerImports = printer.drainImports();
|
|
952
|
-
if (!output) return;
|
|
953
|
-
const needsAnnotation = cyclic && node.type !== "object";
|
|
954
|
-
const isBare = ast.isBareRef(node, printer.options.keysToOmit);
|
|
955
|
-
const isCyclic = Boolean(cyclic || printer.options.cyclicSchemas && containsCircularRef(node, { circularSchemas: printer.options.cyclicSchemas }));
|
|
956
|
-
const value = Boolean(compile) && !isBare && !isCyclic ? typeof compile === "object" && compile.strict ? `z.compile(${output}, { strict: true })` : `z.compile(${output})` : output;
|
|
957
|
-
const targetType = inferTypeName ?? `z.infer<typeof ${name}>`;
|
|
958
|
-
const shouldGenerateIs = isName && (typeof typeGuards === "object" ? typeGuards.is ?? true : Boolean(typeGuards));
|
|
959
|
-
const shouldGenerateAssert = assertName && (typeof typeGuards === "object" ? typeGuards.assert ?? true : Boolean(typeGuards));
|
|
960
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
961
|
-
printerImports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
|
|
962
|
-
name: imp.name,
|
|
963
|
-
path: imp.path,
|
|
964
|
-
root: imp.root,
|
|
965
|
-
isTypeOnly: imp.isTypeOnly,
|
|
966
|
-
isNameSpace: imp.isNameSpace
|
|
967
|
-
}, [
|
|
968
|
-
name,
|
|
969
|
-
imp.path,
|
|
970
|
-
imp.name
|
|
971
|
-
].join("-"))),
|
|
972
|
-
/* @__PURE__ */ jsx(File.Source, {
|
|
973
|
-
name,
|
|
974
|
-
isExportable: true,
|
|
975
|
-
isIndexable: true,
|
|
976
|
-
children: /* @__PURE__ */ jsx(Const, {
|
|
977
|
-
export: true,
|
|
978
|
-
name,
|
|
979
|
-
type: needsAnnotation ? "z.ZodType" : void 0,
|
|
980
|
-
children: value
|
|
981
|
-
})
|
|
982
|
-
}),
|
|
983
|
-
inferTypeName && /* @__PURE__ */ jsx(File.Source, {
|
|
984
|
-
name: inferTypeName,
|
|
985
|
-
isExportable: true,
|
|
986
|
-
isIndexable: true,
|
|
987
|
-
isTypeOnly: true,
|
|
988
|
-
children: /* @__PURE__ */ jsx(Type, {
|
|
989
|
-
export: true,
|
|
990
|
-
name: inferTypeName,
|
|
991
|
-
children: `z.${inferKind}<typeof ${name}>`
|
|
992
|
-
})
|
|
993
|
-
}),
|
|
994
|
-
shouldGenerateIs && /* @__PURE__ */ jsx(File.Source, {
|
|
995
|
-
name: isName,
|
|
996
|
-
isExportable: true,
|
|
997
|
-
isIndexable: true,
|
|
998
|
-
children: /* @__PURE__ */ jsx(Const, {
|
|
999
|
-
export: true,
|
|
1000
|
-
name: isName,
|
|
1001
|
-
JSDoc: { comments: [`Type guard for {@link ${name}}`] },
|
|
1002
|
-
children: mini ? `(data: unknown): data is ${targetType} => z.validate(${name}, data)` : `(data: unknown): data is ${targetType} => ${name}.validate(data)`
|
|
1003
|
-
})
|
|
1004
|
-
}),
|
|
1005
|
-
shouldGenerateAssert && /* @__PURE__ */ jsx(File.Source, {
|
|
1006
|
-
name: assertName,
|
|
1007
|
-
isExportable: true,
|
|
1008
|
-
isIndexable: true,
|
|
1009
|
-
children: /* @__PURE__ */ jsx(Function, {
|
|
1010
|
-
export: true,
|
|
1011
|
-
name: assertName,
|
|
1012
|
-
params: "data: unknown",
|
|
1013
|
-
returnType: `asserts data is ${targetType}`,
|
|
1014
|
-
JSDoc: { comments: [`Asserter for {@link ${name}}`, "@throws {z.ZodError} If data is invalid"] },
|
|
1015
|
-
children: mini ? `if (!z.validate(${name}, data)) {\n z.parse(${name}, data)\n}` : `if (!${name}.validate(data)) {\n ${name}.parse(data)\n}`
|
|
1016
|
-
})
|
|
1017
|
-
})
|
|
1018
|
-
] });
|
|
768
|
+
return ctx.memo("circularNames:set", () => ast.graph.getCircularNames(ctx.driver.inputNode?.schemas ?? []));
|
|
1019
769
|
}
|
|
1020
770
|
// src/utils.ts
|
|
1021
771
|
/**
|
|
@@ -1036,7 +786,7 @@ function shouldCoerce(coercion, type) {
|
|
|
1036
786
|
*/
|
|
1037
787
|
function isClosedKeySchema(node) {
|
|
1038
788
|
if (!node) return false;
|
|
1039
|
-
const resolved =
|
|
789
|
+
const resolved = ast.ref.mergeSchema(node);
|
|
1040
790
|
if (resolved.type === "enum") return true;
|
|
1041
791
|
if (resolved.type === "union") {
|
|
1042
792
|
const members = resolved.members ?? [];
|
|
@@ -1046,12 +796,12 @@ function isClosedKeySchema(node) {
|
|
|
1046
796
|
}
|
|
1047
797
|
/**
|
|
1048
798
|
* Whether the node is a plain inline object whose shape can be lifted into an `.extend({ … })`
|
|
1049
|
-
* argument.
|
|
799
|
+
* argument. Input checks, a catchall, `patternProperties`, or a nullable/optional wrapper cannot, so those stay
|
|
1050
800
|
* on `.and(…)`.
|
|
1051
801
|
*/
|
|
1052
802
|
function isPlainInlineObject(node) {
|
|
1053
803
|
if (node.type !== "object") return false;
|
|
1054
|
-
if (node
|
|
804
|
+
if (hasInputChecks(node)) return false;
|
|
1055
805
|
if (node.nullable || node.optional || node.nullish) return false;
|
|
1056
806
|
const hasAdditionalProperties = node.additionalProperties !== void 0;
|
|
1057
807
|
const hasPatternProperties = Boolean(node.patternProperties);
|
|
@@ -1062,21 +812,21 @@ function isPlainInlineObject(node) {
|
|
|
1062
812
|
* `z.discriminatedUnion` option. Covers object nodes, `$ref`s that resolve to (or are still
|
|
1063
813
|
* unresolved) objects, single-member unions of an object, and object-composable `allOf`.
|
|
1064
814
|
*
|
|
1065
|
-
*
|
|
815
|
+
* Input checks, such as property counts and `conditionals`, render as a pipe. A ref into `cyclicSchemas` renders as `z.lazy(…)`.
|
|
1066
816
|
*/
|
|
1067
817
|
function isObjectSchemaNode(node, cyclicSchemas) {
|
|
1068
818
|
if (node.nullable || node.optional || node.nullish) return false;
|
|
819
|
+
if (hasInputChecks(node)) return false;
|
|
1069
820
|
if (node.type === "object") {
|
|
1070
|
-
if (node.minProperties !== void 0 || node.maxProperties !== void 0) return false;
|
|
1071
821
|
const hasProperties = Boolean(node.properties?.length);
|
|
1072
822
|
const hasTypedAdditionalProperties = Boolean(node.additionalProperties && node.additionalProperties !== true);
|
|
1073
823
|
const hasPatternProperties = Boolean(node.patternProperties);
|
|
1074
824
|
return !(!hasProperties && (hasTypedAdditionalProperties || hasPatternProperties || node.propertyNames));
|
|
1075
825
|
}
|
|
1076
826
|
if (node.type === "ref") {
|
|
1077
|
-
const refName = ast.
|
|
827
|
+
const refName = ast.ref.getName(node);
|
|
1078
828
|
if (refName && cyclicSchemas?.has(refName)) return false;
|
|
1079
|
-
const resolved =
|
|
829
|
+
const resolved = ast.ref.mergeSchema(node);
|
|
1080
830
|
return resolved.type === "ref" || isObjectSchemaNode(resolved, cyclicSchemas);
|
|
1081
831
|
}
|
|
1082
832
|
if (node.type === "union") {
|
|
@@ -1099,23 +849,23 @@ function preserveRequiredProperties(members) {
|
|
|
1099
849
|
const hasRequiredProperty = ({ node, propertyName, seen = /* @__PURE__ */ new Set() }) => {
|
|
1100
850
|
if (seen.has(node)) return false;
|
|
1101
851
|
seen.add(node);
|
|
1102
|
-
const resolved =
|
|
1103
|
-
const composition = ast.
|
|
852
|
+
const resolved = ast.ref.mergeSchema(node);
|
|
853
|
+
const composition = ast.schema.narrow(resolved, ast.schema.types.intersection) ?? ast.schema.narrow(resolved, ast.schema.types.union);
|
|
1104
854
|
if (composition) {
|
|
1105
|
-
if (composition.type === ast.
|
|
855
|
+
if (composition.type === ast.schema.types.union && composition.members?.length !== 1) return false;
|
|
1106
856
|
return composition.members?.some((child) => hasRequiredProperty({
|
|
1107
857
|
node: child,
|
|
1108
858
|
propertyName,
|
|
1109
859
|
seen
|
|
1110
860
|
})) ?? false;
|
|
1111
861
|
}
|
|
1112
|
-
return ast.
|
|
862
|
+
return ast.schema.findProperties({
|
|
1113
863
|
node: resolved,
|
|
1114
864
|
propertyName
|
|
1115
865
|
}).some((property) => property.required !== false && !property.schema.optional && !property.schema.nullish);
|
|
1116
866
|
};
|
|
1117
867
|
return members.map((member, index) => {
|
|
1118
|
-
const object = ast.
|
|
868
|
+
const object = ast.schema.narrow(member, ast.schema.types.object);
|
|
1119
869
|
if (!object || index === 0) return member;
|
|
1120
870
|
return {
|
|
1121
871
|
...object,
|
|
@@ -1144,7 +894,7 @@ function preserveRequiredProperties(members) {
|
|
|
1144
894
|
* representation, strings are quoted.
|
|
1145
895
|
*/
|
|
1146
896
|
function formatDefault(value) {
|
|
1147
|
-
if (typeof value === "string") return
|
|
897
|
+
if (typeof value === "string") return text.quote(value);
|
|
1148
898
|
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
1149
899
|
return String(value ?? "");
|
|
1150
900
|
}
|
|
@@ -1159,16 +909,17 @@ function formatDefault(value) {
|
|
|
1159
909
|
*/
|
|
1160
910
|
function defaultLiteral(node, value) {
|
|
1161
911
|
if (value === null) return null;
|
|
1162
|
-
const resolved = node ?
|
|
1163
|
-
if (resolved
|
|
912
|
+
const resolved = node ? ast.ref.mergeSchema(node) : void 0;
|
|
913
|
+
if (resolved?.type === "blob") return null;
|
|
914
|
+
if (resolved && ast.schema.narrow(resolved, "bigint")) {
|
|
1164
915
|
if (typeof value === "bigint") return `BigInt(${value})`;
|
|
1165
916
|
if (typeof value === "number" && Number.isInteger(value)) return `BigInt(${value})`;
|
|
1166
917
|
return null;
|
|
1167
918
|
}
|
|
1168
|
-
if (resolved && ast.
|
|
1169
|
-
const enumNode = resolved ? ast.
|
|
919
|
+
if (resolved && ast.schema.narrow(resolved, "array")) return Array.isArray(value) ? JSON.stringify(value) : null;
|
|
920
|
+
const enumNode = resolved ? ast.schema.narrow(resolved, "enum") : void 0;
|
|
1170
921
|
if (enumNode) {
|
|
1171
|
-
const values =
|
|
922
|
+
const values = ast.schema.getLiterals(enumNode);
|
|
1172
923
|
if (values.length) {
|
|
1173
924
|
const match = values.find((member) => member === value || String(member) === String(value));
|
|
1174
925
|
return match != null ? formatDefault(match) : null;
|
|
@@ -1237,13 +988,13 @@ function patternKeySchemaMini({ patterns, regexType }) {
|
|
|
1237
988
|
* Build `.min()` / `.max()` / `.gt()` / `.lt()` constraint chains for numbers
|
|
1238
989
|
* using the standard chainable Zod v4 API.
|
|
1239
990
|
*/
|
|
1240
|
-
function numberConstraints({ min, max,
|
|
991
|
+
function numberConstraints({ min, max, exclusive, multipleOf }, suffix = "") {
|
|
1241
992
|
return [
|
|
1242
|
-
min !== void 0 ? `.min(${min})` : "",
|
|
1243
|
-
max !== void 0 ? `.max(${max})` : "",
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
multipleOf !== void 0 ? `.multipleOf(${multipleOf})` : ""
|
|
993
|
+
min !== void 0 ? `.min(${min}${suffix})` : "",
|
|
994
|
+
max !== void 0 ? `.max(${max}${suffix})` : "",
|
|
995
|
+
exclusive?.min !== void 0 ? `.gt(${exclusive.min}${suffix})` : "",
|
|
996
|
+
exclusive?.max !== void 0 ? `.lt(${exclusive.max}${suffix})` : "",
|
|
997
|
+
multipleOf !== void 0 ? `.multipleOf(${multipleOf}${suffix})` : ""
|
|
1247
998
|
].join("");
|
|
1248
999
|
}
|
|
1249
1000
|
/**
|
|
@@ -1258,36 +1009,71 @@ function lengthConstraints({ min, max, pattern, regexType }) {
|
|
|
1258
1009
|
].join("");
|
|
1259
1010
|
}
|
|
1260
1011
|
/**
|
|
1012
|
+
* Numeric bounds for a bigint: the inclusive, whole, safe bounds faker mocks against, and a whole, safe `multipleOf`.
|
|
1013
|
+
*/
|
|
1014
|
+
function bigintConstraints({ min, max, exclusive, multipleOf }) {
|
|
1015
|
+
return {
|
|
1016
|
+
...ast.schema.getNumberBounds({
|
|
1017
|
+
min,
|
|
1018
|
+
max,
|
|
1019
|
+
exclusive,
|
|
1020
|
+
integer: true
|
|
1021
|
+
}),
|
|
1022
|
+
multipleOf: Number.isSafeInteger(multipleOf) ? multipleOf : void 0
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1261
1026
|
* Build `.check(z.minimum(), z.maximum())` for `zod/mini` numeric constraints.
|
|
1262
1027
|
*/
|
|
1263
|
-
function numberChecksMini({ min, max,
|
|
1264
|
-
const checks = [
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1028
|
+
function numberChecksMini({ min, max, exclusive, multipleOf }, suffix = "") {
|
|
1029
|
+
const checks = [
|
|
1030
|
+
min !== void 0 && `z.minimum(${min}${suffix})`,
|
|
1031
|
+
max !== void 0 && `z.maximum(${max}${suffix})`,
|
|
1032
|
+
exclusive?.min !== void 0 && `z.gt(${exclusive.min}${suffix})`,
|
|
1033
|
+
exclusive?.max !== void 0 && `z.lt(${exclusive.max}${suffix})`,
|
|
1034
|
+
multipleOf !== void 0 && `z.multipleOf(${multipleOf}${suffix})`
|
|
1035
|
+
].filter(Boolean);
|
|
1270
1036
|
return checks.length ? `.check(${checks.join(", ")})` : "";
|
|
1271
1037
|
}
|
|
1272
1038
|
/**
|
|
1273
1039
|
* Build `.check(z.minLength(), z.maxLength(), z.regex())` for `zod/mini` length constraints.
|
|
1274
1040
|
*/
|
|
1275
1041
|
function lengthChecksMini({ min, max, pattern, regexType }) {
|
|
1276
|
-
const checks = [
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1042
|
+
const checks = [
|
|
1043
|
+
min !== void 0 && `z.minLength(${min})`,
|
|
1044
|
+
max !== void 0 && `z.maxLength(${max})`,
|
|
1045
|
+
pattern !== void 0 && `z.regex(${regexSource(pattern, regexType)})`
|
|
1046
|
+
].filter(Boolean);
|
|
1280
1047
|
return checks.length ? `.check(${checks.join(", ")})` : "";
|
|
1281
1048
|
}
|
|
1282
1049
|
/**
|
|
1283
|
-
*
|
|
1050
|
+
* Whether `propertyNames` needs its own check: `z.record` checks the keys of a map, but `z.object` and a record keyed by
|
|
1051
|
+
* `patternProperties` do not.
|
|
1052
|
+
*/
|
|
1053
|
+
function hasKeyCheck(node) {
|
|
1054
|
+
if (node.type !== "object" || !node.propertyNames) return false;
|
|
1055
|
+
const hasTypedAdditionalProperties = Boolean(node.additionalProperties && node.additionalProperties !== true);
|
|
1056
|
+
return node.properties.length > 0 || Boolean(node.patternProperties) && !hasTypedAdditionalProperties;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Whether the node prints inside `z.pipe(z.unknown().check(…), …)`, see {@link inputChecks}.
|
|
1060
|
+
*/
|
|
1061
|
+
function hasInputChecks(node) {
|
|
1062
|
+
return Boolean(node.conditionals?.length) || node.type === "object" && (node.min !== void 0 || node.max !== void 0) || hasKeyCheck(node);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Checks the raw input, before parsing strips unknown keys or inserts defaults: property counts, key names, then the
|
|
1066
|
+
* printed `conditionals`.
|
|
1284
1067
|
*/
|
|
1285
|
-
function
|
|
1286
|
-
if (node.type !== "object") return value;
|
|
1287
|
-
const checks = [];
|
|
1068
|
+
function inputChecks({ value, node, keySchema, conditionals }) {
|
|
1288
1069
|
const skip = "typeof value !== \"object\" || value === null || Array.isArray(value)";
|
|
1289
|
-
|
|
1290
|
-
|
|
1070
|
+
const { min, max } = node.type === "object" ? node : {};
|
|
1071
|
+
const checks = [
|
|
1072
|
+
min !== void 0 && `z.refine(value => ${skip} || Object.keys(value).length >= ${min}, { message: "Object must have at least ${min} properties" })`,
|
|
1073
|
+
max !== void 0 && `z.refine(value => ${skip} || Object.keys(value).length <= ${max}, { message: "Object must have at most ${max} properties" })`,
|
|
1074
|
+
keySchema && `z.refine(value => ${skip} || Object.keys(value).every((key) => ${keySchema}.safeParse(key).success), { message: "Object keys must match the property names schema" })`,
|
|
1075
|
+
...conditionals
|
|
1076
|
+
].filter(Boolean);
|
|
1291
1077
|
return checks.length ? `z.pipe(z.unknown().check(${checks.join(", ")}), ${value})` : value;
|
|
1292
1078
|
}
|
|
1293
1079
|
/**
|
|
@@ -1303,8 +1089,8 @@ function applyModifiers({ value, schema, nullable, optional, nullish, defaultVal
|
|
|
1303
1089
|
})();
|
|
1304
1090
|
const literal = defaultValue !== void 0 ? defaultLiteral(schema, defaultValue) : null;
|
|
1305
1091
|
const withDefault = literal !== null ? `${withModifier}.default(${literal})` : withModifier;
|
|
1306
|
-
const withDescription = description ? `${withDefault}.describe(${
|
|
1307
|
-
return examples?.length ? `${withDescription}.meta({ examples: [${examples.map(formatDefault).join(", ")}] })` : withDescription;
|
|
1092
|
+
const withDescription = description ? `${withDefault}.describe(${text.quote(description)})` : withDefault;
|
|
1093
|
+
return examples?.length ? `${withDescription}.meta({ examples: [${examples.map((example) => example === null ? "null" : formatDefault(example)).join(", ")}] })` : withDescription;
|
|
1308
1094
|
}
|
|
1309
1095
|
function modifierDepth(schema) {
|
|
1310
1096
|
if (schema.nullish || schema.nullable && schema.optional) return 2;
|
|
@@ -1322,7 +1108,7 @@ function modifierDepth(schema) {
|
|
|
1322
1108
|
* `.unwrap()`, so its printer passes `._zod.def.innerType` as the `step`.
|
|
1323
1109
|
*/
|
|
1324
1110
|
function omitUnwrapChain(node, step = ".unwrap()") {
|
|
1325
|
-
const ref = ast.
|
|
1111
|
+
const ref = ast.schema.narrow(node, "ref");
|
|
1326
1112
|
if (!ref) return "";
|
|
1327
1113
|
const target = ref.schema ?? ref;
|
|
1328
1114
|
const depth = modifierDepth(target) + (target.default !== void 0 ? 1 : 0);
|
|
@@ -1344,7 +1130,7 @@ function applyMiniModifiers({ value, schema, nullable, optional, nullish, defaul
|
|
|
1344
1130
|
/**
|
|
1345
1131
|
* Calls for string `format`s with a dedicated Zod constructor; a `Map` so `constructor` cannot hit `Object.prototype`.
|
|
1346
1132
|
*/
|
|
1347
|
-
const
|
|
1133
|
+
const stringFormatCalls = /* @__PURE__ */ new Map([
|
|
1348
1134
|
["byte", "z.base64()"],
|
|
1349
1135
|
["base64", "z.base64()"],
|
|
1350
1136
|
["base64url", "z.base64url()"],
|
|
@@ -1355,6 +1141,16 @@ const STRING_FORMAT_CALLS = /* @__PURE__ */ new Map([
|
|
|
1355
1141
|
["hostname", "z.hostname()"]
|
|
1356
1142
|
]);
|
|
1357
1143
|
/**
|
|
1144
|
+
* Calls for a string's `contentEncoding` with a dedicated Zod constructor.
|
|
1145
|
+
*/
|
|
1146
|
+
const contentEncodingCalls = /* @__PURE__ */ new Map([["base64", "z.base64()"], ["base64url", "z.base64url()"]]);
|
|
1147
|
+
/**
|
|
1148
|
+
* The check for a string that holds a JSON document: it must parse, and the parsed value must pass `schema`.
|
|
1149
|
+
*/
|
|
1150
|
+
function jsonContentCheck(schema) {
|
|
1151
|
+
return `(value) => { try { return ${schema}.safeParse(JSON.parse(value)).success } catch { return false } }, { message: "String must hold JSON that matches its content schema" }`;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1358
1154
|
* Marks an inline `oneOf` object strict, so `object()` builds `z.strictObject(...)` and no eager `.shape` read hits a deferred getter.
|
|
1359
1155
|
*/
|
|
1360
1156
|
function strictOneOfNode(node) {
|
|
@@ -1367,13 +1163,14 @@ function strictOneOfNode(node) {
|
|
|
1367
1163
|
* The constraint chain an `allOf` member adds to a bare string, number or array; each printer passes its own renderers.
|
|
1368
1164
|
*/
|
|
1369
1165
|
function getMemberConstraint({ member, regexType, lengths, numbers }) {
|
|
1166
|
+
if (hasInputChecks(member) || ast.schema.narrow(member, "array")?.contains) return void 0;
|
|
1370
1167
|
if (member.primitive === "string") return lengths({
|
|
1371
|
-
...ast.
|
|
1168
|
+
...ast.schema.narrow(member, "string") ?? {},
|
|
1372
1169
|
regexType
|
|
1373
1170
|
}) || void 0;
|
|
1374
|
-
if (member.primitive === "number" || member.primitive === "integer") return numbers(ast.
|
|
1171
|
+
if (member.primitive === "number" || member.primitive === "integer") return numbers(ast.schema.narrow(member, "number") ?? ast.schema.narrow(member, "integer") ?? {}) || void 0;
|
|
1375
1172
|
if (member.primitive === "array") return lengths({
|
|
1376
|
-
...ast.
|
|
1173
|
+
...ast.schema.narrow(member, "array") ?? {},
|
|
1377
1174
|
regexType
|
|
1378
1175
|
}) || void 0;
|
|
1379
1176
|
}
|
|
@@ -1381,24 +1178,26 @@ function getMemberConstraint({ member, regexType, lengths, numbers }) {
|
|
|
1381
1178
|
* Builds the `{ key: value, … }` shape shared by the `z.object(...)` and `.extend(...)` renderings.
|
|
1382
1179
|
*/
|
|
1383
1180
|
function buildObjectShape(ctx, node, renderProperty) {
|
|
1384
|
-
const objectNode = ast.
|
|
1181
|
+
const objectNode = ast.schema.narrow(node, "object");
|
|
1385
1182
|
if (!objectNode) return "{}";
|
|
1386
|
-
const isCyclic = (schema) => ctx.options.cyclicSchemas != null &&
|
|
1387
|
-
|
|
1183
|
+
const isCyclic = (schema) => ctx.options.cyclicSchemas != null && ast.graph.containsCircular(schema, { circularSchemas: ctx.options.cyclicSchemas });
|
|
1184
|
+
const transformed = objectNode.properties.map(({ schema }) => {
|
|
1388
1185
|
const hasSelfRef = isCyclic(schema);
|
|
1389
1186
|
const savedCyclicSchemas = ctx.options.cyclicSchemas;
|
|
1390
1187
|
if (hasSelfRef) ctx.options.cyclicSchemas = void 0;
|
|
1391
1188
|
const baseOutput = ctx.transform(schema) ?? ctx.transform(ast.factory.createSchema({ type: "unknown" }));
|
|
1392
1189
|
if (hasSelfRef) ctx.options.cyclicSchemas = savedCyclicSchemas;
|
|
1393
1190
|
return baseOutput;
|
|
1394
|
-
})
|
|
1395
|
-
|
|
1191
|
+
});
|
|
1192
|
+
return buildObject(objectNode.properties.map((property, index) => {
|
|
1193
|
+
const { name, schema } = property;
|
|
1194
|
+
const value = transformed[index];
|
|
1396
1195
|
return renderProperty({
|
|
1397
1196
|
name,
|
|
1398
1197
|
value,
|
|
1399
1198
|
schema,
|
|
1400
1199
|
required: property.required,
|
|
1401
|
-
meta:
|
|
1200
|
+
meta: ast.ref.mergeSchema(schema),
|
|
1402
1201
|
cyclic: isCyclic(schema)
|
|
1403
1202
|
});
|
|
1404
1203
|
}));
|
|
@@ -1410,14 +1209,14 @@ function printWithOmit(ctx, node, modify, mini = false) {
|
|
|
1410
1209
|
const { keysToOmit } = ctx.options;
|
|
1411
1210
|
const transformed = ctx.transform(node);
|
|
1412
1211
|
if (!transformed) return null;
|
|
1413
|
-
const meta =
|
|
1212
|
+
const meta = ast.ref.mergeSchema(node);
|
|
1414
1213
|
return modify((() => {
|
|
1415
1214
|
if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
|
|
1416
1215
|
const unwrap = omitUnwrapChain(node, mini ? "._zod.def.innerType" : ".unwrap()");
|
|
1417
1216
|
const mask = `{ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }`;
|
|
1418
1217
|
const omit = (target) => {
|
|
1419
1218
|
const inner = `${target}${unwrap}`;
|
|
1420
|
-
const hasObjectConstraints =
|
|
1219
|
+
const hasObjectConstraints = hasInputChecks(meta);
|
|
1421
1220
|
const pipe = mini ? `${inner}._zod.def` : inner;
|
|
1422
1221
|
const object = hasObjectConstraints ? `${pipe}.out` : inner;
|
|
1423
1222
|
const omitted = mini ? `z.omit(${object}, ${mask})` : `${object}.omit(${mask})`;
|
|
@@ -1436,7 +1235,7 @@ function printWithOmit(ctx, node, modify, mini = false) {
|
|
|
1436
1235
|
function getLiteralUnion({ node, options }) {
|
|
1437
1236
|
if (node.strategy === "one" && options.oneOfType === "xor" || options.nodes?.enum) return void 0;
|
|
1438
1237
|
const values = (node.members ?? []).map((member) => {
|
|
1439
|
-
const enumNode = ast.
|
|
1238
|
+
const enumNode = ast.schema.narrow(member, ast.schema.types.enum);
|
|
1440
1239
|
return enumNode && !enumNode.name && !enumNode.nullable && !enumNode.namedEnumValues && enumNode.enumValues?.length === 1 ? enumNode.enumValues[0] : void 0;
|
|
1441
1240
|
});
|
|
1442
1241
|
if (!values.length || !values.every((value) => value !== void 0 && value !== null)) return void 0;
|
|
@@ -1449,11 +1248,11 @@ function getLiteralUnion({ node, options }) {
|
|
|
1449
1248
|
* Whether a discriminated union member's discriminator property prints as a coerced literal union.
|
|
1450
1249
|
*/
|
|
1451
1250
|
function hasCoercedDiscriminator({ member, propertyName, options }) {
|
|
1452
|
-
return ast.
|
|
1251
|
+
return ast.schema.findProperties({
|
|
1453
1252
|
node: member,
|
|
1454
1253
|
propertyName
|
|
1455
1254
|
}).some(({ schema }) => {
|
|
1456
|
-
const resolved =
|
|
1255
|
+
const resolved = ast.ref.mergeSchema(schema);
|
|
1457
1256
|
return resolved.type === "union" && !!getLiteralUnion({
|
|
1458
1257
|
node: resolved,
|
|
1459
1258
|
options
|
|
@@ -1461,7 +1260,48 @@ function hasCoercedDiscriminator({ member, propertyName, options }) {
|
|
|
1461
1260
|
});
|
|
1462
1261
|
}
|
|
1463
1262
|
/**
|
|
1464
|
-
*
|
|
1263
|
+
* Whether `z.compile(schema, { strict: true })` throws for the node. Zod runs an unsupported schema below an object
|
|
1264
|
+
* property or an array, tuple or record item on its runtime parser, so only a coerced scalar or a `z.xor` that compiles
|
|
1265
|
+
* inline with the root throws: the node itself, or one reached through refs and union or intersection members.
|
|
1266
|
+
* `siblings` resolves a generated ref, which names another schema of the same file and carries no target.
|
|
1267
|
+
*
|
|
1268
|
+
* Mirrors the built-in handlers, so a type with a `printer.nodes` override is judged as if it printed the built-in output.
|
|
1269
|
+
*/
|
|
1270
|
+
function throwsOnStrictCompile({ node, options, siblings, seen = /* @__PURE__ */ new Set() }) {
|
|
1271
|
+
if (!node || seen.has(node)) return false;
|
|
1272
|
+
seen.add(node);
|
|
1273
|
+
const throws = (child) => throwsOnStrictCompile({
|
|
1274
|
+
node: child,
|
|
1275
|
+
options,
|
|
1276
|
+
siblings,
|
|
1277
|
+
seen
|
|
1278
|
+
});
|
|
1279
|
+
switch (node.type) {
|
|
1280
|
+
case ast.schema.types.ref: return throws(node.schema ?? (node.ref || !node.name ? void 0 : siblings?.get(node.name)));
|
|
1281
|
+
case ast.schema.types.union:
|
|
1282
|
+
if (node.strategy === "one" && options.oneOfType === "xor") return true;
|
|
1283
|
+
return !!getLiteralUnion({
|
|
1284
|
+
node,
|
|
1285
|
+
options
|
|
1286
|
+
})?.coerce || (node.members ?? []).some(throws);
|
|
1287
|
+
case ast.schema.types.intersection: return (node.members ?? []).some(throws);
|
|
1288
|
+
case ast.schema.types.bigint: return true;
|
|
1289
|
+
case ast.schema.types.string: return shouldCoerce(options.coercion, "strings");
|
|
1290
|
+
case ast.schema.types.number:
|
|
1291
|
+
case ast.schema.types.integer: return shouldCoerce(options.coercion, "numbers");
|
|
1292
|
+
case ast.schema.types.date: return node.representation === "date" && options.direction !== "encode" && shouldCoerce(options.coercion, "dates");
|
|
1293
|
+
default: return false;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function hasPlainDiscriminator({ member, propertyName }) {
|
|
1297
|
+
const schemas = ast.schema.findProperties({
|
|
1298
|
+
node: member,
|
|
1299
|
+
propertyName
|
|
1300
|
+
});
|
|
1301
|
+
return schemas.length > 0 && !schemas.some(({ schema }) => ast.schema.getLiterals(ast.ref.mergeSchema(schema)).length > 0);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Builds the scalar handlers both dialects share. Only a string with a `contentSchema` recurses into a child. The temporal
|
|
1465
1305
|
* scalars and `bigint` differ per dialect, so each printer adds its own.
|
|
1466
1306
|
*/
|
|
1467
1307
|
function buildScalarNodes(dialect) {
|
|
@@ -1474,18 +1314,20 @@ function buildScalarNodes(dialect) {
|
|
|
1474
1314
|
null: () => "z.null()",
|
|
1475
1315
|
string(node) {
|
|
1476
1316
|
const coerce = shouldCoerce(this.options.coercion, "strings");
|
|
1477
|
-
const
|
|
1478
|
-
if (call) return `${call}${dialect.lengths({
|
|
1479
|
-
...node,
|
|
1480
|
-
regexType: this.options.regexType
|
|
1481
|
-
})}`;
|
|
1482
|
-
const base = coerce ? "z.coerce.string()" : "z.string()";
|
|
1317
|
+
const base = (coerce ? void 0 : stringFormatCalls.get(node.format) ?? contentEncodingCalls.get(node.content?.encoding?.toLowerCase())) ?? (coerce ? "z.coerce.string()" : "z.string()");
|
|
1483
1318
|
const pattern = node.pattern ?? integerFormatPattern(node.format);
|
|
1484
|
-
|
|
1319
|
+
const value = `${base}${dialect.lengths({
|
|
1485
1320
|
...node,
|
|
1486
1321
|
pattern,
|
|
1487
1322
|
regexType: this.options.regexType
|
|
1488
1323
|
})}`;
|
|
1324
|
+
const contentNode = ast.schema.getEmbeddedJson(node);
|
|
1325
|
+
if (!contentNode) return value;
|
|
1326
|
+
const direction = this.options.direction;
|
|
1327
|
+
this.options.direction = "decode";
|
|
1328
|
+
const contentSchema = this.transform(contentNode);
|
|
1329
|
+
this.options.direction = direction;
|
|
1330
|
+
return contentSchema ? dialect.refine(value, jsonContentCheck(contentSchema)) : value;
|
|
1489
1331
|
},
|
|
1490
1332
|
number(node) {
|
|
1491
1333
|
return `${shouldCoerce(this.options.coercion, "numbers") ? "z.coerce.number()" : "z.number()"}${dialect.numbers(node)}`;
|
|
@@ -1518,7 +1360,7 @@ function buildScalarNodes(dialect) {
|
|
|
1518
1360
|
ipv6: () => "z.ipv6()",
|
|
1519
1361
|
blob: () => "z.instanceof(Blob)",
|
|
1520
1362
|
enum(node) {
|
|
1521
|
-
const nonNullValues =
|
|
1363
|
+
const nonNullValues = ast.schema.getLiterals(node).filter((v) => v !== null);
|
|
1522
1364
|
if (node.namedEnumValues?.length) {
|
|
1523
1365
|
const literals = nonNullValues.map((v) => buildLiteral(v));
|
|
1524
1366
|
if (literals.length === 1) return literals[0];
|
|
@@ -1537,11 +1379,12 @@ function buildScalarNodes(dialect) {
|
|
|
1537
1379
|
*/
|
|
1538
1380
|
function buildZodNodes(dialect, cyclicSchemas) {
|
|
1539
1381
|
const unionsWithSiblings = /* @__PURE__ */ new WeakSet();
|
|
1382
|
+
const printNullable = (schema, output) => output && schema.nullable ? dialect.nullable(output) : output;
|
|
1540
1383
|
return {
|
|
1541
1384
|
...buildScalarNodes(dialect),
|
|
1542
1385
|
ref(node) {
|
|
1543
1386
|
if (!node.name) return null;
|
|
1544
|
-
const refName = ast.
|
|
1387
|
+
const refName = ast.ref.getName(node);
|
|
1545
1388
|
if (!refName) return null;
|
|
1546
1389
|
const useInputVariant = node.ref != null && (dialect.useInputVariant?.(node, this.options) ?? false);
|
|
1547
1390
|
const resolvedName = node.ref ? useInputVariant ? this.options.resolver?.schema.inputName(refName) ?? refName : this.options.resolver?.name(refName) ?? refName : node.name;
|
|
@@ -1556,47 +1399,50 @@ function buildZodNodes(dialect, cyclicSchemas) {
|
|
|
1556
1399
|
const isStrict = node.additionalProperties === false && patterns.length === 0;
|
|
1557
1400
|
const isLoose = node.additionalProperties === true;
|
|
1558
1401
|
const objectBase = `${isStrict ? "z.strictObject" : isLoose ? "z.looseObject" : "z.object"}(${dialect.objectShape(this, node)})`;
|
|
1559
|
-
return
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
})(),
|
|
1588
|
-
node
|
|
1589
|
-
});
|
|
1402
|
+
return (() => {
|
|
1403
|
+
const recordFn = isClosedKeySchema(propertyNamesNode) ? "z.partialRecord" : "z.record";
|
|
1404
|
+
if (node.additionalProperties && node.additionalProperties !== true) {
|
|
1405
|
+
const catchallType = this.transform(node.additionalProperties);
|
|
1406
|
+
if (entries.length === 0) return catchallType ? `${recordFn}(${propertyNamesKeySchema || "z.string()"}, ${catchallType})` : objectBase;
|
|
1407
|
+
return catchallType ? dialect.catchall(objectBase, catchallType) : objectBase;
|
|
1408
|
+
}
|
|
1409
|
+
if (entries.length === 0 && patterns.length === 0 && propertyNamesKeySchema && !isStrict) {
|
|
1410
|
+
const unknownType = this.transform(ast.factory.createSchema({ type: ast.schema.types.unknown }));
|
|
1411
|
+
return `${recordFn}(${propertyNamesKeySchema}, ${unknownType})`;
|
|
1412
|
+
}
|
|
1413
|
+
if (node.additionalProperties === true) return objectBase;
|
|
1414
|
+
if (isStrict) return objectBase;
|
|
1415
|
+
if (patterns.length > 0) {
|
|
1416
|
+
const values = patterns.map(([, valueSchema]) => {
|
|
1417
|
+
const valueType = this.transform(valueSchema) ?? this.transform(ast.factory.createSchema({ type: ast.schema.types.unknown }));
|
|
1418
|
+
return valueSchema.nullable ? dialect.nullable(valueType) : valueType;
|
|
1419
|
+
});
|
|
1420
|
+
const distinct = [...new Set(values)];
|
|
1421
|
+
const value = distinct.length === 1 ? distinct[0] : `z.union([${distinct.join(", ")}])`;
|
|
1422
|
+
if (entries.length > 0) return dialect.catchall(objectBase, value);
|
|
1423
|
+
return `z.record(${dialect.patternKey({
|
|
1424
|
+
patterns: patterns.map(([pattern]) => pattern),
|
|
1425
|
+
regexType: this.options.regexType
|
|
1426
|
+
})}, ${value})`;
|
|
1427
|
+
}
|
|
1428
|
+
return objectBase;
|
|
1429
|
+
})();
|
|
1590
1430
|
},
|
|
1591
1431
|
array(node) {
|
|
1592
|
-
const base = `z.array(${
|
|
1432
|
+
const base = `z.array(${(node.items ?? []).map((item) => printNullable(item, this.transform(item))).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: ast.schema.types.unknown }))})${dialect.lengths({
|
|
1593
1433
|
...node,
|
|
1594
1434
|
regexType: this.options.regexType
|
|
1595
1435
|
})}`;
|
|
1596
|
-
|
|
1436
|
+
const unique = node.unique ? dialect.unique(base) : base;
|
|
1437
|
+
const contains = node.contains && containsCheck(this, node.contains);
|
|
1438
|
+
return contains ? dialect.refine(unique, contains) : unique;
|
|
1597
1439
|
},
|
|
1598
1440
|
tuple(node) {
|
|
1599
|
-
|
|
1441
|
+
const items = (node.items ?? []).map((item) => printNullable(item, this.transform(item))).filter(Boolean);
|
|
1442
|
+
const rest = node.rest ? this.transform(node.rest) : void 0;
|
|
1443
|
+
const tuple = `z.tuple(${buildList(items)}${rest ? `, ${rest}` : ""})`;
|
|
1444
|
+
const contains = node.contains && containsCheck(this, node.contains);
|
|
1445
|
+
return contains ? dialect.refine(tuple, contains) : tuple;
|
|
1600
1446
|
},
|
|
1601
1447
|
union(node) {
|
|
1602
1448
|
const literalUnion = getLiteralUnion({
|
|
@@ -1609,24 +1455,30 @@ function buildZodNodes(dialect, cyclicSchemas) {
|
|
|
1609
1455
|
}
|
|
1610
1456
|
const nodeMembers = node.members ?? [];
|
|
1611
1457
|
const { discriminatorPropertyName } = node;
|
|
1612
|
-
const isDiscriminated = !!discriminatorPropertyName && nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemas)
|
|
1458
|
+
const isDiscriminated = !!discriminatorPropertyName && nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemas) && !hasPlainDiscriminator({
|
|
1459
|
+
member: m,
|
|
1460
|
+
propertyName: discriminatorPropertyName
|
|
1461
|
+
})) && !nodeMembers.some((m) => hasCoercedDiscriminator({
|
|
1613
1462
|
member: m,
|
|
1614
1463
|
propertyName: discriminatorPropertyName,
|
|
1615
1464
|
options: this.options
|
|
1616
1465
|
}));
|
|
1617
1466
|
const isXor = node.strategy === "one" && !isDiscriminated && this.options.oneOfType === "xor";
|
|
1618
|
-
const isStrict = node.strategy === "one" && !isXor && !unionsWithSiblings.has(node);
|
|
1619
|
-
const members =
|
|
1467
|
+
const isStrict = node.strategy === "one" && !isXor && !isDiscriminated && !unionsWithSiblings.has(node);
|
|
1468
|
+
const members = (node.members ?? []).map((memberNode) => {
|
|
1469
|
+
const output = this.transform(isStrict ? strictOneOfNode(memberNode) : memberNode);
|
|
1470
|
+
return output && isStrict && dialect.strictMember ? dialect.strictMember(output, memberNode, cyclicSchemas) : output;
|
|
1471
|
+
}).map((output, index) => printNullable(nodeMembers[index], output)).filter(Boolean);
|
|
1620
1472
|
if (members.length === 0) return "";
|
|
1621
1473
|
if (members.length === 1) return members[0];
|
|
1622
|
-
if (isDiscriminated) return `z.discriminatedUnion(${
|
|
1474
|
+
if (isDiscriminated) return `z.discriminatedUnion(${text.quote(node.discriminatorPropertyName)}, ${buildList(members)})`;
|
|
1623
1475
|
if (isXor) return `z.xor(${buildList(members)})`;
|
|
1624
1476
|
return `z.union(${buildList(members)})`;
|
|
1625
1477
|
},
|
|
1626
1478
|
intersection(node) {
|
|
1627
1479
|
const members = node.members ?? [];
|
|
1628
1480
|
if (members.length === 0) return "";
|
|
1629
|
-
const hasSiblingProperties = members.some((member) => member.type === ast.
|
|
1481
|
+
const hasSiblingProperties = members.some((member) => member.type === ast.schema.types.object && member.properties.length > 0);
|
|
1630
1482
|
const transform = (member) => {
|
|
1631
1483
|
if (!hasSiblingProperties) return this.transform(member);
|
|
1632
1484
|
unionsWithSiblings.add(member);
|
|
@@ -1640,7 +1492,18 @@ function buildZodNodes(dialect, cyclicSchemas) {
|
|
|
1640
1492
|
if (!first) return "";
|
|
1641
1493
|
const firstBase = transform(first);
|
|
1642
1494
|
if (!firstBase) return "";
|
|
1643
|
-
if (rest.length > 0 && isObjectComposableIntersection(node, cyclicSchemas))
|
|
1495
|
+
if (rest.length > 0 && isObjectComposableIntersection(node, cyclicSchemas)) {
|
|
1496
|
+
const discriminator = ast.schema.narrow(first, ast.schema.types.union)?.discriminatorPropertyName;
|
|
1497
|
+
return preserveRequiredProperties(members).slice(1).reduce((acc, member) => {
|
|
1498
|
+
const object = ast.schema.narrow(member, ast.schema.types.object);
|
|
1499
|
+
if (!discriminator || !object?.properties.some((property) => property.name === discriminator)) return dialect.extend(acc, dialect.objectShape(this, member));
|
|
1500
|
+
const properties = object.properties.filter((property) => property.name !== discriminator);
|
|
1501
|
+
return properties.length ? dialect.extend(acc, dialect.objectShape(this, {
|
|
1502
|
+
...object,
|
|
1503
|
+
properties
|
|
1504
|
+
})) : acc;
|
|
1505
|
+
}, firstBase);
|
|
1506
|
+
}
|
|
1644
1507
|
return rest.reduce((acc, member) => {
|
|
1645
1508
|
const constraint = getMemberConstraint({
|
|
1646
1509
|
member,
|
|
@@ -1655,15 +1518,162 @@ function buildZodNodes(dialect, cyclicSchemas) {
|
|
|
1655
1518
|
}
|
|
1656
1519
|
};
|
|
1657
1520
|
}
|
|
1521
|
+
function containsCheck(ctx, { schema, min = 1, max }) {
|
|
1522
|
+
if (min === 0 && max === void 0) return null;
|
|
1523
|
+
const countBounds = [`count >= ${min}`, max !== void 0 && `count <= ${max}`].filter(Boolean).join(" && ");
|
|
1524
|
+
const message = max !== void 0 ? `Array must contain ${min} to ${max} matching items` : `Array must contain at least ${min} matching items`;
|
|
1525
|
+
return `(items) => { const count = items.filter((item) => ${ctx.transform(schema) ?? "z.unknown()"}.safeParse(item).success).length; return ${countBounds} }, { message: ${text.quote(message)} }`;
|
|
1526
|
+
}
|
|
1527
|
+
function conditionExpression(ctx, schema) {
|
|
1528
|
+
if (!schema) return "true";
|
|
1529
|
+
const object = ast.schema.narrow(schema, ast.schema.types.object);
|
|
1530
|
+
const keyOnlyNames = object?.properties.filter((property) => property.required && property.schema.type === ast.schema.types.unknown).map((property) => property.name) ?? [];
|
|
1531
|
+
const restSchema = object ? {
|
|
1532
|
+
...object,
|
|
1533
|
+
properties: object.properties.filter((property) => !keyOnlyNames.includes(property.name))
|
|
1534
|
+
} : schema;
|
|
1535
|
+
const isOnlyKeys = keyOnlyNames.length > 0 && restSchema.type === "object" && restSchema.properties.length === 0 && restSchema.additionalProperties === void 0 && !restSchema.patternProperties && !restSchema.propertyNames && !hasInputChecks(restSchema);
|
|
1536
|
+
return [keyOnlyNames.length > 0 && `typeof value === "object" && value !== null && ${JSON.stringify(keyOnlyNames)}.every((key) => key in value)`, !isOnlyKeys && `${ctx.transform(restSchema) ?? "z.unknown()"}.safeParse(value).success`].filter(Boolean).join(" && ");
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Wraps each handler so its output first checks the raw input, see {@link inputChecks}.
|
|
1540
|
+
*/
|
|
1541
|
+
function withInputChecks(nodes) {
|
|
1542
|
+
const wrapHandler = (type) => {
|
|
1543
|
+
const handler = nodes[type];
|
|
1544
|
+
if (!handler) return [];
|
|
1545
|
+
const checkedHandler = function(node) {
|
|
1546
|
+
const output = handler.call(this, node);
|
|
1547
|
+
if (!output || !hasInputChecks(node)) return output;
|
|
1548
|
+
return inputChecks({
|
|
1549
|
+
value: output,
|
|
1550
|
+
node,
|
|
1551
|
+
keySchema: hasKeyCheck(node) ? this.transform(node.propertyNames) : void 0,
|
|
1552
|
+
conditionals: (node.conditionals ?? []).map((entry) => `z.refine((value) => (${conditionExpression(this, entry.if)} ? ${conditionExpression(this, entry.then)} : ${conditionExpression(this, entry.else)}), { message: "Value does not meet a conditional schema" })`)
|
|
1553
|
+
});
|
|
1554
|
+
};
|
|
1555
|
+
return [[type, checkedHandler]];
|
|
1556
|
+
};
|
|
1557
|
+
return Object.fromEntries(Object.values(ast.schema.types).flatMap(wrapHandler));
|
|
1558
|
+
}
|
|
1559
|
+
// src/components/Zod.tsx
|
|
1560
|
+
/**
|
|
1561
|
+
* Whether the schema reaches a circular schema through its refs. `containsCircular` reads the refs the node holds,
|
|
1562
|
+
* while Zod refuses to compile a cycle anywhere below the root. A generated ref carries no target, so `siblings` resolves it.
|
|
1563
|
+
*/
|
|
1564
|
+
function reachesCycle({ node, siblings, cyclicSchemas, seen = /* @__PURE__ */ new Set() }) {
|
|
1565
|
+
if (!cyclicSchemas?.size || seen.has(node)) return false;
|
|
1566
|
+
seen.add(node);
|
|
1567
|
+
return ast.schema.find(node, {
|
|
1568
|
+
followRefs: true,
|
|
1569
|
+
predicate(schema) {
|
|
1570
|
+
if (schema.type !== ast.schema.types.ref) return false;
|
|
1571
|
+
const name = ast.ref.getName(schema);
|
|
1572
|
+
if (name && cyclicSchemas.has(name)) return true;
|
|
1573
|
+
const sibling = schema.ref || !schema.name ? void 0 : siblings?.get(schema.name);
|
|
1574
|
+
return !!sibling && reachesCycle({
|
|
1575
|
+
node: sibling,
|
|
1576
|
+
siblings,
|
|
1577
|
+
cyclicSchemas,
|
|
1578
|
+
seen
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}) !== void 0;
|
|
1582
|
+
}
|
|
1583
|
+
function Zod({ name, node, printer, inferTypeName, inferKind = "infer", typeGuards, isName, assertName, mini, cyclic, compile, siblings }) {
|
|
1584
|
+
const output = printer.print(node);
|
|
1585
|
+
const printerImports = printer.drainImports();
|
|
1586
|
+
if (!output) return;
|
|
1587
|
+
const needsAnnotation = cyclic && node.type !== "object";
|
|
1588
|
+
const isAlias = ast.ref.isAlias(node, printer.options.keysToOmit);
|
|
1589
|
+
const isCyclic = Boolean(cyclic || printer.options.cyclicSchemas && ast.graph.containsCircular(node, { circularSchemas: printer.options.cyclicSchemas }));
|
|
1590
|
+
const isStrict = typeof compile === "object" && Boolean(compile.strict);
|
|
1591
|
+
const throwsOnCompile = isStrict && (throwsOnStrictCompile({
|
|
1592
|
+
node,
|
|
1593
|
+
options: printer.options,
|
|
1594
|
+
siblings
|
|
1595
|
+
}) || reachesCycle({
|
|
1596
|
+
node,
|
|
1597
|
+
siblings,
|
|
1598
|
+
cyclicSchemas: printer.options.cyclicSchemas
|
|
1599
|
+
}));
|
|
1600
|
+
const value = Boolean(compile) && !isAlias && !isCyclic && !throwsOnCompile ? isStrict ? `z.compile(${output}, { strict: true })` : `z.compile(${output})` : output;
|
|
1601
|
+
const targetType = inferTypeName ?? `z.infer<typeof ${name}>`;
|
|
1602
|
+
const shouldGenerateIs = isName && (typeof typeGuards === "object" ? typeGuards.is ?? true : Boolean(typeGuards));
|
|
1603
|
+
const shouldGenerateAssert = assertName && (typeof typeGuards === "object" ? typeGuards.assert ?? true : Boolean(typeGuards));
|
|
1604
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1605
|
+
printerImports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
|
|
1606
|
+
name: imp.name,
|
|
1607
|
+
path: imp.path,
|
|
1608
|
+
root: imp.root,
|
|
1609
|
+
isTypeOnly: imp.isTypeOnly,
|
|
1610
|
+
isNameSpace: imp.isNameSpace
|
|
1611
|
+
}, [
|
|
1612
|
+
name,
|
|
1613
|
+
imp.path,
|
|
1614
|
+
imp.name
|
|
1615
|
+
].join("-"))),
|
|
1616
|
+
/* @__PURE__ */ jsx(File.Source, {
|
|
1617
|
+
name,
|
|
1618
|
+
isExportable: true,
|
|
1619
|
+
isIndexable: true,
|
|
1620
|
+
children: /* @__PURE__ */ jsx(Const, {
|
|
1621
|
+
export: true,
|
|
1622
|
+
name,
|
|
1623
|
+
type: needsAnnotation ? mini ? "z.ZodMiniType" : "z.ZodType" : void 0,
|
|
1624
|
+
children: value
|
|
1625
|
+
})
|
|
1626
|
+
}),
|
|
1627
|
+
inferTypeName && /* @__PURE__ */ jsx(File.Source, {
|
|
1628
|
+
name: inferTypeName,
|
|
1629
|
+
isExportable: true,
|
|
1630
|
+
isIndexable: true,
|
|
1631
|
+
isTypeOnly: true,
|
|
1632
|
+
children: /* @__PURE__ */ jsx(Type, {
|
|
1633
|
+
export: true,
|
|
1634
|
+
name: inferTypeName,
|
|
1635
|
+
children: `z.${inferKind}<typeof ${name}>`
|
|
1636
|
+
})
|
|
1637
|
+
}),
|
|
1638
|
+
shouldGenerateIs && /* @__PURE__ */ jsx(File.Source, {
|
|
1639
|
+
name: isName,
|
|
1640
|
+
isExportable: true,
|
|
1641
|
+
isIndexable: true,
|
|
1642
|
+
children: /* @__PURE__ */ jsx(Const, {
|
|
1643
|
+
export: true,
|
|
1644
|
+
name: isName,
|
|
1645
|
+
JSDoc: { comments: [`Type guard for {@link ${name}}`] },
|
|
1646
|
+
children: mini ? `(data: unknown): data is ${targetType} => z.validate(${name}, data)` : `(data: unknown): data is ${targetType} => ${name}.validate(data)`
|
|
1647
|
+
})
|
|
1648
|
+
}),
|
|
1649
|
+
shouldGenerateAssert && /* @__PURE__ */ jsx(File.Source, {
|
|
1650
|
+
name: assertName,
|
|
1651
|
+
isExportable: true,
|
|
1652
|
+
isIndexable: true,
|
|
1653
|
+
children: /* @__PURE__ */ jsx(Function, {
|
|
1654
|
+
export: true,
|
|
1655
|
+
name: assertName,
|
|
1656
|
+
params: "data: unknown",
|
|
1657
|
+
returnType: `asserts data is ${targetType}`,
|
|
1658
|
+
JSDoc: { comments: [`Asserter for {@link ${name}}`, "@throws {z.ZodError} If data is invalid"] },
|
|
1659
|
+
children: mini ? `if (!z.validate(${name}, data)) {\n z.parse(${name}, data)\n}` : `if (!${name}.validate(data)) {\n ${name}.parse(data)\n}`
|
|
1660
|
+
})
|
|
1661
|
+
})
|
|
1662
|
+
] });
|
|
1663
|
+
}
|
|
1658
1664
|
// src/printers/printerZod.ts
|
|
1659
1665
|
function strictOneOfMember(member, node, cyclicSchemas) {
|
|
1660
|
-
if (node.type === ast.
|
|
1666
|
+
if (node.type === ast.schema.types.ref) {
|
|
1661
1667
|
if (member.startsWith("z.lazy(")) return member;
|
|
1662
|
-
const refName = ast.
|
|
1668
|
+
const refName = ast.ref.getName(node);
|
|
1663
1669
|
if (refName && cyclicSchemas?.has(refName)) return member;
|
|
1664
|
-
const schema =
|
|
1670
|
+
const schema = ast.ref.mergeSchema(node);
|
|
1665
1671
|
if (schema.nullable || schema.optional || node.nullable || node.optional) return member;
|
|
1666
|
-
if (schema.type === ast.
|
|
1672
|
+
if (schema.type === ast.schema.types.object && (schema.additionalProperties === void 0 || schema.additionalProperties === false)) {
|
|
1673
|
+
const checks = [node, node.schema].filter((entry) => entry && hasInputChecks(entry)).length;
|
|
1674
|
+
if (checks === 2) return member;
|
|
1675
|
+
return checks ? `z.pipe(${member}.in, ${member}.out.strict())` : `${member}.strict()`;
|
|
1676
|
+
}
|
|
1667
1677
|
}
|
|
1668
1678
|
return member;
|
|
1669
1679
|
}
|
|
@@ -1679,7 +1689,7 @@ function buildZodObjectShape(ctx, node) {
|
|
|
1679
1689
|
optional: schema.optional || required === false,
|
|
1680
1690
|
nullish: schema.nullish,
|
|
1681
1691
|
defaultValue: meta.default,
|
|
1682
|
-
description: schema.type !== ast.
|
|
1692
|
+
description: schema.type !== ast.schema.types.ref && meta.type === ast.schema.types.ref ? void 0 : meta.description,
|
|
1683
1693
|
examples: meta.examples
|
|
1684
1694
|
});
|
|
1685
1695
|
if (!cyclic) return `${objectKey(name)}: ${body}`;
|
|
@@ -1700,21 +1710,22 @@ function buildZodObjectShape(ctx, node) {
|
|
|
1700
1710
|
function recursiveGetterType({ ctx, schema, required }) {
|
|
1701
1711
|
function baseType(schema) {
|
|
1702
1712
|
if (ctx.options.nodes?.[schema.type]) return void 0;
|
|
1703
|
-
if (schema.type === ast.
|
|
1713
|
+
if (schema.type === ast.schema.types.ref) {
|
|
1704
1714
|
const saved = ctx.options.cyclicSchemas;
|
|
1705
1715
|
ctx.options.cyclicSchemas = void 0;
|
|
1706
1716
|
const name = ctx.transform(schema);
|
|
1707
1717
|
ctx.options.cyclicSchemas = saved;
|
|
1708
1718
|
return name ? `typeof ${name}` : void 0;
|
|
1709
1719
|
}
|
|
1710
|
-
if (schema.type === ast.
|
|
1720
|
+
if (schema.type === ast.schema.types.array && schema.items?.length === 1) {
|
|
1711
1721
|
const item = baseType(schema.items[0]);
|
|
1712
|
-
|
|
1722
|
+
if (!item) return void 0;
|
|
1723
|
+
return `z.ZodArray<${schema.items[0].nullable ? `z.ZodNullable<${item}>` : item}>`;
|
|
1713
1724
|
}
|
|
1714
1725
|
}
|
|
1715
1726
|
const base = baseType(schema);
|
|
1716
1727
|
if (!base) return void 0;
|
|
1717
|
-
const meta =
|
|
1728
|
+
const meta = ast.ref.mergeSchema(schema);
|
|
1718
1729
|
const optional = schema.optional || required === false;
|
|
1719
1730
|
const wrapped = (() => {
|
|
1720
1731
|
if (schema.nullish || meta.nullable && optional) return `z.ZodOptional<z.ZodNullable<${base}>>`;
|
|
@@ -1729,12 +1740,12 @@ function recursiveGetterType({ ctx, schema, required }) {
|
|
|
1729
1740
|
* `direction` themselves, so {@link containsDirectionalNode} probes their children instead.
|
|
1730
1741
|
*/
|
|
1731
1742
|
const CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
1732
|
-
ast.
|
|
1733
|
-
ast.
|
|
1734
|
-
ast.
|
|
1735
|
-
ast.
|
|
1736
|
-
ast.
|
|
1737
|
-
ast.
|
|
1743
|
+
ast.schema.types.object,
|
|
1744
|
+
ast.schema.types.array,
|
|
1745
|
+
ast.schema.types.tuple,
|
|
1746
|
+
ast.schema.types.union,
|
|
1747
|
+
ast.schema.types.intersection,
|
|
1748
|
+
ast.schema.types.ref
|
|
1738
1749
|
]);
|
|
1739
1750
|
/**
|
|
1740
1751
|
* Runs the node's effective handler (a `printer.nodes` override, else the built-in) once per
|
|
@@ -1765,22 +1776,31 @@ function variesByDirection({ node, printerOptions }) {
|
|
|
1765
1776
|
*/
|
|
1766
1777
|
function containsDirectionalNode({ node, printerOptions }) {
|
|
1767
1778
|
if (!node) return false;
|
|
1768
|
-
return ast.
|
|
1779
|
+
return ast.schema.find(node, {
|
|
1769
1780
|
predicate: (schema) => variesByDirection({
|
|
1770
1781
|
node: schema,
|
|
1771
1782
|
printerOptions
|
|
1772
1783
|
}),
|
|
1773
|
-
followRefs: true
|
|
1784
|
+
followRefs: true,
|
|
1785
|
+
cache: getDirectionalCache(printerOptions)
|
|
1774
1786
|
}) !== void 0;
|
|
1775
1787
|
}
|
|
1776
1788
|
/**
|
|
1789
|
+
* The `find` cache for {@link containsDirectionalNode}, one per plugin per build and per option set
|
|
1790
|
+
* the probe reads. A `printer.nodes` override has no string key, so it searches without a cache.
|
|
1791
|
+
*/
|
|
1792
|
+
function getDirectionalCache({ resolver, nodes, coercion, dateType }) {
|
|
1793
|
+
if (!resolver || nodes) return void 0;
|
|
1794
|
+
return resolver.memo(`zodDirectionalNodes:${shouldCoerce(coercion, "dates")}:${dateType}`, () => /* @__PURE__ */ new WeakMap());
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1777
1797
|
* Names of the `$ref` schemas the generator should route to their input (encode) variant.
|
|
1778
1798
|
*/
|
|
1779
1799
|
function collectDirectionalRefNames({ node, printerOptions }) {
|
|
1780
|
-
return ast.
|
|
1800
|
+
return ast.node.collect(node, { schema: (n) => n.type === ast.schema.types.ref && n.ref && containsDirectionalNode({
|
|
1781
1801
|
node: n,
|
|
1782
1802
|
printerOptions
|
|
1783
|
-
}) ? ast.
|
|
1803
|
+
}) ? ast.ref.getName(n) ?? void 0 : void 0 });
|
|
1784
1804
|
}
|
|
1785
1805
|
/**
|
|
1786
1806
|
* Handlers that never recurse into children, so {@link variesByDirection} can call one directly
|
|
@@ -1802,6 +1822,7 @@ const standardDialect = {
|
|
|
1802
1822
|
coercedNumber: (schema) => `z.coerce.number().pipe(${schema})`,
|
|
1803
1823
|
catchall: (object, value) => `${object}.catchall(${value})`,
|
|
1804
1824
|
nullable: (value) => `${value}.nullable()`,
|
|
1825
|
+
refine: (value, check) => `${value}.refine(${check})`,
|
|
1805
1826
|
unique: (array) => `${array}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`,
|
|
1806
1827
|
extend: (base, shape) => `${base}.extend(${shape})`,
|
|
1807
1828
|
intersection: (left, right) => `${left}.and(${right})`,
|
|
@@ -1813,8 +1834,8 @@ const standardDialect = {
|
|
|
1813
1834
|
};
|
|
1814
1835
|
const scalarNodes = {
|
|
1815
1836
|
...buildScalarNodes(standardDialect),
|
|
1816
|
-
bigint() {
|
|
1817
|
-
return
|
|
1837
|
+
bigint(node) {
|
|
1838
|
+
return `z.coerce.bigint()${numberConstraints(bigintConstraints(node), "n")}`;
|
|
1818
1839
|
},
|
|
1819
1840
|
date(node) {
|
|
1820
1841
|
if (node.representation !== "date") return "z.iso.date()";
|
|
@@ -1847,14 +1868,14 @@ const scalarNodes = {
|
|
|
1847
1868
|
* const code = printer.print(stringNode) // "z.string()"
|
|
1848
1869
|
* ```
|
|
1849
1870
|
*/
|
|
1850
|
-
const printerZod =
|
|
1871
|
+
const printerZod = createPrinter((options) => {
|
|
1851
1872
|
return {
|
|
1852
1873
|
name: "zod",
|
|
1853
1874
|
options,
|
|
1854
|
-
nodes: {
|
|
1875
|
+
nodes: withInputChecks({
|
|
1855
1876
|
...buildZodNodes(standardDialect, options.cyclicSchemas),
|
|
1856
1877
|
...scalarNodes
|
|
1857
|
-
},
|
|
1878
|
+
}),
|
|
1858
1879
|
overrides: options.nodes,
|
|
1859
1880
|
print(node) {
|
|
1860
1881
|
return printWithOmit(this, node, (value, meta) => applyModifiers({
|
|
@@ -1911,18 +1932,19 @@ const miniDialect = {
|
|
|
1911
1932
|
coercedNumber: (schema) => `z.pipe(z.coerce.number(), ${schema})`,
|
|
1912
1933
|
catchall: (object, value) => `z.catchall(${object}, ${value})`,
|
|
1913
1934
|
nullable: (value) => `z.nullable(${value})`,
|
|
1935
|
+
refine: (value, check) => `${value}.check(z.refine(${check}))`,
|
|
1914
1936
|
unique: (array) => `${array}.check(z.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" }))`,
|
|
1915
1937
|
extend: (base, shape) => `z.extend(${base}, ${shape})`,
|
|
1916
1938
|
intersection: (left, right) => `z.intersection(${left}, ${right})`
|
|
1917
1939
|
};
|
|
1918
|
-
const printerZodMini =
|
|
1940
|
+
const printerZodMini = createPrinter((options) => {
|
|
1919
1941
|
return {
|
|
1920
1942
|
name: "zod-mini",
|
|
1921
1943
|
options,
|
|
1922
|
-
nodes: {
|
|
1944
|
+
nodes: withInputChecks({
|
|
1923
1945
|
...buildZodNodes(miniDialect, options.cyclicSchemas),
|
|
1924
1946
|
bigint(node) {
|
|
1925
|
-
return `z.coerce.bigint()${numberChecksMini(node)}`;
|
|
1947
|
+
return `z.coerce.bigint()${numberChecksMini(bigintConstraints(node), "n")}`;
|
|
1926
1948
|
},
|
|
1927
1949
|
date(node) {
|
|
1928
1950
|
if (node.representation === "string") return "z.iso.date()";
|
|
@@ -1935,7 +1957,7 @@ const printerZodMini = ast.createPrinter((options) => {
|
|
|
1935
1957
|
if (node.representation === "string") return "z.iso.time()";
|
|
1936
1958
|
return "z.date()";
|
|
1937
1959
|
}
|
|
1938
|
-
},
|
|
1960
|
+
}),
|
|
1939
1961
|
overrides: options.nodes,
|
|
1940
1962
|
print(node) {
|
|
1941
1963
|
return printWithOmit(this, node, (value, meta) => applyMiniModifiers({
|
|
@@ -2022,7 +2044,8 @@ const zodGenerator = defineGenerator({
|
|
|
2022
2044
|
node,
|
|
2023
2045
|
root,
|
|
2024
2046
|
output,
|
|
2025
|
-
group: group ?? void 0
|
|
2047
|
+
group: group ?? void 0,
|
|
2048
|
+
checks: true
|
|
2026
2049
|
});
|
|
2027
2050
|
const inputImportEntries = hasDirectionalNode ? [...directionalRefNames].map((schemaName) => ({
|
|
2028
2051
|
name: [resolver.schema.inputName(schemaName)],
|
|
@@ -2113,7 +2136,7 @@ const zodGenerator = defineGenerator({
|
|
|
2113
2136
|
});
|
|
2114
2137
|
},
|
|
2115
2138
|
operation(node, ctx) {
|
|
2116
|
-
if (!ast.
|
|
2139
|
+
if (!ast.http.isOperation(node)) return null;
|
|
2117
2140
|
const { adapter, config, resolver, root } = ctx;
|
|
2118
2141
|
const { output, coercion, guidType, regexType, oneOfType, compile, mini, inferred, importPath, group, printer } = ctx.options;
|
|
2119
2142
|
const dateType = getOasAdapter(adapter).options.dateType;
|
|
@@ -2136,23 +2159,26 @@ const zodGenerator = defineGenerator({
|
|
|
2136
2159
|
cyclicSchemas: getCircularNames(ctx),
|
|
2137
2160
|
nodes: printer?.nodes
|
|
2138
2161
|
};
|
|
2162
|
+
const siblings = /* @__PURE__ */ new Map();
|
|
2139
2163
|
function renderSchemaEntry({ schema, name, keysToOmit, direction = "decode" }) {
|
|
2140
2164
|
if (!schema) return null;
|
|
2165
|
+
siblings.set(name, schema);
|
|
2141
2166
|
const directionalRefNames = direction === "encode" && !mini ? new Set(collectDirectionalRefNames({
|
|
2142
2167
|
node: schema,
|
|
2143
2168
|
printerOptions
|
|
2144
2169
|
})) : null;
|
|
2170
|
+
const schemaPrinter = getPrinter(resolver, {
|
|
2171
|
+
...printerOptions,
|
|
2172
|
+
keysToOmit
|
|
2173
|
+
}, mini, direction);
|
|
2145
2174
|
const imports = resolver.imports({
|
|
2146
2175
|
node: schema,
|
|
2147
2176
|
root,
|
|
2148
2177
|
output,
|
|
2149
2178
|
group: group ?? void 0,
|
|
2179
|
+
checks: true,
|
|
2150
2180
|
name: (schemaName) => directionalRefNames?.has(schemaName) ? resolver.schema.inputName(schemaName) : resolver.name(schemaName)
|
|
2151
2181
|
});
|
|
2152
|
-
const schemaPrinter = getPrinter(resolver, {
|
|
2153
|
-
...printerOptions,
|
|
2154
|
-
keysToOmit
|
|
2155
|
-
}, mini, direction);
|
|
2156
2182
|
return /* @__PURE__ */ jsxs(Fragment, { children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
|
|
2157
2183
|
root: meta.file.path,
|
|
2158
2184
|
path: imp.path,
|
|
@@ -2168,7 +2194,8 @@ const zodGenerator = defineGenerator({
|
|
|
2168
2194
|
inferTypeName: inferred ? resolver.schema.type(name) : null,
|
|
2169
2195
|
inferKind: aliasKind(inferred, direction),
|
|
2170
2196
|
cyclic: false,
|
|
2171
|
-
compile
|
|
2197
|
+
compile,
|
|
2198
|
+
siblings
|
|
2172
2199
|
})] });
|
|
2173
2200
|
}
|
|
2174
2201
|
function buildContentTypeVariants(entries, baseName, decorate, direction) {
|
|
@@ -2192,7 +2219,7 @@ const zodGenerator = defineGenerator({
|
|
|
2192
2219
|
})] });
|
|
2193
2220
|
}
|
|
2194
2221
|
function buildResponseUnion({ responses, name, fallbackUnknown }) {
|
|
2195
|
-
if (new Set(
|
|
2222
|
+
if (new Set(ast.http.responses.getImportedNames(responses).map((refName) => resolver.name(refName))).has(name)) return null;
|
|
2196
2223
|
const members = responses.map((res) => ast.factory.createSchema({
|
|
2197
2224
|
type: "ref",
|
|
2198
2225
|
name: resolver.response.status(node, res.statusCode)
|
|
@@ -2225,14 +2252,27 @@ const zodGenerator = defineGenerator({
|
|
|
2225
2252
|
keysToOmit: primary?.keysToOmit
|
|
2226
2253
|
});
|
|
2227
2254
|
});
|
|
2255
|
+
const toParamsSchema = (params) => ast.factory.createSchema({
|
|
2256
|
+
type: "object",
|
|
2257
|
+
primitive: "object",
|
|
2258
|
+
properties: params.map((param) => ast.factory.createProperty({
|
|
2259
|
+
name: param.name,
|
|
2260
|
+
required: param.required,
|
|
2261
|
+
schema: param.schema
|
|
2262
|
+
}))
|
|
2263
|
+
});
|
|
2264
|
+
const responseHeaderSchemas = node.responses.map((res) => res.headers?.length ? renderSchemaEntry({
|
|
2265
|
+
schema: toParamsSchema(res.headers),
|
|
2266
|
+
name: resolver.response.headers(node, res.statusCode)
|
|
2267
|
+
}) : null);
|
|
2228
2268
|
const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
|
|
2229
|
-
const successResponsesWithSchema =
|
|
2269
|
+
const successResponsesWithSchema = responsesWithSchema.filter((res) => ast.http.status.isSuccess(res));
|
|
2230
2270
|
const responseUnionSchema = responsesWithSchema.length > 0 ? buildResponseUnion({
|
|
2231
2271
|
responses: successResponsesWithSchema,
|
|
2232
2272
|
name: resolver.response.response(node),
|
|
2233
2273
|
fallbackUnknown: true
|
|
2234
2274
|
}) : null;
|
|
2235
|
-
const errorResponsesWithSchema = responsesWithSchema.filter((res) => !
|
|
2275
|
+
const errorResponsesWithSchema = responsesWithSchema.filter((res) => !ast.http.status.isSuccess(res));
|
|
2236
2276
|
const errorUnionSchema = errorResponsesWithSchema.length > 0 ? buildResponseUnion({
|
|
2237
2277
|
responses: errorResponsesWithSchema,
|
|
2238
2278
|
name: resolver.response.error(node),
|
|
@@ -2259,7 +2299,7 @@ const zodGenerator = defineGenerator({
|
|
|
2259
2299
|
description: node.requestBody.description ?? schema.description
|
|
2260
2300
|
}), "encode");
|
|
2261
2301
|
})();
|
|
2262
|
-
const { path, query, header, cookie } =
|
|
2302
|
+
const { path, query, header, cookie } = ast.http.groupParameters(node);
|
|
2263
2303
|
const paramGroupSchemas = [
|
|
2264
2304
|
{
|
|
2265
2305
|
kind: "path",
|
|
@@ -2278,15 +2318,7 @@ const zodGenerator = defineGenerator({
|
|
|
2278
2318
|
params: cookie
|
|
2279
2319
|
}
|
|
2280
2320
|
].filter(({ params }) => params.length > 0).map(({ kind, params }) => renderSchemaEntry({
|
|
2281
|
-
schema:
|
|
2282
|
-
type: "object",
|
|
2283
|
-
primitive: "object",
|
|
2284
|
-
properties: params.map((param) => ast.factory.createProperty({
|
|
2285
|
-
name: param.name,
|
|
2286
|
-
required: param.required,
|
|
2287
|
-
schema: param.schema
|
|
2288
|
-
}))
|
|
2289
|
-
}),
|
|
2321
|
+
schema: toParamsSchema(params),
|
|
2290
2322
|
name: resolver.param[kind](node, params[0]),
|
|
2291
2323
|
direction: "encode"
|
|
2292
2324
|
}));
|
|
@@ -2327,6 +2359,7 @@ const zodGenerator = defineGenerator({
|
|
|
2327
2359
|
}),
|
|
2328
2360
|
paramSchemas,
|
|
2329
2361
|
responseSchemas,
|
|
2362
|
+
responseHeaderSchemas,
|
|
2330
2363
|
responseUnionSchema,
|
|
2331
2364
|
errorUnionSchema,
|
|
2332
2365
|
requestSchema,
|
|
@@ -2339,11 +2372,11 @@ const zodGenerator = defineGenerator({
|
|
|
2339
2372
|
});
|
|
2340
2373
|
// kubb-extension.json
|
|
2341
2374
|
var compatibility = {
|
|
2342
|
-
"kubb": ">=5.
|
|
2375
|
+
"kubb": ">=5.18.4",
|
|
2343
2376
|
"node": ">=22.22"
|
|
2344
2377
|
};
|
|
2345
2378
|
// package.json
|
|
2346
|
-
var version = "5.9.
|
|
2379
|
+
var version = "5.9.1";
|
|
2347
2380
|
// src/resolvers/resolverZod.ts
|
|
2348
2381
|
/**
|
|
2349
2382
|
* Default resolver used by `@kubb/plugin-zod`. Decides the names and file
|
|
@@ -2363,15 +2396,15 @@ var version = "5.9.0-canary.20261111T122006";
|
|
|
2363
2396
|
const resolverZod = createResolver({
|
|
2364
2397
|
pluginName: "plugin-zod",
|
|
2365
2398
|
name(name) {
|
|
2366
|
-
return this.memo(`name:${name}`, () => ensureValidVarName(
|
|
2399
|
+
return this.memo(`name:${name}`, () => ensureValidVarName(casing.camel(name, { suffix: "schema" })));
|
|
2367
2400
|
},
|
|
2368
|
-
file: createCasedFile((part) =>
|
|
2401
|
+
file: createCasedFile((part) => casing.camel(part, { suffix: "schema" })),
|
|
2369
2402
|
schema: {
|
|
2370
2403
|
typeName(name) {
|
|
2371
|
-
return this.memo(`typeName:${name}`, () => ensureValidVarName(
|
|
2404
|
+
return this.memo(`typeName:${name}`, () => ensureValidVarName(casing.pascal(name, { suffix: "schema type" })));
|
|
2372
2405
|
},
|
|
2373
2406
|
type(name) {
|
|
2374
|
-
return this.memo(`type:${name}`, () => ensureValidVarName(
|
|
2407
|
+
return this.memo(`type:${name}`, () => ensureValidVarName(casing.pascal(name, { suffix: "type" })));
|
|
2375
2408
|
},
|
|
2376
2409
|
inputName(name) {
|
|
2377
2410
|
return this.name(`${name} input`);
|
|
@@ -2380,10 +2413,10 @@ const resolverZod = createResolver({
|
|
|
2380
2413
|
return this.schema.typeName(`${name} input`);
|
|
2381
2414
|
},
|
|
2382
2415
|
isName(name) {
|
|
2383
|
-
return ensureValidVarName(
|
|
2416
|
+
return ensureValidVarName(casing.camel(name, { prefix: "is" }));
|
|
2384
2417
|
},
|
|
2385
2418
|
assertName(name) {
|
|
2386
|
-
return ensureValidVarName(
|
|
2419
|
+
return ensureValidVarName(casing.camel(name, { prefix: "assert" }));
|
|
2387
2420
|
}
|
|
2388
2421
|
},
|
|
2389
2422
|
param: createOperationParamResolver(),
|
|
@@ -2457,7 +2490,7 @@ const pluginZod = defineExtension({
|
|
|
2457
2490
|
exclude,
|
|
2458
2491
|
include,
|
|
2459
2492
|
override,
|
|
2460
|
-
group:
|
|
2493
|
+
group: group ?? null,
|
|
2461
2494
|
importPath,
|
|
2462
2495
|
coercion,
|
|
2463
2496
|
inferred: inferred === "direction" ? "codec" : inferred,
|