@kubb/ast 5.0.0-beta.4 → 5.0.0-beta.41
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 +28 -19
- package/dist/index.cjs +866 -395
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +33 -3310
- package/dist/index.js +859 -391
- package/dist/index.js.map +1 -1
- package/dist/types-CEIHPTfs.d.ts +3596 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/package.json +8 -4
- package/src/constants.ts +2 -52
- package/src/dedupe.ts +200 -0
- package/src/dialect.ts +58 -0
- package/src/dispatch.ts +53 -0
- package/src/factory.ts +133 -17
- package/src/guards.ts +18 -48
- package/src/index.ts +9 -5
- package/src/infer.ts +16 -14
- package/src/nodes/base.ts +2 -0
- package/src/nodes/code.ts +22 -22
- package/src/nodes/content.ts +37 -0
- package/src/nodes/file.ts +16 -14
- package/src/nodes/function.ts +11 -11
- package/src/nodes/index.ts +7 -3
- package/src/nodes/operation.ts +98 -62
- package/src/nodes/response.ts +21 -14
- package/src/nodes/root.ts +72 -10
- package/src/nodes/schema.ts +12 -9
- package/src/printer.ts +42 -36
- package/src/refs.ts +0 -54
- package/src/resolvers.ts +4 -4
- package/src/signature.ts +232 -0
- package/src/transformers.ts +20 -15
- package/src/types.ts +9 -2
- package/src/utils.ts +118 -77
- package/src/visitor.ts +237 -279
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
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
|
/**
|
|
@@ -247,7 +249,7 @@ export type CreateOperationParamsOptions = {
|
|
|
247
249
|
* Applies a uniform transformation to every resolved type name before it is used
|
|
248
250
|
* in a parameter node. Use this for framework-level type wrappers.
|
|
249
251
|
*
|
|
250
|
-
* @example Vue Query
|
|
252
|
+
* @example Vue Query, wrap every parameter type with `MaybeRefOrGetter`
|
|
251
253
|
* `typeWrapper: (t) => \`MaybeRefOrGetter<${t}>\``
|
|
252
254
|
*/
|
|
253
255
|
typeWrapper?: (type: string) => string
|
|
@@ -313,7 +315,7 @@ export function createOperationParams(node: OperationNode, options: CreateOperat
|
|
|
313
315
|
variant: 'reference',
|
|
314
316
|
name: typeWrapper ? typeWrapper(type) : type,
|
|
315
317
|
})
|
|
316
|
-
// Only reference-variant TypeNodes are wrapped
|
|
318
|
+
// Only reference-variant TypeNodes are wrapped, they hold a plain type name string that needs casing applied.
|
|
317
319
|
// Member and struct TypeNodes are pre-resolved structured expressions and are passed through unchanged.
|
|
318
320
|
const wrapTypeNode = (type: ParamsTypeNode): ParamsTypeNode => (type.kind === 'ParamsType' && type.variant === 'reference' ? wrapType(type.name) : type)
|
|
319
321
|
|
|
@@ -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 {
|
|
@@ -554,23 +556,23 @@ function sourceKey(source: SourceNode): string {
|
|
|
554
556
|
return `${nameKey}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`
|
|
555
557
|
}
|
|
556
558
|
|
|
557
|
-
function pathTypeKey(path: string, isTypeOnly: boolean | undefined): string {
|
|
559
|
+
function pathTypeKey(path: string, isTypeOnly: boolean | null | undefined): string {
|
|
558
560
|
return `${path}:${isTypeOnly ?? false}`
|
|
559
561
|
}
|
|
560
562
|
|
|
561
|
-
function exportKey(path: string, name: string | undefined, isTypeOnly: boolean | undefined, asAlias: boolean | undefined): string {
|
|
563
|
+
function exportKey(path: string, name: string | null | undefined, isTypeOnly: boolean | null | undefined, asAlias: boolean | null | undefined): string {
|
|
562
564
|
return `${path}:${name ?? ''}:${isTypeOnly ?? false}:${asAlias ?? ''}`
|
|
563
565
|
}
|
|
564
566
|
|
|
565
|
-
function importKey(path: string, name: string | undefined, isTypeOnly: boolean | undefined): string {
|
|
567
|
+
function importKey(path: string, name: string | null | undefined, isTypeOnly: boolean | null | undefined): string {
|
|
566
568
|
return `${path}:${name ?? ''}:${isTypeOnly ?? false}`
|
|
567
569
|
}
|
|
568
570
|
|
|
569
571
|
/**
|
|
570
572
|
* Computes a multi-level sort key for exports and imports:
|
|
571
|
-
* non-array names first (wildcards/namespace aliases)
|
|
573
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
572
574
|
*/
|
|
573
|
-
function sortKey(node: { name?: string | Array<unknown
|
|
575
|
+
function sortKey(node: { name?: string | Array<unknown> | null; isTypeOnly?: boolean | null; path: string }): string {
|
|
574
576
|
const isArray = Array.isArray(node.name) ? '1' : '0'
|
|
575
577
|
const typeOnly = node.isTypeOnly ? '0' : '1'
|
|
576
578
|
const hasName = node.name != null ? '1' : '0'
|
|
@@ -592,6 +594,17 @@ export function combineSources(sources: Array<SourceNode>): Array<SourceNode> {
|
|
|
592
594
|
return [...seen.values()]
|
|
593
595
|
}
|
|
594
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Merges `incoming` names into `existing`, preserving order and dropping duplicates.
|
|
599
|
+
*
|
|
600
|
+
* Shared by `combineExports` and `combineImports` for the same-path name-merge case.
|
|
601
|
+
*/
|
|
602
|
+
function mergeNameArrays<TName>(existing: Array<TName>, incoming: Array<TName>): Array<TName> {
|
|
603
|
+
const merged = new Set(existing)
|
|
604
|
+
for (const name of incoming) merged.add(name)
|
|
605
|
+
return [...merged]
|
|
606
|
+
}
|
|
607
|
+
|
|
595
608
|
/**
|
|
596
609
|
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
597
610
|
*
|
|
@@ -605,7 +618,7 @@ export function combineExports(exports: Array<ExportNode>): Array<ExportNode> {
|
|
|
605
618
|
// Deduplicates non-array exports by their exact identity
|
|
606
619
|
const seen = new Set<string>()
|
|
607
620
|
|
|
608
|
-
// Precompute sort keys once
|
|
621
|
+
// Precompute sort keys once, avoids recomputing per comparison.
|
|
609
622
|
const keyed = exports.map((node) => ({ node, key: sortKey(node) }))
|
|
610
623
|
keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
|
|
611
624
|
|
|
@@ -619,9 +632,7 @@ export function combineExports(exports: Array<ExportNode>): Array<ExportNode> {
|
|
|
619
632
|
const existing = namedByPath.get(key)
|
|
620
633
|
|
|
621
634
|
if (existing && Array.isArray(existing.name)) {
|
|
622
|
-
|
|
623
|
-
for (const n of name) merged.add(n)
|
|
624
|
-
existing.name = [...merged]
|
|
635
|
+
existing.name = mergeNameArrays(existing.name, name)
|
|
625
636
|
} else {
|
|
626
637
|
const newItem: ExportNode = { ...curr, name: [...new Set(name)] }
|
|
627
638
|
result.push(newItem)
|
|
@@ -653,7 +664,7 @@ export function combineImports(imports: Array<ImportNode>, exports: Array<Export
|
|
|
653
664
|
const isUsed = (importName: string): boolean => !source || source.includes(importName) || exportedNames.has(importName)
|
|
654
665
|
|
|
655
666
|
// Memoize object import names so the same logical (propertyName, name) pair always
|
|
656
|
-
// reuses the same object reference
|
|
667
|
+
// reuses the same object reference. Set-based deduplication then works correctly.
|
|
657
668
|
const importNameMemo = new Map<string, { propertyName: string; name?: string }>()
|
|
658
669
|
const canonicalizeName = (n: string | { propertyName: string; name?: string }): string | { propertyName: string; name?: string } => {
|
|
659
670
|
if (typeof n === 'string') return n
|
|
@@ -662,13 +673,24 @@ export function combineImports(imports: Array<ImportNode>, exports: Array<Export
|
|
|
662
673
|
return importNameMemo.get(key)!
|
|
663
674
|
}
|
|
664
675
|
|
|
676
|
+
// Paths that keep at least one used named import. A default import from such a path is retained
|
|
677
|
+
// even when its binding can't be found in `source` e.g. a generated `client` default import
|
|
678
|
+
// alongside `import type { Client } from <same path>`, where merged grouped output omits the body.
|
|
679
|
+
const pathsWithUsedNamedImport = new Set<string>()
|
|
680
|
+
for (const node of imports) {
|
|
681
|
+
if (!Array.isArray(node.name)) continue
|
|
682
|
+
if (node.name.some((item) => (typeof item === 'string' ? isUsed(item) : isUsed(item.name ?? item.propertyName)))) {
|
|
683
|
+
pathsWithUsedNamedImport.add(node.path)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
665
687
|
const result: Array<ImportNode> = []
|
|
666
688
|
// Accumulates array-named imports keyed by `path:isTypeOnly` for name-merging
|
|
667
689
|
const namedByPath = new Map<string, ImportNode>()
|
|
668
690
|
// Deduplicates non-array imports by their exact identity
|
|
669
691
|
const seen = new Set<string>()
|
|
670
692
|
|
|
671
|
-
// Precompute sort keys once
|
|
693
|
+
// Precompute sort keys once, avoids recomputing per comparison.
|
|
672
694
|
const keyed = imports.map((node) => ({ node, key: sortKey(node) }))
|
|
673
695
|
keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
|
|
674
696
|
|
|
@@ -686,16 +708,14 @@ export function combineImports(imports: Array<ImportNode>, exports: Array<Export
|
|
|
686
708
|
const existing = namedByPath.get(key)
|
|
687
709
|
|
|
688
710
|
if (existing && Array.isArray(existing.name)) {
|
|
689
|
-
|
|
690
|
-
for (const n of name) merged.add(n)
|
|
691
|
-
existing.name = [...merged]
|
|
711
|
+
existing.name = mergeNameArrays(existing.name, name)
|
|
692
712
|
} else {
|
|
693
713
|
const newItem: ImportNode = { ...curr, name }
|
|
694
714
|
result.push(newItem)
|
|
695
715
|
namedByPath.set(key, newItem)
|
|
696
716
|
}
|
|
697
717
|
} else {
|
|
698
|
-
if (name && !isUsed(name)) continue
|
|
718
|
+
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue
|
|
699
719
|
|
|
700
720
|
const key = importKey(path, name, isTypeOnly)
|
|
701
721
|
if (!seen.has(key)) {
|
|
@@ -723,13 +743,18 @@ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): str
|
|
|
723
743
|
if (node.kind === 'Text') return node.value
|
|
724
744
|
if (node.kind === 'Break') return ''
|
|
725
745
|
if (node.kind === 'Jsx') return node.value
|
|
726
|
-
|
|
746
|
+
|
|
747
|
+
const parts: Array<string> = []
|
|
748
|
+
|
|
727
749
|
if ('params' in node && node.params) parts.push(node.params)
|
|
728
750
|
if ('generics' in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(', ') : node.generics)
|
|
729
751
|
if ('returnType' in node && node.returnType) parts.push(node.returnType)
|
|
730
752
|
if ('type' in node && typeof node.type === 'string') parts.push(node.type)
|
|
753
|
+
|
|
731
754
|
const nested = extractStringsFromNodes(node.nodes)
|
|
755
|
+
|
|
732
756
|
if (nested) parts.push(nested)
|
|
757
|
+
|
|
733
758
|
return parts.join('\n')
|
|
734
759
|
})
|
|
735
760
|
.filter(Boolean)
|
|
@@ -739,7 +764,7 @@ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): str
|
|
|
739
764
|
/**
|
|
740
765
|
* Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
|
|
741
766
|
*
|
|
742
|
-
* Returns `
|
|
767
|
+
* Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
|
|
743
768
|
* identifier for type definitions or error messages.
|
|
744
769
|
*
|
|
745
770
|
* @example
|
|
@@ -748,17 +773,17 @@ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): str
|
|
|
748
773
|
* // => 'Pet'
|
|
749
774
|
* ```
|
|
750
775
|
*/
|
|
751
|
-
export function resolveRefName(node: SchemaNode | undefined): string |
|
|
752
|
-
if (!node || node.type !== 'ref') return
|
|
753
|
-
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ??
|
|
776
|
+
export function resolveRefName(node: SchemaNode | undefined): string | null {
|
|
777
|
+
if (!node || node.type !== 'ref') return null
|
|
778
|
+
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null
|
|
754
779
|
|
|
755
|
-
return node.name ?? node.schema?.name ??
|
|
780
|
+
return node.name ?? node.schema?.name ?? null
|
|
756
781
|
}
|
|
757
782
|
|
|
758
783
|
/**
|
|
759
784
|
* Collects every named schema referenced (transitively) from a node via ref edges.
|
|
760
785
|
*
|
|
761
|
-
* Refs are followed by name only
|
|
786
|
+
* Refs are followed by name only, the resolved `node.schema` is not traversed inline.
|
|
762
787
|
* Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
|
|
763
788
|
*
|
|
764
789
|
* @example Collect refs from a single schema
|
|
@@ -775,18 +800,22 @@ export function resolveRefName(node: SchemaNode | undefined): string | undefined
|
|
|
775
800
|
* }
|
|
776
801
|
* ```
|
|
777
802
|
*/
|
|
778
|
-
|
|
779
|
-
|
|
803
|
+
const collectSchemaRefs = memoize(new WeakMap<SchemaNode, ReadonlySet<string>>(), (node: SchemaNode): ReadonlySet<string> => {
|
|
804
|
+
const refs = new Set<string>()
|
|
780
805
|
collect<void>(node, {
|
|
781
806
|
schema(child) {
|
|
782
807
|
if (child.type === 'ref') {
|
|
783
808
|
const name = resolveRefName(child)
|
|
784
|
-
|
|
785
|
-
if (name) out.add(name)
|
|
809
|
+
if (name) refs.add(name)
|
|
786
810
|
}
|
|
787
|
-
return undefined
|
|
788
811
|
},
|
|
789
812
|
})
|
|
813
|
+
return refs
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
export function collectReferencedSchemaNames(node: SchemaNode | undefined, out: Set<string> = new Set()): Set<string> {
|
|
817
|
+
if (!node) return out
|
|
818
|
+
for (const name of collectSchemaRefs(node)) out.add(name)
|
|
790
819
|
return out
|
|
791
820
|
}
|
|
792
821
|
|
|
@@ -794,7 +823,7 @@ export function collectReferencedSchemaNames(node: SchemaNode | undefined, out:
|
|
|
794
823
|
* Collects the names of all top-level schemas transitively used by a set of operations.
|
|
795
824
|
*
|
|
796
825
|
* An operation uses a schema when any of its parameters, request body content, or responses
|
|
797
|
-
* reference it
|
|
826
|
+
* reference it, directly or indirectly through other named schemas.
|
|
798
827
|
* The walk is iterative and safe against reference cycles.
|
|
799
828
|
*
|
|
800
829
|
* Use this together with `include` filters to determine which schemas from `components/schemas`
|
|
@@ -803,10 +832,10 @@ export function collectReferencedSchemaNames(node: SchemaNode | undefined, out:
|
|
|
803
832
|
*
|
|
804
833
|
* @example Only generate schemas referenced by included operations
|
|
805
834
|
* ```ts
|
|
806
|
-
* const includedOps =
|
|
807
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
835
|
+
* const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
|
|
836
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
808
837
|
*
|
|
809
|
-
* for (const schema of
|
|
838
|
+
* for (const schema of schemas) {
|
|
810
839
|
* if (schema.name && !allowed.has(schema.name)) continue
|
|
811
840
|
* // … generate schema
|
|
812
841
|
* }
|
|
@@ -814,16 +843,18 @@ export function collectReferencedSchemaNames(node: SchemaNode | undefined, out:
|
|
|
814
843
|
*
|
|
815
844
|
* @example Check whether a specific schema is needed
|
|
816
845
|
* ```ts
|
|
817
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
846
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
818
847
|
* allowed.has('OrderStatus') // false when no included operation references OrderStatus
|
|
819
848
|
* ```
|
|
820
849
|
*/
|
|
821
|
-
|
|
850
|
+
const collectUsedSchemaNamesMemo = memoize(new WeakMap<ReadonlyArray<OperationNode>, (schemas: ReadonlyArray<SchemaNode>) => Set<string>>(), (ops) =>
|
|
851
|
+
memoize(new WeakMap<ReadonlyArray<SchemaNode>, Set<string>>(), (schemas) => computeUsedSchemaNames(ops, schemas)),
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
function computeUsedSchemaNames(operations: ReadonlyArray<OperationNode>, schemas: ReadonlyArray<SchemaNode>): Set<string> {
|
|
822
855
|
const schemaMap = new Map<string, SchemaNode>()
|
|
823
856
|
for (const schema of schemas) {
|
|
824
|
-
if (schema.name)
|
|
825
|
-
schemaMap.set(schema.name, schema)
|
|
826
|
-
}
|
|
857
|
+
if (schema.name) schemaMap.set(schema.name, schema)
|
|
827
858
|
}
|
|
828
859
|
|
|
829
860
|
const result = new Set<string>()
|
|
@@ -834,15 +865,13 @@ export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>,
|
|
|
834
865
|
if (!result.has(name)) {
|
|
835
866
|
result.add(name)
|
|
836
867
|
const namedSchema = schemaMap.get(name)
|
|
837
|
-
if (namedSchema)
|
|
838
|
-
visitSchema(namedSchema)
|
|
839
|
-
}
|
|
868
|
+
if (namedSchema) visitSchema(namedSchema)
|
|
840
869
|
}
|
|
841
870
|
}
|
|
842
871
|
}
|
|
843
872
|
|
|
844
873
|
for (const op of operations) {
|
|
845
|
-
for (const schema of
|
|
874
|
+
for (const schema of collectLazy<SchemaNode>(op, { depth: 'shallow', schema: (node) => node })) {
|
|
846
875
|
visitSchema(schema)
|
|
847
876
|
}
|
|
848
877
|
}
|
|
@@ -850,16 +879,13 @@ export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>,
|
|
|
850
879
|
return result
|
|
851
880
|
}
|
|
852
881
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
861
|
-
*/
|
|
862
|
-
export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<string> {
|
|
882
|
+
export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>, schemas: ReadonlyArray<SchemaNode>): Set<string> {
|
|
883
|
+
return collectUsedSchemaNamesMemo(operations)(schemas)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const EMPTY_CIRCULAR_SET = new Set<string>()
|
|
887
|
+
|
|
888
|
+
const findCircularSchemasMemo = memoize(new WeakMap<ReadonlyArray<SchemaNode>, Set<string>>(), (schemas: ReadonlyArray<SchemaNode>): Set<string> => {
|
|
863
889
|
const graph = new Map<string, Set<string>>()
|
|
864
890
|
|
|
865
891
|
for (const schema of schemas) {
|
|
@@ -870,7 +896,7 @@ export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<str
|
|
|
870
896
|
const circular = new Set<string>()
|
|
871
897
|
for (const start of graph.keys()) {
|
|
872
898
|
const visited = new Set<string>()
|
|
873
|
-
const stack: string
|
|
899
|
+
const stack: Array<string> = [...(graph.get(start) ?? [])]
|
|
874
900
|
while (stack.length > 0) {
|
|
875
901
|
const node = stack.pop()!
|
|
876
902
|
if (node === start) {
|
|
@@ -886,6 +912,20 @@ export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<str
|
|
|
886
912
|
}
|
|
887
913
|
|
|
888
914
|
return circular
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Identifies all schemas that participate in circular dependency chains, including direct self-loops.
|
|
919
|
+
*
|
|
920
|
+
* Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
|
|
921
|
+
* in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
|
|
922
|
+
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
923
|
+
*
|
|
924
|
+
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
925
|
+
*/
|
|
926
|
+
export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<string> {
|
|
927
|
+
if (schemas.length === 0) return EMPTY_CIRCULAR_SET
|
|
928
|
+
return findCircularSchemasMemo(schemas)
|
|
889
929
|
}
|
|
890
930
|
|
|
891
931
|
/**
|
|
@@ -894,7 +934,7 @@ export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<str
|
|
|
894
934
|
* Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
|
|
895
935
|
* Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
|
|
896
936
|
*
|
|
897
|
-
* @note Returns `true` for the first matching circular ref found
|
|
937
|
+
* @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
|
|
898
938
|
*/
|
|
899
939
|
export function containsCircularRef(
|
|
900
940
|
node: SchemaNode | undefined,
|
|
@@ -902,14 +942,15 @@ export function containsCircularRef(
|
|
|
902
942
|
): boolean {
|
|
903
943
|
if (!node || circularSchemas.size === 0) return false
|
|
904
944
|
|
|
905
|
-
const
|
|
945
|
+
for (const _ of collectLazy<true>(node, {
|
|
906
946
|
schema(child) {
|
|
907
|
-
if (child.type !== 'ref') return
|
|
947
|
+
if (child.type !== 'ref') return null
|
|
908
948
|
const name = resolveRefName(child)
|
|
909
|
-
|
|
910
|
-
return name && name !== excludeName && circularSchemas.has(name) ? true : undefined
|
|
949
|
+
return name && name !== excludeName && circularSchemas.has(name) ? true : null
|
|
911
950
|
},
|
|
912
|
-
})
|
|
951
|
+
})) {
|
|
952
|
+
return true
|
|
953
|
+
}
|
|
913
954
|
|
|
914
|
-
return
|
|
955
|
+
return false
|
|
915
956
|
}
|