@kubb/ast 5.0.0-beta.5 → 5.0.0-beta.51
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/LICENSE +17 -10
- package/README.md +28 -19
- package/dist/index.cjs +878 -788
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +33 -3326
- package/dist/index.js +863 -750
- package/dist/index.js.map +1 -1
- package/dist/types-BaaNZbSi.d.ts +3581 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/dist/utils-BIcKgbbc.js +626 -0
- package/dist/utils-BIcKgbbc.js.map +1 -0
- package/dist/utils-CMRZrT-w.cjs +794 -0
- package/dist/utils-CMRZrT-w.cjs.map +1 -0
- package/dist/utils.cjs +18 -0
- package/dist/utils.d.ts +205 -0
- package/dist/utils.js +2 -0
- package/package.json +13 -5
- package/src/constants.ts +10 -49
- package/src/dedupe.ts +200 -0
- package/src/dialect.ts +58 -0
- package/src/dispatch.ts +53 -0
- package/src/factory.ts +154 -21
- package/src/guards.ts +18 -48
- package/src/index.ts +11 -8
- package/src/infer.ts +16 -14
- package/src/nodes/base.ts +2 -0
- package/src/nodes/code.ts +22 -28
- package/src/nodes/content.ts +37 -0
- package/src/nodes/file.ts +17 -15
- package/src/nodes/function.ts +11 -11
- package/src/nodes/http.ts +1 -35
- package/src/nodes/index.ts +10 -12
- package/src/nodes/operation.ts +98 -62
- package/src/nodes/response.ts +21 -14
- package/src/nodes/root.ts +72 -10
- package/src/nodes/schema.ts +18 -15
- package/src/printer.ts +44 -38
- package/src/resolvers.ts +5 -19
- package/src/signature.ts +232 -0
- package/src/transformers.ts +21 -16
- package/src/types.ts +8 -18
- package/src/{utils.ts → utils/ast.ts} +125 -84
- package/src/utils/index.ts +295 -0
- package/src/visitor.ts +239 -281
- package/src/refs.ts +0 -67
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,388 +1,45 @@
|
|
|
1
|
-
import "./chunk
|
|
2
|
-
import {
|
|
1
|
+
import "./chunk-C0LytTxp.js";
|
|
2
|
+
import { S as visitorDepths, _ as isValidVarName, a as enumPropName, b as isScalarPrimitive, h as trimExtName, o as extractRefName, v as camelCase, x as schemaTypes, y as httpMethods } from "./utils-BIcKgbbc.js";
|
|
3
|
+
import { hash } from "node:crypto";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
//#region src/
|
|
5
|
-
const visitorDepths = {
|
|
6
|
-
shallow: "shallow",
|
|
7
|
-
deep: "deep"
|
|
8
|
-
};
|
|
9
|
-
const nodeKinds = {
|
|
10
|
-
input: "Input",
|
|
11
|
-
output: "Output",
|
|
12
|
-
operation: "Operation",
|
|
13
|
-
schema: "Schema",
|
|
14
|
-
property: "Property",
|
|
15
|
-
parameter: "Parameter",
|
|
16
|
-
response: "Response",
|
|
17
|
-
functionParameter: "FunctionParameter",
|
|
18
|
-
parameterGroup: "ParameterGroup",
|
|
19
|
-
functionParameters: "FunctionParameters",
|
|
20
|
-
type: "Type",
|
|
21
|
-
file: "File",
|
|
22
|
-
import: "Import",
|
|
23
|
-
export: "Export",
|
|
24
|
-
source: "Source",
|
|
25
|
-
text: "Text",
|
|
26
|
-
break: "Break"
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Schema type discriminators used by all AST schema nodes.
|
|
30
|
-
*
|
|
31
|
-
* These values serve as stable discriminators across the AST (e.g., `schema.type === schemaTypes.object`).
|
|
32
|
-
* Grouped by category: primitives (`string`, `number`, `boolean`), structural types (`object`, `array`, `union`),
|
|
33
|
-
* and format-specific types (`date`, `uuid`, `email`). Use `isScalarPrimitive()` to check for scalar types.
|
|
34
|
-
*/
|
|
35
|
-
const schemaTypes = {
|
|
36
|
-
/**
|
|
37
|
-
* Text value.
|
|
38
|
-
*/
|
|
39
|
-
string: "string",
|
|
40
|
-
/**
|
|
41
|
-
* Floating-point number (`float`, `double`).
|
|
42
|
-
*/
|
|
43
|
-
number: "number",
|
|
44
|
-
/**
|
|
45
|
-
* Whole number (`int32`). Use `bigint` for `int64`.
|
|
46
|
-
*/
|
|
47
|
-
integer: "integer",
|
|
48
|
-
/**
|
|
49
|
-
* 64-bit integer (`int64`). Only used when `integerType` is set to `'bigint'`.
|
|
50
|
-
*/
|
|
51
|
-
bigint: "bigint",
|
|
52
|
-
/**
|
|
53
|
-
* Boolean value
|
|
54
|
-
*/
|
|
55
|
-
boolean: "boolean",
|
|
56
|
-
/**
|
|
57
|
-
* Explicit null value.
|
|
58
|
-
*/
|
|
59
|
-
null: "null",
|
|
60
|
-
/**
|
|
61
|
-
* Any value (no type restriction).
|
|
62
|
-
*/
|
|
63
|
-
any: "any",
|
|
64
|
-
/**
|
|
65
|
-
* Unknown value (must be narrowed before usage).
|
|
66
|
-
*/
|
|
67
|
-
unknown: "unknown",
|
|
68
|
-
/**
|
|
69
|
-
* No return value (`void`).
|
|
70
|
-
*/
|
|
71
|
-
void: "void",
|
|
72
|
-
/**
|
|
73
|
-
* Object with named properties.
|
|
74
|
-
*/
|
|
75
|
-
object: "object",
|
|
76
|
-
/**
|
|
77
|
-
* Sequential list of items.
|
|
78
|
-
*/
|
|
79
|
-
array: "array",
|
|
80
|
-
/**
|
|
81
|
-
* Fixed-length list with position-specific items.
|
|
82
|
-
*/
|
|
83
|
-
tuple: "tuple",
|
|
84
|
-
/**
|
|
85
|
-
* "One of" multiple schema members.
|
|
86
|
-
*/
|
|
87
|
-
union: "union",
|
|
88
|
-
/**
|
|
89
|
-
* "All of" multiple schema members.
|
|
90
|
-
*/
|
|
91
|
-
intersection: "intersection",
|
|
92
|
-
/**
|
|
93
|
-
* Enum schema.
|
|
94
|
-
*/
|
|
95
|
-
enum: "enum",
|
|
96
|
-
/**
|
|
97
|
-
* Reference to another schema.
|
|
98
|
-
*/
|
|
99
|
-
ref: "ref",
|
|
100
|
-
/**
|
|
101
|
-
* Calendar date (for example `2026-03-24`).
|
|
102
|
-
*/
|
|
103
|
-
date: "date",
|
|
104
|
-
/**
|
|
105
|
-
* Date-time value (for example `2026-03-24T09:00:00Z`).
|
|
106
|
-
*/
|
|
107
|
-
datetime: "datetime",
|
|
108
|
-
/**
|
|
109
|
-
* Time-only value (for example `09:00:00`).
|
|
110
|
-
*/
|
|
111
|
-
time: "time",
|
|
112
|
-
/**
|
|
113
|
-
* UUID value.
|
|
114
|
-
*/
|
|
115
|
-
uuid: "uuid",
|
|
116
|
-
/**
|
|
117
|
-
* Email address value.
|
|
118
|
-
*/
|
|
119
|
-
email: "email",
|
|
120
|
-
/**
|
|
121
|
-
* URL value.
|
|
122
|
-
*/
|
|
123
|
-
url: "url",
|
|
124
|
-
/**
|
|
125
|
-
* IPv4 address value.
|
|
126
|
-
*/
|
|
127
|
-
ipv4: "ipv4",
|
|
128
|
-
/**
|
|
129
|
-
* IPv6 address value.
|
|
130
|
-
*/
|
|
131
|
-
ipv6: "ipv6",
|
|
132
|
-
/**
|
|
133
|
-
* Binary/blob value.
|
|
134
|
-
*/
|
|
135
|
-
blob: "blob",
|
|
136
|
-
/**
|
|
137
|
-
* Impossible value (`never`).
|
|
138
|
-
*/
|
|
139
|
-
never: "never"
|
|
140
|
-
};
|
|
141
|
-
/**
|
|
142
|
-
* Scalar primitive schema types used for union simplification and type narrowing.
|
|
143
|
-
*
|
|
144
|
-
* Use `isScalarPrimitive()` to safely check whether a type is a scalar primitive.
|
|
145
|
-
*/
|
|
146
|
-
const SCALAR_PRIMITIVE_TYPES = new Set([
|
|
147
|
-
"string",
|
|
148
|
-
"number",
|
|
149
|
-
"integer",
|
|
150
|
-
"bigint",
|
|
151
|
-
"boolean"
|
|
152
|
-
]);
|
|
153
|
-
/**
|
|
154
|
-
* Type guard that returns `true` when `type` is a scalar primitive schema type.
|
|
155
|
-
*
|
|
156
|
-
* Use this to check if a schema type can be directly assigned without wrapping (e.g., `string | number | boolean`).
|
|
157
|
-
*/
|
|
158
|
-
function isScalarPrimitive(type) {
|
|
159
|
-
return SCALAR_PRIMITIVE_TYPES.has(type);
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* HTTP method identifiers used by operation nodes.
|
|
163
|
-
*
|
|
164
|
-
* Includes all standard HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE).
|
|
165
|
-
*/
|
|
166
|
-
const httpMethods = {
|
|
167
|
-
get: "GET",
|
|
168
|
-
post: "POST",
|
|
169
|
-
put: "PUT",
|
|
170
|
-
patch: "PATCH",
|
|
171
|
-
delete: "DELETE",
|
|
172
|
-
head: "HEAD",
|
|
173
|
-
options: "OPTIONS",
|
|
174
|
-
trace: "TRACE"
|
|
175
|
-
};
|
|
176
|
-
/**
|
|
177
|
-
* Common MIME types used in request/response content negotiation.
|
|
178
|
-
*
|
|
179
|
-
* Covers JSON, XML, form data, PDFs, images, audio, and video formats.
|
|
180
|
-
* Use these as keys when serializing request/response bodies.
|
|
181
|
-
*/
|
|
182
|
-
const mediaTypes = {
|
|
183
|
-
applicationJson: "application/json",
|
|
184
|
-
applicationXml: "application/xml",
|
|
185
|
-
applicationFormUrlEncoded: "application/x-www-form-urlencoded",
|
|
186
|
-
applicationOctetStream: "application/octet-stream",
|
|
187
|
-
applicationPdf: "application/pdf",
|
|
188
|
-
applicationZip: "application/zip",
|
|
189
|
-
applicationGraphql: "application/graphql",
|
|
190
|
-
multipartFormData: "multipart/form-data",
|
|
191
|
-
textPlain: "text/plain",
|
|
192
|
-
textHtml: "text/html",
|
|
193
|
-
textCsv: "text/csv",
|
|
194
|
-
textXml: "text/xml",
|
|
195
|
-
imagePng: "image/png",
|
|
196
|
-
imageJpeg: "image/jpeg",
|
|
197
|
-
imageGif: "image/gif",
|
|
198
|
-
imageWebp: "image/webp",
|
|
199
|
-
imageSvgXml: "image/svg+xml",
|
|
200
|
-
audioMpeg: "audio/mpeg",
|
|
201
|
-
videoMp4: "video/mp4"
|
|
202
|
-
};
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region ../../internals/utils/src/casing.ts
|
|
205
|
-
/**
|
|
206
|
-
* Shared implementation for camelCase and PascalCase conversion.
|
|
207
|
-
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
208
|
-
* and capitalizes each word according to `pascal`.
|
|
209
|
-
*
|
|
210
|
-
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
211
|
-
*/
|
|
212
|
-
function toCamelOrPascal(text, pascal) {
|
|
213
|
-
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
214
|
-
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
215
|
-
if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
|
|
216
|
-
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
217
|
-
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
218
|
-
}
|
|
5
|
+
//#region ../../internals/utils/src/promise.ts
|
|
219
6
|
/**
|
|
220
|
-
*
|
|
221
|
-
* The last segment receives `isLast = true`, all earlier segments receive `false`.
|
|
222
|
-
* Segments are joined with `/` to form a file path.
|
|
7
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
223
8
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
|
|
229
|
-
* and `'..'` transforms to an empty string). Without this filter the join would produce
|
|
230
|
-
* a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
|
|
231
|
-
* generated files to escape the configured output directory.
|
|
232
|
-
*/
|
|
233
|
-
function applyToFileParts(text, transformPart) {
|
|
234
|
-
const parts = text.split(/\.(?=[a-zA-Z])/);
|
|
235
|
-
return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Converts `text` to camelCase.
|
|
239
|
-
* When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
|
|
9
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
10
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
11
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
12
|
+
* inner (created once per outer miss) keyed by the second.
|
|
240
13
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* camelCase('pet.petId', { isFile: true }) // 'pet/petId'
|
|
244
|
-
*/
|
|
245
|
-
function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
246
|
-
if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
|
|
247
|
-
prefix,
|
|
248
|
-
suffix
|
|
249
|
-
} : {}));
|
|
250
|
-
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Converts `text` to PascalCase.
|
|
254
|
-
* When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
|
|
14
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
15
|
+
* cleared independently of the memoized function.
|
|
255
16
|
*
|
|
256
|
-
* @example
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
|
|
262
|
-
prefix,
|
|
263
|
-
suffix
|
|
264
|
-
}) : camelCase(part));
|
|
265
|
-
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
266
|
-
}
|
|
267
|
-
//#endregion
|
|
268
|
-
//#region ../../internals/utils/src/reserved.ts
|
|
269
|
-
/**
|
|
270
|
-
* JavaScript and Java reserved words.
|
|
271
|
-
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
272
|
-
*/
|
|
273
|
-
const reservedWords = new Set([
|
|
274
|
-
"abstract",
|
|
275
|
-
"arguments",
|
|
276
|
-
"boolean",
|
|
277
|
-
"break",
|
|
278
|
-
"byte",
|
|
279
|
-
"case",
|
|
280
|
-
"catch",
|
|
281
|
-
"char",
|
|
282
|
-
"class",
|
|
283
|
-
"const",
|
|
284
|
-
"continue",
|
|
285
|
-
"debugger",
|
|
286
|
-
"default",
|
|
287
|
-
"delete",
|
|
288
|
-
"do",
|
|
289
|
-
"double",
|
|
290
|
-
"else",
|
|
291
|
-
"enum",
|
|
292
|
-
"eval",
|
|
293
|
-
"export",
|
|
294
|
-
"extends",
|
|
295
|
-
"false",
|
|
296
|
-
"final",
|
|
297
|
-
"finally",
|
|
298
|
-
"float",
|
|
299
|
-
"for",
|
|
300
|
-
"function",
|
|
301
|
-
"goto",
|
|
302
|
-
"if",
|
|
303
|
-
"implements",
|
|
304
|
-
"import",
|
|
305
|
-
"in",
|
|
306
|
-
"instanceof",
|
|
307
|
-
"int",
|
|
308
|
-
"interface",
|
|
309
|
-
"let",
|
|
310
|
-
"long",
|
|
311
|
-
"native",
|
|
312
|
-
"new",
|
|
313
|
-
"null",
|
|
314
|
-
"package",
|
|
315
|
-
"private",
|
|
316
|
-
"protected",
|
|
317
|
-
"public",
|
|
318
|
-
"return",
|
|
319
|
-
"short",
|
|
320
|
-
"static",
|
|
321
|
-
"super",
|
|
322
|
-
"switch",
|
|
323
|
-
"synchronized",
|
|
324
|
-
"this",
|
|
325
|
-
"throw",
|
|
326
|
-
"throws",
|
|
327
|
-
"transient",
|
|
328
|
-
"true",
|
|
329
|
-
"try",
|
|
330
|
-
"typeof",
|
|
331
|
-
"var",
|
|
332
|
-
"void",
|
|
333
|
-
"volatile",
|
|
334
|
-
"while",
|
|
335
|
-
"with",
|
|
336
|
-
"yield",
|
|
337
|
-
"Array",
|
|
338
|
-
"Date",
|
|
339
|
-
"hasOwnProperty",
|
|
340
|
-
"Infinity",
|
|
341
|
-
"isFinite",
|
|
342
|
-
"isNaN",
|
|
343
|
-
"isPrototypeOf",
|
|
344
|
-
"length",
|
|
345
|
-
"Math",
|
|
346
|
-
"name",
|
|
347
|
-
"NaN",
|
|
348
|
-
"Number",
|
|
349
|
-
"Object",
|
|
350
|
-
"prototype",
|
|
351
|
-
"String",
|
|
352
|
-
"toString",
|
|
353
|
-
"undefined",
|
|
354
|
-
"valueOf"
|
|
355
|
-
]);
|
|
356
|
-
/**
|
|
357
|
-
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
17
|
+
* @example Single WeakMap key
|
|
18
|
+
* ```ts
|
|
19
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
20
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
21
|
+
* ```
|
|
358
22
|
*
|
|
359
|
-
* @example
|
|
23
|
+
* @example Single Map key (primitive)
|
|
360
24
|
* ```ts
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* isValidVarName('42foo') // false (starts with digit)
|
|
25
|
+
* const cache = new Map<string, Resolver>()
|
|
26
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
364
27
|
* ```
|
|
365
|
-
*/
|
|
366
|
-
function isValidVarName(name) {
|
|
367
|
-
if (!name || reservedWords.has(name)) return false;
|
|
368
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
369
|
-
}
|
|
370
|
-
//#endregion
|
|
371
|
-
//#region ../../internals/utils/src/string.ts
|
|
372
|
-
/**
|
|
373
|
-
* Strips the file extension from a path or file name.
|
|
374
|
-
* Only removes the last `.ext` segment when the dot is not part of a directory name.
|
|
375
28
|
*
|
|
376
|
-
* @example
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
29
|
+
* @example Two-level (object + primitive)
|
|
30
|
+
* ```ts
|
|
31
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
32
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
33
|
+
* fn(params)('camelcase')
|
|
34
|
+
* ```
|
|
381
35
|
*/
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
36
|
+
function memoize(store, factory) {
|
|
37
|
+
return (key) => {
|
|
38
|
+
if (store.has(key)) return store.get(key);
|
|
39
|
+
const value = factory(key);
|
|
40
|
+
store.set(key, value);
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
386
43
|
}
|
|
387
44
|
//#endregion
|
|
388
45
|
//#region src/guards.ts
|
|
@@ -392,11 +49,11 @@ function trimExtName(text) {
|
|
|
392
49
|
* @example
|
|
393
50
|
* ```ts
|
|
394
51
|
* const schema = createSchema({ type: 'string' })
|
|
395
|
-
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode |
|
|
52
|
+
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
|
|
396
53
|
* ```
|
|
397
54
|
*/
|
|
398
55
|
function narrowSchema(node, type) {
|
|
399
|
-
return node?.type === type ? node :
|
|
56
|
+
return node?.type === type ? node : null;
|
|
400
57
|
}
|
|
401
58
|
function isKind(kind) {
|
|
402
59
|
return (node) => node.kind === kind;
|
|
@@ -435,37 +92,29 @@ const isOutputNode = isKind("Output");
|
|
|
435
92
|
*/
|
|
436
93
|
const isOperationNode = isKind("Operation");
|
|
437
94
|
/**
|
|
438
|
-
*
|
|
95
|
+
* Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
|
|
439
96
|
*
|
|
440
97
|
* @example
|
|
441
98
|
* ```ts
|
|
442
|
-
* if (
|
|
443
|
-
* console.log(node.
|
|
99
|
+
* if (isHttpOperationNode(node)) {
|
|
100
|
+
* console.log(node.method, node.path)
|
|
444
101
|
* }
|
|
445
102
|
* ```
|
|
446
103
|
*/
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
isKind("Response");
|
|
451
|
-
isKind("FunctionParameter");
|
|
452
|
-
isKind("ParameterGroup");
|
|
453
|
-
isKind("FunctionParameters");
|
|
454
|
-
//#endregion
|
|
455
|
-
//#region src/refs.ts
|
|
104
|
+
function isHttpOperationNode(node) {
|
|
105
|
+
return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
|
|
106
|
+
}
|
|
456
107
|
/**
|
|
457
|
-
* Returns
|
|
458
|
-
*
|
|
459
|
-
* Example: `#/components/schemas/Pet` becomes `Pet`.
|
|
108
|
+
* Returns `true` when the input is a `SchemaNode`.
|
|
460
109
|
*
|
|
461
110
|
* @example
|
|
462
111
|
* ```ts
|
|
463
|
-
*
|
|
112
|
+
* if (isSchemaNode(node)) {
|
|
113
|
+
* console.log(node.type)
|
|
114
|
+
* }
|
|
464
115
|
* ```
|
|
465
116
|
*/
|
|
466
|
-
|
|
467
|
-
return ref.split("/").at(-1) ?? ref;
|
|
468
|
-
}
|
|
117
|
+
const isSchemaNode = isKind("Schema");
|
|
469
118
|
//#endregion
|
|
470
119
|
//#region src/visitor.ts
|
|
471
120
|
/**
|
|
@@ -503,53 +152,92 @@ function createLimit(concurrency) {
|
|
|
503
152
|
});
|
|
504
153
|
};
|
|
505
154
|
}
|
|
155
|
+
const visitorKeysByKind = {
|
|
156
|
+
Input: ["schemas", "operations"],
|
|
157
|
+
Operation: [
|
|
158
|
+
"parameters",
|
|
159
|
+
"requestBody",
|
|
160
|
+
"responses"
|
|
161
|
+
],
|
|
162
|
+
RequestBody: ["content"],
|
|
163
|
+
Content: ["schema"],
|
|
164
|
+
Response: ["content"],
|
|
165
|
+
Schema: [
|
|
166
|
+
"properties",
|
|
167
|
+
"items",
|
|
168
|
+
"members",
|
|
169
|
+
"additionalProperties"
|
|
170
|
+
],
|
|
171
|
+
Property: ["schema"],
|
|
172
|
+
Parameter: ["schema"]
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Returns `true` when `value` is an AST node (an object carrying a `kind`).
|
|
176
|
+
*/
|
|
177
|
+
function isNode(value) {
|
|
178
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
179
|
+
}
|
|
506
180
|
/**
|
|
507
|
-
* Returns the immediate traversable children of `node
|
|
181
|
+
* Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
|
|
508
182
|
*
|
|
509
|
-
*
|
|
510
|
-
* `additionalProperties`) are only included
|
|
511
|
-
* when `recurse` is `true`; shallow mode skips them.
|
|
183
|
+
* `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
|
|
512
184
|
*
|
|
513
185
|
* @example
|
|
514
186
|
* ```ts
|
|
515
187
|
* const children = getChildren(operationNode, true)
|
|
516
|
-
* // returns parameters,
|
|
517
|
-
* ```
|
|
518
|
-
*/
|
|
519
|
-
function getChildren(node, recurse) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const children = [];
|
|
530
|
-
if (!recurse) return [];
|
|
531
|
-
if ("properties" in node && node.properties.length > 0) children.push(...node.properties);
|
|
532
|
-
if ("items" in node && node.items) children.push(...node.items);
|
|
533
|
-
if ("members" in node && node.members) children.push(...node.members);
|
|
534
|
-
if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
|
|
535
|
-
return children;
|
|
536
|
-
}
|
|
537
|
-
case "Property": return [node.schema];
|
|
538
|
-
case "Parameter": return [node.schema];
|
|
539
|
-
case "Response": return node.schema ? [node.schema] : [];
|
|
540
|
-
case "FunctionParameter":
|
|
541
|
-
case "ParameterGroup":
|
|
542
|
-
case "FunctionParameters":
|
|
543
|
-
case "Type": return [];
|
|
544
|
-
default: return [];
|
|
188
|
+
* // returns parameters, the request body, and responses
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
function* getChildren(node, recurse) {
|
|
192
|
+
if (node.kind === "Schema" && !recurse) return;
|
|
193
|
+
const keys = visitorKeysByKind[node.kind];
|
|
194
|
+
if (!keys) return;
|
|
195
|
+
const record = node;
|
|
196
|
+
for (const key of keys) {
|
|
197
|
+
const value = record[key];
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
for (const item of value) if (isNode(item)) yield item;
|
|
200
|
+
} else if (isNode(value)) yield value;
|
|
545
201
|
}
|
|
546
202
|
}
|
|
547
203
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
204
|
+
* Maps a node `kind` to the matching visitor callback name. Only the seven
|
|
205
|
+
* traversable node kinds have an entry. Every other kind resolves to
|
|
206
|
+
* `undefined` and is skipped.
|
|
207
|
+
*/
|
|
208
|
+
const VISITOR_KEY_BY_KIND = {
|
|
209
|
+
Input: "input",
|
|
210
|
+
Output: "output",
|
|
211
|
+
Operation: "operation",
|
|
212
|
+
Schema: "schema",
|
|
213
|
+
Property: "property",
|
|
214
|
+
Parameter: "parameter",
|
|
215
|
+
Response: "response"
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Invokes the visitor callback that matches `node.kind`, passing the traversal
|
|
219
|
+
* context. Returns the callback's result (a replacement node, a collected
|
|
220
|
+
* value, or `undefined` when no callback is registered for the kind).
|
|
551
221
|
*
|
|
552
|
-
*
|
|
222
|
+
* Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
|
|
223
|
+
* in one place. `TResult` is the caller's expected return: the same node type
|
|
224
|
+
* for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
|
|
225
|
+
*/
|
|
226
|
+
function applyVisitor(node, visitor, parent) {
|
|
227
|
+
const key = VISITOR_KEY_BY_KIND[node.kind];
|
|
228
|
+
if (!key) return void 0;
|
|
229
|
+
const fn = visitor[key];
|
|
230
|
+
return fn?.(node, { parent });
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Async depth-first traversal for side effects. Visitor return values are
|
|
234
|
+
* ignored. Use `transform` when you want to rewrite nodes.
|
|
235
|
+
*
|
|
236
|
+
* Sibling nodes at each depth run concurrently up to `options.concurrency`
|
|
237
|
+
* (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
|
|
238
|
+
* work. Lower values reduce memory pressure.
|
|
239
|
+
*
|
|
240
|
+
* @example Log every operation
|
|
553
241
|
* ```ts
|
|
554
242
|
* await walk(root, {
|
|
555
243
|
* operation(node) {
|
|
@@ -558,216 +246,118 @@ function getChildren(node, recurse) {
|
|
|
558
246
|
* })
|
|
559
247
|
* ```
|
|
560
248
|
*
|
|
561
|
-
* @example
|
|
249
|
+
* @example Only visit the root node
|
|
562
250
|
* ```ts
|
|
563
|
-
*
|
|
564
|
-
* await walk(root, { depth: 'shallow', root: () => {} })
|
|
251
|
+
* await walk(root, { depth: 'shallow', input: () => {} })
|
|
565
252
|
* ```
|
|
566
253
|
*/
|
|
567
254
|
async function walk(node, options) {
|
|
568
255
|
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
569
256
|
}
|
|
570
257
|
async function _walk(node, visitor, recurse, limit, parent) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
case "Output":
|
|
576
|
-
await limit(() => visitor.output?.(node, { parent }));
|
|
577
|
-
break;
|
|
578
|
-
case "Operation":
|
|
579
|
-
await limit(() => visitor.operation?.(node, { parent }));
|
|
580
|
-
break;
|
|
581
|
-
case "Schema":
|
|
582
|
-
await limit(() => visitor.schema?.(node, { parent }));
|
|
583
|
-
break;
|
|
584
|
-
case "Property":
|
|
585
|
-
await limit(() => visitor.property?.(node, { parent }));
|
|
586
|
-
break;
|
|
587
|
-
case "Parameter":
|
|
588
|
-
await limit(() => visitor.parameter?.(node, { parent }));
|
|
589
|
-
break;
|
|
590
|
-
case "Response":
|
|
591
|
-
await limit(() => visitor.response?.(node, { parent }));
|
|
592
|
-
break;
|
|
593
|
-
case "FunctionParameter":
|
|
594
|
-
case "ParameterGroup":
|
|
595
|
-
case "FunctionParameters": break;
|
|
596
|
-
}
|
|
597
|
-
const children = getChildren(node, recurse);
|
|
598
|
-
for (const child of children) await _walk(child, visitor, recurse, limit, node);
|
|
258
|
+
await limit(() => applyVisitor(node, visitor, parent));
|
|
259
|
+
const children = Array.from(getChildren(node, recurse));
|
|
260
|
+
if (children.length === 0) return;
|
|
261
|
+
await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
|
|
599
262
|
}
|
|
600
263
|
function transform(node, options) {
|
|
601
264
|
const { depth, parent, ...visitor } = options;
|
|
602
265
|
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
parent: op
|
|
643
|
-
}) : void 0
|
|
644
|
-
}))
|
|
645
|
-
} : void 0,
|
|
646
|
-
responses: op.responses.map((r) => transform(r, {
|
|
647
|
-
...options,
|
|
648
|
-
parent: op
|
|
649
|
-
}))
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
case "Schema": {
|
|
653
|
-
let schema = node;
|
|
654
|
-
const replaced = visitor.schema?.(schema, { parent });
|
|
655
|
-
if (replaced) schema = replaced;
|
|
656
|
-
const childOptions = {
|
|
657
|
-
...options,
|
|
658
|
-
parent: schema
|
|
659
|
-
};
|
|
660
|
-
return {
|
|
661
|
-
...schema,
|
|
662
|
-
..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
|
|
663
|
-
..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
|
|
664
|
-
..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
|
|
665
|
-
..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
case "Property": {
|
|
669
|
-
let prop = node;
|
|
670
|
-
const replaced = visitor.property?.(prop, { parent });
|
|
671
|
-
if (replaced) prop = replaced;
|
|
672
|
-
return createProperty({
|
|
673
|
-
...prop,
|
|
674
|
-
schema: transform(prop.schema, {
|
|
675
|
-
...options,
|
|
676
|
-
parent: prop
|
|
677
|
-
})
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
case "Parameter": {
|
|
681
|
-
let param = node;
|
|
682
|
-
const replaced = visitor.parameter?.(param, { parent });
|
|
683
|
-
if (replaced) param = replaced;
|
|
684
|
-
return createParameter({
|
|
685
|
-
...param,
|
|
686
|
-
schema: transform(param.schema, {
|
|
687
|
-
...options,
|
|
688
|
-
parent: param
|
|
689
|
-
})
|
|
266
|
+
const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
|
|
267
|
+
if (rebuilt === node) return node;
|
|
268
|
+
const finalize = nodeFinalizers[rebuilt.kind];
|
|
269
|
+
return finalize ? finalize(rebuilt) : rebuilt;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
|
|
273
|
+
* resync schema optionality against their `required` flag once the schema may
|
|
274
|
+
* have changed.
|
|
275
|
+
*/
|
|
276
|
+
const nodeFinalizers = {
|
|
277
|
+
Property: (node) => createProperty(node),
|
|
278
|
+
Parameter: (node) => createParameter(node)
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
|
|
282
|
+
* each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
|
|
283
|
+
* `Schema` children are skipped in shallow mode.
|
|
284
|
+
*/
|
|
285
|
+
function transformChildren(node, options, recurse) {
|
|
286
|
+
if (node.kind === "Schema" && !recurse) return node;
|
|
287
|
+
const keys = visitorKeysByKind[node.kind];
|
|
288
|
+
if (!keys) return node;
|
|
289
|
+
const record = node;
|
|
290
|
+
const childOptions = {
|
|
291
|
+
...options,
|
|
292
|
+
parent: node
|
|
293
|
+
};
|
|
294
|
+
let updates;
|
|
295
|
+
for (const key of keys) {
|
|
296
|
+
if (!(key in record)) continue;
|
|
297
|
+
const value = record[key];
|
|
298
|
+
if (Array.isArray(value)) {
|
|
299
|
+
let changed = false;
|
|
300
|
+
const mapped = value.map((item) => {
|
|
301
|
+
if (!isNode(item)) return item;
|
|
302
|
+
const next = transform(item, childOptions);
|
|
303
|
+
if (next !== item) changed = true;
|
|
304
|
+
return next;
|
|
690
305
|
});
|
|
306
|
+
if (changed) (updates ??= {})[key] = mapped;
|
|
307
|
+
} else if (isNode(value)) {
|
|
308
|
+
const next = transform(value, childOptions);
|
|
309
|
+
if (next !== value) (updates ??= {})[key] = next;
|
|
691
310
|
}
|
|
692
|
-
case "Response": {
|
|
693
|
-
let response = node;
|
|
694
|
-
const replaced = visitor.response?.(response, { parent });
|
|
695
|
-
if (replaced) response = replaced;
|
|
696
|
-
return {
|
|
697
|
-
...response,
|
|
698
|
-
schema: transform(response.schema, {
|
|
699
|
-
...options,
|
|
700
|
-
parent: response
|
|
701
|
-
})
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
case "FunctionParameter":
|
|
705
|
-
case "ParameterGroup":
|
|
706
|
-
case "FunctionParameters":
|
|
707
|
-
case "Type": return node;
|
|
708
|
-
default: return node;
|
|
709
311
|
}
|
|
312
|
+
return updates ? {
|
|
313
|
+
...node,
|
|
314
|
+
...updates
|
|
315
|
+
} : node;
|
|
710
316
|
}
|
|
711
317
|
/**
|
|
712
|
-
*
|
|
318
|
+
* Lazy depth-first collection pass. Yields every non-null value returned by
|
|
319
|
+
* the visitor callbacks. Use `collect` for the eager array form.
|
|
713
320
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
* @example
|
|
321
|
+
* @example Collect every operationId
|
|
717
322
|
* ```ts
|
|
718
|
-
* const ids =
|
|
323
|
+
* const ids: string[] = []
|
|
324
|
+
* for (const id of collectLazy<string>(root, {
|
|
719
325
|
* operation(node) {
|
|
720
326
|
* return node.operationId
|
|
721
327
|
* },
|
|
722
|
-
* })
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* @example
|
|
726
|
-
* ```ts
|
|
727
|
-
* // Collect from only the current node
|
|
728
|
-
* const values = collect(root, { depth: 'shallow', root: () => 'root' })
|
|
328
|
+
* })) {
|
|
329
|
+
* ids.push(id)
|
|
330
|
+
* }
|
|
729
331
|
* ```
|
|
730
332
|
*/
|
|
731
|
-
function
|
|
333
|
+
function* collectLazy(node, options) {
|
|
732
334
|
const { depth, parent, ...visitor } = options;
|
|
733
335
|
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
case "Input":
|
|
738
|
-
v = visitor.input?.(node, { parent });
|
|
739
|
-
break;
|
|
740
|
-
case "Output":
|
|
741
|
-
v = visitor.output?.(node, { parent });
|
|
742
|
-
break;
|
|
743
|
-
case "Operation":
|
|
744
|
-
v = visitor.operation?.(node, { parent });
|
|
745
|
-
break;
|
|
746
|
-
case "Schema":
|
|
747
|
-
v = visitor.schema?.(node, { parent });
|
|
748
|
-
break;
|
|
749
|
-
case "Property":
|
|
750
|
-
v = visitor.property?.(node, { parent });
|
|
751
|
-
break;
|
|
752
|
-
case "Parameter":
|
|
753
|
-
v = visitor.parameter?.(node, { parent });
|
|
754
|
-
break;
|
|
755
|
-
case "Response":
|
|
756
|
-
v = visitor.response?.(node, { parent });
|
|
757
|
-
break;
|
|
758
|
-
case "FunctionParameter":
|
|
759
|
-
case "ParameterGroup":
|
|
760
|
-
case "FunctionParameters": break;
|
|
761
|
-
}
|
|
762
|
-
if (v !== void 0) results.push(v);
|
|
763
|
-
for (const child of getChildren(node, recurse)) for (const item of collect(child, {
|
|
336
|
+
const v = applyVisitor(node, visitor, parent);
|
|
337
|
+
if (v != null) yield v;
|
|
338
|
+
for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
|
|
764
339
|
...options,
|
|
765
340
|
parent: node
|
|
766
|
-
})
|
|
767
|
-
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Eager depth-first collection pass. Returns an array of every non-null value
|
|
345
|
+
* the visitor callbacks return.
|
|
346
|
+
*
|
|
347
|
+
* @example Collect every operationId
|
|
348
|
+
* ```ts
|
|
349
|
+
* const ids = collect<string>(root, {
|
|
350
|
+
* operation(node) {
|
|
351
|
+
* return node.operationId
|
|
352
|
+
* },
|
|
353
|
+
* })
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
function collect(node, options) {
|
|
357
|
+
return Array.from(collectLazy(node, options));
|
|
768
358
|
}
|
|
769
359
|
//#endregion
|
|
770
|
-
//#region src/utils.ts
|
|
360
|
+
//#region src/utils/ast.ts
|
|
771
361
|
const plainStringTypes = new Set([
|
|
772
362
|
"string",
|
|
773
363
|
"uuid",
|
|
@@ -818,15 +408,16 @@ function isStringType(node) {
|
|
|
818
408
|
* the desired casing while preserving `OperationNode.parameters` for other consumers.
|
|
819
409
|
* The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
|
|
820
410
|
*/
|
|
411
|
+
const caseParamsMemo = memoize(/* @__PURE__ */ new WeakMap(), (params) => memoize(/* @__PURE__ */ new Map(), (casing) => params.map((param) => {
|
|
412
|
+
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
413
|
+
return {
|
|
414
|
+
...param,
|
|
415
|
+
name: transformed
|
|
416
|
+
};
|
|
417
|
+
})));
|
|
821
418
|
function caseParams(params, casing) {
|
|
822
419
|
if (!casing) return params;
|
|
823
|
-
return params
|
|
824
|
-
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
825
|
-
return {
|
|
826
|
-
...param,
|
|
827
|
-
name: transformed
|
|
828
|
-
};
|
|
829
|
-
});
|
|
420
|
+
return caseParamsMemo(params)(casing);
|
|
830
421
|
}
|
|
831
422
|
/**
|
|
832
423
|
* Creates a single-property object schema used as a discriminator literal.
|
|
@@ -955,7 +546,7 @@ function createOperationParams(node, options) {
|
|
|
955
546
|
}));
|
|
956
547
|
} else {
|
|
957
548
|
if (pathParams.length) if (pathParamsType === "inlineSpread") {
|
|
958
|
-
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0])
|
|
549
|
+
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]);
|
|
959
550
|
params.push(createFunctionParameter({
|
|
960
551
|
name: pathName,
|
|
961
552
|
type: spreadType ? wrapType(spreadType) : void 0,
|
|
@@ -1031,13 +622,13 @@ function buildGroupParam({ name, node, params, groupType, resolver, wrapType })
|
|
|
1031
622
|
}
|
|
1032
623
|
/**
|
|
1033
624
|
* Derives a {@link ParamGroupType} from the resolver's group method.
|
|
1034
|
-
* Returns `
|
|
625
|
+
* Returns `null` when the group name equals the individual param name (no real group).
|
|
1035
626
|
*/
|
|
1036
627
|
function resolveGroupType({ node, params, groupMethod, resolver }) {
|
|
1037
|
-
if (!params.length) return;
|
|
628
|
+
if (!params.length) return null;
|
|
1038
629
|
const firstParam = params[0];
|
|
1039
630
|
const groupName = groupMethod.call(resolver, node, firstParam);
|
|
1040
|
-
if (groupName === resolver.resolveParamName(node, firstParam)) return;
|
|
631
|
+
if (groupName === resolver.resolveParamName(node, firstParam)) return null;
|
|
1041
632
|
const allOptional = params.every((p) => !p.required);
|
|
1042
633
|
return {
|
|
1043
634
|
type: createParamsType({
|
|
@@ -1081,13 +672,13 @@ function importKey(path, name, isTypeOnly) {
|
|
|
1081
672
|
}
|
|
1082
673
|
/**
|
|
1083
674
|
* Computes a multi-level sort key for exports and imports:
|
|
1084
|
-
* non-array names first (wildcards/namespace aliases)
|
|
675
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
1085
676
|
*/
|
|
1086
677
|
function sortKey(node) {
|
|
1087
678
|
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
1088
679
|
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
1089
680
|
const hasName = node.name != null ? "1" : "0";
|
|
1090
|
-
const name = Array.isArray(node.name) ?
|
|
681
|
+
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
1091
682
|
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
1092
683
|
}
|
|
1093
684
|
/**
|
|
@@ -1104,6 +695,16 @@ function combineSources(sources) {
|
|
|
1104
695
|
return [...seen.values()];
|
|
1105
696
|
}
|
|
1106
697
|
/**
|
|
698
|
+
* Merges `incoming` names into `existing`, preserving order and dropping duplicates.
|
|
699
|
+
*
|
|
700
|
+
* Shared by `combineExports` and `combineImports` for the same-path name-merge case.
|
|
701
|
+
*/
|
|
702
|
+
function mergeNameArrays(existing, incoming) {
|
|
703
|
+
const merged = new Set(existing);
|
|
704
|
+
for (const name of incoming) merged.add(name);
|
|
705
|
+
return [...merged];
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
1107
708
|
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
1108
709
|
*
|
|
1109
710
|
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
@@ -1124,11 +725,8 @@ function combineExports(exports) {
|
|
|
1124
725
|
if (!name.length) continue;
|
|
1125
726
|
const key = pathTypeKey(path, isTypeOnly);
|
|
1126
727
|
const existing = namedByPath.get(key);
|
|
1127
|
-
if (existing && Array.isArray(existing.name))
|
|
1128
|
-
|
|
1129
|
-
for (const n of name) merged.add(n);
|
|
1130
|
-
existing.name = [...merged];
|
|
1131
|
-
} else {
|
|
728
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
729
|
+
else {
|
|
1132
730
|
const newItem = {
|
|
1133
731
|
...curr,
|
|
1134
732
|
name: [...new Set(name)]
|
|
@@ -1164,6 +762,11 @@ function combineImports(imports, exports, source) {
|
|
|
1164
762
|
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
1165
763
|
return importNameMemo.get(key);
|
|
1166
764
|
};
|
|
765
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
766
|
+
for (const node of imports) {
|
|
767
|
+
if (!Array.isArray(node.name)) continue;
|
|
768
|
+
if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
|
|
769
|
+
}
|
|
1167
770
|
const result = [];
|
|
1168
771
|
const namedByPath = /* @__PURE__ */ new Map();
|
|
1169
772
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1181,11 +784,8 @@ function combineImports(imports, exports, source) {
|
|
|
1181
784
|
if (!name.length) continue;
|
|
1182
785
|
const key = pathTypeKey(path, isTypeOnly);
|
|
1183
786
|
const existing = namedByPath.get(key);
|
|
1184
|
-
if (existing && Array.isArray(existing.name))
|
|
1185
|
-
|
|
1186
|
-
for (const n of name) merged.add(n);
|
|
1187
|
-
existing.name = [...merged];
|
|
1188
|
-
} else {
|
|
787
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
788
|
+
else {
|
|
1189
789
|
const newItem = {
|
|
1190
790
|
...curr,
|
|
1191
791
|
name
|
|
@@ -1194,7 +794,7 @@ function combineImports(imports, exports, source) {
|
|
|
1194
794
|
namedByPath.set(key, newItem);
|
|
1195
795
|
}
|
|
1196
796
|
} else {
|
|
1197
|
-
if (name && !isUsed(name)) continue;
|
|
797
|
+
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
1198
798
|
const key = importKey(path, name, isTypeOnly);
|
|
1199
799
|
if (!seen.has(key)) {
|
|
1200
800
|
result.push(curr);
|
|
@@ -1230,7 +830,7 @@ function extractStringsFromNodes(nodes) {
|
|
|
1230
830
|
/**
|
|
1231
831
|
* Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
|
|
1232
832
|
*
|
|
1233
|
-
* Returns `
|
|
833
|
+
* Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
|
|
1234
834
|
* identifier for type definitions or error messages.
|
|
1235
835
|
*
|
|
1236
836
|
* @example
|
|
@@ -1240,14 +840,14 @@ function extractStringsFromNodes(nodes) {
|
|
|
1240
840
|
* ```
|
|
1241
841
|
*/
|
|
1242
842
|
function resolveRefName(node) {
|
|
1243
|
-
if (!node || node.type !== "ref") return
|
|
1244
|
-
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ??
|
|
1245
|
-
return node.name ?? node.schema?.name ??
|
|
843
|
+
if (!node || node.type !== "ref") return null;
|
|
844
|
+
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null;
|
|
845
|
+
return node.name ?? node.schema?.name ?? null;
|
|
1246
846
|
}
|
|
1247
847
|
/**
|
|
1248
848
|
* Collects every named schema referenced (transitively) from a node via ref edges.
|
|
1249
849
|
*
|
|
1250
|
-
* Refs are followed by name only
|
|
850
|
+
* Refs are followed by name only, the resolved `node.schema` is not traversed inline.
|
|
1251
851
|
* Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
|
|
1252
852
|
*
|
|
1253
853
|
* @example Collect refs from a single schema
|
|
@@ -1264,21 +864,26 @@ function resolveRefName(node) {
|
|
|
1264
864
|
* }
|
|
1265
865
|
* ```
|
|
1266
866
|
*/
|
|
1267
|
-
|
|
1268
|
-
|
|
867
|
+
const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
|
|
868
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1269
869
|
collect(node, { schema(child) {
|
|
1270
870
|
if (child.type === "ref") {
|
|
1271
871
|
const name = resolveRefName(child);
|
|
1272
|
-
if (name)
|
|
872
|
+
if (name) refs.add(name);
|
|
1273
873
|
}
|
|
1274
874
|
} });
|
|
875
|
+
return refs;
|
|
876
|
+
});
|
|
877
|
+
function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
878
|
+
if (!node) return out;
|
|
879
|
+
for (const name of collectSchemaRefs(node)) out.add(name);
|
|
1275
880
|
return out;
|
|
1276
881
|
}
|
|
1277
882
|
/**
|
|
1278
883
|
* Collects the names of all top-level schemas transitively used by a set of operations.
|
|
1279
884
|
*
|
|
1280
885
|
* An operation uses a schema when any of its parameters, request body content, or responses
|
|
1281
|
-
* reference it
|
|
886
|
+
* reference it, directly or indirectly through other named schemas.
|
|
1282
887
|
* The walk is iterative and safe against reference cycles.
|
|
1283
888
|
*
|
|
1284
889
|
* Use this together with `include` filters to determine which schemas from `components/schemas`
|
|
@@ -1287,10 +892,10 @@ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
|
1287
892
|
*
|
|
1288
893
|
* @example Only generate schemas referenced by included operations
|
|
1289
894
|
* ```ts
|
|
1290
|
-
* const includedOps =
|
|
1291
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
895
|
+
* const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
|
|
896
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1292
897
|
*
|
|
1293
|
-
* for (const schema of
|
|
898
|
+
* for (const schema of schemas) {
|
|
1294
899
|
* if (schema.name && !allowed.has(schema.name)) continue
|
|
1295
900
|
* // … generate schema
|
|
1296
901
|
* }
|
|
@@ -1298,11 +903,12 @@ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
|
1298
903
|
*
|
|
1299
904
|
* @example Check whether a specific schema is needed
|
|
1300
905
|
* ```ts
|
|
1301
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
906
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1302
907
|
* allowed.has('OrderStatus') // false when no included operation references OrderStatus
|
|
1303
908
|
* ```
|
|
1304
909
|
*/
|
|
1305
|
-
|
|
910
|
+
const collectUsedSchemaNamesMemo = memoize(/* @__PURE__ */ new WeakMap(), (ops) => memoize(/* @__PURE__ */ new WeakMap(), (schemas) => computeUsedSchemaNames(ops, schemas)));
|
|
911
|
+
function computeUsedSchemaNames(operations, schemas) {
|
|
1306
912
|
const schemaMap = /* @__PURE__ */ new Map();
|
|
1307
913
|
for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
|
|
1308
914
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -1314,22 +920,17 @@ function collectUsedSchemaNames(operations, schemas) {
|
|
|
1314
920
|
if (namedSchema) visitSchema(namedSchema);
|
|
1315
921
|
}
|
|
1316
922
|
}
|
|
1317
|
-
for (const op of operations) for (const schema of
|
|
923
|
+
for (const op of operations) for (const schema of collectLazy(op, {
|
|
1318
924
|
depth: "shallow",
|
|
1319
925
|
schema: (node) => node
|
|
1320
926
|
})) visitSchema(schema);
|
|
1321
927
|
return result;
|
|
1322
928
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
1329
|
-
*
|
|
1330
|
-
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
1331
|
-
*/
|
|
1332
|
-
function findCircularSchemas(schemas) {
|
|
929
|
+
function collectUsedSchemaNames(operations, schemas) {
|
|
930
|
+
return collectUsedSchemaNamesMemo(operations)(schemas);
|
|
931
|
+
}
|
|
932
|
+
const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
|
|
933
|
+
const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
|
|
1333
934
|
const graph = /* @__PURE__ */ new Map();
|
|
1334
935
|
for (const schema of schemas) {
|
|
1335
936
|
if (!schema.name) continue;
|
|
@@ -1352,6 +953,19 @@ function findCircularSchemas(schemas) {
|
|
|
1352
953
|
}
|
|
1353
954
|
}
|
|
1354
955
|
return circular;
|
|
956
|
+
});
|
|
957
|
+
/**
|
|
958
|
+
* Identifies all schemas that participate in circular dependency chains, including direct self-loops.
|
|
959
|
+
*
|
|
960
|
+
* Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
|
|
961
|
+
* in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
|
|
962
|
+
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
963
|
+
*
|
|
964
|
+
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
965
|
+
*/
|
|
966
|
+
function findCircularSchemas(schemas) {
|
|
967
|
+
if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
|
|
968
|
+
return findCircularSchemasMemo(schemas);
|
|
1355
969
|
}
|
|
1356
970
|
/**
|
|
1357
971
|
* Type guard returning `true` when a schema or anything nested within it contains a ref to a circular schema.
|
|
@@ -1359,23 +973,27 @@ function findCircularSchemas(schemas) {
|
|
|
1359
973
|
* Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
|
|
1360
974
|
* Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
|
|
1361
975
|
*
|
|
1362
|
-
* @note Returns `true` for the first matching circular ref found
|
|
976
|
+
* @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
|
|
1363
977
|
*/
|
|
1364
978
|
function containsCircularRef(node, { circularSchemas, excludeName }) {
|
|
1365
979
|
if (!node || circularSchemas.size === 0) return false;
|
|
1366
|
-
|
|
1367
|
-
if (child.type !== "ref") return
|
|
980
|
+
for (const _ of collectLazy(node, { schema(child) {
|
|
981
|
+
if (child.type !== "ref") return null;
|
|
1368
982
|
const name = resolveRefName(child);
|
|
1369
|
-
return name && name !== excludeName && circularSchemas.has(name) ? true :
|
|
1370
|
-
} })
|
|
983
|
+
return name && name !== excludeName && circularSchemas.has(name) ? true : null;
|
|
984
|
+
} })) return true;
|
|
985
|
+
return false;
|
|
1371
986
|
}
|
|
1372
987
|
//#endregion
|
|
1373
988
|
//#region src/factory.ts
|
|
1374
989
|
/**
|
|
1375
|
-
*
|
|
990
|
+
* Updates a schema's `optional` and `nullish` flags from a parent's `required`
|
|
991
|
+
* value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
|
|
992
|
+
* object properties combine "required" and "nullable" into a single AST.
|
|
1376
993
|
*
|
|
1377
|
-
* -
|
|
1378
|
-
* -
|
|
994
|
+
* - Non-required + non-nullable → `optional: true`.
|
|
995
|
+
* - Non-required + nullable → `nullish: true`.
|
|
996
|
+
* - Required → both flags cleared.
|
|
1379
997
|
*/
|
|
1380
998
|
function syncOptionality(schema, required) {
|
|
1381
999
|
const nullable = schema.nullable ?? false;
|
|
@@ -1386,6 +1004,29 @@ function syncOptionality(schema, required) {
|
|
|
1386
1004
|
};
|
|
1387
1005
|
}
|
|
1388
1006
|
/**
|
|
1007
|
+
* Identity-preserving node update: returns `node` unchanged when every field in
|
|
1008
|
+
* `changes` already equals (by reference) the current value, otherwise a new node
|
|
1009
|
+
* with the changes applied.
|
|
1010
|
+
*
|
|
1011
|
+
* Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
|
|
1012
|
+
* structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
|
|
1013
|
+
* downstream passes can detect "nothing changed" by identity. Comparison is
|
|
1014
|
+
* shallow: a structurally-equal but newly-allocated array/object counts as a change.
|
|
1015
|
+
*
|
|
1016
|
+
* @example
|
|
1017
|
+
* ```ts
|
|
1018
|
+
* update(node, { name: node.name }) // -> same `node` reference
|
|
1019
|
+
* update(node, { name: 'renamed' }) // -> new node, `name` replaced
|
|
1020
|
+
* ```
|
|
1021
|
+
*/
|
|
1022
|
+
function update(node, changes) {
|
|
1023
|
+
for (const key in changes) if (changes[key] !== node[key]) return {
|
|
1024
|
+
...node,
|
|
1025
|
+
...changes
|
|
1026
|
+
};
|
|
1027
|
+
return node;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1389
1030
|
* Creates an `InputNode` with stable defaults for `schemas` and `operations`.
|
|
1390
1031
|
*
|
|
1391
1032
|
* @example
|
|
@@ -1404,11 +1045,31 @@ function createInput(overrides = {}) {
|
|
|
1404
1045
|
return {
|
|
1405
1046
|
schemas: [],
|
|
1406
1047
|
operations: [],
|
|
1048
|
+
meta: {
|
|
1049
|
+
circularNames: [],
|
|
1050
|
+
enumNames: []
|
|
1051
|
+
},
|
|
1407
1052
|
...overrides,
|
|
1408
1053
|
kind: "Input"
|
|
1409
1054
|
};
|
|
1410
1055
|
}
|
|
1411
1056
|
/**
|
|
1057
|
+
* Creates an `InputStreamNode` from pre-built `AsyncIterable` sources.
|
|
1058
|
+
*
|
|
1059
|
+
* @example
|
|
1060
|
+
* ```ts
|
|
1061
|
+
* const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
|
|
1062
|
+
* ```
|
|
1063
|
+
*/
|
|
1064
|
+
function createStreamInput(schemas, operations, meta) {
|
|
1065
|
+
return {
|
|
1066
|
+
kind: "Input",
|
|
1067
|
+
schemas,
|
|
1068
|
+
operations,
|
|
1069
|
+
meta
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1412
1073
|
* Creates an `OutputNode` with a stable default for `files`.
|
|
1413
1074
|
*
|
|
1414
1075
|
* @example
|
|
@@ -1430,40 +1091,40 @@ function createOutput(overrides = {}) {
|
|
|
1430
1091
|
};
|
|
1431
1092
|
}
|
|
1432
1093
|
/**
|
|
1433
|
-
* Creates
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
*
|
|
1443
|
-
* ```
|
|
1444
|
-
*
|
|
1445
|
-
* @example
|
|
1446
|
-
* ```ts
|
|
1447
|
-
* const operation = createOperation({
|
|
1448
|
-
* operationId: 'findPets',
|
|
1449
|
-
* method: 'GET',
|
|
1450
|
-
* path: '/pet/findByStatus',
|
|
1451
|
-
* tags: ['pet'],
|
|
1452
|
-
* })
|
|
1453
|
-
* ```
|
|
1094
|
+
* Creates a `ContentNode` for a single request-body or response content type.
|
|
1095
|
+
*/
|
|
1096
|
+
function createContent(props) {
|
|
1097
|
+
return {
|
|
1098
|
+
...props,
|
|
1099
|
+
kind: "Content"
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
|
|
1454
1104
|
*/
|
|
1105
|
+
function createRequestBody(props) {
|
|
1106
|
+
return {
|
|
1107
|
+
...props,
|
|
1108
|
+
kind: "RequestBody",
|
|
1109
|
+
content: props.content?.map(createContent)
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1455
1112
|
function createOperation(props) {
|
|
1113
|
+
const { requestBody, ...rest } = props;
|
|
1114
|
+
const isHttp = rest.method !== void 0 && rest.path !== void 0;
|
|
1456
1115
|
return {
|
|
1457
1116
|
tags: [],
|
|
1458
1117
|
parameters: [],
|
|
1459
1118
|
responses: [],
|
|
1460
|
-
...
|
|
1461
|
-
|
|
1119
|
+
...rest,
|
|
1120
|
+
...isHttp ? { protocol: "http" } : {},
|
|
1121
|
+
kind: "Operation",
|
|
1122
|
+
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
1462
1123
|
};
|
|
1463
1124
|
}
|
|
1464
1125
|
/**
|
|
1465
1126
|
* Maps schema `type` to its underlying `primitive`.
|
|
1466
|
-
* Primitive types map to themselves
|
|
1127
|
+
* Primitive types map to themselves. Special string formats map to `'string'`.
|
|
1467
1128
|
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
1468
1129
|
*/
|
|
1469
1130
|
const TYPE_TO_PRIMITIVE = {
|
|
@@ -1572,19 +1233,29 @@ function createParameter(props) {
|
|
|
1572
1233
|
/**
|
|
1573
1234
|
* Creates a `ResponseNode`.
|
|
1574
1235
|
*
|
|
1236
|
+
* Response body schemas live inside `content`. For convenience a single legacy `schema`
|
|
1237
|
+
* (with optional `mediaType`/`keysToOmit`) is normalized into one `content` entry, so the same
|
|
1238
|
+
* schema is never stored both at the node root and inside `content`.
|
|
1239
|
+
*
|
|
1575
1240
|
* @example
|
|
1576
1241
|
* ```ts
|
|
1577
1242
|
* const response = createResponse({
|
|
1578
1243
|
* statusCode: '200',
|
|
1579
|
-
*
|
|
1580
|
-
* schema: createSchema({ type: 'object', properties: [] }),
|
|
1244
|
+
* content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
|
|
1581
1245
|
* })
|
|
1582
1246
|
* ```
|
|
1583
1247
|
*/
|
|
1584
1248
|
function createResponse(props) {
|
|
1249
|
+
const { schema, mediaType, keysToOmit, content, ...rest } = props;
|
|
1250
|
+
const entries = content ?? (schema ? [{
|
|
1251
|
+
contentType: mediaType ?? "application/json",
|
|
1252
|
+
schema,
|
|
1253
|
+
keysToOmit: keysToOmit ?? null
|
|
1254
|
+
}] : void 0);
|
|
1585
1255
|
return {
|
|
1586
|
-
...
|
|
1587
|
-
kind: "Response"
|
|
1256
|
+
...rest,
|
|
1257
|
+
kind: "Response",
|
|
1258
|
+
content: entries?.map(createContent)
|
|
1588
1259
|
};
|
|
1589
1260
|
}
|
|
1590
1261
|
/**
|
|
@@ -1604,7 +1275,7 @@ function createResponse(props) {
|
|
|
1604
1275
|
* // → params?: QueryParams
|
|
1605
1276
|
* ```
|
|
1606
1277
|
*
|
|
1607
|
-
* @example Param with default (implicitly optional
|
|
1278
|
+
* @example Param with default (implicitly optional. Cannot combine with `optional: true`)
|
|
1608
1279
|
* ```ts
|
|
1609
1280
|
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
|
|
1610
1281
|
* // → config: RequestConfig = {}
|
|
@@ -1661,7 +1332,7 @@ function createParamsType(props) {
|
|
|
1661
1332
|
* // call → { id, name }
|
|
1662
1333
|
* ```
|
|
1663
1334
|
*
|
|
1664
|
-
* @example Inline (spread)
|
|
1335
|
+
* @example Inline (spread), children emitted as individual top-level parameters
|
|
1665
1336
|
* ```ts
|
|
1666
1337
|
* createParameterGroup({
|
|
1667
1338
|
* properties: [createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false })],
|
|
@@ -1763,9 +1434,9 @@ function createSource(props) {
|
|
|
1763
1434
|
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1764
1435
|
*
|
|
1765
1436
|
* Computes:
|
|
1766
|
-
* - `id`
|
|
1767
|
-
* - `name`
|
|
1768
|
-
* - `extname`
|
|
1437
|
+
* - `id` SHA256 hash of the file path
|
|
1438
|
+
* - `name` `baseName` without extension
|
|
1439
|
+
* - `extname` extension extracted from `baseName`
|
|
1769
1440
|
*
|
|
1770
1441
|
* Deduplicates:
|
|
1771
1442
|
* - `sources` via `combineSources`
|
|
@@ -1793,12 +1464,23 @@ function createFile(input) {
|
|
|
1793
1464
|
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
1794
1465
|
const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
|
|
1795
1466
|
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
1796
|
-
const
|
|
1467
|
+
const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
|
|
1468
|
+
const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name) => Boolean(name)));
|
|
1469
|
+
const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
1470
|
+
const resolvedImports = combinedImports.filter((imp) => imp.path !== input.path).flatMap((imp) => {
|
|
1471
|
+
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
1472
|
+
const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
|
|
1473
|
+
if (!kept.length) return [];
|
|
1474
|
+
return [kept.length === imp.name.length ? imp : {
|
|
1475
|
+
...imp,
|
|
1476
|
+
name: kept
|
|
1477
|
+
}];
|
|
1478
|
+
});
|
|
1797
1479
|
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
1798
1480
|
return {
|
|
1799
1481
|
kind: "File",
|
|
1800
1482
|
...input,
|
|
1801
|
-
id:
|
|
1483
|
+
id: hash("sha256", input.path, "hex"),
|
|
1802
1484
|
name: trimExtName(input.baseName),
|
|
1803
1485
|
extname,
|
|
1804
1486
|
imports: resolvedImports,
|
|
@@ -1993,24 +1675,466 @@ function createJsx(value) {
|
|
|
1993
1675
|
};
|
|
1994
1676
|
}
|
|
1995
1677
|
//#endregion
|
|
1996
|
-
//#region src/
|
|
1678
|
+
//#region src/signature.ts
|
|
1679
|
+
/**
|
|
1680
|
+
* The shape-affecting flags shared by every node kind: base primitive, format, and `nullable`.
|
|
1681
|
+
* Documentation and usage-slot flags (`optional`/`nullish`/`readOnly`/`writeOnly`) are
|
|
1682
|
+
* intentionally excluded, they describe the property slot, not the type.
|
|
1683
|
+
*/
|
|
1684
|
+
function flagsDescriptor(node) {
|
|
1685
|
+
return `${node.primitive ?? ""};${node.format ?? ""};${node.nullable ? 1 : 0}`;
|
|
1686
|
+
}
|
|
1687
|
+
function refTargetName(node) {
|
|
1688
|
+
if (node.ref) return extractRefName(node.ref);
|
|
1689
|
+
return node.name ?? "";
|
|
1690
|
+
}
|
|
1691
|
+
const arrayTupleFields = [
|
|
1692
|
+
{
|
|
1693
|
+
kind: "children",
|
|
1694
|
+
key: "items",
|
|
1695
|
+
prefix: "i"
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
kind: "child",
|
|
1699
|
+
key: "rest",
|
|
1700
|
+
prefix: "r"
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
kind: "scalar",
|
|
1704
|
+
key: "min",
|
|
1705
|
+
prefix: "mn"
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
kind: "scalar",
|
|
1709
|
+
key: "max",
|
|
1710
|
+
prefix: "mx"
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
kind: "bool",
|
|
1714
|
+
key: "unique",
|
|
1715
|
+
prefix: "u"
|
|
1716
|
+
}
|
|
1717
|
+
];
|
|
1718
|
+
const numericFields = [
|
|
1719
|
+
{
|
|
1720
|
+
kind: "scalar",
|
|
1721
|
+
key: "min",
|
|
1722
|
+
prefix: "mn"
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
kind: "scalar",
|
|
1726
|
+
key: "max",
|
|
1727
|
+
prefix: "mx"
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
kind: "scalar",
|
|
1731
|
+
key: "exclusiveMinimum",
|
|
1732
|
+
prefix: "emn"
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
kind: "scalar",
|
|
1736
|
+
key: "exclusiveMaximum",
|
|
1737
|
+
prefix: "emx"
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
kind: "scalar",
|
|
1741
|
+
key: "multipleOf",
|
|
1742
|
+
prefix: "mo"
|
|
1743
|
+
}
|
|
1744
|
+
];
|
|
1745
|
+
const rangeFields = [{
|
|
1746
|
+
kind: "scalar",
|
|
1747
|
+
key: "min",
|
|
1748
|
+
prefix: "mn"
|
|
1749
|
+
}, {
|
|
1750
|
+
kind: "scalar",
|
|
1751
|
+
key: "max",
|
|
1752
|
+
prefix: "mx"
|
|
1753
|
+
}];
|
|
1754
|
+
/**
|
|
1755
|
+
* Maps each schema node `type` to the ordered list of shape-contributing fields.
|
|
1756
|
+
* Node types absent from this map (scalar types like boolean, null, any, etc.) fall
|
|
1757
|
+
* back to `${type}|${flags}` with no additional fields.
|
|
1758
|
+
*/
|
|
1759
|
+
const SHAPE_KEYS = {
|
|
1760
|
+
object: [
|
|
1761
|
+
{ kind: "objectProps" },
|
|
1762
|
+
{ kind: "additionalProps" },
|
|
1763
|
+
{ kind: "patternProps" },
|
|
1764
|
+
{
|
|
1765
|
+
kind: "scalar",
|
|
1766
|
+
key: "minProperties",
|
|
1767
|
+
prefix: "mn"
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
kind: "scalar",
|
|
1771
|
+
key: "maxProperties",
|
|
1772
|
+
prefix: "mx"
|
|
1773
|
+
}
|
|
1774
|
+
],
|
|
1775
|
+
array: arrayTupleFields,
|
|
1776
|
+
tuple: arrayTupleFields,
|
|
1777
|
+
union: [
|
|
1778
|
+
{
|
|
1779
|
+
kind: "scalar",
|
|
1780
|
+
key: "strategy",
|
|
1781
|
+
prefix: "s"
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
kind: "scalar",
|
|
1785
|
+
key: "discriminatorPropertyName",
|
|
1786
|
+
prefix: "d"
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
kind: "children",
|
|
1790
|
+
key: "members",
|
|
1791
|
+
prefix: "m"
|
|
1792
|
+
}
|
|
1793
|
+
],
|
|
1794
|
+
intersection: [{
|
|
1795
|
+
kind: "children",
|
|
1796
|
+
key: "members",
|
|
1797
|
+
prefix: "m"
|
|
1798
|
+
}],
|
|
1799
|
+
enum: [{ kind: "enumValues" }],
|
|
1800
|
+
ref: [{ kind: "refTarget" }],
|
|
1801
|
+
string: [
|
|
1802
|
+
{
|
|
1803
|
+
kind: "scalar",
|
|
1804
|
+
key: "min",
|
|
1805
|
+
prefix: "mn"
|
|
1806
|
+
},
|
|
1807
|
+
{
|
|
1808
|
+
kind: "scalar",
|
|
1809
|
+
key: "max",
|
|
1810
|
+
prefix: "mx"
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
kind: "scalar",
|
|
1814
|
+
key: "pattern",
|
|
1815
|
+
prefix: "pt"
|
|
1816
|
+
}
|
|
1817
|
+
],
|
|
1818
|
+
number: numericFields,
|
|
1819
|
+
integer: numericFields,
|
|
1820
|
+
bigint: numericFields,
|
|
1821
|
+
url: [
|
|
1822
|
+
{
|
|
1823
|
+
kind: "scalar",
|
|
1824
|
+
key: "path",
|
|
1825
|
+
prefix: "path"
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
kind: "scalar",
|
|
1829
|
+
key: "min",
|
|
1830
|
+
prefix: "mn"
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
kind: "scalar",
|
|
1834
|
+
key: "max",
|
|
1835
|
+
prefix: "mx"
|
|
1836
|
+
}
|
|
1837
|
+
],
|
|
1838
|
+
uuid: rangeFields,
|
|
1839
|
+
email: rangeFields,
|
|
1840
|
+
datetime: [{
|
|
1841
|
+
kind: "bool",
|
|
1842
|
+
key: "offset",
|
|
1843
|
+
prefix: "o"
|
|
1844
|
+
}, {
|
|
1845
|
+
kind: "bool",
|
|
1846
|
+
key: "local",
|
|
1847
|
+
prefix: "l"
|
|
1848
|
+
}],
|
|
1849
|
+
date: [{
|
|
1850
|
+
kind: "scalar",
|
|
1851
|
+
key: "representation",
|
|
1852
|
+
prefix: "rep"
|
|
1853
|
+
}],
|
|
1854
|
+
time: [{
|
|
1855
|
+
kind: "scalar",
|
|
1856
|
+
key: "representation",
|
|
1857
|
+
prefix: "rep"
|
|
1858
|
+
}]
|
|
1859
|
+
};
|
|
1860
|
+
function serializeShapeField(field, node, record) {
|
|
1861
|
+
switch (field.kind) {
|
|
1862
|
+
case "scalar": return `${field.prefix}:${record[field.key] ?? ""}`;
|
|
1863
|
+
case "bool": return `${field.prefix}:${record[field.key] ? 1 : 0}`;
|
|
1864
|
+
case "child": {
|
|
1865
|
+
const child = record[field.key];
|
|
1866
|
+
return `${field.prefix}:${child ? signatureOf(child) : ""}`;
|
|
1867
|
+
}
|
|
1868
|
+
case "children": {
|
|
1869
|
+
const children = record[field.key] ?? [];
|
|
1870
|
+
return `${field.prefix}[${children.map((c) => signatureOf(c)).join(",")}]`;
|
|
1871
|
+
}
|
|
1872
|
+
case "objectProps": return `p[${(node.properties ?? []).map((prop) => `${prop.name}${prop.required ? "!" : "?"}${signatureOf(prop.schema)}`).join(",")}]`;
|
|
1873
|
+
case "additionalProps": {
|
|
1874
|
+
const obj = node;
|
|
1875
|
+
if (typeof obj.additionalProperties === "boolean") return `ab:${obj.additionalProperties}`;
|
|
1876
|
+
if (obj.additionalProperties) return `as:${signatureOf(obj.additionalProperties)}`;
|
|
1877
|
+
return "";
|
|
1878
|
+
}
|
|
1879
|
+
case "patternProps": {
|
|
1880
|
+
const obj = node;
|
|
1881
|
+
return `pp[${obj.patternProperties ? Object.keys(obj.patternProperties).sort().map((key) => `${key}=${signatureOf(obj.patternProperties[key])}`).join(",") : ""}]`;
|
|
1882
|
+
}
|
|
1883
|
+
case "enumValues": {
|
|
1884
|
+
const en = node;
|
|
1885
|
+
let values = "";
|
|
1886
|
+
if (en.namedEnumValues?.length) values = en.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(",");
|
|
1887
|
+
else if (en.enumValues?.length) values = en.enumValues.map((value) => `${value === null ? "null" : typeof value}:${String(value)}`).join(",");
|
|
1888
|
+
return `v[${values}]`;
|
|
1889
|
+
}
|
|
1890
|
+
case "refTarget": return `->${refTargetName(node)}`;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Builds the local, shape-only descriptor for a node: its kind, flags, constraints, and its
|
|
1895
|
+
* children's signatures. {@link signatureOf} hashes this string. Children contribute their
|
|
1896
|
+
* fixed-length signature rather than their own full descriptor, which keeps the result bounded.
|
|
1897
|
+
*/
|
|
1898
|
+
function describeShape(node) {
|
|
1899
|
+
const flags = flagsDescriptor(node);
|
|
1900
|
+
const fields = SHAPE_KEYS[node.type];
|
|
1901
|
+
if (!fields) return `${node.type}|${flags}`;
|
|
1902
|
+
const record = node;
|
|
1903
|
+
const parts = [`${node.type}|${flags}`];
|
|
1904
|
+
for (const field of fields) parts.push(serializeShapeField(field, node, record));
|
|
1905
|
+
return parts.join("|");
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Persistent hash-consing cache: `SchemaNode` → signature digest, keyed by node identity.
|
|
1909
|
+
*
|
|
1910
|
+
* A `WeakMap` so entries are released once the node is garbage-collected, and so a node hashed
|
|
1911
|
+
* during dedupe planning is not re-hashed when the same tree is rewritten during streaming
|
|
1912
|
+
* (where `schemaSignature` and `applyDedupe` would otherwise each walk it from scratch). Reuse
|
|
1913
|
+
* across calls is sound because a signature depends only on a node's content, and schema nodes
|
|
1914
|
+
* are immutable once created, transforms allocate new objects rather than mutating in place.
|
|
1915
|
+
*/
|
|
1916
|
+
const signatureCache = /* @__PURE__ */ new WeakMap();
|
|
1917
|
+
/**
|
|
1918
|
+
* Hash-consing: each node's signature is a fixed-length digest of its local shape plus its
|
|
1919
|
+
* children's digests (a Merkle hash). Children contribute their 64-char hash instead of their
|
|
1920
|
+
* full nested descriptor, so a signature stays bounded regardless of subtree depth, and the
|
|
1921
|
+
* digest is identical across calls because it depends only on content, never on traversal
|
|
1922
|
+
* order. This keeps the keys built during planning consistent with the ones recomputed later
|
|
1923
|
+
* during streaming. {@link signatureCache} memoizes node → digest across every computation.
|
|
1924
|
+
*/
|
|
1925
|
+
function signatureOf(node) {
|
|
1926
|
+
const cached = signatureCache.get(node);
|
|
1927
|
+
if (cached !== void 0) return cached;
|
|
1928
|
+
const signature = hash("sha256", describeShape(node), "hex");
|
|
1929
|
+
signatureCache.set(node, signature);
|
|
1930
|
+
return signature;
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Computes a deterministic, shape-only signature (a fixed-length content hash) for a schema node.
|
|
1934
|
+
*
|
|
1935
|
+
* Two schemas share a signature when they are structurally identical, ignoring
|
|
1936
|
+
* documentation (`name`, `title`, `description`, `example`, `default`, `deprecated`)
|
|
1937
|
+
* and usage-slot flags (`optional`, `nullish`, `readOnly`, `writeOnly`). `nullable`
|
|
1938
|
+
* is kept because it changes the produced type. `ref` nodes compare by target name,
|
|
1939
|
+
* which also keeps the algorithm terminating on circular shapes.
|
|
1940
|
+
*
|
|
1941
|
+
* @example Two enums with different descriptions share a signature
|
|
1942
|
+
* ```ts
|
|
1943
|
+
* schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
|
|
1944
|
+
* schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
|
|
1945
|
+
* ```
|
|
1946
|
+
*/
|
|
1947
|
+
function schemaSignature(node) {
|
|
1948
|
+
return signatureOf(node);
|
|
1949
|
+
}
|
|
1950
|
+
//#endregion
|
|
1951
|
+
//#region src/dedupe.ts
|
|
1952
|
+
/**
|
|
1953
|
+
* Builds the shared `ref` replacement for a duplicate occurrence, carrying the
|
|
1954
|
+
* usage-slot and documentation fields that are not part of the canonical type.
|
|
1955
|
+
*/
|
|
1956
|
+
function createRefNode(node, canonical) {
|
|
1957
|
+
return createSchema({
|
|
1958
|
+
type: "ref",
|
|
1959
|
+
name: canonical.name,
|
|
1960
|
+
ref: canonical.ref,
|
|
1961
|
+
optional: node.optional,
|
|
1962
|
+
nullish: node.nullish,
|
|
1963
|
+
readOnly: node.readOnly,
|
|
1964
|
+
writeOnly: node.writeOnly,
|
|
1965
|
+
deprecated: node.deprecated,
|
|
1966
|
+
description: node.description,
|
|
1967
|
+
default: node.default,
|
|
1968
|
+
example: node.example
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
function applyDedupe(node, canonicalBySignature, skipRootMatch = false) {
|
|
1972
|
+
if (canonicalBySignature.size === 0) return node;
|
|
1973
|
+
const root = node;
|
|
1974
|
+
return transform(node, { schema(schemaNode) {
|
|
1975
|
+
const signature = signatureOf(schemaNode);
|
|
1976
|
+
if (skipRootMatch && schemaNode === root) return void 0;
|
|
1977
|
+
const canonical = canonicalBySignature.get(signature);
|
|
1978
|
+
if (!canonical) return void 0;
|
|
1979
|
+
return createRefNode(schemaNode, canonical);
|
|
1980
|
+
} });
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Strips usage-slot flags from a hoisted definition and applies its canonical name.
|
|
1984
|
+
* A standalone definition is never optional, so `optional`/`nullish` are cleared.
|
|
1985
|
+
*/
|
|
1986
|
+
function cleanDefinition(node, name) {
|
|
1987
|
+
return {
|
|
1988
|
+
...node,
|
|
1989
|
+
name,
|
|
1990
|
+
optional: void 0,
|
|
1991
|
+
nullish: void 0
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Scans a forest of schema and operation nodes and produces a {@link DedupePlan}.
|
|
1996
|
+
*
|
|
1997
|
+
* A shape that occurs at least `minOccurrences` times is deduplicated: if any occurrence
|
|
1998
|
+
* is a named top-level schema, that name becomes the canonical (so other top-level duplicates
|
|
1999
|
+
* and inline copies turn into references to it). Otherwise a new definition is hoisted using
|
|
2000
|
+
* `nameFor`. The plan is then applied per node with {@link applyDedupe}.
|
|
2001
|
+
*
|
|
2002
|
+
* @example
|
|
2003
|
+
* ```ts
|
|
2004
|
+
* const plan = buildDedupePlan([...schemaNodes, ...operationNodes], {
|
|
2005
|
+
* isCandidate: (node) => node.type === 'enum' || node.type === 'object',
|
|
2006
|
+
* nameFor: (node) => node.name ?? null,
|
|
2007
|
+
* refFor: (name) => `#/components/schemas/${name}`,
|
|
2008
|
+
* })
|
|
2009
|
+
* ```
|
|
2010
|
+
*/
|
|
2011
|
+
function buildDedupePlan(roots, options) {
|
|
2012
|
+
const { isCandidate, nameFor, refFor, minOccurrences = 2 } = options;
|
|
2013
|
+
const topLevelNodes = /* @__PURE__ */ new Set();
|
|
2014
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2015
|
+
function record(schemaNode) {
|
|
2016
|
+
const signature = signatureOf(schemaNode);
|
|
2017
|
+
if (!isCandidate(schemaNode)) return;
|
|
2018
|
+
const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
|
|
2019
|
+
const group = groups.get(signature);
|
|
2020
|
+
if (group) {
|
|
2021
|
+
group.count++;
|
|
2022
|
+
if (isTopLevel && !group.topLevelName) group.topLevelName = schemaNode.name;
|
|
2023
|
+
} else groups.set(signature, {
|
|
2024
|
+
count: 1,
|
|
2025
|
+
representative: schemaNode,
|
|
2026
|
+
topLevelName: isTopLevel ? schemaNode.name : void 0
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
for (const root of roots) {
|
|
2030
|
+
if (root.kind === "Schema") topLevelNodes.add(root);
|
|
2031
|
+
for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
|
|
2032
|
+
}
|
|
2033
|
+
const canonicalBySignature = /* @__PURE__ */ new Map();
|
|
2034
|
+
const pendingHoists = [];
|
|
2035
|
+
for (const [signature, group] of groups) {
|
|
2036
|
+
if (group.count < minOccurrences) continue;
|
|
2037
|
+
if (group.topLevelName) {
|
|
2038
|
+
canonicalBySignature.set(signature, {
|
|
2039
|
+
name: group.topLevelName,
|
|
2040
|
+
ref: refFor(group.topLevelName)
|
|
2041
|
+
});
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
const name = nameFor(group.representative, signature);
|
|
2045
|
+
if (!name) continue;
|
|
2046
|
+
canonicalBySignature.set(signature, {
|
|
2047
|
+
name,
|
|
2048
|
+
ref: refFor(name)
|
|
2049
|
+
});
|
|
2050
|
+
pendingHoists.push({
|
|
2051
|
+
name,
|
|
2052
|
+
representative: group.representative
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
return {
|
|
2056
|
+
canonicalBySignature,
|
|
2057
|
+
hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, canonicalBySignature, true), name))
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
//#endregion
|
|
2061
|
+
//#region src/dialect.ts
|
|
2062
|
+
/**
|
|
2063
|
+
* Identity helper that types a {@link SchemaDialect} for an adapter. Like
|
|
2064
|
+
* `defineParser`, it adds no runtime behavior, it pins the dialect's type for
|
|
2065
|
+
* inference and gives adapter authors a discoverable anchor.
|
|
2066
|
+
*
|
|
2067
|
+
* @example
|
|
2068
|
+
* ```ts
|
|
2069
|
+
* export const oasDialect = defineSchemaDialect({
|
|
2070
|
+
* name: 'oas',
|
|
2071
|
+
* isNullable,
|
|
2072
|
+
* isReference,
|
|
2073
|
+
* isDiscriminator,
|
|
2074
|
+
* isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
|
|
2075
|
+
* resolveRef,
|
|
2076
|
+
* })
|
|
2077
|
+
* ```
|
|
2078
|
+
*/
|
|
2079
|
+
function defineSchemaDialect(dialect) {
|
|
2080
|
+
return dialect;
|
|
2081
|
+
}
|
|
2082
|
+
//#endregion
|
|
2083
|
+
//#region src/dispatch.ts
|
|
1997
2084
|
/**
|
|
1998
|
-
*
|
|
2085
|
+
* Walks an ordered list of {@link DispatchRule}s and returns the first node produced.
|
|
2086
|
+
*
|
|
2087
|
+
* This is the shared backbone for spec adapters (OpenAPI today, AsyncAPI and others later).
|
|
2088
|
+
* The contract an adapter follows is intentionally minimal:
|
|
2089
|
+
*
|
|
2090
|
+
* context → [rule.match → rule.convert] → node
|
|
2091
|
+
*
|
|
2092
|
+
* An adapter derives a context from a source spec node, then declares an ordered table of
|
|
2093
|
+
* rules mapping spec shapes onto Kubb AST nodes. To add support for a new spec, write a new
|
|
2094
|
+
* context type and a new rules table, the traversal here is reused unchanged.
|
|
2095
|
+
*
|
|
2096
|
+
* Order is significant: earlier rules win, so list higher-precedence or more specific shapes
|
|
2097
|
+
* first (e.g. composition keywords before plain `type`). A rule whose `match` returns `true`
|
|
2098
|
+
* may still `convert` to `null` to defer to later rules. When no rule produces a node this
|
|
2099
|
+
* returns `null`, leaving the caller to apply its own fallback.
|
|
1999
2100
|
*
|
|
2000
|
-
*
|
|
2101
|
+
* @example
|
|
2102
|
+
* ```ts
|
|
2103
|
+
* const node = dispatch(schemaRules, schemaContext) ?? createSchema({ type: fallbackType })
|
|
2104
|
+
* ```
|
|
2105
|
+
*/
|
|
2106
|
+
function dispatch(rules, context) {
|
|
2107
|
+
for (const rule of rules) {
|
|
2108
|
+
if (!rule.match(context)) continue;
|
|
2109
|
+
const node = rule.convert(context);
|
|
2110
|
+
if (node !== null && node !== void 0) return node;
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
//#endregion
|
|
2115
|
+
//#region src/printer.ts
|
|
2116
|
+
/**
|
|
2117
|
+
* Defines a schema printer: a function that takes a `SchemaNode` and emits
|
|
2118
|
+
* code in your target language. Each plugin that produces code from schemas
|
|
2119
|
+
* (TypeScript types, Zod schemas, Faker factories) ships a printer built
|
|
2120
|
+
* with this helper.
|
|
2001
2121
|
*
|
|
2002
2122
|
* The builder receives resolved options and returns:
|
|
2003
|
-
* - `name` — a unique identifier for the printer
|
|
2004
|
-
* - `options` — options stored on the returned printer instance
|
|
2005
|
-
* - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
|
|
2006
|
-
* - `print` _(optional)_ — top-level override exposed as `printer.print`
|
|
2007
|
-
* - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
|
|
2008
|
-
* - This keeps recursion safe and avoids self-calls
|
|
2009
2123
|
*
|
|
2010
|
-
*
|
|
2124
|
+
* - `name` unique identifier for the printer.
|
|
2125
|
+
* - `options` stored on the returned printer instance.
|
|
2126
|
+
* - `nodes` map of `SchemaType` → handler. Handlers return the rendered
|
|
2127
|
+
* output (a string, a TypeScript AST node, ...) for that schema type.
|
|
2128
|
+
* - `print` (optional), top-level override exposed as `printer.print`.
|
|
2129
|
+
* Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
|
|
2130
|
+
*
|
|
2131
|
+
* Without a `print` override, `printer.print` falls back to `printer.transform`
|
|
2132
|
+
* (the node-level dispatcher).
|
|
2011
2133
|
*
|
|
2012
|
-
* @example
|
|
2134
|
+
* @example Tiny Zod printer
|
|
2013
2135
|
* ```ts
|
|
2136
|
+
* import { definePrinter, type PrinterFactoryOptions } from '@kubb/ast'
|
|
2137
|
+
*
|
|
2014
2138
|
* type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
|
|
2015
2139
|
*
|
|
2016
2140
|
* export const zodPrinter = definePrinter<PrinterZod>((options) => ({
|
|
@@ -2019,7 +2143,9 @@ function createJsx(value) {
|
|
|
2019
2143
|
* nodes: {
|
|
2020
2144
|
* string: () => 'z.string()',
|
|
2021
2145
|
* object(node) {
|
|
2022
|
-
* const props = node.properties
|
|
2146
|
+
* const props = node.properties
|
|
2147
|
+
* .map((p) => `${p.name}: ${this.transform(p.schema)}`)
|
|
2148
|
+
* .join(', ')
|
|
2023
2149
|
* return `z.object({ ${props} })`
|
|
2024
2150
|
* },
|
|
2025
2151
|
* },
|
|
@@ -2047,7 +2173,7 @@ function createPrinterFactory(getKey) {
|
|
|
2047
2173
|
options: resolvedOptions,
|
|
2048
2174
|
transform: (node) => {
|
|
2049
2175
|
const key = getKey(node);
|
|
2050
|
-
if (key ===
|
|
2176
|
+
if (key === null) return null;
|
|
2051
2177
|
const handler = nodes[key];
|
|
2052
2178
|
if (!handler) return null;
|
|
2053
2179
|
return handler.call(context, node);
|
|
@@ -2064,30 +2190,16 @@ function createPrinterFactory(getKey) {
|
|
|
2064
2190
|
}
|
|
2065
2191
|
//#endregion
|
|
2066
2192
|
//#region src/resolvers.ts
|
|
2067
|
-
function findDiscriminator(mapping, ref) {
|
|
2068
|
-
if (!mapping || !ref) return null;
|
|
2069
|
-
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
2070
|
-
}
|
|
2071
|
-
function childName(parentName, propName) {
|
|
2072
|
-
return parentName ? pascalCase([parentName, propName].join(" ")) : null;
|
|
2073
|
-
}
|
|
2074
|
-
function enumPropName(parentName, propName, enumSuffix) {
|
|
2075
|
-
return pascalCase([
|
|
2076
|
-
parentName,
|
|
2077
|
-
propName,
|
|
2078
|
-
enumSuffix
|
|
2079
|
-
].filter(Boolean).join(" "));
|
|
2080
|
-
}
|
|
2081
2193
|
/**
|
|
2082
2194
|
* Collects import entries for all `ref` schema nodes in `node`.
|
|
2083
2195
|
*/
|
|
2084
2196
|
function collectImports({ node, nameMapping, resolve }) {
|
|
2085
2197
|
return collect(node, { schema(schemaNode) {
|
|
2086
2198
|
const schemaRef = narrowSchema(schemaNode, "ref");
|
|
2087
|
-
if (!schemaRef?.ref) return;
|
|
2199
|
+
if (!schemaRef?.ref) return null;
|
|
2088
2200
|
const rawName = extractRefName(schemaRef.ref);
|
|
2089
2201
|
const result = resolve(nameMapping.get(rawName) ?? rawName);
|
|
2090
|
-
if (!result) return;
|
|
2202
|
+
if (!result) return null;
|
|
2091
2203
|
return result;
|
|
2092
2204
|
} });
|
|
2093
2205
|
}
|
|
@@ -2141,23 +2253,24 @@ function setDiscriminatorEnum({ node, propertyName, values, enumName }) {
|
|
|
2141
2253
|
* ])
|
|
2142
2254
|
* ```
|
|
2143
2255
|
*/
|
|
2144
|
-
function
|
|
2145
|
-
|
|
2256
|
+
function* mergeAdjacentObjectsLazy(members) {
|
|
2257
|
+
let acc;
|
|
2258
|
+
for (const member of members) {
|
|
2146
2259
|
const objectMember = narrowSchema(member, "object");
|
|
2147
|
-
if (objectMember && !objectMember.name) {
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
...
|
|
2153
|
-
properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
|
|
2260
|
+
if (objectMember && !objectMember.name && acc !== void 0) {
|
|
2261
|
+
const accObject = narrowSchema(acc, "object");
|
|
2262
|
+
if (accObject && !accObject.name) {
|
|
2263
|
+
acc = createSchema({
|
|
2264
|
+
...accObject,
|
|
2265
|
+
properties: [...accObject.properties ?? [], ...objectMember.properties ?? []]
|
|
2154
2266
|
});
|
|
2155
|
-
|
|
2267
|
+
continue;
|
|
2156
2268
|
}
|
|
2157
2269
|
}
|
|
2158
|
-
acc
|
|
2159
|
-
|
|
2160
|
-
}
|
|
2270
|
+
if (acc !== void 0) yield acc;
|
|
2271
|
+
acc = member;
|
|
2272
|
+
}
|
|
2273
|
+
if (acc !== void 0) yield acc;
|
|
2161
2274
|
}
|
|
2162
2275
|
/**
|
|
2163
2276
|
* Removes enum members that are covered by broader scalar primitives in the same union.
|
|
@@ -2189,7 +2302,7 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
|
2189
2302
|
const enumNode = narrowSchema(propNode, "enum");
|
|
2190
2303
|
if (enumNode?.primitive === "boolean") return {
|
|
2191
2304
|
...propNode,
|
|
2192
|
-
name:
|
|
2305
|
+
name: null
|
|
2193
2306
|
};
|
|
2194
2307
|
if (enumNode) return {
|
|
2195
2308
|
...propNode,
|
|
@@ -2198,6 +2311,6 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
|
2198
2311
|
return propNode;
|
|
2199
2312
|
}
|
|
2200
2313
|
//#endregion
|
|
2201
|
-
export {
|
|
2314
|
+
export { applyDedupe, buildDedupePlan, caseParams, collect, collectImports, collectUsedSchemaNames, containsCircularRef, createArrowFunction, createBreak, createConst, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter, createParameterGroup, createParamsType, createPrinterFactory, createProperty, createResponse, createSchema, createSource, createStreamInput, createText, createType, definePrinter, defineSchemaDialect, dispatch, extractStringsFromNodes, findCircularSchemas, httpMethods, isHttpOperationNode, isInputNode, isOperationNode, isOutputNode, isSchemaNode, isStringType, mergeAdjacentObjectsLazy, narrowSchema, schemaSignature, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, update, walk };
|
|
2202
2315
|
|
|
2203
2316
|
//# sourceMappingURL=index.js.map
|