@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/README.md +1 -1
- package/dist/index.cjs +270 -206
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +118 -95
- package/dist/index.js +268 -207
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/factory.ts +15 -0
- package/src/guards.ts +3 -3
- package/src/index.ts +3 -2
- package/src/nodes/index.ts +1 -1
- package/src/nodes/operation.ts +1 -1
- package/src/nodes/response.ts +1 -1
- package/src/nodes/root.ts +72 -10
- package/src/nodes/schema.ts +9 -3
- package/src/printer.ts +15 -16
- package/src/refs.ts +4 -2
- package/src/resolvers.ts +4 -4
- package/src/transformers.ts +20 -15
- package/src/types.ts +1 -0
- package/src/utils.ts +89 -56
- package/src/visitor.ts +133 -181
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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]!)
|
|
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 `
|
|
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 |
|
|
513
|
+
}): ParamGroupType | null {
|
|
512
514
|
if (!params.length) {
|
|
513
|
-
return
|
|
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
|
|
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 `
|
|
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 |
|
|
742
|
-
if (!node || node.type !== 'ref') return
|
|
743
|
-
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ??
|
|
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 ??
|
|
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
|
-
|
|
769
|
-
|
|
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 =
|
|
797
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
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
|
|
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,
|
|
828
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
808
829
|
* allowed.has('OrderStatus') // false when no included operation references OrderStatus
|
|
809
830
|
* ```
|
|
810
831
|
*/
|
|
811
|
-
|
|
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
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
|
927
|
+
for (const _ of collectLazy<true>(node, {
|
|
896
928
|
schema(child) {
|
|
897
|
-
if (child.type !== 'ref') return
|
|
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
|
|
937
|
+
return false
|
|
905
938
|
}
|