@kubb/ast 5.0.0-beta.2 → 5.0.0-beta.21

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/src/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { camelCase, isValidVarName } from '@internals/utils'
1
+ import { camelCase, isValidVarName, memoize } from '@internals/utils'
2
2
 
3
3
  import { createFunctionParameter, createFunctionParameters, createParameterGroup, createParamsType, createProperty, createSchema } from './factory.ts'
4
4
  import { narrowSchema } from './guards.ts'
@@ -17,7 +17,7 @@ import type {
17
17
  } from './nodes/index.ts'
18
18
  import type { SchemaType } from './nodes/schema.ts'
19
19
  import { extractRefName } from './refs.ts'
20
- import { collect } from './visitor.ts'
20
+ import { collect, collectLazy } from './visitor.ts'
21
21
 
22
22
  const plainStringTypes = new Set<SchemaType>(['string', 'uuid', 'email', 'url', 'datetime'] as const)
23
23
 
@@ -74,16 +74,18 @@ export function isStringType(node: SchemaNode): boolean {
74
74
  * the desired casing while preserving `OperationNode.parameters` for other consumers.
75
75
  * The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
76
76
  */
77
- export function caseParams(params: Array<ParameterNode>, casing: 'camelcase' | undefined): Array<ParameterNode> {
78
- if (!casing) {
79
- return params
80
- }
77
+ const caseParamsMemo = memoize(new WeakMap<Array<ParameterNode>, (casing: string) => Array<ParameterNode>>(), (params) =>
78
+ memoize(new Map<string, Array<ParameterNode>>(), (casing: string) =>
79
+ params.map((param) => {
80
+ const transformed = casing === 'camelcase' || !isValidVarName(param.name) ? camelCase(param.name) : param.name
81
+ return { ...param, name: transformed }
82
+ }),
83
+ ),
84
+ )
81
85
 
82
- return params.map((param) => {
83
- const transformed = casing === 'camelcase' || !isValidVarName(param.name) ? camelCase(param.name) : param.name
84
-
85
- return { ...param, name: transformed }
86
- })
86
+ export function caseParams(params: Array<ParameterNode>, casing: 'camelcase' | undefined): Array<ParameterNode> {
87
+ if (!casing) return params
88
+ return caseParamsMemo(params)(casing)
87
89
  }
88
90
 
89
91
  /**
@@ -392,7 +394,7 @@ export function createOperationParams(node: OperationNode, options: CreateOperat
392
394
  } else {
393
395
  if (pathParams.length) {
394
396
  if (pathParamsType === 'inlineSpread') {
395
- const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]!) ?? undefined
397
+ const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]!)
396
398
  params.push(
397
399
  createFunctionParameter({
398
400
  name: pathName,
@@ -474,7 +476,7 @@ function buildGroupParam({
474
476
  name: string
475
477
  node: OperationNode
476
478
  params: Array<ParameterNode>
477
- groupType: ParamGroupType | undefined
479
+ groupType: ParamGroupType | null | undefined
478
480
  resolver: OperationParamsResolver | undefined
479
481
  wrapType: (type: string) => ParamsTypeNode
480
482
  }): Array<FunctionParameterNode> {
@@ -496,7 +498,7 @@ function buildGroupParam({
496
498
 
497
499
  /**
498
500
  * Derives a {@link ParamGroupType} from the resolver's group method.
499
- * Returns `undefined` when the group name equals the individual param name (no real group).
501
+ * Returns `null` when the group name equals the individual param name (no real group).
500
502
  */
501
503
  function resolveGroupType({
502
504
  node,
@@ -508,14 +510,14 @@ function resolveGroupType({
508
510
  params: Array<ParameterNode>
509
511
  groupMethod: (_node: OperationNode, _param: ParameterNode) => string
510
512
  resolver: OperationParamsResolver
511
- }): ParamGroupType | undefined {
513
+ }): ParamGroupType | null {
512
514
  if (!params.length) {
513
- return undefined
515
+ return null
514
516
  }
515
517
  const firstParam = params[0]!
516
518
  const groupName = groupMethod.call(resolver, node, firstParam)
517
519
  if (groupName === resolver.resolveParamName(node, firstParam)) {
518
- return undefined
520
+ return null
519
521
  }
520
522
  const allOptional = params.every((p) => !p.required)
521
523
  return {
@@ -652,6 +654,16 @@ export function combineImports(imports: Array<ImportNode>, exports: Array<Export
652
654
  const exportedNames = new Set(exports.flatMap((e) => (Array.isArray(e.name) ? e.name : e.name ? [e.name] : [])))
653
655
  const isUsed = (importName: string): boolean => !source || source.includes(importName) || exportedNames.has(importName)
654
656
 
657
+ // Memoize object import names so the same logical (propertyName, name) pair always
658
+ // reuses the same object reference — Set-based deduplication then works correctly.
659
+ const importNameMemo = new Map<string, { propertyName: string; name?: string }>()
660
+ const canonicalizeName = (n: string | { propertyName: string; name?: string }): string | { propertyName: string; name?: string } => {
661
+ if (typeof n === 'string') return n
662
+ const key = `${n.propertyName}:${n.name ?? ''}`
663
+ if (!importNameMemo.has(key)) importNameMemo.set(key, n)
664
+ return importNameMemo.get(key)!
665
+ }
666
+
655
667
  const result: Array<ImportNode> = []
656
668
  // Accumulates array-named imports keyed by `path:isTypeOnly` for name-merging
657
669
  const namedByPath = new Map<string, ImportNode>()
@@ -669,7 +681,7 @@ export function combineImports(imports: Array<ImportNode>, exports: Array<Export
669
681
  let { name } = curr
670
682
 
671
683
  if (Array.isArray(name)) {
672
- name = [...new Set(name)].filter((item) => (typeof item === 'string' ? isUsed(item) : isUsed(item.name ?? item.propertyName)))
684
+ name = [...new Set(name.map(canonicalizeName))].filter((item) => (typeof item === 'string' ? isUsed(item) : isUsed(item.name ?? item.propertyName)))
673
685
  if (!name.length) continue
674
686
 
675
687
  const key = pathTypeKey(path, isTypeOnly)
@@ -713,13 +725,18 @@ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): str
713
725
  if (node.kind === 'Text') return node.value
714
726
  if (node.kind === 'Break') return ''
715
727
  if (node.kind === 'Jsx') return node.value
728
+
716
729
  const parts: string[] = []
730
+
717
731
  if ('params' in node && node.params) parts.push(node.params)
718
732
  if ('generics' in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(', ') : node.generics)
719
733
  if ('returnType' in node && node.returnType) parts.push(node.returnType)
720
734
  if ('type' in node && typeof node.type === 'string') parts.push(node.type)
735
+
721
736
  const nested = extractStringsFromNodes(node.nodes)
737
+
722
738
  if (nested) parts.push(nested)
739
+
723
740
  return parts.join('\n')
724
741
  })
725
742
  .filter(Boolean)
@@ -729,7 +746,7 @@ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): str
729
746
  /**
730
747
  * Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
731
748
  *
732
- * Returns `undefined` for non-ref nodes or when no name can be resolved. Use this to get a schema's
749
+ * Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
733
750
  * identifier for type definitions or error messages.
734
751
  *
735
752
  * @example
@@ -738,11 +755,11 @@ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): str
738
755
  * // => 'Pet'
739
756
  * ```
740
757
  */
741
- export function resolveRefName(node: SchemaNode | undefined): string | undefined {
742
- if (!node || node.type !== 'ref') return undefined
743
- if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? undefined
758
+ export function resolveRefName(node: SchemaNode | undefined): string | null {
759
+ if (!node || node.type !== 'ref') return null
760
+ if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null
744
761
 
745
- return node.name ?? node.schema?.name ?? undefined
762
+ return node.name ?? node.schema?.name ?? null
746
763
  }
747
764
 
748
765
  /**
@@ -765,18 +782,22 @@ export function resolveRefName(node: SchemaNode | undefined): string | undefined
765
782
  * }
766
783
  * ```
767
784
  */
768
- export function collectReferencedSchemaNames(node: SchemaNode | undefined, out: Set<string> = new Set()): Set<string> {
769
- if (!node) return out
785
+ const collectSchemaRefs = memoize(new WeakMap<SchemaNode, ReadonlySet<string>>(), (node: SchemaNode): ReadonlySet<string> => {
786
+ const refs = new Set<string>()
770
787
  collect<void>(node, {
771
788
  schema(child) {
772
789
  if (child.type === 'ref') {
773
790
  const name = resolveRefName(child)
774
-
775
- if (name) out.add(name)
791
+ if (name) refs.add(name)
776
792
  }
777
- return undefined
778
793
  },
779
794
  })
795
+ return refs
796
+ })
797
+
798
+ export function collectReferencedSchemaNames(node: SchemaNode | undefined, out: Set<string> = new Set()): Set<string> {
799
+ if (!node) return out
800
+ for (const name of collectSchemaRefs(node)) out.add(name)
780
801
  return out
781
802
  }
782
803
 
@@ -793,10 +814,10 @@ export function collectReferencedSchemaNames(node: SchemaNode | undefined, out:
793
814
  *
794
815
  * @example Only generate schemas referenced by included operations
795
816
  * ```ts
796
- * const includedOps = inputNode.operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
797
- * const allowed = collectUsedSchemaNames(includedOps, inputNode.schemas)
817
+ * const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
818
+ * const allowed = collectUsedSchemaNames(includedOps, schemas)
798
819
  *
799
- * for (const schema of inputNode.schemas) {
820
+ * for (const schema of schemas) {
800
821
  * if (schema.name && !allowed.has(schema.name)) continue
801
822
  * // … generate schema
802
823
  * }
@@ -804,16 +825,18 @@ export function collectReferencedSchemaNames(node: SchemaNode | undefined, out:
804
825
  *
805
826
  * @example Check whether a specific schema is needed
806
827
  * ```ts
807
- * const allowed = collectUsedSchemaNames(includedOps, inputNode.schemas)
828
+ * const allowed = collectUsedSchemaNames(includedOps, schemas)
808
829
  * allowed.has('OrderStatus') // false when no included operation references OrderStatus
809
830
  * ```
810
831
  */
811
- export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>, schemas: ReadonlyArray<SchemaNode>): Set<string> {
832
+ const collectUsedSchemaNamesMemo = memoize(new WeakMap<ReadonlyArray<OperationNode>, (schemas: ReadonlyArray<SchemaNode>) => Set<string>>(), (ops) =>
833
+ memoize(new WeakMap<ReadonlyArray<SchemaNode>, Set<string>>(), (schemas) => computeUsedSchemaNames(ops, schemas)),
834
+ )
835
+
836
+ function computeUsedSchemaNames(operations: ReadonlyArray<OperationNode>, schemas: ReadonlyArray<SchemaNode>): Set<string> {
812
837
  const schemaMap = new Map<string, SchemaNode>()
813
838
  for (const schema of schemas) {
814
- if (schema.name) {
815
- schemaMap.set(schema.name, schema)
816
- }
839
+ if (schema.name) schemaMap.set(schema.name, schema)
817
840
  }
818
841
 
819
842
  const result = new Set<string>()
@@ -824,15 +847,13 @@ export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>,
824
847
  if (!result.has(name)) {
825
848
  result.add(name)
826
849
  const namedSchema = schemaMap.get(name)
827
- if (namedSchema) {
828
- visitSchema(namedSchema)
829
- }
850
+ if (namedSchema) visitSchema(namedSchema)
830
851
  }
831
852
  }
832
853
  }
833
854
 
834
855
  for (const op of operations) {
835
- for (const schema of collect<SchemaNode>(op, { depth: 'shallow', schema: (node) => node })) {
856
+ for (const schema of collectLazy<SchemaNode>(op, { depth: 'shallow', schema: (node) => node })) {
836
857
  visitSchema(schema)
837
858
  }
838
859
  }
@@ -840,16 +861,13 @@ export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>,
840
861
  return result
841
862
  }
842
863
 
843
- /**
844
- * Identifies all schemas that participate in circular dependency chains, including direct self-loops.
845
- *
846
- * Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
847
- * in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
848
- * Refs are followed by name only, keeping the algorithm linear in the schema graph size.
849
- *
850
- * @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
851
- */
852
- export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<string> {
864
+ export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>, schemas: ReadonlyArray<SchemaNode>): Set<string> {
865
+ return collectUsedSchemaNamesMemo(operations)(schemas)
866
+ }
867
+
868
+ const EMPTY_CIRCULAR_SET = new Set<string>()
869
+
870
+ const findCircularSchemasMemo = memoize(new WeakMap<ReadonlyArray<SchemaNode>, Set<string>>(), (schemas: ReadonlyArray<SchemaNode>): Set<string> => {
853
871
  const graph = new Map<string, Set<string>>()
854
872
 
855
873
  for (const schema of schemas) {
@@ -876,6 +894,20 @@ export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<str
876
894
  }
877
895
 
878
896
  return circular
897
+ })
898
+
899
+ /**
900
+ * Identifies all schemas that participate in circular dependency chains, including direct self-loops.
901
+ *
902
+ * Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
903
+ * in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
904
+ * Refs are followed by name only, keeping the algorithm linear in the schema graph size.
905
+ *
906
+ * @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
907
+ */
908
+ export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<string> {
909
+ if (schemas.length === 0) return EMPTY_CIRCULAR_SET
910
+ return findCircularSchemasMemo(schemas)
879
911
  }
880
912
 
881
913
  /**
@@ -892,14 +924,15 @@ export function containsCircularRef(
892
924
  ): boolean {
893
925
  if (!node || circularSchemas.size === 0) return false
894
926
 
895
- const matches = collect<true>(node, {
927
+ for (const _ of collectLazy<true>(node, {
896
928
  schema(child) {
897
- if (child.type !== 'ref') return undefined
929
+ if (child.type !== 'ref') return null
898
930
  const name = resolveRefName(child)
899
-
900
- return name && name !== excludeName && circularSchemas.has(name) ? true : undefined
931
+ return name && name !== excludeName && circularSchemas.has(name) ? true : null
901
932
  },
902
- })
933
+ })) {
934
+ return true
935
+ }
903
936
 
904
- return matches.length > 0
937
+ return false
905
938
  }