@kubb/ast 5.8.4 → 5.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,9 +30,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  enumerable: true
31
31
  }) : target, mod));
32
32
  //#endregion
33
- let node_crypto = require("node:crypto");
34
33
  let node_path = require("node:path");
35
34
  node_path = __toESM(node_path, 1);
35
+ let node_crypto = require("node:crypto");
36
36
  //#region src/constants.ts
37
37
  const visitorDepths = {
38
38
  shallow: "shallow",
@@ -177,11 +177,32 @@ function isHttpOperationNode(node) {
177
177
  return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
178
178
  }
179
179
  //#endregion
180
- //#region src/defineNode.ts
180
+ //#region src/visitor.ts
181
181
  /**
182
- * Visitor callback names, one per traversable node kind, in traversal order.
183
- * Kept in sync with the keys of `Visitor` in `visitor.ts`.
182
+ * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
184
183
  */
184
+ const VISITOR_KEYS = {
185
+ Input: ["schemas", "operations"],
186
+ Operation: [
187
+ "parameters",
188
+ "requestBody",
189
+ "responses"
190
+ ],
191
+ RequestBody: ["content"],
192
+ Content: ["schema"],
193
+ Response: ["content"],
194
+ Schema: [
195
+ "properties",
196
+ "items",
197
+ "members",
198
+ "additionalProperties",
199
+ "patternProperties",
200
+ "rest"
201
+ ],
202
+ Property: ["schema"],
203
+ Parameter: ["schema"]
204
+ };
205
+ /** Visitor callback names in traversal order, the keys of {@link Visitor}. */
185
206
  const visitorKeys = [
186
207
  "input",
187
208
  "output",
@@ -192,176 +213,395 @@ const visitorKeys = [
192
213
  "response"
193
214
  ];
194
215
  /**
195
- * Builds a type guard that matches nodes of the given `kind`.
216
+ * Maps a node kind to the matching visitor callback name.
196
217
  */
197
- function isKind(kind) {
198
- return (node) => node?.kind === kind;
199
- }
218
+ const VISITOR_KEY_BY_KIND = {
219
+ Input: "input",
220
+ Output: "output",
221
+ Operation: "operation",
222
+ Schema: "schema",
223
+ Property: "property",
224
+ Parameter: "parameter",
225
+ Response: "response"
226
+ };
227
+ const visitorKeysByKind = VISITOR_KEYS;
200
228
  /**
201
- * Defines a node once and derives its `create` builder, `is` guard, and traversal
202
- * metadata. `create` merges `defaults`, the `build` hook (or the raw input), and the
203
- * `kind`, so node construction lives in one place without scattered `as` casts.
204
- *
205
- * @example Simple node
206
- * ```ts
207
- * const importDef = defineNode<ImportNode>({ kind: 'Import' })
208
- * const createImport = importDef.create
209
- * ```
210
- *
211
- * @example Node with a build hook
212
- * ```ts
213
- * const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
214
- * kind: 'Property',
215
- * build: (props) => ({ ...props, required: props.required ?? false }),
216
- * children: ['schema'],
217
- * visitorKey: 'property',
218
- * })
219
- * ```
229
+ * Returns `true` when `value` is an AST node (an object carrying a `kind`).
220
230
  */
221
- function defineNode(config) {
222
- const { kind, defaults, build, children, visitorKey } = config;
223
- function create(input) {
224
- const base = build ? build(input) : input;
225
- const node = {
226
- kind,
227
- ...defaults,
228
- ...base
229
- };
230
- node.kind = kind;
231
- return node;
232
- }
233
- return {
234
- kind,
235
- create,
236
- is: isKind(kind),
237
- children,
238
- visitorKey
239
- };
231
+ function isNode(value) {
232
+ return typeof value === "object" && value !== null && typeof value.kind === "string";
240
233
  }
241
- //#endregion
242
- //#region src/nodes/code.ts
243
- /**
244
- * Definition for the {@link ConstNode}.
245
- */
246
- const constDef = defineNode({ kind: "Const" });
247
- /**
248
- * Definition for the {@link TypeNode}.
249
- */
250
- const typeDef = defineNode({ kind: "Type" });
251
- /**
252
- * Definition for the {@link FunctionNode}.
253
- */
254
- const functionDef = defineNode({ kind: "Function" });
255
- /**
256
- * Definition for the {@link ArrowFunctionNode}.
257
- */
258
- const arrowFunctionDef = defineNode({ kind: "ArrowFunction" });
259
- /**
260
- * Definition for the {@link TextNode}.
261
- */
262
- const textDef = defineNode({
263
- kind: "Text",
264
- build: (value) => ({ value })
265
- });
266
234
  /**
267
- * Definition for the {@link BreakNode}.
235
+ * Returns `true` for a plain object that maps names to nodes, such as `patternProperties`.
268
236
  */
269
- const breakDef = defineNode({
270
- kind: "Break",
271
- build: () => ({})
272
- });
237
+ function isNodeRecord(value) {
238
+ return typeof value === "object" && value !== null && !Array.isArray(value);
239
+ }
273
240
  /**
274
- * Definition for the {@link JsxNode}.
241
+ * Returns the child fields of `node` to walk, from {@link VISITOR_KEYS}, or `undefined` when there
242
+ * is nothing to walk. `Schema` children are only walked when `recurse` is `true`.
275
243
  */
276
- const jsxDef = defineNode({
277
- kind: "Jsx",
278
- build: (value) => ({ value })
279
- });
244
+ function childKeys(node, recurse) {
245
+ if (node.kind === "Schema" && !recurse) return void 0;
246
+ return visitorKeysByKind[node.kind];
247
+ }
280
248
  /**
281
- * Creates a `ConstNode` representing a TypeScript `const` declaration.
249
+ * Returns the immediate traversable children of `node`.
282
250
  *
283
- * @example Exported constant with type and `as const`
251
+ * @example
284
252
  * ```ts
285
- * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
286
- * // export const pets: Pet[] = ... as const
253
+ * const children = getChildren(operationNode, true)
254
+ * // returns parameters, the request body, and responses
287
255
  * ```
288
256
  */
289
- const createConst = constDef.create;
257
+ function* getChildren(node, recurse) {
258
+ const keys = childKeys(node, recurse);
259
+ if (!keys) return;
260
+ const record = node;
261
+ for (const key of keys) {
262
+ const value = record[key];
263
+ if (Array.isArray(value)) {
264
+ for (const item of value) if (isNode(item)) yield item;
265
+ continue;
266
+ }
267
+ if (isNode(value)) {
268
+ yield value;
269
+ continue;
270
+ }
271
+ if (isNodeRecord(value)) {
272
+ for (const item of Object.values(value)) if (isNode(item)) yield item;
273
+ }
274
+ }
275
+ }
290
276
  /**
291
- * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
277
+ * Runs the visitor callback that matches `node.kind` with the traversal
278
+ * context. The result is a replacement node, a collected value, or `undefined`
279
+ * when no callback is registered for the kind.
292
280
  *
293
- * @example
294
- * ```ts
295
- * createType({ name: 'Pet', export: true })
296
- * // export type Pet = ...
297
- * ```
281
+ * Shared by `transform` and `collect` so node-kind dispatch lives in one place.
282
+ * `TResult` is the caller's expected return: the same node type for `transform`,
283
+ * the collected value type for `collect`.
298
284
  */
299
- const createType = typeDef.create;
285
+ function applyVisitor(node, visitor, parent) {
286
+ const key = VISITOR_KEY_BY_KIND[node.kind];
287
+ if (!key) return void 0;
288
+ const fn = visitor[key];
289
+ return fn?.(node, { parent });
290
+ }
300
291
  /**
301
- * Creates a `FunctionNode` representing a TypeScript `function` declaration.
292
+ * Synchronous depth-first transform. Each visitor callback can return a
293
+ * replacement node. Returning `undefined` keeps the original.
302
294
  *
303
- * @example
295
+ * The original tree is never mutated, a new tree is returned. Pass
296
+ * `depth: 'shallow'` to skip recursion into children.
297
+ *
298
+ * @example Prefix every operationId
304
299
  * ```ts
305
- * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
306
- * // export async function fetchPet(): Promise<Pet> { ... }
300
+ * const next = transform(root, {
301
+ * operation(node) {
302
+ * return { ...node, operationId: `prefixed_${node.operationId}` }
303
+ * },
304
+ * })
307
305
  * ```
308
- */
309
- const createFunction = functionDef.create;
310
- /**
311
- * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
312
306
  *
313
- * @example
307
+ * @example Replace only the root node
314
308
  * ```ts
315
- * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
316
- * // export const double = (n: number) => ...
309
+ * const next = transform(root, {
310
+ * depth: 'shallow',
311
+ * input: (node) => ({ ...node, meta: { ...node.meta, title: 'Rewritten' } }),
312
+ * })
317
313
  * ```
318
314
  */
319
- const createArrowFunction = arrowFunctionDef.create;
315
+ function transform(node, options) {
316
+ const { depth, parent, ...visitor } = options;
317
+ return transformNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
318
+ }
320
319
  /**
321
- * Creates a {@link TextNode} representing a raw string fragment in the source output.
320
+ * Visits a single node, then immutably rebuilds its children. Returns the original
321
+ * reference when neither the visitor nor the child rebuild changed anything, so callers
322
+ * can detect "nothing changed" by identity and ancestors avoid reallocating.
323
+ */
324
+ function transformNode(node, visitor, recurse, parent) {
325
+ return transformChildren(applyVisitor(node, visitor, parent) ?? node, visitor, recurse);
326
+ }
327
+ /**
328
+ * Immutably rebuilds a node's children, transforming each child node and leaving non-node values
329
+ * (e.g. `additionalProperties: true`) intact.
330
+ */
331
+ function transformChildren(node, visitor, recurse) {
332
+ const keys = childKeys(node, recurse);
333
+ if (!keys) return node;
334
+ const record = node;
335
+ let updates;
336
+ for (const key of keys) {
337
+ if (!(key in record)) continue;
338
+ const value = record[key];
339
+ if (Array.isArray(value)) {
340
+ let mapped;
341
+ for (const [i, item] of value.entries()) {
342
+ const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
343
+ if (mapped) {
344
+ mapped.push(next);
345
+ continue;
346
+ }
347
+ if (next !== item) mapped = [...value.slice(0, i), next];
348
+ }
349
+ if (mapped) (updates ??= {})[key] = mapped;
350
+ continue;
351
+ }
352
+ if (isNode(value)) {
353
+ const next = transformNode(value, visitor, recurse, node);
354
+ if (next !== value) (updates ??= {})[key] = next;
355
+ continue;
356
+ }
357
+ if (isNodeRecord(value)) {
358
+ let mapped;
359
+ for (const [name, item] of Object.entries(value)) {
360
+ const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
361
+ if (next !== item) (mapped ??= { ...value })[name] = next;
362
+ }
363
+ if (mapped) (updates ??= {})[key] = mapped;
364
+ }
365
+ }
366
+ if (!updates) return node;
367
+ return {
368
+ ...node,
369
+ ...updates
370
+ };
371
+ }
372
+ /**
373
+ * Lazy depth-first collection pass. Yields every non-null value returned by
374
+ * the visitor callbacks. Use `collectSync` for the eager array form.
322
375
  *
323
- * @example
376
+ * @example Collect every operationId
324
377
  * ```ts
325
- * createText('return fetch(id)')
326
- * // { kind: 'Text', value: 'return fetch(id)' }
378
+ * const ids: string[] = []
379
+ * for (const id of collect<string>(root, {
380
+ * operation(node) {
381
+ * return node.operationId
382
+ * },
383
+ * })) {
384
+ * ids.push(id)
385
+ * }
327
386
  * ```
328
387
  */
329
- const createText = textDef.create;
388
+ function* collect(node, options) {
389
+ const { depth, parent, ...visitor } = options;
390
+ yield* collectNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
391
+ }
392
+ function* collectNode(node, visitor, recurse, parent) {
393
+ const v = applyVisitor(node, visitor, parent);
394
+ if (v != null) yield v;
395
+ for (const child of getChildren(node, recurse)) yield* collectNode(child, visitor, recurse, node);
396
+ }
330
397
  /**
331
- * Creates a {@link BreakNode} representing a line break in the source output.
398
+ * Eager depth-first collection pass. Gathers every non-null value the visitor
399
+ * callbacks return into an array.
332
400
  *
333
- * @example
401
+ * @example Collect every operationId
334
402
  * ```ts
335
- * createBreak()
336
- * // { kind: 'Break' }
403
+ * const ids = collectSync<string>(root, {
404
+ * operation(node) {
405
+ * return node.operationId
406
+ * },
407
+ * })
337
408
  * ```
338
409
  */
339
- function createBreak() {
340
- return breakDef.create();
410
+ function collectSync(node, options) {
411
+ return Array.from(collect(node, options));
341
412
  }
413
+ //#endregion
414
+ //#region src/defineMacro.ts
342
415
  /**
343
- * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
416
+ * Sort weight for an `enforce` hint. `pre` sorts before unmarked items and `post` after, so a plain
417
+ * list keeps its authored order.
418
+ */
419
+ function enforceWeight(enforce) {
420
+ if (enforce === "pre") return 0;
421
+ if (enforce === "post") return 2;
422
+ return 1;
423
+ }
424
+ /**
425
+ * Types a macro for inference and a single construction site, mirroring `defineExtension`.
426
+ * Adds no runtime behavior.
344
427
  *
345
428
  * @example
346
429
  * ```ts
347
- * createJsx('<>\n <a href={href}>Open</a>\n</>')
348
- * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
430
+ * const macroUntagged = defineMacro({
431
+ * name: 'untagged',
432
+ * operation(node) {
433
+ * return node.tags?.length ? undefined : { ...node, tags: ['untagged'] }
434
+ * },
435
+ * })
349
436
  * ```
350
437
  */
351
- const createJsx = jsxDef.create;
352
- //#endregion
353
- //#region src/nodes/content.ts
438
+ function defineMacro(macro) {
439
+ return macro;
440
+ }
354
441
  /**
355
- * Definition for the {@link ContentNode}.
442
+ * Runs every macro's callback for one node kind in order, chaining the result so each macro sees
443
+ * the previous macro's output. Returns `undefined` when nothing changed, so `transform` keeps the
444
+ * original reference (structural sharing).
356
445
  */
357
- const contentDef = defineNode({
358
- kind: "Content",
359
- children: ["schema"]
360
- });
446
+ function chain({ macros, key, node, context }) {
447
+ let current = node;
448
+ for (const macro of macros) {
449
+ const callback = macro[key];
450
+ if (!callback) continue;
451
+ if (macro.match && !macro.match(current)) continue;
452
+ const next = callback(current, context);
453
+ if (next != null) current = next;
454
+ }
455
+ return current === node ? void 0 : current;
456
+ }
361
457
  /**
362
- * Creates a `ContentNode` for a single request-body or response content type.
458
+ * Folds an ordered list of macros into a single {@link Visitor} that `transform` (and the per-plugin
459
+ * transform layer in `@kubb/core`) can run. Macros are stable-sorted by `enforce`, then applied
460
+ * sequentially per node so later macros see earlier output. This differs from a plain visitor, which
461
+ * has no names, ordering, or composition.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * const visitor = composeMacros([macroSimplifyUnion, macroDiscriminatorEnum])
466
+ * const next = transform(root, visitor)
467
+ * ```
468
+ */
469
+ function composeMacros(macros) {
470
+ const ordered = [...macros].sort((a, b) => enforceWeight(a.enforce) - enforceWeight(b.enforce));
471
+ const visitor = {};
472
+ for (const key of visitorKeys) {
473
+ if (!ordered.some((macro) => typeof macro[key] === "function")) continue;
474
+ const callback = (node, context) => chain({
475
+ macros: ordered,
476
+ key,
477
+ node,
478
+ context
479
+ });
480
+ visitor[key] = callback;
481
+ }
482
+ return visitor;
483
+ }
484
+ /**
485
+ * Runs a list of macros over a node tree and returns the rewritten tree. Keeps `transform`'s
486
+ * structural sharing, so an empty or no-op macro list returns the same reference. Pass
487
+ * `depth: 'shallow'` to rewrite the root node only.
488
+ *
489
+ * @example
490
+ * ```ts
491
+ * const next = applyMacros(root, [macroIntegerToString])
492
+ * ```
493
+ *
494
+ * @example Apply to the root node only
495
+ * ```ts
496
+ * const named = applyMacros(node, [macroEnumName({ parentName, propName, enumSuffix })], { depth: 'shallow' })
497
+ * ```
498
+ */
499
+ function applyMacros(root, macros, options) {
500
+ if (macros.length === 0) return root;
501
+ return transform(root, {
502
+ ...composeMacros(macros),
503
+ ...options
504
+ });
505
+ }
506
+ //#endregion
507
+ //#region src/optionality.ts
508
+ /**
509
+ * Generic JSON Schema optionality: a non-required field is optional, and a
510
+ * non-required nullable field is nullish.
511
+ */
512
+ function optionality(schema, required) {
513
+ const nullable = schema.nullable ?? false;
514
+ return {
515
+ ...schema,
516
+ optional: !required && !nullable ? true : void 0,
517
+ nullish: !required && nullable ? true : void 0
518
+ };
519
+ }
520
+ /**
521
+ * Defaults `required` to `false` and derives the schema's `optional`/`nullish` flags from it.
522
+ * Shared build step of `createProperty` and `createParameter`.
523
+ */
524
+ function withRequired(props) {
525
+ const required = props.required ?? false;
526
+ return {
527
+ ...props,
528
+ required,
529
+ schema: optionality(props.schema, required)
530
+ };
531
+ }
532
+ //#endregion
533
+ //#region src/createPrinter.ts
534
+ /**
535
+ * Creates a schema printer: a function that takes a `SchemaNode` and emits
536
+ * code in your target language. Each plugin that produces code from schemas
537
+ * (TypeScript types, Zod schemas, Faker factories) ships a printer built
538
+ * with this helper.
539
+ *
540
+ * The builder receives resolved options and returns:
541
+ *
542
+ * - `name` unique identifier for the printer.
543
+ * - `options` stored on the returned printer instance.
544
+ * - `nodes` map of `SchemaType` → handler. Handlers return the rendered
545
+ * output (a string, a TypeScript AST node, ...) for that schema type.
546
+ * - `overrides` (optional), user-supplied handlers that win over `nodes`.
547
+ * An override can call `this.base(node)` to reuse the handler it replaced.
548
+ * - `print` (optional), top-level override exposed as `printer.print`.
549
+ * Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
550
+ *
551
+ * Without a `print` override, `printer.print` falls back to `printer.transform`
552
+ * (the node-level dispatcher).
553
+ *
554
+ * @example Tiny Zod printer
555
+ * ```ts
556
+ * import { createPrinter, type PrinterFactoryOptions } from '@kubb/ast'
557
+ *
558
+ * type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
559
+ *
560
+ * export const zodPrinter = createPrinter<PrinterZod>((options) => ({
561
+ * name: 'zod',
562
+ * options: { strict: options.strict ?? true },
563
+ * nodes: {
564
+ * string: () => 'z.string()',
565
+ * object(node) {
566
+ * const props = node.properties
567
+ * .map((p) => `${p.name}: ${this.transform(p.schema)}`)
568
+ * .join(', ')
569
+ * return `z.object({ ${props} })`
570
+ * },
571
+ * },
572
+ * }))
573
+ * ```
363
574
  */
364
- const createContent = contentDef.create;
575
+ function createPrinter(build) {
576
+ return (options) => {
577
+ const { name, options: resolvedOptions, nodes, overrides, print: printOverride } = build(options ?? {});
578
+ const merged = overrides ? {
579
+ ...nodes,
580
+ ...overrides
581
+ } : nodes;
582
+ const collectedImports = [];
583
+ const dispatch = (table) => (node) => {
584
+ const handler = table[node.type];
585
+ if (!handler) return null;
586
+ return handler.call(context, node);
587
+ };
588
+ const context = {
589
+ options: resolvedOptions,
590
+ import: (node) => {
591
+ collectedImports.push(node);
592
+ },
593
+ transform: dispatch(merged),
594
+ base: dispatch(nodes)
595
+ };
596
+ return {
597
+ name,
598
+ options: resolvedOptions,
599
+ transform: context.transform,
600
+ print: printOverride ? printOverride.bind(context) : context.transform,
601
+ drainImports: () => collectedImports.splice(0)
602
+ };
603
+ };
604
+ }
365
605
  //#endregion
366
606
  //#region src/utils/extractStringsFromNodes.ts
367
607
  /**
@@ -395,1278 +635,909 @@ function extractStringsFromNodes(nodes) {
395
635
  return collected.join("\n");
396
636
  }
397
637
  //#endregion
398
- //#region src/utils/combineFileMembers.ts
399
- const IDENTIFIER_RUN = /[\w$]+/g;
400
- /**
401
- * How many imports a file needs before indexing the source beats scanning it once per name.
402
- */
403
- const INDEX_ABOVE_IMPORTS = 128;
404
- /**
405
- * Returns the local binding an import name item introduces: the alias when there is one, else the
406
- * imported name.
407
- */
408
- function importLocalName(item) {
409
- return typeof item === "string" ? item : item.name ?? item.propertyName;
410
- }
638
+ //#region src/utils/refs.ts
411
639
  /**
412
- * Computes a multi-level sort key for exports and imports:
413
- * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
640
+ * Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
641
+ * the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
642
+ * then `name`, then the nested `schema.name`.
643
+ *
644
+ * Returns `null` for non-ref nodes or when no name resolves.
645
+ *
646
+ * @example
647
+ * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
648
+ *
649
+ * @example Collision-renamed target
650
+ * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
414
651
  */
415
- function sortKey(node) {
416
- const isArray = Array.isArray(node.name) ? "1" : "0";
417
- const typeOnly = node.isTypeOnly ? "0" : "1";
418
- const hasName = node.name != null ? "1" : "0";
419
- const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
420
- return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
652
+ function resolveRefName(node) {
653
+ if (!node || node.type !== "ref") return null;
654
+ if (node.targetName) return node.targetName;
655
+ if (node.ref) return node.ref.split("/").at(-1) ?? node.ref;
656
+ return node.name ?? node.schema?.name ?? null;
421
657
  }
422
658
  /**
423
- * Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
659
+ * Merges a ref node with its resolved schema, giving the fields set at the usage site precedence.
660
+ *
661
+ * Every field on the ref except `kind`, `type`, `name`, `ref`, and `schema` overrides the same field
662
+ * of the resolved `schema` (for example `description`, `nullable`, `readOnly`, `deprecated`). Fields
663
+ * left `undefined` on the ref do not shadow the resolved schema. A ref without a resolved `schema`
664
+ * is returned as is.
665
+ *
666
+ * @example
667
+ * ```ts
668
+ * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
669
+ * mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
670
+ * ```
424
671
  */
425
- function sortMembers(nodes) {
426
- return nodes.map((node) => ({
427
- node,
428
- key: sortKey(node)
429
- })).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
672
+ function mergeRefWithSchema(node) {
673
+ if (!node.schema) return node;
674
+ const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = node;
675
+ const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0));
676
+ return {
677
+ ...node.schema,
678
+ ...definedOverrides
679
+ };
430
680
  }
431
681
  /**
432
- * Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
433
- * seen folds its names into that entry, otherwise it joins `result` as a new entry.
682
+ * Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
683
+ *
684
+ * Modifiers inherited from the resolved schema are included in the check because a ref emits the
685
+ * referenced schema variable with those modifiers already applied.
434
686
  */
435
- function mergeByPath(result) {
436
- const byPath = /* @__PURE__ */ new Map();
437
- return ({ node, names }) => {
438
- const key = `${node.path}:${node.isTypeOnly ?? false}`;
439
- const existing = byPath.get(key);
440
- if (existing && Array.isArray(existing.name)) {
441
- existing.name = [.../* @__PURE__ */ new Set([...existing.name, ...names])];
442
- return;
443
- }
444
- const item = {
445
- ...node,
446
- name: names
447
- };
448
- result.push(item);
449
- byPath.set(key, item);
450
- };
687
+ function isBareRef(node, keysToOmit) {
688
+ if (!node || node.type !== "ref") return false;
689
+ const meta = mergeRefWithSchema(node);
690
+ return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
451
691
  }
692
+ //#endregion
693
+ //#region ../../internals/utils/src/promise.ts
452
694
  /**
453
- * Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
454
- * key. Unnamed sources fall back to their extracted node strings as the name part of the key. Returns
455
- * the deduplicated array in original order.
456
- */
457
- function combineSources(sources) {
458
- const seen = /* @__PURE__ */ new Map();
459
- for (const source of sources) {
460
- const key = `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
461
- if (!seen.has(key)) seen.set(key, source);
462
- }
463
- return [...seen.values()];
464
- }
465
- /**
466
- * Deduplicates and merges `ExportNode` objects by path and type.
467
- *
468
- * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
469
- * Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
470
- */
471
- function combineExports(exports) {
472
- const result = [];
473
- const merge = mergeByPath(result);
474
- const seen = /* @__PURE__ */ new Set();
475
- for (const curr of sortMembers(exports)) {
476
- const { name, path, isTypeOnly, asAlias } = curr;
477
- if (Array.isArray(name)) {
478
- if (name.length) merge({
479
- node: curr,
480
- names: [...new Set(name)]
481
- });
482
- continue;
483
- }
484
- const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
485
- if (!seen.has(key)) {
486
- result.push(curr);
487
- seen.add(key);
488
- }
489
- }
490
- return result;
491
- }
492
- /**
493
- * Deduplicates and merges `ImportNode` objects, filtering out unused imports.
494
- *
495
- * Retains imports that are referenced in `source` or re-exported. Imports with the same path and
496
- * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
497
- */
498
- function combineImports(imports, exports, source) {
499
- const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
500
- const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
501
- const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
502
- const importNameMemo = /* @__PURE__ */ new Map();
503
- const canonicalizeName = (n) => {
504
- if (typeof n === "string") return n;
505
- const key = `${n.propertyName}:${n.name ?? ""}`;
506
- if (!importNameMemo.has(key)) importNameMemo.set(key, n);
507
- return importNameMemo.get(key);
508
- };
509
- const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
510
- for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
511
- const result = [];
512
- const merge = mergeByPath(result);
513
- const seen = /* @__PURE__ */ new Set();
514
- for (const curr of sortMembers(imports)) {
515
- if (curr.path === curr.root) continue;
516
- const { name, path, isTypeOnly } = curr;
517
- if (Array.isArray(name)) {
518
- const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
519
- if (names.length) merge({
520
- node: curr,
521
- names
522
- });
523
- continue;
524
- }
525
- if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
526
- const key = `${path}:${name}:${isTypeOnly ?? false}`;
527
- if (!seen.has(key)) {
528
- result.push(curr);
529
- seen.add(key);
530
- }
531
- }
532
- return result;
533
- }
534
- //#endregion
535
- //#region src/nodes/file.ts
536
- /**
537
- * Definition for the {@link ImportNode}.
538
- */
539
- const importDef = defineNode({ kind: "Import" });
540
- /**
541
- * Definition for the {@link ExportNode}.
542
- */
543
- const exportDef = defineNode({ kind: "Export" });
544
- /**
545
- * Definition for the {@link SourceNode}.
546
- */
547
- const sourceDef = defineNode({ kind: "Source" });
548
- /**
549
- * Definition for the {@link FileNode}. The fully resolved builder lives in
550
- * `createFile`, so this definition only supplies the guard.
551
- */
552
- const fileDef = defineNode({ kind: "File" });
553
- /**
554
- * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
695
+ * Wraps `factory` with a keyed cache backed by the provided store.
555
696
  *
556
- * @example Named import
557
- * ```ts
558
- * createImport({ name: ['useState'], path: 'react' })
559
- * // import { useState } from 'react'
560
- * ```
561
- */
562
- const createImport = importDef.create;
563
- /**
564
- * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
697
+ * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
698
+ * collected) or a `Map` for primitive keys. For multi-argument functions,
699
+ * nest two `memoize` calls — the outer keyed by the first argument, the
700
+ * inner (created once per outer miss) keyed by the second.
565
701
  *
566
- * @example Named export
567
- * ```ts
568
- * createExport({ name: ['Pet'], path: './Pet' })
569
- * // export { Pet } from './Pet'
570
- * ```
571
- */
572
- const createExport = exportDef.create;
573
- /**
574
- * Creates a `SourceNode` representing a fragment of source code within a file.
702
+ * Because the cache is owned by the caller, it can be shared, inspected, or
703
+ * cleared independently of the memoized function.
575
704
  *
576
- * @example
705
+ * @example Single WeakMap key
577
706
  * ```ts
578
- * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
707
+ * const cache = new WeakMap<SchemaNode, Set<string>>()
708
+ * const getRefs = memoize(cache, (node) => collectRefs(node))
579
709
  * ```
580
- */
581
- const createSource = sourceDef.create;
582
- /**
583
- * Creates a fully resolved `FileNode` from a file input descriptor.
584
710
  *
585
- * Computes:
586
- * - `id` SHA256 hash of the file path
587
- * - `name` `baseName` without extension
588
- * - `extname` extension extracted from `baseName`
589
- *
590
- * Deduplicates:
591
- * - `sources` via `combineSources`
592
- * - `exports` via `combineExports`
593
- * - `imports` via `combineImports` (also filters unused imports)
594
- *
595
- * @throws {Error} when `baseName` has no extension.
596
- *
597
- * @example
711
+ * @example Single Map key (primitive)
598
712
  * ```ts
599
- * const file = createFile({
600
- * baseName: 'petStore.ts',
601
- * path: 'src/models/petStore.ts',
602
- * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
603
- * imports: [createImport({ name: ['z'], path: 'zod' })],
604
- * exports: [createExport({ name: ['Pet'], path: './petStore' })],
605
- * })
606
- * // file.id = SHA256 hash of 'src/models/petStore.ts'
607
- * // file.name = 'petStore'
608
- * // file.extname = '.ts'
713
+ * const cache = new Map<string, Resolver>()
714
+ * const getResolver = memoize(cache, (name) => buildResolver(name))
609
715
  * ```
610
716
  *
611
- * @example Copy a real file into the output
717
+ * @example Two-level (object + primitive)
612
718
  * ```ts
613
- * const file = createFile({
614
- * baseName: 'client.ts',
615
- * path: 'src/gen/client.ts',
616
- * copy: '/abs/path/to/templates/client.ts',
617
- * })
719
+ * const outer = new WeakMap<Params[], Map<string, Params[]>>()
720
+ * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
721
+ * fn(params)('camelcase')
618
722
  * ```
619
723
  */
620
- function createFile(input) {
621
- const extname = node_path.default.extname(input.baseName);
622
- if (!extname) throw new Error(`No extname found for ${input.baseName}`);
623
- const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
624
- const resolvedImports = (() => {
625
- if (!input.imports?.length) return [];
626
- const sourceParts = [];
627
- const localNames = /* @__PURE__ */ new Set();
628
- for (const item of input.sources ?? []) {
629
- const extracted = item.nodes && extractStringsFromNodes(item.nodes);
630
- if (extracted) sourceParts.push(extracted);
631
- if (item.name) localNames.add(item.name);
632
- }
633
- const source = sourceParts.join("\n") || void 0;
634
- return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
635
- if (imp.path === input.path) return [];
636
- if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
637
- const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
638
- if (!kept.length) return [];
639
- return [kept.length === imp.name.length ? imp : {
640
- ...imp,
641
- name: kept
642
- }];
643
- });
644
- })();
645
- const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
646
- return {
647
- kind: "File",
648
- ...input,
649
- id: (0, node_crypto.hash)("sha256", input.path, "hex"),
650
- name: node_path.default.basename(input.baseName, extname),
651
- extname,
652
- imports: resolvedImports,
653
- exports: resolvedExports,
654
- sources: resolvedSources,
655
- meta: input.meta ?? {}
724
+ function memoize(store, factory) {
725
+ return (key) => {
726
+ if (store.has(key)) return store.get(key);
727
+ const value = factory(key);
728
+ store.set(key, value);
729
+ return value;
656
730
  };
657
731
  }
658
732
  //#endregion
659
- //#region src/nodes/input.ts
660
- /**
661
- * Definition for the {@link InputNode}.
662
- */
663
- const inputDef = defineNode({
664
- kind: "Input",
665
- build: (props) => ({
666
- schemas: [],
667
- operations: [],
668
- meta: {
669
- circularNames: [],
670
- enumNames: []
671
- },
672
- ...props
673
- }),
674
- children: ["schemas", "operations"],
675
- visitorKey: "input"
676
- });
733
+ //#region src/utils/schemaGraph.ts
677
734
  /**
678
- * Creates an `InputNode`, defaulting `schemas`/`operations` to empty arrays and `meta` per
679
- * {@link inputDef}.
735
+ * Collects the names of every schema a node's subtree references, resolved with `resolveRefName`.
736
+ * Each ref contributes its name only, so the schema it points to is never traversed. The walk is
737
+ * memoized by node identity, which lets `findCircularSchemas` and `containsCircularRef` share one
738
+ * scan per schema.
680
739
  *
681
740
  * @example
682
741
  * ```ts
683
- * const input = createInput()
684
- * // { kind: 'Input', schemas: [], operations: [] }
742
+ * collectSchemaRefs(petSchema)
743
+ * // Set { 'Category', 'Tag' }
685
744
  * ```
686
745
  */
687
- function createInput(overrides = {}) {
688
- return inputDef.create(overrides);
689
- }
690
- //#endregion
691
- //#region src/nodes/requestBody.ts
692
- /**
693
- * Definition for the {@link RequestBodyNode}. Content entries are built upfront with
694
- * {@link createContent}, mirroring how `parameters` and `responses` take prebuilt nodes.
695
- */
696
- const requestBodyDef = defineNode({
697
- kind: "RequestBody",
698
- children: ["content"]
699
- });
700
- /**
701
- * Creates a `RequestBodyNode`.
702
- */
703
- const createRequestBody = requestBodyDef.create;
704
- //#endregion
705
- //#region src/nodes/operation.ts
706
- /**
707
- * Definition for the {@link OperationNode}. HTTP operations (those carrying both
708
- * `method` and `path`) are tagged with `protocol: 'http'`, and the request body is
709
- * normalized into a `RequestBodyNode`.
710
- */
711
- const operationDef = defineNode({
712
- kind: "Operation",
713
- build: (props) => {
714
- const { requestBody, ...rest } = props;
715
- const isHttp = rest.method !== void 0 && rest.path !== void 0;
716
- return {
717
- tags: [],
718
- parameters: [],
719
- responses: [],
720
- ...rest,
721
- ...isHttp ? { protocol: "http" } : {},
722
- requestBody: requestBody ? createRequestBody(requestBody) : void 0
723
- };
724
- },
725
- children: [
726
- "parameters",
727
- "requestBody",
728
- "responses"
729
- ],
730
- visitorKey: "operation"
731
- });
732
- function createOperation(props) {
733
- return operationDef.create(props);
734
- }
735
- //#endregion
736
- //#region src/nodes/output.ts
737
- /**
738
- * Definition for the {@link OutputNode}.
739
- */
740
- const outputDef = defineNode({
741
- kind: "Output",
742
- defaults: { files: [] },
743
- visitorKey: "output"
746
+ const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
747
+ const refs = /* @__PURE__ */ new Set();
748
+ collectSync(node, { schema(child) {
749
+ if (child.type === "ref") {
750
+ const name = resolveRefName(child);
751
+ if (name) refs.add(name);
752
+ }
753
+ } });
754
+ return refs;
744
755
  });
745
756
  /**
746
- * Creates an `OutputNode` with a stable default for `files`.
757
+ * Collects the de-duplicated target names of every pointer-carrying ref in a node's subtree, in
758
+ * first-occurrence order. The walk is memoized by node identity, so the subtree is scanned once and
759
+ * `resolver.imports` reads the same result across the ts, zod, and faker plugins instead of
760
+ * re-scanning the same schema per plugin.
747
761
  *
748
- * @example
749
- * ```ts
750
- * const output = createOutput()
751
- * // { kind: 'Output', files: [] }
752
- * ```
753
- */
754
- function createOutput(overrides = {}) {
755
- return outputDef.create(overrides);
756
- }
757
- //#endregion
758
- //#region src/optionality.ts
759
- /**
760
- * Generic JSON Schema optionality: a non-required field is optional, and a
761
- * non-required nullable field is nullish.
762
- */
763
- function optionality(schema, required) {
764
- const nullable = schema.nullable ?? false;
765
- return {
766
- ...schema,
767
- optional: !required && !nullable ? true : void 0,
768
- nullish: !required && nullable ? true : void 0
769
- };
770
- }
771
- /**
772
- * Defaults `required` to `false` and derives the schema's `optional`/`nullish` flags from it.
773
- * Shared build step of `createProperty` and `createParameter`.
774
- */
775
- function withRequired(props) {
776
- const required = props.required ?? false;
777
- return {
778
- ...props,
779
- required,
780
- schema: optionality(props.schema, required)
781
- };
782
- }
783
- //#endregion
784
- //#region src/nodes/parameter.ts
785
- /**
786
- * Definition for the {@link ParameterNode}. `required` defaults to `false`, and the schema's
787
- * `optional`/`nullish` flags are derived from it through {@link withRequired}.
788
- */
789
- const parameterDef = defineNode({
790
- kind: "Parameter",
791
- build: withRequired,
792
- children: ["schema"],
793
- visitorKey: "parameter"
794
- });
795
- /**
796
- * Creates a `ParameterNode`.
762
+ * Only refs that carry a `$ref` pointer count, so a synthesized ref pointing at a sibling in the
763
+ * same file (a union member created by name) is left out. That leaves exactly the set
764
+ * `resolver.imports` emits.
797
765
  *
798
766
  * @example
799
767
  * ```ts
800
- * const param = createParameter({
801
- * name: 'petId',
802
- * in: 'path',
803
- * required: true,
804
- * schema: createSchema({ type: 'string' }),
805
- * })
768
+ * collectImportedRefNames(petSchema)
769
+ * // ['Category', 'Tag']
806
770
  * ```
807
771
  */
808
- const createParameter = parameterDef.create;
809
- //#endregion
810
- //#region src/nodes/property.ts
811
- /**
812
- * Definition for the {@link PropertyNode}. `required` defaults to `false`, and the schema's
813
- * `optional`/`nullish` flags are derived from it through {@link withRequired}.
814
- */
815
- const propertyDef = defineNode({
816
- kind: "Property",
817
- build: withRequired,
818
- children: ["schema"],
819
- visitorKey: "property"
772
+ const collectImportedRefNames = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
773
+ const seen = /* @__PURE__ */ new Set();
774
+ const names = [];
775
+ collectSync(node, { schema(child) {
776
+ if (child.type !== "ref" || !child.ref) return;
777
+ const name = resolveRefName(child);
778
+ if (name && !seen.has(name)) {
779
+ seen.add(name);
780
+ names.push(name);
781
+ }
782
+ } });
783
+ return names;
820
784
  });
821
785
  /**
822
- * Creates a `PropertyNode`.
786
+ * Collects the names of all top-level schemas transitively used by a set of operations.
823
787
  *
824
- * @example
825
- * ```ts
826
- * const property = createProperty({
827
- * name: 'status',
828
- * required: true,
829
- * schema: createSchema({ type: 'string', nullable: true }),
830
- * })
831
- * // required=true, no optional/nullish
832
- * ```
833
- */
834
- const createProperty = propertyDef.create;
835
- //#endregion
836
- //#region src/nodes/response.ts
837
- /**
838
- * Definition for the {@link ResponseNode}. A single legacy `schema` (with optional
839
- * `mediaType`/`keysToOmit`) is normalized into one `content` entry.
840
- */
841
- const responseDef = defineNode({
842
- kind: "Response",
843
- build: (props) => {
844
- const { schema, mediaType, keysToOmit, content, ...rest } = props;
845
- const entries = content ?? (schema ? [createContent({
846
- contentType: mediaType ?? "application/json",
847
- schema,
848
- keysToOmit: keysToOmit ?? null
849
- })] : void 0);
850
- return {
851
- ...rest,
852
- content: entries
853
- };
854
- },
855
- children: ["content"],
856
- visitorKey: "response"
857
- });
858
- /**
859
- * Creates a `ResponseNode`.
788
+ * An operation uses a schema when its parameters, request body, or responses reference it, directly
789
+ * or through other named schemas. Once a name is added to the result it is not revisited, so
790
+ * reference cycles terminate.
860
791
  *
861
- * @example
792
+ * Pair it with `include` filters so schemas reachable only from excluded operations stay ungenerated.
793
+ *
794
+ * @example Only generate schemas referenced by included operations
862
795
  * ```ts
863
- * const response = createResponse({
864
- * statusCode: '200',
865
- * content: [createContent({ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) })],
866
- * })
796
+ * const includedOps = operations.filter((op) => resolver.default.options(op, { options, include }) !== null)
797
+ * const allowed = collectUsedSchemaNames(includedOps, schemas)
798
+ *
799
+ * for (const schema of schemas) {
800
+ * if (schema.name && !allowed.has(schema.name)) continue
801
+ * // generate schema
802
+ * }
867
803
  * ```
868
804
  */
869
- const createResponse = responseDef.create;
870
- //#endregion
871
- //#region src/nodes/schema.ts
872
- /**
873
- * Maps schema `type` to its underlying `primitive`.
874
- * Primitive types map to themselves and special string formats map to `'string'`.
875
- * Any type not listed here (such as `ref`, `enum`, `union`, `intersection`, `tuple`, `ipv4`, `ipv6`, `blob`) has no `primitive`.
876
- */
877
- const TYPE_TO_PRIMITIVE = {
878
- string: "string",
879
- number: "number",
880
- integer: "integer",
881
- bigint: "bigint",
882
- boolean: "boolean",
883
- null: "null",
884
- any: "any",
885
- unknown: "unknown",
886
- void: "void",
887
- never: "never",
888
- object: "object",
889
- array: "array",
890
- date: "date",
891
- uuid: "string",
892
- email: "string",
893
- url: "string",
894
- datetime: "string",
895
- time: "string"
896
- };
897
- /**
898
- * Definition for the {@link SchemaNode}. Object schemas default `properties` to an
899
- * empty array, and `primitive` is inferred from `type` when not explicitly provided.
900
- */
901
- const schemaDef = defineNode({
902
- kind: "Schema",
903
- build: (props) => {
904
- if (props.type === "object") return {
905
- properties: [],
906
- primitive: "object",
907
- ...props
908
- };
909
- return {
910
- primitive: TYPE_TO_PRIMITIVE[props.type],
911
- ...props
912
- };
913
- },
914
- children: [
915
- "properties",
916
- "items",
917
- "members",
918
- "additionalProperties",
919
- "patternProperties",
920
- "rest"
921
- ],
922
- visitorKey: "schema"
923
- });
924
- function createSchema(props) {
925
- return schemaDef.create(props);
926
- }
927
- //#endregion
928
- //#region src/registry.ts
929
- /**
930
- * Every node definition. Adding a node means adding its `defineNode` to one
931
- * `nodes/*.ts` file and listing it here. The visitor tables in `visitor.ts` derive from it.
932
- */
933
- const nodeDefs = [
934
- inputDef,
935
- outputDef,
936
- operationDef,
937
- requestBodyDef,
938
- contentDef,
939
- responseDef,
940
- schemaDef,
941
- propertyDef,
942
- parameterDef,
943
- constDef,
944
- typeDef,
945
- functionDef,
946
- arrowFunctionDef,
947
- textDef,
948
- breakDef,
949
- jsxDef,
950
- importDef,
951
- exportDef,
952
- sourceDef,
953
- fileDef
954
- ];
955
- //#endregion
956
- //#region src/visitor.ts
957
- /**
958
- * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
959
- * Derived from each definition's `children`.
960
- */
961
- const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => def.children ? [[def.kind, def.children]] : []));
962
- /**
963
- * Maps a node kind to the matching visitor callback name. Derived from each
964
- * definition's `visitorKey`.
965
- */
966
- const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => def.visitorKey ? [[def.kind, def.visitorKey]] : []));
967
- const visitorKeysByKind = VISITOR_KEYS;
968
- /**
969
- * Returns `true` when `value` is an AST node (an object carrying a `kind`).
970
- */
971
- function isNode(value) {
972
- return typeof value === "object" && value !== null && typeof value.kind === "string";
973
- }
974
- /**
975
- * Returns `true` for a plain object that maps names to nodes, such as `patternProperties`.
976
- */
977
- function isNodeRecord(value) {
978
- return typeof value === "object" && value !== null && !Array.isArray(value);
979
- }
980
- /**
981
- * Returns the child fields of `node` to walk, from {@link VISITOR_KEYS}, or `undefined` when there
982
- * is nothing to walk. `Schema` children are only walked when `recurse` is `true`.
983
- */
984
- function childKeys(node, recurse) {
985
- if (node.kind === "Schema" && !recurse) return void 0;
986
- return visitorKeysByKind[node.kind];
805
+ function collectUsedSchemaNames(operations, schemas) {
806
+ const schemaMap = /* @__PURE__ */ new Map();
807
+ for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
808
+ const result = /* @__PURE__ */ new Set();
809
+ function visitSchema(schema) {
810
+ for (const name of collectSchemaRefs(schema)) if (!result.has(name)) {
811
+ result.add(name);
812
+ const namedSchema = schemaMap.get(name);
813
+ if (namedSchema) visitSchema(namedSchema);
814
+ }
815
+ }
816
+ for (const op of operations) for (const schema of collect(op, {
817
+ depth: "shallow",
818
+ schema: (node) => node
819
+ })) visitSchema(schema);
820
+ return result;
987
821
  }
988
822
  /**
989
- * Returns the immediate traversable children of `node`.
823
+ * Finds every schema that takes part in a circular dependency chain in a schema dependency graph
824
+ * that maps each schema name to the names it references directly.
990
825
  *
991
- * @example
992
- * ```ts
993
- * const children = getChildren(operationNode, true)
994
- * // returns parameters, the request body, and responses
995
- * ```
996
- */
997
- function* getChildren(node, recurse) {
998
- const keys = childKeys(node, recurse);
999
- if (!keys) return;
1000
- const record = node;
1001
- for (const key of keys) {
1002
- const value = record[key];
1003
- if (Array.isArray(value)) {
1004
- for (const item of value) if (isNode(item)) yield item;
1005
- continue;
1006
- }
1007
- if (isNode(value)) {
1008
- yield value;
1009
- continue;
1010
- }
1011
- if (isNodeRecord(value)) {
1012
- for (const item of Object.values(value)) if (isNode(item)) yield item;
826
+ * Use this when the graph was already collected during another pass (e.g. the adapter's convert
827
+ * walk), so the schema nodes are not swept a second time. `findCircularSchemas` builds the graph
828
+ * from schema nodes and delegates here.
829
+ *
830
+ * @example
831
+ * ```ts
832
+ * const graph = new Map([
833
+ * ['Pet', new Set(['Category'])],
834
+ * ['Category', new Set(['Pet'])],
835
+ * ])
836
+ * findCircularSchemasFromGraph(graph) // Set { 'Pet', 'Category' }
837
+ * ```
838
+ */
839
+ function findCircularSchemasFromGraph(graph) {
840
+ const circular = /* @__PURE__ */ new Set();
841
+ for (const start of graph.keys()) {
842
+ const visited = /* @__PURE__ */ new Set();
843
+ const stack = [...graph.get(start) ?? []];
844
+ while (stack.length > 0) {
845
+ const node = stack.pop();
846
+ if (node === start) {
847
+ circular.add(start);
848
+ break;
849
+ }
850
+ if (visited.has(node)) continue;
851
+ visited.add(node);
852
+ const next = graph.get(node);
853
+ if (next) for (const r of next) stack.push(r);
1013
854
  }
1014
855
  }
856
+ return circular;
1015
857
  }
1016
858
  /**
1017
- * Runs the visitor callback that matches `node.kind` with the traversal
1018
- * context. The result is a replacement node, a collected value, or `undefined`
1019
- * when no callback is registered for the kind.
859
+ * Finds every schema that takes part in a circular dependency chain, including direct self-loops.
1020
860
  *
1021
- * Shared by `transform` and `collect` so node-kind dispatch lives in one place.
1022
- * `TResult` is the caller's expected return: the same node type for `transform`,
1023
- * the collected value type for `collect`.
861
+ * Wrap the returned schema positions in a deferred construct (a lazy getter or `z.lazy(() => …)`) so
862
+ * the generated code does not recurse forever. Refs are followed by name only, so the walk stays
863
+ * linear in the size of the schema graph.
864
+ *
865
+ * @note Call this once on the full graph, then check individual schemas with `containsCircularRef()`.
1024
866
  */
1025
- function applyVisitor(node, visitor, parent) {
1026
- const key = VISITOR_KEY_BY_KIND[node.kind];
1027
- if (!key) return void 0;
1028
- const fn = visitor[key];
1029
- return fn?.(node, { parent });
867
+ function findCircularSchemas(schemas) {
868
+ const graph = /* @__PURE__ */ new Map();
869
+ for (const schema of schemas) if (schema.name) graph.set(schema.name, collectSchemaRefs(schema));
870
+ return findCircularSchemasFromGraph(graph);
1030
871
  }
872
+ //#endregion
873
+ //#region src/utils/schemaProperties.ts
1031
874
  /**
1032
- * Synchronous depth-first transform. Each visitor callback can return a
1033
- * replacement node. Returning `undefined` keeps the original.
875
+ * Finds every matching property exposed by an object, resolved reference, or intersection.
1034
876
  *
1035
- * The original tree is never mutated, a new tree is returned. Pass
1036
- * `depth: 'shallow'` to skip recursion into children.
877
+ * References are followed through their parsed `schema`. Circular references terminate without
878
+ * revisiting a node. Unions are not traversed because a property on one branch does not describe
879
+ * every branch.
1037
880
  *
1038
- * @example Prefix every operationId
881
+ * @example Faker discriminator narrowing
1039
882
  * ```ts
1040
- * const next = transform(root, {
1041
- * operation(node) {
1042
- * return { ...node, operationId: `prefixed_${node.operationId}` }
1043
- * },
1044
- * })
883
+ * const values = resolveSchemaProperties({ node: member, propertyName: union.discriminatorPropertyName }).flatMap(({ schema }) =>
884
+ * getSchemaLiteralValues(schema),
885
+ * )
886
+ * const typeName = `Extract<Pet, { type: ${values.map(JSON.stringify).join(' | ')} }>`
1045
887
  * ```
1046
888
  *
1047
- * @example Replace only the root node
889
+ * @example Zod discriminated unions
1048
890
  * ```ts
1049
- * const next = transform(root, {
1050
- * depth: 'shallow',
1051
- * input: (node) => ({ ...node, meta: { ...node.meta, title: 'Rewritten' } }),
1052
- * })
891
+ * const canDiscriminate = union.members?.every(
892
+ * (member) => resolveSchemaProperties({ node: member, propertyName: 'type' }).length > 0,
893
+ * )
894
+ * const expression = canDiscriminate ? `z.discriminatedUnion('type', members)` : `z.union(members)`
895
+ * ```
896
+ *
897
+ * @example TypeScript property lookup
898
+ * ```ts
899
+ * const properties = resolveSchemaProperties({ node: schema, propertyName: 'status' })
900
+ * const statusSchemas = properties.map((property) => print(property.schema))
1053
901
  * ```
1054
902
  */
1055
- function transform(node, options) {
1056
- const { depth, parent, ...visitor } = options;
1057
- return transformNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
1058
- }
1059
- /**
1060
- * Visits a single node, then immutably rebuilds its children. Returns the original
1061
- * reference when neither the visitor nor the child rebuild changed anything, so callers
1062
- * can detect "nothing changed" by identity and ancestors avoid reallocating.
1063
- */
1064
- function transformNode(node, visitor, recurse, parent) {
1065
- return transformChildren(applyVisitor(node, visitor, parent) ?? node, visitor, recurse);
1066
- }
1067
- /**
1068
- * Immutably rebuilds a node's children, transforming each child node and leaving non-node values
1069
- * (e.g. `additionalProperties: true`) intact.
1070
- */
1071
- function transformChildren(node, visitor, recurse) {
1072
- const keys = childKeys(node, recurse);
1073
- if (!keys) return node;
1074
- const record = node;
1075
- let updates;
1076
- for (const key of keys) {
1077
- if (!(key in record)) continue;
1078
- const value = record[key];
1079
- if (Array.isArray(value)) {
1080
- let mapped;
1081
- for (const [i, item] of value.entries()) {
1082
- const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
1083
- if (mapped) {
1084
- mapped.push(next);
1085
- continue;
1086
- }
1087
- if (next !== item) mapped = [...value.slice(0, i), next];
1088
- }
1089
- if (mapped) (updates ??= {})[key] = mapped;
1090
- continue;
1091
- }
1092
- if (isNode(value)) {
1093
- const next = transformNode(value, visitor, recurse, node);
1094
- if (next !== value) (updates ??= {})[key] = next;
1095
- continue;
903
+ function resolveSchemaProperties({ node, propertyName }) {
904
+ const properties = [];
905
+ const visited = /* @__PURE__ */ new WeakSet();
906
+ function visit(current) {
907
+ if (!current || visited.has(current)) return;
908
+ visited.add(current);
909
+ if (current.type === "object") {
910
+ const property = current.properties.find((candidate) => candidate.name === propertyName);
911
+ if (property) properties.push(property);
912
+ return;
1096
913
  }
1097
- if (isNodeRecord(value)) {
1098
- let mapped;
1099
- for (const [name, item] of Object.entries(value)) {
1100
- const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
1101
- if (next !== item) (mapped ??= { ...value })[name] = next;
1102
- }
1103
- if (mapped) (updates ??= {})[key] = mapped;
914
+ if (current.type === "ref") {
915
+ visit(current.schema);
916
+ return;
1104
917
  }
918
+ if (current.type === "intersection") for (const member of current.members ?? []) visit(member);
1105
919
  }
1106
- if (!updates) return node;
1107
- return {
1108
- ...node,
1109
- ...updates
1110
- };
920
+ visit(node);
921
+ return properties;
1111
922
  }
1112
923
  /**
1113
- * Lazy depth-first collection pass. Yields every non-null value returned by
1114
- * the visitor callbacks. Use `collectSync` for the eager array form.
924
+ * Reads the unique literal values represented by an enum, resolved reference, or union.
1115
925
  *
1116
- * @example Collect every operationId
926
+ * Named enum values take precedence over simple enum values, matching how renderers consume
927
+ * `EnumSchemaNode`.
928
+ *
929
+ * @example
1117
930
  * ```ts
1118
- * const ids: string[] = []
1119
- * for (const id of collect<string>(root, {
1120
- * operation(node) {
1121
- * return node.operationId
1122
- * },
1123
- * })) {
1124
- * ids.push(id)
1125
- * }
931
+ * const values = getSchemaLiteralValues(enumNode)
932
+ * // ['cat', 'dog']
1126
933
  * ```
1127
934
  */
1128
- function* collect(node, options) {
1129
- const { depth, parent, ...visitor } = options;
1130
- yield* collectNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
935
+ function getSchemaLiteralValues(node) {
936
+ const values = /* @__PURE__ */ new Set();
937
+ const visited = /* @__PURE__ */ new WeakSet();
938
+ function visit(current) {
939
+ if (!current || visited.has(current)) return;
940
+ visited.add(current);
941
+ if (current.type === "enum") {
942
+ const enumValues = current.namedEnumValues?.map(({ value }) => value) ?? current.enumValues ?? [];
943
+ for (const value of enumValues) values.add(value);
944
+ return;
945
+ }
946
+ if (current.type === "ref") {
947
+ visit(current.schema);
948
+ return;
949
+ }
950
+ if (current.type === "union") for (const member of current.members ?? []) visit(member);
951
+ }
952
+ visit(node);
953
+ return [...values];
1131
954
  }
1132
- function* collectNode(node, visitor, recurse, parent) {
1133
- const v = applyVisitor(node, visitor, parent);
1134
- if (v != null) yield v;
1135
- for (const child of getChildren(node, recurse)) yield* collectNode(child, visitor, recurse, node);
955
+ function collectPropertyNames(node, visited = /* @__PURE__ */ new WeakSet()) {
956
+ if (visited.has(node)) return /* @__PURE__ */ new Set();
957
+ visited.add(node);
958
+ if (node.type === "object") return new Set(node.properties.map((property) => property.name));
959
+ if (node.type === "ref") return node.schema ? collectPropertyNames(node.schema, visited) : /* @__PURE__ */ new Set();
960
+ if (node.type === "intersection") {
961
+ const names = /* @__PURE__ */ new Set();
962
+ for (const member of node.members ?? []) for (const name of collectPropertyNames(member, visited)) names.add(name);
963
+ return names;
964
+ }
965
+ return /* @__PURE__ */ new Set();
1136
966
  }
1137
967
  /**
1138
- * Eager depth-first collection pass. Gathers every non-null value the visitor
1139
- * callbacks return into an array.
1140
- *
1141
- * @example Collect every operationId
1142
- * ```ts
1143
- * const ids = collectSync<string>(root, {
1144
- * operation(node) {
1145
- * return node.operationId
1146
- * },
1147
- * })
1148
- * ```
968
+ * Infers an implicit discriminator: a property every member exposes with a distinct single
969
+ * literal value, the same shape a declared OpenAPI `discriminator` already narrows on.
1149
970
  */
1150
- function collectSync(node, options) {
1151
- return Array.from(collect(node, options));
971
+ function inferDiscriminatorPropertyName(members) {
972
+ if (members.length < 2) return void 0;
973
+ let candidates;
974
+ for (const member of members) {
975
+ const names = collectPropertyNames(member);
976
+ candidates = candidates ? new Set([...candidates].filter((name) => names.has(name))) : names;
977
+ }
978
+ for (const name of candidates ?? []) {
979
+ const values = members.map((member) => {
980
+ const literals = new Set(resolveSchemaProperties({
981
+ node: member,
982
+ propertyName: name
983
+ }).flatMap((property) => getSchemaLiteralValues(property.schema)));
984
+ return literals.size === 1 ? [...literals][0] : void 0;
985
+ });
986
+ if (values.every((value) => value !== void 0) && new Set(values).size === members.length) return name;
987
+ }
1152
988
  }
1153
989
  //#endregion
1154
- //#region src/defineMacro.ts
990
+ //#region src/nodes/code.ts
1155
991
  /**
1156
- * Sort weight for an `enforce` hint. `pre` sorts before unmarked items and `post` after, so a plain
1157
- * list keeps its authored order.
1158
- */
1159
- function enforceWeight(enforce) {
1160
- if (enforce === "pre") return 0;
1161
- if (enforce === "post") return 2;
1162
- return 1;
992
+ * Creates a `ConstNode` representing a TypeScript `const` declaration.
993
+ *
994
+ * @example Exported constant with type and `as const`
995
+ * ```ts
996
+ * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
997
+ * // export const pets: Pet[] = ... as const
998
+ * ```
999
+ */
1000
+ function createConst(input) {
1001
+ return {
1002
+ ...input,
1003
+ kind: "Const"
1004
+ };
1163
1005
  }
1164
1006
  /**
1165
- * Types a macro for inference and a single construction site, mirroring `defineExtension`.
1166
- * Adds no runtime behavior.
1007
+ * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
1167
1008
  *
1168
1009
  * @example
1169
1010
  * ```ts
1170
- * const macroUntagged = defineMacro({
1171
- * name: 'untagged',
1172
- * operation(node) {
1173
- * return node.tags?.length ? undefined : { ...node, tags: ['untagged'] }
1174
- * },
1175
- * })
1011
+ * createType({ name: 'Pet', export: true })
1012
+ * // export type Pet = ...
1176
1013
  * ```
1177
1014
  */
1178
- function defineMacro(macro) {
1179
- return macro;
1015
+ function createType(input) {
1016
+ return {
1017
+ ...input,
1018
+ kind: "Type"
1019
+ };
1180
1020
  }
1181
1021
  /**
1182
- * Runs every macro's callback for one node kind in order, chaining the result so each macro sees
1183
- * the previous macro's output. Returns `undefined` when nothing changed, so `transform` keeps the
1184
- * original reference (structural sharing).
1022
+ * Creates a `FunctionNode` representing a TypeScript `function` declaration.
1023
+ *
1024
+ * @example
1025
+ * ```ts
1026
+ * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
1027
+ * // export async function fetchPet(): Promise<Pet> { ... }
1028
+ * ```
1185
1029
  */
1186
- function chain({ macros, key, node, context }) {
1187
- let current = node;
1188
- for (const macro of macros) {
1189
- const callback = macro[key];
1190
- if (!callback) continue;
1191
- if (macro.match && !macro.match(current)) continue;
1192
- const next = callback(current, context);
1193
- if (next != null) current = next;
1194
- }
1195
- return current === node ? void 0 : current;
1030
+ function createFunction(input) {
1031
+ return {
1032
+ ...input,
1033
+ kind: "Function"
1034
+ };
1196
1035
  }
1197
1036
  /**
1198
- * Folds an ordered list of macros into a single {@link Visitor} that `transform` (and the per-plugin
1199
- * transform layer in `@kubb/core`) can run. Macros are stable-sorted by `enforce`, then applied
1200
- * sequentially per node so later macros see earlier output. This differs from a plain visitor, which
1201
- * has no names, ordering, or composition.
1037
+ * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
1202
1038
  *
1203
1039
  * @example
1204
1040
  * ```ts
1205
- * const visitor = composeMacros([macroSimplifyUnion, macroDiscriminatorEnum])
1206
- * const next = transform(root, visitor)
1041
+ * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
1042
+ * // export const double = (n: number) => ...
1207
1043
  * ```
1208
1044
  */
1209
- function composeMacros(macros) {
1210
- const ordered = [...macros].sort((a, b) => enforceWeight(a.enforce) - enforceWeight(b.enforce));
1211
- const visitor = {};
1212
- for (const key of visitorKeys) {
1213
- if (!ordered.some((macro) => typeof macro[key] === "function")) continue;
1214
- const callback = (node, context) => chain({
1215
- macros: ordered,
1216
- key,
1217
- node,
1218
- context
1219
- });
1220
- visitor[key] = callback;
1221
- }
1222
- return visitor;
1045
+ function createArrowFunction(input) {
1046
+ return {
1047
+ ...input,
1048
+ kind: "ArrowFunction"
1049
+ };
1223
1050
  }
1224
1051
  /**
1225
- * Runs a list of macros over a node tree and returns the rewritten tree. Keeps `transform`'s
1226
- * structural sharing, so an empty or no-op macro list returns the same reference. Pass
1227
- * `depth: 'shallow'` to rewrite the root node only.
1052
+ * Creates a {@link TextNode} representing a raw string fragment in the source output.
1228
1053
  *
1229
1054
  * @example
1230
1055
  * ```ts
1231
- * const next = applyMacros(root, [macroIntegerToString])
1056
+ * createText('return fetch(id)')
1057
+ * // { kind: 'Text', value: 'return fetch(id)' }
1232
1058
  * ```
1059
+ */
1060
+ function createText(value) {
1061
+ return {
1062
+ kind: "Text",
1063
+ value
1064
+ };
1065
+ }
1066
+ /**
1067
+ * Creates a {@link BreakNode} representing a line break in the source output.
1233
1068
  *
1234
- * @example Apply to the root node only
1069
+ * @example
1235
1070
  * ```ts
1236
- * const named = applyMacros(node, [macroEnumName({ parentName, propName, enumSuffix })], { depth: 'shallow' })
1071
+ * createBreak()
1072
+ * // { kind: 'Break' }
1237
1073
  * ```
1238
1074
  */
1239
- function applyMacros(root, macros, options) {
1240
- if (macros.length === 0) return root;
1241
- return transform(root, {
1242
- ...composeMacros(macros),
1243
- ...options
1244
- });
1075
+ function createBreak() {
1076
+ return { kind: "Break" };
1245
1077
  }
1246
- //#endregion
1247
- //#region src/createPrinter.ts
1248
1078
  /**
1249
- * Creates a schema printer: a function that takes a `SchemaNode` and emits
1250
- * code in your target language. Each plugin that produces code from schemas
1251
- * (TypeScript types, Zod schemas, Faker factories) ships a printer built
1252
- * with this helper.
1253
- *
1254
- * The builder receives resolved options and returns:
1255
- *
1256
- * - `name` unique identifier for the printer.
1257
- * - `options` stored on the returned printer instance.
1258
- * - `nodes` map of `SchemaType` → handler. Handlers return the rendered
1259
- * output (a string, a TypeScript AST node, ...) for that schema type.
1260
- * - `overrides` (optional), user-supplied handlers that win over `nodes`.
1261
- * An override can call `this.base(node)` to reuse the handler it replaced.
1262
- * - `print` (optional), top-level override exposed as `printer.print`.
1263
- * Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
1264
- *
1265
- * Without a `print` override, `printer.print` falls back to `printer.transform`
1266
- * (the node-level dispatcher).
1079
+ * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
1267
1080
  *
1268
- * @example Tiny Zod printer
1081
+ * @example
1269
1082
  * ```ts
1270
- * import { createPrinter, type PrinterFactoryOptions } from '@kubb/ast'
1271
- *
1272
- * type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
1273
- *
1274
- * export const zodPrinter = createPrinter<PrinterZod>((options) => ({
1275
- * name: 'zod',
1276
- * options: { strict: options.strict ?? true },
1277
- * nodes: {
1278
- * string: () => 'z.string()',
1279
- * object(node) {
1280
- * const props = node.properties
1281
- * .map((p) => `${p.name}: ${this.transform(p.schema)}`)
1282
- * .join(', ')
1283
- * return `z.object({ ${props} })`
1284
- * },
1285
- * },
1286
- * }))
1083
+ * createJsx('<>\n <a href={href}>Open</a>\n</>')
1084
+ * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
1287
1085
  * ```
1288
1086
  */
1289
- function createPrinter(build) {
1290
- return (options) => {
1291
- const { name, options: resolvedOptions, nodes, overrides, print: printOverride } = build(options ?? {});
1292
- const merged = overrides ? {
1293
- ...nodes,
1294
- ...overrides
1295
- } : nodes;
1296
- const collectedImports = [];
1297
- const dispatch = (table) => (node) => {
1298
- const handler = table[node.type];
1299
- if (!handler) return null;
1300
- return handler.call(context, node);
1301
- };
1302
- const context = {
1303
- options: resolvedOptions,
1304
- import: (node) => {
1305
- collectedImports.push(node);
1306
- },
1307
- transform: dispatch(merged),
1308
- base: dispatch(nodes)
1309
- };
1310
- return {
1311
- name,
1312
- options: resolvedOptions,
1313
- transform: context.transform,
1314
- print: printOverride ? printOverride.bind(context) : context.transform,
1315
- drainImports: () => collectedImports.splice(0)
1316
- };
1087
+ function createJsx(value) {
1088
+ return {
1089
+ kind: "Jsx",
1090
+ value
1317
1091
  };
1318
1092
  }
1319
1093
  //#endregion
1320
- //#region src/utils/refs.ts
1094
+ //#region src/nodes/content.ts
1321
1095
  /**
1322
- * Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
1323
- * the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
1324
- * then `name`, then the nested `schema.name`.
1325
- *
1326
- * Returns `null` for non-ref nodes or when no name resolves.
1327
- *
1328
- * @example
1329
- * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
1330
- *
1331
- * @example Collision-renamed target
1332
- * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
1096
+ * Creates a `ContentNode` for a single request-body or response content type.
1333
1097
  */
1334
- function resolveRefName(node) {
1335
- if (!node || node.type !== "ref") return null;
1336
- if (node.targetName) return node.targetName;
1337
- if (node.ref) return node.ref.split("/").at(-1) ?? node.ref;
1338
- return node.name ?? node.schema?.name ?? null;
1098
+ function createContent(input) {
1099
+ return {
1100
+ ...input,
1101
+ kind: "Content"
1102
+ };
1103
+ }
1104
+ //#endregion
1105
+ //#region src/utils/combineFileMembers.ts
1106
+ const IDENTIFIER_RUN = /[\w$]+/g;
1107
+ /**
1108
+ * How many imports a file needs before indexing the source beats scanning it once per name.
1109
+ */
1110
+ const INDEX_ABOVE_IMPORTS = 128;
1111
+ /**
1112
+ * Returns the local binding an import name item introduces: the alias when there is one, else the
1113
+ * imported name.
1114
+ */
1115
+ function importLocalName(item) {
1116
+ return typeof item === "string" ? item : item.name ?? item.propertyName;
1117
+ }
1118
+ /**
1119
+ * Computes a multi-level sort key for exports and imports:
1120
+ * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
1121
+ */
1122
+ function sortKey(node) {
1123
+ const isArray = Array.isArray(node.name) ? "1" : "0";
1124
+ const typeOnly = node.isTypeOnly ? "0" : "1";
1125
+ const hasName = node.name != null ? "1" : "0";
1126
+ const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
1127
+ return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
1128
+ }
1129
+ /**
1130
+ * Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
1131
+ */
1132
+ function sortMembers(nodes) {
1133
+ return nodes.map((node) => ({
1134
+ node,
1135
+ key: sortKey(node)
1136
+ })).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
1137
+ }
1138
+ /**
1139
+ * Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
1140
+ * seen folds its names into that entry, otherwise it joins `result` as a new entry.
1141
+ */
1142
+ function mergeByPath(result) {
1143
+ const byPath = /* @__PURE__ */ new Map();
1144
+ return ({ node, names }) => {
1145
+ const key = `${node.path}:${node.isTypeOnly ?? false}`;
1146
+ const existing = byPath.get(key);
1147
+ if (existing && Array.isArray(existing.name)) {
1148
+ existing.name = [.../* @__PURE__ */ new Set([...existing.name, ...names])];
1149
+ return;
1150
+ }
1151
+ const item = {
1152
+ ...node,
1153
+ name: names
1154
+ };
1155
+ result.push(item);
1156
+ byPath.set(key, item);
1157
+ };
1158
+ }
1159
+ /**
1160
+ * Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
1161
+ * key. Unnamed sources fall back to their extracted node strings as the name part of the key. Returns
1162
+ * the deduplicated array in original order.
1163
+ */
1164
+ function combineSources(sources) {
1165
+ const seen = /* @__PURE__ */ new Map();
1166
+ for (const source of sources) {
1167
+ const key = `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
1168
+ if (!seen.has(key)) seen.set(key, source);
1169
+ }
1170
+ return [...seen.values()];
1339
1171
  }
1340
1172
  /**
1341
- * Merges a ref node with its resolved schema, giving the fields set at the usage site precedence.
1342
- *
1343
- * Every field on the ref except `kind`, `type`, `name`, `ref`, and `schema` overrides the same field
1344
- * of the resolved `schema` (for example `description`, `nullable`, `readOnly`, `deprecated`). Fields
1345
- * left `undefined` on the ref do not shadow the resolved schema. A ref without a resolved `schema`
1346
- * is returned as is.
1173
+ * Deduplicates and merges `ExportNode` objects by path and type.
1347
1174
  *
1348
- * @example
1349
- * ```ts
1350
- * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
1351
- * mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
1352
- * ```
1175
+ * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
1176
+ * Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
1353
1177
  */
1354
- function mergeRefWithSchema(node) {
1355
- if (!node.schema) return node;
1356
- const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = node;
1357
- const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0));
1358
- return {
1359
- ...node.schema,
1360
- ...definedOverrides
1361
- };
1178
+ function combineExports(exports) {
1179
+ const result = [];
1180
+ const merge = mergeByPath(result);
1181
+ const seen = /* @__PURE__ */ new Set();
1182
+ for (const curr of sortMembers(exports)) {
1183
+ const { name, path, isTypeOnly, asAlias } = curr;
1184
+ if (Array.isArray(name)) {
1185
+ if (name.length) merge({
1186
+ node: curr,
1187
+ names: [...new Set(name)]
1188
+ });
1189
+ continue;
1190
+ }
1191
+ const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
1192
+ if (!seen.has(key)) {
1193
+ result.push(curr);
1194
+ seen.add(key);
1195
+ }
1196
+ }
1197
+ return result;
1362
1198
  }
1363
1199
  /**
1364
- * Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
1200
+ * Deduplicates and merges `ImportNode` objects, filtering out unused imports.
1365
1201
  *
1366
- * Modifiers inherited from the resolved schema are included in the check because a ref emits the
1367
- * referenced schema variable with those modifiers already applied.
1202
+ * Retains imports that are referenced in `source` or re-exported. Imports with the same path and
1203
+ * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
1368
1204
  */
1369
- function isBareRef(node, keysToOmit) {
1370
- if (!node || node.type !== "ref") return false;
1371
- const meta = mergeRefWithSchema(node);
1372
- return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
1205
+ function combineImports(imports, exports, source) {
1206
+ const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
1207
+ const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
1208
+ const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
1209
+ const importNameMemo = /* @__PURE__ */ new Map();
1210
+ const canonicalizeName = (n) => {
1211
+ if (typeof n === "string") return n;
1212
+ const key = `${n.propertyName}:${n.name ?? ""}`;
1213
+ if (!importNameMemo.has(key)) importNameMemo.set(key, n);
1214
+ return importNameMemo.get(key);
1215
+ };
1216
+ const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
1217
+ for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
1218
+ const result = [];
1219
+ const merge = mergeByPath(result);
1220
+ const seen = /* @__PURE__ */ new Set();
1221
+ for (const curr of sortMembers(imports)) {
1222
+ if (curr.path === curr.root) continue;
1223
+ const { name, path, isTypeOnly } = curr;
1224
+ if (Array.isArray(name)) {
1225
+ const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
1226
+ if (names.length) merge({
1227
+ node: curr,
1228
+ names
1229
+ });
1230
+ continue;
1231
+ }
1232
+ if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
1233
+ const key = `${path}:${name}:${isTypeOnly ?? false}`;
1234
+ if (!seen.has(key)) {
1235
+ result.push(curr);
1236
+ seen.add(key);
1237
+ }
1238
+ }
1239
+ return result;
1373
1240
  }
1374
1241
  //#endregion
1375
- //#region ../../internals/utils/src/promise.ts
1242
+ //#region src/nodes/file.ts
1376
1243
  /**
1377
- * Wraps `factory` with a keyed cache backed by the provided store.
1378
- *
1379
- * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
1380
- * collected) or a `Map` for primitive keys. For multi-argument functions,
1381
- * nest two `memoize` calls — the outer keyed by the first argument, the
1382
- * inner (created once per outer miss) keyed by the second.
1383
- *
1384
- * Because the cache is owned by the caller, it can be shared, inspected, or
1385
- * cleared independently of the memoized function.
1386
- *
1387
- * @example Single WeakMap key
1388
- * ```ts
1389
- * const cache = new WeakMap<SchemaNode, Set<string>>()
1390
- * const getRefs = memoize(cache, (node) => collectRefs(node))
1391
- * ```
1392
- *
1393
- * @example Single Map key (primitive)
1394
- * ```ts
1395
- * const cache = new Map<string, Resolver>()
1396
- * const getResolver = memoize(cache, (name) => buildResolver(name))
1397
- * ```
1244
+ * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
1398
1245
  *
1399
- * @example Two-level (object + primitive)
1246
+ * @example Named import
1400
1247
  * ```ts
1401
- * const outer = new WeakMap<Params[], Map<string, Params[]>>()
1402
- * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
1403
- * fn(params)('camelcase')
1248
+ * createImport({ name: ['useState'], path: 'react' })
1249
+ * // import { useState } from 'react'
1404
1250
  * ```
1405
1251
  */
1406
- function memoize(store, factory) {
1407
- return (key) => {
1408
- if (store.has(key)) return store.get(key);
1409
- const value = factory(key);
1410
- store.set(key, value);
1411
- return value;
1252
+ function createImport(input) {
1253
+ return {
1254
+ ...input,
1255
+ kind: "Import"
1412
1256
  };
1413
1257
  }
1414
- //#endregion
1415
- //#region src/utils/schemaGraph.ts
1416
1258
  /**
1417
- * Collects the names of every schema a node's subtree references, resolved with `resolveRefName`.
1418
- * Each ref contributes its name only, so the schema it points to is never traversed. The walk is
1419
- * memoized by node identity, which lets `findCircularSchemas` and `containsCircularRef` share one
1420
- * scan per schema.
1259
+ * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
1421
1260
  *
1422
- * @example
1261
+ * @example Named export
1423
1262
  * ```ts
1424
- * collectSchemaRefs(petSchema)
1425
- * // Set { 'Category', 'Tag' }
1263
+ * createExport({ name: ['Pet'], path: './Pet' })
1264
+ * // export { Pet } from './Pet'
1426
1265
  * ```
1427
1266
  */
1428
- const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1429
- const refs = /* @__PURE__ */ new Set();
1430
- collectSync(node, { schema(child) {
1431
- if (child.type === "ref") {
1432
- const name = resolveRefName(child);
1433
- if (name) refs.add(name);
1434
- }
1435
- } });
1436
- return refs;
1437
- });
1267
+ function createExport(input) {
1268
+ return {
1269
+ ...input,
1270
+ kind: "Export"
1271
+ };
1272
+ }
1438
1273
  /**
1439
- * Collects the de-duplicated target names of every pointer-carrying ref in a node's subtree, in
1440
- * first-occurrence order. The walk is memoized by node identity, so the subtree is scanned once and
1441
- * `resolver.imports` reads the same result across the ts, zod, and faker plugins instead of
1442
- * re-scanning the same schema per plugin.
1443
- *
1444
- * Only refs that carry a `$ref` pointer count, so a synthesized ref pointing at a sibling in the
1445
- * same file (a union member created by name) is left out. That leaves exactly the set
1446
- * `resolver.imports` emits.
1274
+ * Creates a `SourceNode` representing a fragment of source code within a file.
1447
1275
  *
1448
1276
  * @example
1449
1277
  * ```ts
1450
- * collectImportedRefNames(petSchema)
1451
- * // ['Category', 'Tag']
1278
+ * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
1452
1279
  * ```
1453
1280
  */
1454
- const collectImportedRefNames = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1455
- const seen = /* @__PURE__ */ new Set();
1456
- const names = [];
1457
- collectSync(node, { schema(child) {
1458
- if (child.type !== "ref" || !child.ref) return;
1459
- const name = resolveRefName(child);
1460
- if (name && !seen.has(name)) {
1461
- seen.add(name);
1462
- names.push(name);
1463
- }
1464
- } });
1465
- return names;
1466
- });
1281
+ function createSource(input) {
1282
+ return {
1283
+ ...input,
1284
+ kind: "Source"
1285
+ };
1286
+ }
1467
1287
  /**
1468
- * Collects the names of all top-level schemas transitively used by a set of operations.
1288
+ * Creates a fully resolved `FileNode` from a file input descriptor.
1469
1289
  *
1470
- * An operation uses a schema when its parameters, request body, or responses reference it, directly
1471
- * or through other named schemas. Once a name is added to the result it is not revisited, so
1472
- * reference cycles terminate.
1290
+ * Computes:
1291
+ * - `id` SHA256 hash of the file path
1292
+ * - `name` `baseName` without extension
1293
+ * - `extname` extension extracted from `baseName`
1473
1294
  *
1474
- * Pair it with `include` filters so schemas reachable only from excluded operations stay ungenerated.
1295
+ * Deduplicates:
1296
+ * - `sources` via `combineSources`
1297
+ * - `exports` via `combineExports`
1298
+ * - `imports` via `combineImports` (also filters unused imports)
1475
1299
  *
1476
- * @example Only generate schemas referenced by included operations
1300
+ * @throws {Error} when `baseName` has no extension.
1301
+ *
1302
+ * @example
1477
1303
  * ```ts
1478
- * const includedOps = operations.filter((op) => resolver.default.options(op, { options, include }) !== null)
1479
- * const allowed = collectUsedSchemaNames(includedOps, schemas)
1304
+ * const file = createFile({
1305
+ * baseName: 'petStore.ts',
1306
+ * path: 'src/models/petStore.ts',
1307
+ * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
1308
+ * imports: [createImport({ name: ['z'], path: 'zod' })],
1309
+ * exports: [createExport({ name: ['Pet'], path: './petStore' })],
1310
+ * })
1311
+ * // file.id = SHA256 hash of 'src/models/petStore.ts'
1312
+ * // file.name = 'petStore'
1313
+ * // file.extname = '.ts'
1314
+ * ```
1480
1315
  *
1481
- * for (const schema of schemas) {
1482
- * if (schema.name && !allowed.has(schema.name)) continue
1483
- * // generate schema
1484
- * }
1316
+ * @example Copy a real file into the output
1317
+ * ```ts
1318
+ * const file = createFile({
1319
+ * baseName: 'client.ts',
1320
+ * path: 'src/gen/client.ts',
1321
+ * copy: '/abs/path/to/templates/client.ts',
1322
+ * })
1485
1323
  * ```
1486
1324
  */
1487
- function collectUsedSchemaNames(operations, schemas) {
1488
- const schemaMap = /* @__PURE__ */ new Map();
1489
- for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
1490
- const result = /* @__PURE__ */ new Set();
1491
- function visitSchema(schema) {
1492
- for (const name of collectSchemaRefs(schema)) if (!result.has(name)) {
1493
- result.add(name);
1494
- const namedSchema = schemaMap.get(name);
1495
- if (namedSchema) visitSchema(namedSchema);
1325
+ function createFile(input) {
1326
+ const extname = node_path.default.extname(input.baseName);
1327
+ if (!extname) throw new Error(`No extname found for ${input.baseName}`);
1328
+ const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
1329
+ const resolvedImports = (() => {
1330
+ if (!input.imports?.length) return [];
1331
+ const sourceParts = [];
1332
+ const localNames = /* @__PURE__ */ new Set();
1333
+ for (const item of input.sources ?? []) {
1334
+ const extracted = item.nodes && extractStringsFromNodes(item.nodes);
1335
+ if (extracted) sourceParts.push(extracted);
1336
+ if (item.name) localNames.add(item.name);
1496
1337
  }
1497
- }
1498
- for (const op of operations) for (const schema of collect(op, {
1499
- depth: "shallow",
1500
- schema: (node) => node
1501
- })) visitSchema(schema);
1502
- return result;
1338
+ const source = sourceParts.join("\n") || void 0;
1339
+ return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
1340
+ if (imp.path === input.path) return [];
1341
+ if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
1342
+ const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
1343
+ if (!kept.length) return [];
1344
+ return [kept.length === imp.name.length ? imp : {
1345
+ ...imp,
1346
+ name: kept
1347
+ }];
1348
+ });
1349
+ })();
1350
+ const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
1351
+ return {
1352
+ ...input,
1353
+ kind: "File",
1354
+ id: (0, node_crypto.hash)("sha256", input.path, "hex"),
1355
+ name: node_path.default.basename(input.baseName, extname),
1356
+ extname,
1357
+ imports: resolvedImports,
1358
+ exports: resolvedExports,
1359
+ sources: resolvedSources,
1360
+ meta: input.meta ?? {}
1361
+ };
1503
1362
  }
1363
+ //#endregion
1364
+ //#region src/nodes/input.ts
1504
1365
  /**
1505
- * Finds every schema that takes part in a circular dependency chain in a schema dependency graph
1506
- * that maps each schema name to the names it references directly.
1507
- *
1508
- * Use this when the graph was already collected during another pass (e.g. the adapter's convert
1509
- * walk), so the schema nodes are not swept a second time. `findCircularSchemas` builds the graph
1510
- * from schema nodes and delegates here.
1366
+ * Creates an `InputNode`, defaulting `schemas`/`operations` to empty arrays and `meta` to empty
1367
+ * name lists. Every call allocates fresh defaults so no two inputs share them.
1511
1368
  *
1512
1369
  * @example
1513
1370
  * ```ts
1514
- * const graph = new Map([
1515
- * ['Pet', new Set(['Category'])],
1516
- * ['Category', new Set(['Pet'])],
1517
- * ])
1518
- * findCircularSchemasFromGraph(graph) // Set { 'Pet', 'Category' }
1371
+ * const input = createInput()
1372
+ * // { kind: 'Input', schemas: [], operations: [] }
1519
1373
  * ```
1520
1374
  */
1521
- function findCircularSchemasFromGraph(graph) {
1522
- const circular = /* @__PURE__ */ new Set();
1523
- for (const start of graph.keys()) {
1524
- const visited = /* @__PURE__ */ new Set();
1525
- const stack = [...graph.get(start) ?? []];
1526
- while (stack.length > 0) {
1527
- const node = stack.pop();
1528
- if (node === start) {
1529
- circular.add(start);
1530
- break;
1531
- }
1532
- if (visited.has(node)) continue;
1533
- visited.add(node);
1534
- const next = graph.get(node);
1535
- if (next) for (const r of next) stack.push(r);
1536
- }
1537
- }
1538
- return circular;
1375
+ function createInput(overrides = {}) {
1376
+ return {
1377
+ schemas: [],
1378
+ operations: [],
1379
+ meta: {
1380
+ circularNames: [],
1381
+ enumNames: []
1382
+ },
1383
+ ...overrides,
1384
+ kind: "Input"
1385
+ };
1539
1386
  }
1387
+ //#endregion
1388
+ //#region src/nodes/requestBody.ts
1540
1389
  /**
1541
- * Finds every schema that takes part in a circular dependency chain, including direct self-loops.
1542
- *
1543
- * Wrap the returned schema positions in a deferred construct (a lazy getter or `z.lazy(() => …)`) so
1544
- * the generated code does not recurse forever. Refs are followed by name only, so the walk stays
1545
- * linear in the size of the schema graph.
1546
- *
1547
- * @note Call this once on the full graph, then check individual schemas with `containsCircularRef()`.
1390
+ * Creates a `RequestBodyNode`.
1548
1391
  */
1549
- function findCircularSchemas(schemas) {
1550
- const graph = /* @__PURE__ */ new Map();
1551
- for (const schema of schemas) if (schema.name) graph.set(schema.name, collectSchemaRefs(schema));
1552
- return findCircularSchemasFromGraph(graph);
1392
+ function createRequestBody(input) {
1393
+ return {
1394
+ ...input,
1395
+ kind: "RequestBody"
1396
+ };
1553
1397
  }
1554
1398
  //#endregion
1555
- //#region src/utils/schemaProperties.ts
1399
+ //#region src/nodes/operation.ts
1400
+ function createOperation(props) {
1401
+ const { requestBody, ...rest } = props;
1402
+ const isHttp = rest.method !== void 0 && rest.path !== void 0;
1403
+ return {
1404
+ tags: [],
1405
+ parameters: [],
1406
+ responses: [],
1407
+ ...rest,
1408
+ kind: "Operation",
1409
+ ...isHttp ? { protocol: "http" } : {},
1410
+ requestBody: requestBody ? createRequestBody(requestBody) : void 0
1411
+ };
1412
+ }
1413
+ //#endregion
1414
+ //#region src/nodes/output.ts
1556
1415
  /**
1557
- * Finds every matching property exposed by an object, resolved reference, or intersection.
1558
- *
1559
- * References are followed through their parsed `schema`. Circular references terminate without
1560
- * revisiting a node. Unions are not traversed because a property on one branch does not describe
1561
- * every branch.
1416
+ * Creates an `OutputNode` with a stable default for `files`.
1562
1417
  *
1563
- * @example Faker discriminator narrowing
1418
+ * @example
1564
1419
  * ```ts
1565
- * const values = resolveSchemaProperties({ node: member, propertyName: union.discriminatorPropertyName }).flatMap(({ schema }) =>
1566
- * getSchemaLiteralValues(schema),
1567
- * )
1568
- * const typeName = `Extract<Pet, { type: ${values.map(JSON.stringify).join(' | ')} }>`
1420
+ * const output = createOutput()
1421
+ * // { kind: 'Output', files: [] }
1569
1422
  * ```
1423
+ */
1424
+ function createOutput(overrides = {}) {
1425
+ return {
1426
+ files: [],
1427
+ ...overrides,
1428
+ kind: "Output"
1429
+ };
1430
+ }
1431
+ //#endregion
1432
+ //#region src/nodes/parameter.ts
1433
+ /**
1434
+ * Creates a `ParameterNode`. `required` defaults to `false`, and the schema's
1435
+ * `optional`/`nullish` flags are derived from it through {@link withRequired}.
1570
1436
  *
1571
- * @example Zod discriminated unions
1437
+ * @example
1572
1438
  * ```ts
1573
- * const canDiscriminate = union.members?.every(
1574
- * (member) => resolveSchemaProperties({ node: member, propertyName: 'type' }).length > 0,
1575
- * )
1576
- * const expression = canDiscriminate ? `z.discriminatedUnion('type', members)` : `z.union(members)`
1439
+ * const param = createParameter({
1440
+ * name: 'petId',
1441
+ * in: 'path',
1442
+ * required: true,
1443
+ * schema: createSchema({ type: 'string' }),
1444
+ * })
1577
1445
  * ```
1446
+ */
1447
+ function createParameter(input) {
1448
+ return {
1449
+ ...withRequired(input),
1450
+ kind: "Parameter"
1451
+ };
1452
+ }
1453
+ //#endregion
1454
+ //#region src/nodes/property.ts
1455
+ /**
1456
+ * Creates a `PropertyNode`. `required` defaults to `false`, and the schema's
1457
+ * `optional`/`nullish` flags are derived from it through {@link withRequired}.
1578
1458
  *
1579
- * @example TypeScript property lookup
1459
+ * @example
1580
1460
  * ```ts
1581
- * const properties = resolveSchemaProperties({ node: schema, propertyName: 'status' })
1582
- * const statusSchemas = properties.map((property) => print(property.schema))
1461
+ * const property = createProperty({
1462
+ * name: 'status',
1463
+ * required: true,
1464
+ * schema: createSchema({ type: 'string', nullable: true }),
1465
+ * })
1466
+ * // required=true, no optional/nullish
1583
1467
  * ```
1584
1468
  */
1585
- function resolveSchemaProperties({ node, propertyName }) {
1586
- const properties = [];
1587
- const visited = /* @__PURE__ */ new WeakSet();
1588
- function visit(current) {
1589
- if (!current || visited.has(current)) return;
1590
- visited.add(current);
1591
- if (current.type === "object") {
1592
- const property = current.properties.find((candidate) => candidate.name === propertyName);
1593
- if (property) properties.push(property);
1594
- return;
1595
- }
1596
- if (current.type === "ref") {
1597
- visit(current.schema);
1598
- return;
1599
- }
1600
- if (current.type === "intersection") for (const member of current.members ?? []) visit(member);
1601
- }
1602
- visit(node);
1603
- return properties;
1469
+ function createProperty(input) {
1470
+ return {
1471
+ ...withRequired(input),
1472
+ kind: "Property"
1473
+ };
1604
1474
  }
1475
+ //#endregion
1476
+ //#region src/nodes/response.ts
1605
1477
  /**
1606
- * Reads the unique literal values represented by an enum, resolved reference, or union.
1607
- *
1608
- * Named enum values take precedence over simple enum values, matching how renderers consume
1609
- * `EnumSchemaNode`.
1478
+ * Creates a `ResponseNode`. A single legacy `schema` (with optional `mediaType`/`keysToOmit`)
1479
+ * is normalized into one `content` entry.
1610
1480
  *
1611
1481
  * @example
1612
1482
  * ```ts
1613
- * const values = getSchemaLiteralValues(enumNode)
1614
- * // ['cat', 'dog']
1483
+ * const response = createResponse({
1484
+ * statusCode: '200',
1485
+ * content: [createContent({ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) })],
1486
+ * })
1615
1487
  * ```
1616
1488
  */
1617
- function getSchemaLiteralValues(node) {
1618
- const values = /* @__PURE__ */ new Set();
1619
- const visited = /* @__PURE__ */ new WeakSet();
1620
- function visit(current) {
1621
- if (!current || visited.has(current)) return;
1622
- visited.add(current);
1623
- if (current.type === "enum") {
1624
- const enumValues = current.namedEnumValues?.map(({ value }) => value) ?? current.enumValues ?? [];
1625
- for (const value of enumValues) values.add(value);
1626
- return;
1627
- }
1628
- if (current.type === "ref") {
1629
- visit(current.schema);
1630
- return;
1631
- }
1632
- if (current.type === "union") for (const member of current.members ?? []) visit(member);
1633
- }
1634
- visit(node);
1635
- return [...values];
1636
- }
1637
- function collectPropertyNames(node, visited = /* @__PURE__ */ new WeakSet()) {
1638
- if (visited.has(node)) return /* @__PURE__ */ new Set();
1639
- visited.add(node);
1640
- if (node.type === "object") return new Set(node.properties.map((property) => property.name));
1641
- if (node.type === "ref") return node.schema ? collectPropertyNames(node.schema, visited) : /* @__PURE__ */ new Set();
1642
- if (node.type === "intersection") {
1643
- const names = /* @__PURE__ */ new Set();
1644
- for (const member of node.members ?? []) for (const name of collectPropertyNames(member, visited)) names.add(name);
1645
- return names;
1646
- }
1647
- return /* @__PURE__ */ new Set();
1489
+ function createResponse(props) {
1490
+ const { schema, mediaType, keysToOmit, content, ...rest } = props;
1491
+ const entries = content ?? (schema ? [createContent({
1492
+ contentType: mediaType ?? "application/json",
1493
+ schema,
1494
+ keysToOmit: keysToOmit ?? null
1495
+ })] : void 0);
1496
+ return {
1497
+ ...rest,
1498
+ content: entries,
1499
+ kind: "Response"
1500
+ };
1648
1501
  }
1502
+ //#endregion
1503
+ //#region src/nodes/schema.ts
1649
1504
  /**
1650
- * Infers an implicit discriminator: a property every member exposes with a distinct single
1651
- * literal value, the same shape a declared OpenAPI `discriminator` already narrows on.
1505
+ * Maps schema `type` to its underlying `primitive`.
1506
+ * Primitive types map to themselves and special string formats map to `'string'`.
1507
+ * Any type not listed here (such as `ref`, `enum`, `union`, `intersection`, `tuple`, `ipv4`, `ipv6`, `blob`) has no `primitive`.
1652
1508
  */
1653
- function inferDiscriminatorPropertyName(members) {
1654
- if (members.length < 2) return void 0;
1655
- let candidates;
1656
- for (const member of members) {
1657
- const names = collectPropertyNames(member);
1658
- candidates = candidates ? new Set([...candidates].filter((name) => names.has(name))) : names;
1659
- }
1660
- for (const name of candidates ?? []) {
1661
- const values = members.map((member) => {
1662
- const literals = new Set(resolveSchemaProperties({
1663
- node: member,
1664
- propertyName: name
1665
- }).flatMap((property) => getSchemaLiteralValues(property.schema)));
1666
- return literals.size === 1 ? [...literals][0] : void 0;
1667
- });
1668
- if (values.every((value) => value !== void 0) && new Set(values).size === members.length) return name;
1669
- }
1509
+ const TYPE_TO_PRIMITIVE = {
1510
+ string: "string",
1511
+ number: "number",
1512
+ integer: "integer",
1513
+ bigint: "bigint",
1514
+ boolean: "boolean",
1515
+ null: "null",
1516
+ any: "any",
1517
+ unknown: "unknown",
1518
+ void: "void",
1519
+ never: "never",
1520
+ object: "object",
1521
+ array: "array",
1522
+ date: "date",
1523
+ uuid: "string",
1524
+ email: "string",
1525
+ url: "string",
1526
+ datetime: "string",
1527
+ time: "string"
1528
+ };
1529
+ function createSchema(props) {
1530
+ if (props.type === "object") return {
1531
+ properties: [],
1532
+ primitive: "object",
1533
+ ...props,
1534
+ kind: "Schema"
1535
+ };
1536
+ return {
1537
+ primitive: TYPE_TO_PRIMITIVE[props.type],
1538
+ ...props,
1539
+ kind: "Schema"
1540
+ };
1670
1541
  }
1671
1542
  //#endregion
1672
1543
  //#region src/factory.ts
@@ -1717,77 +1588,50 @@ function update(node, changes) {
1717
1588
  return node;
1718
1589
  }
1719
1590
  //#endregion
1720
- //#region src/exports.ts
1721
- var exports_exports = /* @__PURE__ */ __exportAll({
1591
+ //#region src/index.ts
1592
+ var src_exports = /* @__PURE__ */ __exportAll({
1722
1593
  applyMacros: () => applyMacros,
1723
- arrowFunctionDef: () => arrowFunctionDef,
1724
- breakDef: () => breakDef,
1594
+ ast: () => src_exports,
1725
1595
  collect: () => collect,
1726
1596
  collectImportedRefNames: () => collectImportedRefNames,
1727
1597
  collectSchemaRefs: () => collectSchemaRefs,
1728
1598
  collectSync: () => collectSync,
1729
1599
  collectUsedSchemaNames: () => collectUsedSchemaNames,
1730
1600
  composeMacros: () => composeMacros,
1731
- constDef: () => constDef,
1732
- contentDef: () => contentDef,
1733
1601
  createPrinter: () => createPrinter,
1734
1602
  defineMacro: () => defineMacro,
1735
- defineNode: () => defineNode,
1736
- exportDef: () => exportDef,
1737
1603
  extractStringsFromNodes: () => extractStringsFromNodes,
1738
1604
  factory: () => factory_exports,
1739
- fileDef: () => fileDef,
1740
1605
  findCircularSchemas: () => findCircularSchemas,
1741
1606
  findCircularSchemasFromGraph: () => findCircularSchemasFromGraph,
1742
- functionDef: () => functionDef,
1743
1607
  getSchemaLiteralValues: () => getSchemaLiteralValues,
1744
- importDef: () => importDef,
1745
1608
  inferDiscriminatorPropertyName: () => inferDiscriminatorPropertyName,
1746
- inputDef: () => inputDef,
1747
1609
  isBareRef: () => isBareRef,
1748
1610
  isHttpOperationNode: () => isHttpOperationNode,
1749
- jsxDef: () => jsxDef,
1750
1611
  mergeRefWithSchema: () => mergeRefWithSchema,
1751
1612
  narrowSchema: () => narrowSchema,
1752
- nodeDefs: () => nodeDefs,
1753
- operationDef: () => operationDef,
1754
1613
  optionality: () => optionality,
1755
- outputDef: () => outputDef,
1756
- parameterDef: () => parameterDef,
1757
- propertyDef: () => propertyDef,
1758
- requestBodyDef: () => requestBodyDef,
1759
1614
  resolveRefName: () => resolveRefName,
1760
1615
  resolveSchemaProperties: () => resolveSchemaProperties,
1761
- responseDef: () => responseDef,
1762
- schemaDef: () => schemaDef,
1763
1616
  schemaTypes: () => schemaTypes,
1764
- sourceDef: () => sourceDef,
1765
- textDef: () => textDef,
1766
- transform: () => transform,
1767
- typeDef: () => typeDef
1617
+ transform: () => transform
1768
1618
  });
1769
1619
  //#endregion
1770
1620
  exports.applyMacros = applyMacros;
1771
- exports.arrowFunctionDef = arrowFunctionDef;
1772
1621
  Object.defineProperty(exports, "ast", {
1773
1622
  enumerable: true,
1774
1623
  get: function() {
1775
- return exports_exports;
1624
+ return src_exports;
1776
1625
  }
1777
1626
  });
1778
- exports.breakDef = breakDef;
1779
1627
  exports.collect = collect;
1780
1628
  exports.collectImportedRefNames = collectImportedRefNames;
1781
1629
  exports.collectSchemaRefs = collectSchemaRefs;
1782
1630
  exports.collectSync = collectSync;
1783
1631
  exports.collectUsedSchemaNames = collectUsedSchemaNames;
1784
1632
  exports.composeMacros = composeMacros;
1785
- exports.constDef = constDef;
1786
- exports.contentDef = contentDef;
1787
1633
  exports.createPrinter = createPrinter;
1788
1634
  exports.defineMacro = defineMacro;
1789
- exports.defineNode = defineNode;
1790
- exports.exportDef = exportDef;
1791
1635
  exports.extractStringsFromNodes = extractStringsFromNodes;
1792
1636
  Object.defineProperty(exports, "factory", {
1793
1637
  enumerable: true,
@@ -1795,34 +1639,18 @@ Object.defineProperty(exports, "factory", {
1795
1639
  return factory_exports;
1796
1640
  }
1797
1641
  });
1798
- exports.fileDef = fileDef;
1799
1642
  exports.findCircularSchemas = findCircularSchemas;
1800
1643
  exports.findCircularSchemasFromGraph = findCircularSchemasFromGraph;
1801
- exports.functionDef = functionDef;
1802
1644
  exports.getSchemaLiteralValues = getSchemaLiteralValues;
1803
- exports.importDef = importDef;
1804
1645
  exports.inferDiscriminatorPropertyName = inferDiscriminatorPropertyName;
1805
- exports.inputDef = inputDef;
1806
1646
  exports.isBareRef = isBareRef;
1807
1647
  exports.isHttpOperationNode = isHttpOperationNode;
1808
- exports.jsxDef = jsxDef;
1809
1648
  exports.mergeRefWithSchema = mergeRefWithSchema;
1810
1649
  exports.narrowSchema = narrowSchema;
1811
- exports.nodeDefs = nodeDefs;
1812
- exports.operationDef = operationDef;
1813
1650
  exports.optionality = optionality;
1814
- exports.outputDef = outputDef;
1815
- exports.parameterDef = parameterDef;
1816
- exports.propertyDef = propertyDef;
1817
- exports.requestBodyDef = requestBodyDef;
1818
1651
  exports.resolveRefName = resolveRefName;
1819
1652
  exports.resolveSchemaProperties = resolveSchemaProperties;
1820
- exports.responseDef = responseDef;
1821
- exports.schemaDef = schemaDef;
1822
1653
  exports.schemaTypes = schemaTypes;
1823
- exports.sourceDef = sourceDef;
1824
- exports.textDef = textDef;
1825
1654
  exports.transform = transform;
1826
- exports.typeDef = typeDef;
1827
1655
 
1828
1656
  //# sourceMappingURL=index.cjs.map