@kubb/ast 5.0.0-beta.3 → 5.0.0-beta.30
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 +473 -331
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1306 -999
- package/dist/index.js +465 -332
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/dialect.ts +64 -0
- package/src/dispatch.ts +53 -0
- package/src/factory.ts +127 -11
- package/src/guards.ts +18 -3
- package/src/index.ts +9 -3
- package/src/infer.ts +16 -5
- package/src/nodes/base.ts +2 -0
- package/src/nodes/code.ts +21 -21
- package/src/nodes/content.ts +37 -0
- package/src/nodes/file.ts +16 -14
- 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 +9 -3
- package/src/printer.ts +34 -28
- package/src/refs.ts +4 -2
- package/src/resolvers.ts +4 -4
- package/src/transformers.ts +20 -15
- package/src/types.ts +7 -0
- package/src/utils.ts +109 -68
- package/src/visitor.ts +229 -275
package/src/printer.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type PrinterHandlerContext<TOutput, TOptions extends object> = {
|
|
|
18
18
|
* Recursively transform a nested `SchemaNode` to `TOutput` using the node-level handlers.
|
|
19
19
|
* Use `this.transform` inside `nodes` handlers and inside the `print` override.
|
|
20
20
|
*/
|
|
21
|
-
transform: (node: SchemaNode) => TOutput | null
|
|
21
|
+
transform: (node: SchemaNode) => TOutput | null
|
|
22
22
|
/**
|
|
23
23
|
* Options for this printer instance.
|
|
24
24
|
*/
|
|
@@ -40,7 +40,7 @@ export type PrinterHandlerContext<TOutput, TOptions extends object> = {
|
|
|
40
40
|
export type PrinterHandler<TOutput, TOptions extends object, T extends SchemaType = SchemaType> = (
|
|
41
41
|
this: PrinterHandlerContext<TOutput, TOptions>,
|
|
42
42
|
node: SchemaNodeByType[T],
|
|
43
|
-
) => TOutput | null
|
|
43
|
+
) => TOutput | null
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Partial map of per-node-type handler overrides for a printer.
|
|
@@ -108,13 +108,13 @@ export type Printer<T extends PrinterFactoryOptions = PrinterFactoryOptions> = {
|
|
|
108
108
|
* Always dispatches through the `nodes` map; never calls the `print` override.
|
|
109
109
|
* Use this when you need the raw output (e.g. `ts.TypeNode`) without declaration wrapping.
|
|
110
110
|
*/
|
|
111
|
-
transform: (node: SchemaNode) => T['output'] | null
|
|
111
|
+
transform: (node: SchemaNode) => T['output'] | null
|
|
112
112
|
/**
|
|
113
113
|
* Public printer. If the builder provides a root-level `print`, this calls that
|
|
114
114
|
* higher-level function (which may produce full declarations).
|
|
115
115
|
* Otherwise, falls back to the node-level dispatcher.
|
|
116
116
|
*/
|
|
117
|
-
print: (node: SchemaNode) => T['printOutput'] | null
|
|
117
|
+
print: (node: SchemaNode) => T['printOutput'] | null
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
@@ -147,24 +147,28 @@ type PrinterBuilder<T extends PrinterFactoryOptions> = (options: T['options']) =
|
|
|
147
147
|
*/
|
|
148
148
|
print?: (this: PrinterHandlerContext<T['output'], T['options']>, node: SchemaNode) => T['printOutput'] | null
|
|
149
149
|
}
|
|
150
|
-
|
|
151
150
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* Defines a schema printer: a function that takes a `SchemaNode` and emits
|
|
152
|
+
* code in your target language. Each plugin that produces code from schemas
|
|
153
|
+
* (TypeScript types, Zod schemas, Faker factories) ships a printer built
|
|
154
|
+
* with this helper.
|
|
155
155
|
*
|
|
156
156
|
* The builder receives resolved options and returns:
|
|
157
|
-
* - `name` — a unique identifier for the printer
|
|
158
|
-
* - `options` — options stored on the returned printer instance
|
|
159
|
-
* - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
|
|
160
|
-
* - `print` _(optional)_ — top-level override exposed as `printer.print`
|
|
161
|
-
* - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
|
|
162
|
-
* - This keeps recursion safe and avoids self-calls
|
|
163
157
|
*
|
|
164
|
-
*
|
|
158
|
+
* - `name` — unique identifier for the printer.
|
|
159
|
+
* - `options` — stored on the returned printer instance.
|
|
160
|
+
* - `nodes` — map of `SchemaType` → handler. Handlers return the rendered
|
|
161
|
+
* output (a string, a TypeScript AST node, ...) for that schema type.
|
|
162
|
+
* - `print` (optional) — top-level override exposed as `printer.print`.
|
|
163
|
+
* Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
|
|
165
164
|
*
|
|
166
|
-
*
|
|
165
|
+
* Without a `print` override, `printer.print` falls back to `printer.transform`
|
|
166
|
+
* (the node-level dispatcher).
|
|
167
|
+
*
|
|
168
|
+
* @example Tiny Zod printer
|
|
167
169
|
* ```ts
|
|
170
|
+
* import { definePrinter, type PrinterFactoryOptions } from '@kubb/ast'
|
|
171
|
+
*
|
|
168
172
|
* type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
|
|
169
173
|
*
|
|
170
174
|
* export const zodPrinter = definePrinter<PrinterZod>((options) => ({
|
|
@@ -173,7 +177,9 @@ type PrinterBuilder<T extends PrinterFactoryOptions> = (options: T['options']) =
|
|
|
173
177
|
* nodes: {
|
|
174
178
|
* string: () => 'z.string()',
|
|
175
179
|
* object(node) {
|
|
176
|
-
* const props = node.properties
|
|
180
|
+
* const props = node.properties
|
|
181
|
+
* .map((p) => `${p.name}: ${this.transform(p.schema)}`)
|
|
182
|
+
* .join(', ')
|
|
177
183
|
* return `z.object({ ${props} })`
|
|
178
184
|
* },
|
|
179
185
|
* },
|
|
@@ -194,7 +200,7 @@ export function definePrinter<T extends PrinterFactoryOptions = PrinterFactoryOp
|
|
|
194
200
|
* )
|
|
195
201
|
* ```
|
|
196
202
|
*/
|
|
197
|
-
export function createPrinterFactory<TNode, TKey extends string, TNodeByKey extends Partial<Record<TKey, TNode>>>(getKey: (node: TNode) => TKey |
|
|
203
|
+
export function createPrinterFactory<TNode, TKey extends string, TNodeByKey extends Partial<Record<TKey, TNode>>>(getKey: (node: TNode) => TKey | null) {
|
|
198
204
|
return function <T extends PrinterFactoryOptions>(
|
|
199
205
|
build: (options: T['options']) => {
|
|
200
206
|
name: T['name']
|
|
@@ -202,40 +208,40 @@ export function createPrinterFactory<TNode, TKey extends string, TNodeByKey exte
|
|
|
202
208
|
nodes: Partial<{
|
|
203
209
|
[K in TKey]: (
|
|
204
210
|
this: {
|
|
205
|
-
transform: (node: TNode) => T['output'] | null
|
|
211
|
+
transform: (node: TNode) => T['output'] | null
|
|
206
212
|
options: T['options']
|
|
207
213
|
},
|
|
208
214
|
node: TNodeByKey[K],
|
|
209
|
-
) => T['output'] | null
|
|
215
|
+
) => T['output'] | null
|
|
210
216
|
}>
|
|
211
217
|
print?: (
|
|
212
218
|
this: {
|
|
213
|
-
transform: (node: TNode) => T['output'] | null
|
|
219
|
+
transform: (node: TNode) => T['output'] | null
|
|
214
220
|
options: T['options']
|
|
215
221
|
},
|
|
216
222
|
node: TNode,
|
|
217
|
-
) => T['printOutput'] | null
|
|
223
|
+
) => T['printOutput'] | null
|
|
218
224
|
},
|
|
219
225
|
): (options?: T['options']) => {
|
|
220
226
|
name: T['name']
|
|
221
227
|
options: T['options']
|
|
222
|
-
transform: (node: TNode) => T['output'] | null
|
|
223
|
-
print: (node: TNode) => T['printOutput'] | null
|
|
228
|
+
transform: (node: TNode) => T['output'] | null
|
|
229
|
+
print: (node: TNode) => T['printOutput'] | null
|
|
224
230
|
} {
|
|
225
231
|
return (options) => {
|
|
226
232
|
const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? ({} as T['options']))
|
|
227
233
|
|
|
228
234
|
const context = {
|
|
229
235
|
options: resolvedOptions,
|
|
230
|
-
transform: (node: TNode): T['output'] | null
|
|
236
|
+
transform: (node: TNode): T['output'] | null => {
|
|
231
237
|
const key = getKey(node)
|
|
232
|
-
if (key ===
|
|
238
|
+
if (key === null) return null
|
|
233
239
|
|
|
234
240
|
const handler = nodes[key]
|
|
235
241
|
|
|
236
242
|
if (!handler) return null
|
|
237
243
|
|
|
238
|
-
return (handler as (this: typeof context, node: TNode) => T['output'] | null
|
|
244
|
+
return (handler as (this: typeof context, node: TNode) => T['output'] | null).call(context, node)
|
|
239
245
|
},
|
|
240
246
|
}
|
|
241
247
|
|
|
@@ -243,7 +249,7 @@ export function createPrinterFactory<TNode, TKey extends string, TNodeByKey exte
|
|
|
243
249
|
name,
|
|
244
250
|
options: resolvedOptions,
|
|
245
251
|
transform: context.transform,
|
|
246
|
-
print: (printOverride ? printOverride.bind(context) : context.transform) as (node: TNode) => T['printOutput'] | null
|
|
252
|
+
print: (printOverride ? printOverride.bind(context) : context.transform) as (node: TNode) => T['printOutput'] | null,
|
|
247
253
|
}
|
|
248
254
|
}
|
|
249
255
|
}
|
package/src/refs.ts
CHANGED
|
@@ -45,13 +45,15 @@ export function buildRefMap(input: InputNode): RefMap {
|
|
|
45
45
|
/**
|
|
46
46
|
* Resolves a schema by name from a `RefMap`.
|
|
47
47
|
*
|
|
48
|
+
* Returns `null` when the ref is not found.
|
|
49
|
+
*
|
|
48
50
|
* @example
|
|
49
51
|
* ```ts
|
|
50
52
|
* const petSchema = resolveRef(refMap, 'Pet')
|
|
51
53
|
* ```
|
|
52
54
|
*/
|
|
53
|
-
export function resolveRef(refMap: RefMap, ref: string): SchemaNode |
|
|
54
|
-
return refMap.get(ref)
|
|
55
|
+
export function resolveRef(refMap: RefMap, ref: string): SchemaNode | null {
|
|
56
|
+
return refMap.get(ref) ?? null
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
package/src/resolvers.ts
CHANGED
|
@@ -27,17 +27,17 @@ export function collectImports<TImport>({
|
|
|
27
27
|
}: {
|
|
28
28
|
node: SchemaNode
|
|
29
29
|
nameMapping: Map<string, string>
|
|
30
|
-
resolve: (schemaName: string) => TImport |
|
|
30
|
+
resolve: (schemaName: string) => TImport | null
|
|
31
31
|
}): Array<TImport> {
|
|
32
32
|
return collect<TImport>(node, {
|
|
33
|
-
schema(schemaNode): TImport |
|
|
33
|
+
schema(schemaNode): TImport | null {
|
|
34
34
|
const schemaRef = narrowSchema(schemaNode, 'ref')
|
|
35
|
-
if (!schemaRef?.ref) return
|
|
35
|
+
if (!schemaRef?.ref) return null
|
|
36
36
|
|
|
37
37
|
const rawName = extractRefName(schemaRef.ref)
|
|
38
38
|
const schemaName = nameMapping.get(rawName) ?? rawName
|
|
39
39
|
const result = resolve(schemaName)
|
|
40
|
-
if (!result) return
|
|
40
|
+
if (!result) return null
|
|
41
41
|
|
|
42
42
|
return result
|
|
43
43
|
},
|
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
|
/**
|
|
@@ -145,7 +150,7 @@ export function setEnumName(propNode: SchemaNode, parentName: string | null | un
|
|
|
145
150
|
const enumNode = narrowSchema(propNode, 'enum')
|
|
146
151
|
|
|
147
152
|
if (enumNode?.primitive === 'boolean') {
|
|
148
|
-
return { ...propNode, name:
|
|
153
|
+
return { ...propNode, name: null }
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
if (enumNode) {
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { VisitorDepth } from './constants.ts'
|
|
2
|
+
export type { SchemaDialect } from './dialect.ts'
|
|
3
|
+
export type { DispatchRule } from './dispatch.ts'
|
|
2
4
|
export type { DistributiveOmit } from './factory.ts'
|
|
3
5
|
export type { InferSchema, InferSchemaNode, ParserOptions } from './infer.ts'
|
|
4
6
|
export type {
|
|
@@ -21,11 +23,14 @@ export type {
|
|
|
21
23
|
FunctionParameterNode,
|
|
22
24
|
FunctionParametersNode,
|
|
23
25
|
FunctionParamNode,
|
|
26
|
+
GenericOperationNode,
|
|
24
27
|
HttpMethod,
|
|
28
|
+
HttpOperationNode,
|
|
25
29
|
HttpStatusCode,
|
|
26
30
|
ImportNode,
|
|
27
31
|
InputMeta,
|
|
28
32
|
InputNode,
|
|
33
|
+
InputStreamNode,
|
|
29
34
|
IntersectionSchemaNode,
|
|
30
35
|
Ipv4SchemaNode,
|
|
31
36
|
Ipv6SchemaNode,
|
|
@@ -37,6 +42,8 @@ export type {
|
|
|
37
42
|
NumberSchemaNode,
|
|
38
43
|
ObjectSchemaNode,
|
|
39
44
|
OperationNode,
|
|
45
|
+
OperationNodeBase,
|
|
46
|
+
OperationProtocol,
|
|
40
47
|
OutputNode,
|
|
41
48
|
ParameterGroupNode,
|
|
42
49
|
ParameterLocation,
|
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 {
|
|
@@ -554,15 +556,15 @@ 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
|
|
|
@@ -570,7 +572,7 @@ function importKey(path: string, name: string | undefined, isTypeOnly: boolean |
|
|
|
570
572
|
* Computes a multi-level sort key for exports and imports:
|
|
571
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
|
*
|
|
@@ -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)
|
|
@@ -662,6 +673,17 @@ 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>()
|
|
@@ -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,11 +773,11 @@ 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
|
/**
|
|
@@ -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
|
|
|
@@ -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
|
/**
|
|
@@ -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
|
}
|