@kubb/ast 5.0.0-beta.5 → 5.0.0-beta.50

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