@kubb/ast 5.5.6 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -363,63 +363,6 @@ const contentDef = defineNode({
363
363
  */
364
364
  const createContent = contentDef.create;
365
365
  //#endregion
366
- //#region ../../internals/utils/src/fs.ts
367
- /**
368
- * Strips the file extension from a path or file name.
369
- * Only removes the last `.ext` segment when the dot is not part of a directory name.
370
- *
371
- * @example
372
- * trimExtName('petStore.ts') // 'petStore'
373
- * trimExtName('/src/models/pet.ts') // '/src/models/pet'
374
- * trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'
375
- * trimExtName('noExtension') // 'noExtension'
376
- */
377
- function trimExtName(text) {
378
- const dotIndex = text.lastIndexOf(".");
379
- if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
380
- return text;
381
- }
382
- //#endregion
383
- //#region ../../internals/utils/src/promise.ts
384
- /**
385
- * Wraps `factory` with a keyed cache backed by the provided store.
386
- *
387
- * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
388
- * collected) or a `Map` for primitive keys. For multi-argument functions,
389
- * nest two `memoize` calls — the outer keyed by the first argument, the
390
- * inner (created once per outer miss) keyed by the second.
391
- *
392
- * Because the cache is owned by the caller, it can be shared, inspected, or
393
- * cleared independently of the memoized function.
394
- *
395
- * @example Single WeakMap key
396
- * ```ts
397
- * const cache = new WeakMap<SchemaNode, Set<string>>()
398
- * const getRefs = memoize(cache, (node) => collectRefs(node))
399
- * ```
400
- *
401
- * @example Single Map key (primitive)
402
- * ```ts
403
- * const cache = new Map<string, Resolver>()
404
- * const getResolver = memoize(cache, (name) => buildResolver(name))
405
- * ```
406
- *
407
- * @example Two-level (object + primitive)
408
- * ```ts
409
- * const outer = new WeakMap<Params[], Map<string, Params[]>>()
410
- * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
411
- * fn(params)('camelcase')
412
- * ```
413
- */
414
- function memoize(store, factory) {
415
- return (key) => {
416
- if (store.has(key)) return store.get(key);
417
- const value = factory(key);
418
- store.set(key, value);
419
- return value;
420
- };
421
- }
422
- //#endregion
423
366
  //#region src/utils/extractStringsFromNodes.ts
424
367
  /**
425
368
  * Extracts all string content from a `CodeNode` tree recursively.
@@ -435,15 +378,11 @@ function extractStringsFromNodes(nodes) {
435
378
  if (node) collected.push(node);
436
379
  continue;
437
380
  }
438
- if (node.kind === "Text") {
381
+ if (node.kind === "Text" || node.kind === "Jsx") {
439
382
  if (node.value) collected.push(node.value);
440
383
  continue;
441
384
  }
442
385
  if (node.kind === "Break") continue;
443
- if (node.kind === "Jsx") {
444
- if (node.value) collected.push(node.value);
445
- continue;
446
- }
447
386
  const parts = [];
448
387
  if ("params" in node && node.params) parts.push(node.params);
449
388
  if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
@@ -463,22 +402,11 @@ const IDENTIFIER_RUN = /[\w$]+/g;
463
402
  */
464
403
  const INDEX_ABOVE_IMPORTS = 128;
465
404
  /**
466
- * Every unbroken run of identifier characters in the source.
405
+ * Returns the local binding an import name item introduces: the alias when there is one, else the
406
+ * imported name.
467
407
  */
468
- function collectIdentifiers(source) {
469
- return new Set(source.match(IDENTIFIER_RUN));
470
- }
471
- function sourceKey(source) {
472
- return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
473
- }
474
- function pathTypeKey(path, isTypeOnly) {
475
- return `${path}:${isTypeOnly ?? false}`;
476
- }
477
- function exportKey(path, name, isTypeOnly, asAlias) {
478
- return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
479
- }
480
- function importKey(path, name, isTypeOnly) {
481
- return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
408
+ function importLocalName(item) {
409
+ return typeof item === "string" ? item : item.name ?? item.propertyName;
482
410
  }
483
411
  /**
484
412
  * Computes a multi-level sort key for exports and imports:
@@ -492,6 +420,36 @@ function sortKey(node) {
492
420
  return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
493
421
  }
494
422
  /**
423
+ * Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
424
+ */
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);
430
+ }
431
+ /**
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.
434
+ */
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
+ };
451
+ }
452
+ /**
495
453
  * Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
496
454
  * key. Unnamed sources fall back to their extracted node strings as the name part of the key. Returns
497
455
  * the deduplicated array in original order.
@@ -499,22 +457,12 @@ function sortKey(node) {
499
457
  function combineSources(sources) {
500
458
  const seen = /* @__PURE__ */ new Map();
501
459
  for (const source of sources) {
502
- const key = sourceKey(source);
460
+ const key = `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
503
461
  if (!seen.has(key)) seen.set(key, source);
504
462
  }
505
463
  return [...seen.values()];
506
464
  }
507
465
  /**
508
- * Merges `incoming` names into `existing`, preserving order and dropping duplicates.
509
- *
510
- * Shared by `combineExports` and `combineImports` for the same-path name-merge case.
511
- */
512
- function mergeNameArrays(existing, incoming) {
513
- const merged = new Set(existing);
514
- for (const name of incoming) merged.add(name);
515
- return [...merged];
516
- }
517
- /**
518
466
  * Deduplicates and merges `ExportNode` objects by path and type.
519
467
  *
520
468
  * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
@@ -522,34 +470,21 @@ function mergeNameArrays(existing, incoming) {
522
470
  */
523
471
  function combineExports(exports) {
524
472
  const result = [];
525
- const namedByPath = /* @__PURE__ */ new Map();
473
+ const merge = mergeByPath(result);
526
474
  const seen = /* @__PURE__ */ new Set();
527
- const keyed = exports.map((node) => ({
528
- node,
529
- key: sortKey(node)
530
- }));
531
- keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
532
- for (const { node: curr } of keyed) {
475
+ for (const curr of sortMembers(exports)) {
533
476
  const { name, path, isTypeOnly, asAlias } = curr;
534
477
  if (Array.isArray(name)) {
535
- if (!name.length) continue;
536
- const key = pathTypeKey(path, isTypeOnly);
537
- const existing = namedByPath.get(key);
538
- if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
539
- else {
540
- const newItem = {
541
- ...curr,
542
- name: [...new Set(name)]
543
- };
544
- result.push(newItem);
545
- namedByPath.set(key, newItem);
546
- }
547
- } else {
548
- const key = exportKey(path, name, isTypeOnly, asAlias);
549
- if (!seen.has(key)) {
550
- result.push(curr);
551
- seen.add(key);
552
- }
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);
553
488
  }
554
489
  }
555
490
  return result;
@@ -562,7 +497,7 @@ function combineExports(exports) {
562
497
  */
563
498
  function combineImports(imports, exports, source) {
564
499
  const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
565
- const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? collectIdentifiers(source) : null;
500
+ const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
566
501
  const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
567
502
  const importNameMemo = /* @__PURE__ */ new Map();
568
503
  const canonicalizeName = (n) => {
@@ -572,43 +507,26 @@ function combineImports(imports, exports, source) {
572
507
  return importNameMemo.get(key);
573
508
  };
574
509
  const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
575
- for (const node of imports) {
576
- if (!Array.isArray(node.name)) continue;
577
- if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
578
- }
510
+ for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
579
511
  const result = [];
580
- const namedByPath = /* @__PURE__ */ new Map();
512
+ const merge = mergeByPath(result);
581
513
  const seen = /* @__PURE__ */ new Set();
582
- const keyed = imports.map((node) => ({
583
- node,
584
- key: sortKey(node)
585
- }));
586
- keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
587
- for (const { node: curr } of keyed) {
514
+ for (const curr of sortMembers(imports)) {
588
515
  if (curr.path === curr.root) continue;
589
- const { path, isTypeOnly } = curr;
590
- let { name } = curr;
516
+ const { name, path, isTypeOnly } = curr;
591
517
  if (Array.isArray(name)) {
592
- name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
593
- if (!name.length) continue;
594
- const key = pathTypeKey(path, isTypeOnly);
595
- const existing = namedByPath.get(key);
596
- if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
597
- else {
598
- const newItem = {
599
- ...curr,
600
- name
601
- };
602
- result.push(newItem);
603
- namedByPath.set(key, newItem);
604
- }
605
- } else {
606
- if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
607
- const key = importKey(path, name, isTypeOnly);
608
- if (!seen.has(key)) {
609
- result.push(curr);
610
- seen.add(key);
611
- }
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);
612
530
  }
613
531
  }
614
532
  return result;
@@ -713,12 +631,10 @@ function createFile(input) {
713
631
  if (item.name) localNames.add(item.name);
714
632
  }
715
633
  const source = sourceParts.join("\n") || void 0;
716
- const combinedImports = combineImports(input.imports, resolvedExports, source);
717
- const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
718
- return combinedImports.flatMap((imp) => {
634
+ return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
719
635
  if (imp.path === input.path) return [];
720
636
  if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
721
- const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
637
+ const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
722
638
  if (!kept.length) return [];
723
639
  return [kept.length === imp.name.length ? imp : {
724
640
  ...imp,
@@ -731,7 +647,7 @@ function createFile(input) {
731
647
  kind: "File",
732
648
  ...input,
733
649
  id: (0, node_crypto.hash)("sha256", input.path, "hex"),
734
- name: trimExtName(input.baseName),
650
+ name: node_path.default.basename(input.baseName, extname),
735
651
  extname,
736
652
  imports: resolvedImports,
737
653
  exports: resolvedExports,
@@ -746,14 +662,15 @@ function createFile(input) {
746
662
  */
747
663
  const inputDef = defineNode({
748
664
  kind: "Input",
749
- defaults: {
665
+ build: (props) => ({
750
666
  schemas: [],
751
667
  operations: [],
752
668
  meta: {
753
669
  circularNames: [],
754
670
  enumNames: []
755
- }
756
- },
671
+ },
672
+ ...props
673
+ }),
757
674
  children: ["schemas", "operations"],
758
675
  visitorKey: "input"
759
676
  });
@@ -851,22 +768,27 @@ function optionality(schema, required) {
851
768
  nullish: !required && nullable ? true : void 0
852
769
  };
853
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
+ }
854
783
  //#endregion
855
784
  //#region src/nodes/parameter.ts
856
785
  /**
857
786
  * Definition for the {@link ParameterNode}. `required` defaults to `false`, and the schema's
858
- * `optional`/`nullish` flags are derived from it through {@link optionality}.
787
+ * `optional`/`nullish` flags are derived from it through {@link withRequired}.
859
788
  */
860
789
  const parameterDef = defineNode({
861
790
  kind: "Parameter",
862
- build: (props) => {
863
- const required = props.required ?? false;
864
- return {
865
- ...props,
866
- required,
867
- schema: optionality(props.schema, required)
868
- };
869
- },
791
+ build: withRequired,
870
792
  children: ["schema"],
871
793
  visitorKey: "parameter"
872
794
  });
@@ -888,18 +810,11 @@ const createParameter = parameterDef.create;
888
810
  //#region src/nodes/property.ts
889
811
  /**
890
812
  * Definition for the {@link PropertyNode}. `required` defaults to `false`, and the schema's
891
- * `optional`/`nullish` flags are derived from it through {@link optionality}.
813
+ * `optional`/`nullish` flags are derived from it through {@link withRequired}.
892
814
  */
893
815
  const propertyDef = defineNode({
894
816
  kind: "Property",
895
- build: (props) => {
896
- const required = props.required ?? false;
897
- return {
898
- ...props,
899
- required,
900
- schema: optionality(props.schema, required)
901
- };
902
- },
817
+ build: withRequired,
903
818
  children: ["schema"],
904
819
  visitorKey: "property"
905
820
  });
@@ -1000,7 +915,9 @@ const schemaDef = defineNode({
1000
915
  "properties",
1001
916
  "items",
1002
917
  "members",
1003
- "additionalProperties"
918
+ "additionalProperties",
919
+ "patternProperties",
920
+ "rest"
1004
921
  ],
1005
922
  visitorKey: "schema"
1006
923
  });
@@ -1055,9 +972,21 @@ function isNode(value) {
1055
972
  return typeof value === "object" && value !== null && typeof value.kind === "string";
1056
973
  }
1057
974
  /**
1058
- * Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
1059
- *
1060
- * `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
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];
987
+ }
988
+ /**
989
+ * Returns the immediate traversable children of `node`.
1061
990
  *
1062
991
  * @example
1063
992
  * ```ts
@@ -1066,8 +995,7 @@ function isNode(value) {
1066
995
  * ```
1067
996
  */
1068
997
  function* getChildren(node, recurse) {
1069
- if (node.kind === "Schema" && !recurse) return;
1070
- const keys = visitorKeysByKind[node.kind];
998
+ const keys = childKeys(node, recurse);
1071
999
  if (!keys) return;
1072
1000
  const record = node;
1073
1001
  for (const key of keys) {
@@ -1076,7 +1004,13 @@ function* getChildren(node, recurse) {
1076
1004
  for (const item of value) if (isNode(item)) yield item;
1077
1005
  continue;
1078
1006
  }
1079
- if (isNode(value)) yield value;
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;
1013
+ }
1080
1014
  }
1081
1015
  }
1082
1016
  /**
@@ -1094,6 +1028,30 @@ function applyVisitor(node, visitor, parent) {
1094
1028
  const fn = visitor[key];
1095
1029
  return fn?.(node, { parent });
1096
1030
  }
1031
+ /**
1032
+ * Synchronous depth-first transform. Each visitor callback can return a
1033
+ * replacement node. Returning `undefined` keeps the original.
1034
+ *
1035
+ * The original tree is never mutated, a new tree is returned. Pass
1036
+ * `depth: 'shallow'` to skip recursion into children.
1037
+ *
1038
+ * @example Prefix every operationId
1039
+ * ```ts
1040
+ * const next = transform(root, {
1041
+ * operation(node) {
1042
+ * return { ...node, operationId: `prefixed_${node.operationId}` }
1043
+ * },
1044
+ * })
1045
+ * ```
1046
+ *
1047
+ * @example Replace only the root node
1048
+ * ```ts
1049
+ * const next = transform(root, {
1050
+ * depth: 'shallow',
1051
+ * input: (node) => ({ ...node, meta: { ...node.meta, title: 'Rewritten' } }),
1052
+ * })
1053
+ * ```
1054
+ */
1097
1055
  function transform(node, options) {
1098
1056
  const { depth, parent, ...visitor } = options;
1099
1057
  return transformNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
@@ -1107,13 +1065,11 @@ function transformNode(node, visitor, recurse, parent) {
1107
1065
  return transformChildren(applyVisitor(node, visitor, parent) ?? node, visitor, recurse);
1108
1066
  }
1109
1067
  /**
1110
- * Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
1111
- * each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
1112
- * `Schema` children are skipped in shallow mode.
1068
+ * Immutably rebuilds a node's children, transforming each child node and leaving non-node values
1069
+ * (e.g. `additionalProperties: true`) intact.
1113
1070
  */
1114
1071
  function transformChildren(node, visitor, recurse) {
1115
- if (node.kind === "Schema" && !recurse) return node;
1116
- const keys = visitorKeysByKind[node.kind];
1072
+ const keys = childKeys(node, recurse);
1117
1073
  if (!keys) return node;
1118
1074
  const record = node;
1119
1075
  let updates;
@@ -1136,6 +1092,15 @@ function transformChildren(node, visitor, recurse) {
1136
1092
  if (isNode(value)) {
1137
1093
  const next = transformNode(value, visitor, recurse, node);
1138
1094
  if (next !== value) (updates ??= {})[key] = next;
1095
+ continue;
1096
+ }
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;
1139
1104
  }
1140
1105
  }
1141
1106
  if (!updates) return node;
@@ -1329,21 +1294,18 @@ function createPrinter(build) {
1329
1294
  ...overrides
1330
1295
  } : nodes;
1331
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
+ };
1332
1302
  const context = {
1333
1303
  options: resolvedOptions,
1334
1304
  import: (node) => {
1335
1305
  collectedImports.push(node);
1336
1306
  },
1337
- transform: (node) => {
1338
- const handler = merged[node.type];
1339
- if (!handler) return null;
1340
- return handler.call(context, node);
1341
- },
1342
- base: (node) => {
1343
- const handler = nodes[node.type];
1344
- if (!handler) return null;
1345
- return handler.call(context, node);
1346
- }
1307
+ transform: dispatch(merged),
1308
+ base: dispatch(nodes)
1347
1309
  };
1348
1310
  return {
1349
1311
  name,
@@ -1376,6 +1338,29 @@ function resolveRefName(node) {
1376
1338
  return node.name ?? node.schema?.name ?? null;
1377
1339
  }
1378
1340
  /**
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.
1347
+ *
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
+ * ```
1353
+ */
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
+ };
1362
+ }
1363
+ /**
1379
1364
  * Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
1380
1365
  *
1381
1366
  * Modifiers inherited from the resolved schema are included in the check because a ref emits the
@@ -1383,21 +1368,62 @@ function resolveRefName(node) {
1383
1368
  */
1384
1369
  function isBareRef(node, keysToOmit) {
1385
1370
  if (!node || node.type !== "ref") return false;
1386
- const meta = (() => {
1387
- if (!node.schema) return node;
1388
- const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = node;
1389
- const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0));
1390
- return {
1391
- ...node.schema,
1392
- ...definedOverrides
1393
- };
1394
- })();
1371
+ const meta = mergeRefWithSchema(node);
1395
1372
  return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
1396
1373
  }
1397
1374
  //#endregion
1375
+ //#region ../../internals/utils/src/promise.ts
1376
+ /**
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
+ * ```
1398
+ *
1399
+ * @example Two-level (object + primitive)
1400
+ * ```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')
1404
+ * ```
1405
+ */
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;
1412
+ };
1413
+ }
1414
+ //#endregion
1398
1415
  //#region src/utils/schemaGraph.ts
1399
1416
  /**
1400
- * Memoized inner pass that walks a single node and returns the names of every schema it references.
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.
1421
+ *
1422
+ * @example
1423
+ * ```ts
1424
+ * collectSchemaRefs(petSchema)
1425
+ * // Set { 'Category', 'Tag' }
1426
+ * ```
1401
1427
  */
1402
1428
  const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1403
1429
  const refs = /* @__PURE__ */ new Set();
@@ -1410,31 +1436,6 @@ const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1410
1436
  return refs;
1411
1437
  });
1412
1438
  /**
1413
- * Collects the names of every ref found anywhere inside a node's own subtree.
1414
- *
1415
- * Each ref contributes its name only, so the schema it points to is never traversed here. Pass `out`
1416
- * to accumulate names from several nodes into one set.
1417
- *
1418
- * @example Collect refs from a single schema
1419
- * ```ts
1420
- * const names = collectReferencedSchemaNames(petSchema)
1421
- * // Set { 'Category', 'Tag' }
1422
- * ```
1423
- *
1424
- * @example Accumulate refs from multiple schemas into one set
1425
- * ```ts
1426
- * const out = new Set<string>()
1427
- * for (const schema of schemas) {
1428
- * collectReferencedSchemaNames(schema, out)
1429
- * }
1430
- * ```
1431
- */
1432
- function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
1433
- if (!node) return out;
1434
- for (const name of collectSchemaRefs(node)) out.add(name);
1435
- return out;
1436
- }
1437
- /**
1438
1439
  * Collects the de-duplicated target names of every pointer-carrying ref in a node's subtree, in
1439
1440
  * first-occurrence order. The walk is memoized by node identity, so the subtree is scanned once and
1440
1441
  * `resolver.imports` reads the same result across the ts, zod, and faker plugins instead of
@@ -1442,8 +1443,7 @@ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
1442
1443
  *
1443
1444
  * Only refs that carry a `$ref` pointer count, so a synthesized ref pointing at a sibling in the
1444
1445
  * same file (a union member created by name) is left out. That leaves exactly the set
1445
- * `resolver.imports` emits. This is the ordered, import-facing counterpart to
1446
- * {@link collectReferencedSchemaNames}, which returns an unordered set for graph analysis.
1446
+ * `resolver.imports` emits.
1447
1447
  *
1448
1448
  * @example
1449
1449
  * ```ts
@@ -1464,24 +1464,6 @@ const collectImportedRefNames = memoize(/* @__PURE__ */ new WeakMap(), (node) =>
1464
1464
  } });
1465
1465
  return names;
1466
1466
  });
1467
- function computeUsedSchemaNames(operations, schemas) {
1468
- const schemaMap = /* @__PURE__ */ new Map();
1469
- for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
1470
- const result = /* @__PURE__ */ new Set();
1471
- function visitSchema(schema) {
1472
- const directRefs = collectReferencedSchemaNames(schema);
1473
- for (const name of directRefs) if (!result.has(name)) {
1474
- result.add(name);
1475
- const namedSchema = schemaMap.get(name);
1476
- if (namedSchema) visitSchema(namedSchema);
1477
- }
1478
- }
1479
- for (const op of operations) for (const schema of collect(op, {
1480
- depth: "shallow",
1481
- schema: (node) => node
1482
- })) visitSchema(schema);
1483
- return result;
1484
- }
1485
1467
  /**
1486
1468
  * Collects the names of all top-level schemas transitively used by a set of operations.
1487
1469
  *
@@ -1503,9 +1485,22 @@ function computeUsedSchemaNames(operations, schemas) {
1503
1485
  * ```
1504
1486
  */
1505
1487
  function collectUsedSchemaNames(operations, schemas) {
1506
- return computeUsedSchemaNames(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);
1496
+ }
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;
1507
1503
  }
1508
- const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
1509
1504
  /**
1510
1505
  * Finds every schema that takes part in a circular dependency chain in a schema dependency graph
1511
1506
  * that maps each schema name to the names it references directly.
@@ -1542,14 +1537,6 @@ function findCircularSchemasFromGraph(graph) {
1542
1537
  }
1543
1538
  return circular;
1544
1539
  }
1545
- const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
1546
- const graph = /* @__PURE__ */ new Map();
1547
- for (const schema of schemas) {
1548
- if (!schema.name) continue;
1549
- graph.set(schema.name, collectReferencedSchemaNames(schema));
1550
- }
1551
- return findCircularSchemasFromGraph(graph);
1552
- });
1553
1540
  /**
1554
1541
  * Finds every schema that takes part in a circular dependency chain, including direct self-loops.
1555
1542
  *
@@ -1560,8 +1547,9 @@ const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas)
1560
1547
  * @note Call this once on the full graph, then check individual schemas with `containsCircularRef()`.
1561
1548
  */
1562
1549
  function findCircularSchemas(schemas) {
1563
- if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
1564
- return findCircularSchemasMemo(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);
1565
1553
  }
1566
1554
  //#endregion
1567
1555
  //#region src/utils/schemaProperties.ts
@@ -1736,6 +1724,7 @@ var exports_exports = /* @__PURE__ */ __exportAll({
1736
1724
  breakDef: () => breakDef,
1737
1725
  collect: () => collect,
1738
1726
  collectImportedRefNames: () => collectImportedRefNames,
1727
+ collectSchemaRefs: () => collectSchemaRefs,
1739
1728
  collectSync: () => collectSync,
1740
1729
  collectUsedSchemaNames: () => collectUsedSchemaNames,
1741
1730
  composeMacros: () => composeMacros,
@@ -1758,6 +1747,7 @@ var exports_exports = /* @__PURE__ */ __exportAll({
1758
1747
  isBareRef: () => isBareRef,
1759
1748
  isHttpOperationNode: () => isHttpOperationNode,
1760
1749
  jsxDef: () => jsxDef,
1750
+ mergeRefWithSchema: () => mergeRefWithSchema,
1761
1751
  narrowSchema: () => narrowSchema,
1762
1752
  nodeDefs: () => nodeDefs,
1763
1753
  operationDef: () => operationDef,
@@ -1788,6 +1778,7 @@ Object.defineProperty(exports, "ast", {
1788
1778
  exports.breakDef = breakDef;
1789
1779
  exports.collect = collect;
1790
1780
  exports.collectImportedRefNames = collectImportedRefNames;
1781
+ exports.collectSchemaRefs = collectSchemaRefs;
1791
1782
  exports.collectSync = collectSync;
1792
1783
  exports.collectUsedSchemaNames = collectUsedSchemaNames;
1793
1784
  exports.composeMacros = composeMacros;
@@ -1815,6 +1806,7 @@ exports.inputDef = inputDef;
1815
1806
  exports.isBareRef = isBareRef;
1816
1807
  exports.isHttpOperationNode = isHttpOperationNode;
1817
1808
  exports.jsxDef = jsxDef;
1809
+ exports.mergeRefWithSchema = mergeRefWithSchema;
1818
1810
  exports.narrowSchema = narrowSchema;
1819
1811
  exports.nodeDefs = nodeDefs;
1820
1812
  exports.operationDef = operationDef;