@kubb/ast 5.0.0-beta.2 → 5.0.0-beta.20
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 +253 -189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +86 -65
- package/dist/index.js +251 -190
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/factory.ts +15 -0
- package/src/index.ts +3 -2
- package/src/nodes/index.ts +1 -1
- package/src/nodes/root.ts +71 -9
- package/src/nodes/schema.ts +4 -0
- package/src/transformers.ts +19 -14
- package/src/types.ts +1 -0
- package/src/utils.ts +76 -43
- package/src/visitor.ts +124 -172
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/ast",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "Spec-agnostic AST layer for Kubb. Defines
|
|
3
|
+
"version": "5.0.0-beta.20",
|
|
4
|
+
"description": "Spec-agnostic AST layer for Kubb. Defines the node tree, visitor pattern, factory functions, and type guards used across all code generation plugins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
7
7
|
"codegen",
|
|
8
8
|
"kubb",
|
|
9
|
-
"openapi",
|
|
10
9
|
"typescript"
|
|
11
10
|
],
|
|
12
11
|
"license": "MIT",
|
|
@@ -40,7 +39,7 @@
|
|
|
40
39
|
"registry": "https://registry.npmjs.org/"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^
|
|
42
|
+
"@types/node": "^22.19.19",
|
|
44
43
|
"@internals/utils": "0.0.0"
|
|
45
44
|
},
|
|
46
45
|
"engines": {
|
package/src/factory.ts
CHANGED
|
@@ -12,7 +12,9 @@ import type {
|
|
|
12
12
|
FunctionParameterNode,
|
|
13
13
|
FunctionParametersNode,
|
|
14
14
|
ImportNode,
|
|
15
|
+
InputMeta,
|
|
15
16
|
InputNode,
|
|
17
|
+
InputStreamNode,
|
|
16
18
|
JsxNode,
|
|
17
19
|
ObjectSchemaNode,
|
|
18
20
|
OperationNode,
|
|
@@ -84,11 +86,24 @@ export function createInput(overrides: Partial<Omit<InputNode, 'kind'>> = {}): I
|
|
|
84
86
|
return {
|
|
85
87
|
schemas: [],
|
|
86
88
|
operations: [],
|
|
89
|
+
meta: { circularNames: [], enumNames: [] },
|
|
87
90
|
...overrides,
|
|
88
91
|
kind: 'Input',
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Creates an `InputStreamNode` from pre-built `AsyncIterable` sources.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function createStreamInput(schemas: AsyncIterable<SchemaNode>, operations: AsyncIterable<OperationNode>, meta?: InputMeta): InputStreamNode {
|
|
104
|
+
return { kind: 'Input', schemas, operations, meta }
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
/**
|
|
93
108
|
* Creates an `OutputNode` with a stable default for `files`.
|
|
94
109
|
*
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
createFunctionParameters,
|
|
11
11
|
createImport,
|
|
12
12
|
createInput,
|
|
13
|
+
createStreamInput,
|
|
13
14
|
createJsx,
|
|
14
15
|
createOperation,
|
|
15
16
|
createOutput,
|
|
@@ -28,7 +29,7 @@ export { isInputNode, isOperationNode, isOutputNode, isSchemaNode, narrowSchema
|
|
|
28
29
|
export { createPrinterFactory, definePrinter } from './printer.ts'
|
|
29
30
|
export { extractRefName } from './refs.ts'
|
|
30
31
|
export { childName, collectImports, enumPropName, findDiscriminator } from './resolvers.ts'
|
|
31
|
-
export { mergeAdjacentObjects, setDiscriminatorEnum, setEnumName, simplifyUnion } from './transformers.ts'
|
|
32
|
+
export { mergeAdjacentObjects, mergeAdjacentObjectsLazy, setDiscriminatorEnum, setEnumName, simplifyUnion } from './transformers.ts'
|
|
32
33
|
export type * from './types.ts'
|
|
33
34
|
export {
|
|
34
35
|
caseParams,
|
|
@@ -43,4 +44,4 @@ export {
|
|
|
43
44
|
resolveRefName,
|
|
44
45
|
syncSchemaRef,
|
|
45
46
|
} from './utils.ts'
|
|
46
|
-
export { collect, transform, walk } from './visitor.ts'
|
|
47
|
+
export { collect, collectLazy, transform, walk } from './visitor.ts'
|
package/src/nodes/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type { OutputNode } from './output.ts'
|
|
|
19
19
|
export type { ParameterLocation, ParameterNode } from './parameter.ts'
|
|
20
20
|
export type { PropertyNode } from './property.ts'
|
|
21
21
|
export type { ResponseNode } from './response.ts'
|
|
22
|
-
export type { InputMeta, InputNode } from './root.ts'
|
|
22
|
+
export type { InputMeta, InputNode, InputStreamNode } from './root.ts'
|
|
23
23
|
export type {
|
|
24
24
|
ArraySchemaNode,
|
|
25
25
|
ComplexSchemaType,
|
package/src/nodes/root.ts
CHANGED
|
@@ -3,32 +3,62 @@ import type { OperationNode } from './operation.ts'
|
|
|
3
3
|
import type { SchemaNode } from './schema.ts'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Metadata for an API document, populated by the adapter and available to every generator.
|
|
7
|
+
*
|
|
8
|
+
* All fields are plain JSON-serializable values — no `Set`, no `Map`, no class instances.
|
|
9
|
+
* Computed fields (`circularNames`, `enumNames`) are pre-calculated once during the adapter
|
|
10
|
+
* pre-scan so generators never need to iterate the full schema list themselves.
|
|
8
11
|
*
|
|
9
12
|
* @example
|
|
10
13
|
* ```ts
|
|
11
|
-
* const meta: InputMeta = { title: 'Pet
|
|
14
|
+
* const meta: InputMeta = { title: 'Pet Store', version: '1.0.0', baseURL: 'https://petstore.swagger.io/v2', circularNames: [], enumNames: [] }
|
|
12
15
|
* ```
|
|
13
16
|
*/
|
|
14
17
|
export type InputMeta = {
|
|
15
18
|
/**
|
|
16
|
-
* API title
|
|
19
|
+
* API title from `info.title` in the source document.
|
|
17
20
|
*/
|
|
18
21
|
title?: string
|
|
19
22
|
/**
|
|
20
|
-
* API description
|
|
23
|
+
* API description from `info.description` in the source document.
|
|
21
24
|
*/
|
|
22
25
|
description?: string
|
|
23
26
|
/**
|
|
24
|
-
* API version string
|
|
27
|
+
* API version string from `info.version` in the source document.
|
|
25
28
|
*/
|
|
26
29
|
version?: string
|
|
27
30
|
/**
|
|
28
|
-
* Resolved
|
|
29
|
-
* For OpenAPI and AsyncAPI, this comes from the selected server URL.
|
|
31
|
+
* Resolved base URL from the first matching server entry in the source document.
|
|
30
32
|
*/
|
|
31
33
|
baseURL?: string
|
|
34
|
+
/**
|
|
35
|
+
* Names of schemas that participate in a circular reference chain.
|
|
36
|
+
* Computed once during the adapter pre-scan — use this instead of calling
|
|
37
|
+
* `findCircularSchemas` per generator call.
|
|
38
|
+
*
|
|
39
|
+
* Convert to a `Set` once at the start of a generator, not per-schema,
|
|
40
|
+
* to keep lookup O(1) without repeated allocations.
|
|
41
|
+
*
|
|
42
|
+
* @example Wrap a circular schema in z.lazy()
|
|
43
|
+
* ```ts
|
|
44
|
+
* const circular = new Set(meta.circularNames)
|
|
45
|
+
* if (circular.has(schema.name)) { ... }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
circularNames: readonly string[]
|
|
49
|
+
/**
|
|
50
|
+
* Names of schemas whose type is `enum`.
|
|
51
|
+
* Computed once during the adapter pre-scan — use this instead of filtering
|
|
52
|
+
* schemas per generator call.
|
|
53
|
+
*
|
|
54
|
+
* Convert to a `Set` once at the start of a generator when you need repeated
|
|
55
|
+
* membership checks, rather than calling `.includes()` per schema.
|
|
56
|
+
*
|
|
57
|
+
* @example Check if a referenced schema is an enum
|
|
58
|
+
* `const enums = new Set(meta.enumNames)`
|
|
59
|
+
* `const isEnum = enums.has(schemaName)`
|
|
60
|
+
*/
|
|
61
|
+
enumNames: readonly string[]
|
|
32
62
|
}
|
|
33
63
|
|
|
34
64
|
/**
|
|
@@ -58,7 +88,39 @@ export type InputNode = BaseNode & {
|
|
|
58
88
|
*/
|
|
59
89
|
operations: Array<OperationNode>
|
|
60
90
|
/**
|
|
61
|
-
*
|
|
91
|
+
* Document metadata populated by the adapter.
|
|
92
|
+
*/
|
|
93
|
+
meta: InputMeta
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Streaming variant of `InputNode` for memory-efficient processing of large API specs.
|
|
98
|
+
*
|
|
99
|
+
* `schemas` and `operations` are `AsyncIterable` rather than arrays — each `for await`
|
|
100
|
+
* loop creates a fresh parse pass from the cached in-memory document, so multiple
|
|
101
|
+
* consumers (plugins) can iterate independently without keeping all nodes in memory.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* for await (const schema of inputStreamNode.schemas) {
|
|
106
|
+
* // only this one SchemaNode is live here; previous ones are GC-eligible
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export type InputStreamNode = {
|
|
111
|
+
kind: 'Input'
|
|
112
|
+
/**
|
|
113
|
+
* Lazily parsed schema nodes. Each `for await` creates a fresh parse pass, so
|
|
114
|
+
* multiple plugins can iterate independently without sharing state.
|
|
115
|
+
*/
|
|
116
|
+
schemas: AsyncIterable<SchemaNode>
|
|
117
|
+
/**
|
|
118
|
+
* Lazily parsed operation nodes. Each `for await` creates a fresh parse pass, so
|
|
119
|
+
* multiple plugins can iterate independently without sharing state.
|
|
120
|
+
*/
|
|
121
|
+
operations: AsyncIterable<OperationNode>
|
|
122
|
+
/**
|
|
123
|
+
* Document metadata available immediately, before the first yielded node.
|
|
62
124
|
*/
|
|
63
125
|
meta?: InputMeta
|
|
64
126
|
}
|
package/src/nodes/schema.ts
CHANGED
package/src/transformers.ts
CHANGED
|
@@ -73,25 +73,30 @@ export function setDiscriminatorEnum({
|
|
|
73
73
|
* ])
|
|
74
74
|
* ```
|
|
75
75
|
*/
|
|
76
|
-
export function
|
|
77
|
-
|
|
76
|
+
export function* mergeAdjacentObjectsLazy(members: Iterable<SchemaNode>): Generator<SchemaNode, void, undefined> {
|
|
77
|
+
let acc: SchemaNode | undefined
|
|
78
|
+
|
|
79
|
+
for (const member of members) {
|
|
78
80
|
const objectMember = narrowSchema(member, 'object')
|
|
79
|
-
if (objectMember && !objectMember.name) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
...previousObject,
|
|
86
|
-
properties: [...(previousObject.properties ?? []), ...(objectMember.properties ?? [])],
|
|
81
|
+
if (objectMember && !objectMember.name && acc !== undefined) {
|
|
82
|
+
const accObject = narrowSchema(acc, 'object')
|
|
83
|
+
if (accObject && !accObject.name) {
|
|
84
|
+
acc = createSchema({
|
|
85
|
+
...accObject,
|
|
86
|
+
properties: [...(accObject.properties ?? []), ...(objectMember.properties ?? [])],
|
|
87
87
|
})
|
|
88
|
-
|
|
88
|
+
continue
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
if (acc !== undefined) yield acc
|
|
92
|
+
acc = member
|
|
93
|
+
}
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
if (acc !== undefined) yield acc
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function mergeAdjacentObjects(members: Array<SchemaNode>): Array<SchemaNode> {
|
|
99
|
+
return [...mergeAdjacentObjectsLazy(members)]
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
/**
|
package/src/types.ts
CHANGED
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
|
/**
|
|
@@ -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)
|
|
@@ -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
929
|
if (child.type !== 'ref') return undefined
|
|
898
930
|
const name = resolveRefName(child)
|
|
899
|
-
|
|
900
931
|
return name && name !== excludeName && circularSchemas.has(name) ? true : undefined
|
|
901
932
|
},
|
|
902
|
-
})
|
|
933
|
+
})) {
|
|
934
|
+
return true
|
|
935
|
+
}
|
|
903
936
|
|
|
904
|
-
return
|
|
937
|
+
return false
|
|
905
938
|
}
|