@kubb/ast 5.0.0-beta.58 → 5.0.0-beta.59
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 +10 -0
- package/dist/casing-BE2R1RXg.cjs +88 -0
- package/dist/casing-BE2R1RXg.cjs.map +1 -0
- package/dist/extractStringsFromNodes-WMYJ8nQL.d.ts +82 -0
- package/dist/factory-BmcGBdeg.cjs +1251 -0
- package/dist/factory-BmcGBdeg.cjs.map +1 -0
- package/dist/factory-Du7nEP4B.js +282 -0
- package/dist/factory-Du7nEP4B.js.map +1 -0
- package/dist/factory.cjs +25 -28
- package/dist/factory.d.ts +3 -3
- package/dist/factory.js +3 -3
- package/dist/{ast-ClnJg9BN.d.ts → index-BzjwdK2M.d.ts} +74 -372
- package/dist/index.cjs +445 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +94 -59
- package/dist/index.js +7 -113
- package/dist/index.js.map +1 -1
- package/dist/operationParams-BZ07xDm0.d.ts +204 -0
- package/dist/response-DKxTr522.js +683 -0
- package/dist/response-DKxTr522.js.map +1 -0
- package/dist/{types-CB2oY8Dw.d.ts → types-olVl9v5p.d.ts} +222 -227
- package/dist/types.d.ts +4 -3
- package/dist/utils-BCtRXfhI.cjs +275 -0
- package/dist/utils-BCtRXfhI.cjs.map +1 -0
- package/dist/{utils-DN4XLVqz.js → utils-SdZU0F3H.js} +472 -1235
- package/dist/utils-SdZU0F3H.js.map +1 -0
- package/dist/utils.cjs +127 -22
- package/dist/utils.d.ts +112 -80
- package/dist/utils.js +3 -2
- package/package.json +1 -1
- package/src/constants.ts +8 -14
- package/src/dedupe.ts +8 -0
- package/src/factory.ts +1 -2
- package/src/guards.ts +1 -1
- package/src/index.ts +4 -13
- package/src/nodes/code.ts +29 -47
- package/src/nodes/content.ts +2 -2
- package/src/nodes/file.ts +28 -23
- package/src/nodes/function.ts +0 -15
- package/src/nodes/input.ts +6 -6
- package/src/nodes/operation.ts +1 -1
- package/src/nodes/parameter.ts +0 -3
- package/src/nodes/property.ts +0 -3
- package/src/nodes/requestBody.ts +3 -6
- package/src/nodes/schema.ts +3 -2
- package/src/printer.ts +1 -1
- package/src/registry.ts +31 -26
- package/src/signature.ts +3 -3
- package/src/transformers.ts +28 -1
- package/src/types.ts +2 -53
- package/src/utils/codegen.ts +104 -0
- package/src/utils/fileMerge.ts +184 -0
- package/src/utils/index.ts +7 -339
- package/src/utils/operationParams.ts +353 -0
- package/src/utils/refs.ts +112 -0
- package/src/utils/schemaGraph.ts +169 -0
- package/src/utils/strings.ts +139 -0
- package/src/visitor.ts +43 -19
- package/dist/extractStringsFromNodes-Bn9cOos9.d.ts +0 -14
- package/dist/factory-C5gHvtLU.js +0 -138
- package/dist/factory-C5gHvtLU.js.map +0 -1
- package/dist/factory-JN-Ylfl6.cjs +0 -155
- package/dist/factory-JN-Ylfl6.cjs.map +0 -1
- package/dist/utils-C8bWAzhv.cjs +0 -2696
- package/dist/utils-C8bWAzhv.cjs.map +0 -1
- package/dist/utils-DN4XLVqz.js.map +0 -1
- package/src/utils/ast.ts +0 -816
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { pascalCase } from '@internals/utils'
|
|
2
|
+
import { narrowSchema } from '../guards.ts'
|
|
3
|
+
import type { OperationNode, ParameterNode, SchemaNode } from '../nodes/index.ts'
|
|
4
|
+
import type { SchemaType } from '../nodes/schema.ts'
|
|
5
|
+
import type { OperationParamsResolver, ParamGroupType } from './operationParams.ts'
|
|
6
|
+
|
|
7
|
+
const plainStringTypes = new Set<SchemaType>(['string', 'uuid', 'email', 'url', 'datetime'] as const)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the last path segment of a reference string.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* extractRefName('#/components/schemas/Pet') // 'Pet'
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function extractRefName(ref: string): string {
|
|
18
|
+
return ref.split('/').at(-1) ?? ref
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
|
|
23
|
+
*
|
|
24
|
+
* Returns `null` for non-ref nodes or when no name resolves.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' })
|
|
29
|
+
* // => 'Pet'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function resolveRefName(node: SchemaNode | undefined): string | null {
|
|
33
|
+
if (!node || node.type !== 'ref') return null
|
|
34
|
+
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null
|
|
35
|
+
|
|
36
|
+
return node.name ?? node.schema?.name ?? null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds a PascalCase child schema name by joining a parent name and property name.
|
|
41
|
+
* Returns `null` when there is no parent to nest under.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* childName('Order', 'shipping_address') // 'OrderShippingAddress'
|
|
46
|
+
* childName(undefined, 'params') // null
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function childName(parentName: string | null | undefined, propName: string): string | null {
|
|
50
|
+
return parentName ? pascalCase([parentName, propName].join(' ')) : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds a PascalCase enum name from the parent name, property name, and a suffix, skipping any
|
|
55
|
+
* empty parts.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* enumPropName('Order', 'status', 'enum') // 'OrderStatusEnum'
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function enumPropName(parentName: string | null | undefined, propName: string, enumSuffix: string): string {
|
|
63
|
+
return pascalCase([parentName, propName, enumSuffix].filter(Boolean).join(' '))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Type guard that returns `true` when a schema emits as a plain `string` type.
|
|
68
|
+
*
|
|
69
|
+
* Covers `string`, `uuid`, `email`, `url`, and `datetime` types. For `date` and `time`
|
|
70
|
+
* types, returns `true` only when `representation` is `'string'` rather than `'date'`.
|
|
71
|
+
*/
|
|
72
|
+
export function isStringType(node: SchemaNode): boolean {
|
|
73
|
+
if (plainStringTypes.has(node.type)) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const temporal = narrowSchema(node, 'date') ?? narrowSchema(node, 'time')
|
|
78
|
+
if (temporal) {
|
|
79
|
+
return temporal.representation !== 'date'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Derives a {@link ParamGroupType} for a query or header group from the resolver.
|
|
87
|
+
*
|
|
88
|
+
* Returns `null` when there is no resolver, no params, or the group name equals the
|
|
89
|
+
* individual param name (so there is no real group to emit).
|
|
90
|
+
*/
|
|
91
|
+
export function resolveGroupType({
|
|
92
|
+
node,
|
|
93
|
+
params,
|
|
94
|
+
group,
|
|
95
|
+
resolver,
|
|
96
|
+
}: {
|
|
97
|
+
node: OperationNode
|
|
98
|
+
params: Array<ParameterNode>
|
|
99
|
+
group: 'query' | 'header'
|
|
100
|
+
resolver: OperationParamsResolver | undefined
|
|
101
|
+
}): ParamGroupType | null {
|
|
102
|
+
if (!resolver || !params.length) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
const firstParam = params[0]!
|
|
106
|
+
const groupMethod = group === 'query' ? resolver.resolveQueryParamsName : resolver.resolveHeaderParamsName
|
|
107
|
+
const groupName = groupMethod.call(resolver, node, firstParam)
|
|
108
|
+
if (groupName === resolver.resolveParamName(node, firstParam)) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
return { type: groupName, optional: params.every((p) => !p.required) }
|
|
112
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { memoize } from '@internals/utils'
|
|
2
|
+
import type { OperationNode, SchemaNode } from '../nodes/index.ts'
|
|
3
|
+
import { collect, collectLazy } from '../visitor.ts'
|
|
4
|
+
import { resolveRefName } from './refs.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collects every named schema referenced transitively from a node through its ref edges.
|
|
8
|
+
*
|
|
9
|
+
* Refs are followed by name only, so the resolved `node.schema` is never traversed inline.
|
|
10
|
+
*
|
|
11
|
+
* @example Collect refs from a single schema
|
|
12
|
+
* ```ts
|
|
13
|
+
* const names = collectReferencedSchemaNames(petSchema)
|
|
14
|
+
* // → Set { 'Category', 'Tag' }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example Accumulate refs from multiple schemas into one set
|
|
18
|
+
* ```ts
|
|
19
|
+
* const out = new Set<string>()
|
|
20
|
+
* for (const schema of schemas) {
|
|
21
|
+
* collectReferencedSchemaNames(schema, out)
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
const collectSchemaRefs = memoize(new WeakMap<SchemaNode, ReadonlySet<string>>(), (node: SchemaNode): ReadonlySet<string> => {
|
|
26
|
+
const refs = new Set<string>()
|
|
27
|
+
collect<void>(node, {
|
|
28
|
+
schema(child) {
|
|
29
|
+
if (child.type === 'ref') {
|
|
30
|
+
const name = resolveRefName(child)
|
|
31
|
+
if (name) refs.add(name)
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
return refs
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export function collectReferencedSchemaNames(node: SchemaNode | undefined, out: Set<string> = new Set()): Set<string> {
|
|
39
|
+
if (!node) return out
|
|
40
|
+
for (const name of collectSchemaRefs(node)) out.add(name)
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Collects the names of all top-level schemas transitively used by a set of operations.
|
|
46
|
+
*
|
|
47
|
+
* An operation uses a schema when its parameters, request body, or responses reference it, directly
|
|
48
|
+
* or through other named schemas. The walk is iterative, so reference cycles are safe.
|
|
49
|
+
*
|
|
50
|
+
* Pair it with `include` filters so schemas reachable only from excluded operations stay ungenerated.
|
|
51
|
+
*
|
|
52
|
+
* @example Only generate schemas referenced by included operations
|
|
53
|
+
* ```ts
|
|
54
|
+
* const includedOps = operations.filter((op) => resolver.resolveOptions(op, { options, include }) !== null)
|
|
55
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
56
|
+
*
|
|
57
|
+
* for (const schema of schemas) {
|
|
58
|
+
* if (schema.name && !allowed.has(schema.name)) continue
|
|
59
|
+
* // … generate schema
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
const collectUsedSchemaNamesMemo = memoize(new WeakMap<ReadonlyArray<OperationNode>, (schemas: ReadonlyArray<SchemaNode>) => Set<string>>(), (ops) =>
|
|
64
|
+
memoize(new WeakMap<ReadonlyArray<SchemaNode>, Set<string>>(), (schemas) => computeUsedSchemaNames(ops, schemas)),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
function computeUsedSchemaNames(operations: ReadonlyArray<OperationNode>, schemas: ReadonlyArray<SchemaNode>): Set<string> {
|
|
68
|
+
const schemaMap = new Map<string, SchemaNode>()
|
|
69
|
+
for (const schema of schemas) {
|
|
70
|
+
if (schema.name) schemaMap.set(schema.name, schema)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = new Set<string>()
|
|
74
|
+
|
|
75
|
+
function visitSchema(schema: SchemaNode): void {
|
|
76
|
+
const directRefs = collectReferencedSchemaNames(schema)
|
|
77
|
+
for (const name of directRefs) {
|
|
78
|
+
if (!result.has(name)) {
|
|
79
|
+
result.add(name)
|
|
80
|
+
const namedSchema = schemaMap.get(name)
|
|
81
|
+
if (namedSchema) visitSchema(namedSchema)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const op of operations) {
|
|
87
|
+
for (const schema of collectLazy<SchemaNode>(op, { depth: 'shallow', schema: (node) => node })) {
|
|
88
|
+
visitSchema(schema)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function collectUsedSchemaNames(operations: ReadonlyArray<OperationNode>, schemas: ReadonlyArray<SchemaNode>): Set<string> {
|
|
96
|
+
return collectUsedSchemaNamesMemo(operations)(schemas)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const EMPTY_CIRCULAR_SET = new Set<string>()
|
|
100
|
+
|
|
101
|
+
const findCircularSchemasMemo = memoize(new WeakMap<ReadonlyArray<SchemaNode>, Set<string>>(), (schemas: ReadonlyArray<SchemaNode>): Set<string> => {
|
|
102
|
+
const graph = new Map<string, Set<string>>()
|
|
103
|
+
|
|
104
|
+
for (const schema of schemas) {
|
|
105
|
+
if (!schema.name) continue
|
|
106
|
+
graph.set(schema.name, collectReferencedSchemaNames(schema))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const circular = new Set<string>()
|
|
110
|
+
for (const start of graph.keys()) {
|
|
111
|
+
const visited = new Set<string>()
|
|
112
|
+
const stack: Array<string> = [...(graph.get(start) ?? [])]
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const node = stack.pop()!
|
|
115
|
+
if (node === start) {
|
|
116
|
+
circular.add(start)
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
if (visited.has(node)) continue
|
|
120
|
+
visited.add(node)
|
|
121
|
+
|
|
122
|
+
const next = graph.get(node)
|
|
123
|
+
if (next) for (const r of next) stack.push(r)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return circular
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Finds every schema that takes part in a circular dependency chain, including direct self-loops.
|
|
132
|
+
*
|
|
133
|
+
* Wrap the returned schema positions in a deferred construct (a lazy getter or `z.lazy(() => …)`) so
|
|
134
|
+
* the generated code does not recurse forever. Refs are followed by name only, so the walk stays
|
|
135
|
+
* linear in the size of the schema graph.
|
|
136
|
+
*
|
|
137
|
+
* @note Call this once on the full graph, then check individual schemas with `containsCircularRef()`.
|
|
138
|
+
*/
|
|
139
|
+
export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<string> {
|
|
140
|
+
if (schemas.length === 0) return EMPTY_CIRCULAR_SET
|
|
141
|
+
return findCircularSchemasMemo(schemas)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns `true` when a schema, or anything nested inside it, references a circular schema.
|
|
146
|
+
*
|
|
147
|
+
* Pass `excludeName` to skip refs to a specific schema, which helps when self-references are handled
|
|
148
|
+
* on their own. Pair it with `findCircularSchemas()` to decide where lazy wrappers go.
|
|
149
|
+
*
|
|
150
|
+
* @note Stops at the first matching circular ref.
|
|
151
|
+
*/
|
|
152
|
+
export function containsCircularRef(
|
|
153
|
+
node: SchemaNode | undefined,
|
|
154
|
+
{ circularSchemas, excludeName }: { circularSchemas: ReadonlySet<string>; excludeName?: string },
|
|
155
|
+
): boolean {
|
|
156
|
+
if (!node || circularSchemas.size === 0) return false
|
|
157
|
+
|
|
158
|
+
for (const _ of collectLazy<true>(node, {
|
|
159
|
+
schema(child) {
|
|
160
|
+
if (child.type !== 'ref') return null
|
|
161
|
+
const name = resolveRefName(child)
|
|
162
|
+
return name && name !== excludeName && circularSchemas.has(name) ? true : null
|
|
163
|
+
},
|
|
164
|
+
})) {
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
|
|
3
|
+
* Returns the string unchanged when no balanced quote pair is found.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* trimQuotes('"hello"') // 'hello'
|
|
8
|
+
* trimQuotes('hello') // 'hello'
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export function trimQuotes(text: string): string {
|
|
12
|
+
if (text.length >= 2) {
|
|
13
|
+
const first = text[0]
|
|
14
|
+
const last = text[text.length - 1]
|
|
15
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'") || (first === '`' && last === '`')) {
|
|
16
|
+
return text.slice(1, -1)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return text
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Serializes a primitive to a single-quoted string literal, stripping any surrounding quotes first.
|
|
24
|
+
*
|
|
25
|
+
* Escaping runs through `JSON.stringify`, then the result switches to single quotes so the generated
|
|
26
|
+
* code matches the repo style without a formatter.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* stringify('hello') // "'hello'"
|
|
31
|
+
* stringify('"hello"') // "'hello'"
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function stringify(value: string | number | boolean | undefined): string {
|
|
35
|
+
if (value === undefined || value === null) return "''"
|
|
36
|
+
const json = JSON.stringify(trimQuotes(value.toString()))
|
|
37
|
+
const inner = json.slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'")
|
|
38
|
+
return `'${inner}'`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Escapes characters that are not allowed inside JS string literals, covering quotes, backslashes,
|
|
43
|
+
* and the Unicode line terminators U+2028 and U+2029.
|
|
44
|
+
*
|
|
45
|
+
* @see http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* jsStringEscape('say "hi"\nbye') // 'say \\"hi\\"\\nbye'
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function jsStringEscape(input: unknown): string {
|
|
53
|
+
return `${input}`.replace(/["'\\\n\r\u2028\u2029]/g, (character) => {
|
|
54
|
+
switch (character) {
|
|
55
|
+
case '"':
|
|
56
|
+
case "'":
|
|
57
|
+
case '\\':
|
|
58
|
+
return `\\${character}`
|
|
59
|
+
case '\n':
|
|
60
|
+
return '\\n'
|
|
61
|
+
case '\r':
|
|
62
|
+
return '\\r'
|
|
63
|
+
case '\u2028':
|
|
64
|
+
return '\\u2028'
|
|
65
|
+
case '\u2029':
|
|
66
|
+
return '\\u2029'
|
|
67
|
+
default:
|
|
68
|
+
return ''
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Converts a pattern string into a `new RegExp(...)` constructor call or a regex literal string.
|
|
75
|
+
* Inline flags expressed as a `^(?im)` prefix are extracted and applied to the resulting expression.
|
|
76
|
+
* Pass `null` as the second argument to emit a `/pattern/flags` literal instead.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* toRegExpString('^(?im)foo') // 'new RegExp("^foo", "im")'
|
|
81
|
+
* toRegExpString('^(?im)foo', null) // '/^foo/im'
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function toRegExpString(text: string, func: string | null = 'RegExp'): string {
|
|
85
|
+
const raw = trimQuotes(text)
|
|
86
|
+
|
|
87
|
+
const match = raw.match(/^\^(\(\?([igmsuy]+)\))/i)
|
|
88
|
+
const replacementTarget = match?.[1] ?? ''
|
|
89
|
+
const matchedFlags = match?.[2]
|
|
90
|
+
const cleaned = raw
|
|
91
|
+
.replace(/^\\?\//, '')
|
|
92
|
+
.replace(/\\?\/$/, '')
|
|
93
|
+
.replace(replacementTarget, '')
|
|
94
|
+
|
|
95
|
+
const { source, flags } = new RegExp(cleaned, matchedFlags)
|
|
96
|
+
|
|
97
|
+
if (func === null) return `/${source}/${flags}`
|
|
98
|
+
|
|
99
|
+
return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ''})`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Renders a plain object as multi-line `key: value` source for embedding in generated code. Nested
|
|
104
|
+
* objects recurse with fixed indentation, so the result drops straight into an object literal
|
|
105
|
+
* without re-parsing.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* stringifyObject({ foo: 'bar', nested: { a: 1 } })
|
|
110
|
+
* // 'foo: bar,\nnested: {\n a: 1\n }'
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function stringifyObject(value: Record<string, unknown>): string {
|
|
114
|
+
const items = Object.entries(value)
|
|
115
|
+
.map(([key, val]) => {
|
|
116
|
+
if (val !== null && typeof val === 'object') {
|
|
117
|
+
return `${key}: {\n ${stringifyObject(val as Record<string, unknown>)}\n }`
|
|
118
|
+
}
|
|
119
|
+
return `${key}: ${val}`
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
return items.join(',\n')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Renders a dotted path or string array as an optional-chaining accessor expression rooted at
|
|
127
|
+
* `accessor`. Returns `null` for an empty path.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* getNestedAccessor('pagination.next.id', 'lastPage')
|
|
132
|
+
* // "lastPage?.['pagination']?.['next']?.['id']"
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function getNestedAccessor(param: string | Array<string>, accessor: string): string | null {
|
|
136
|
+
const parts = Array.isArray(param) ? param : param.split('.')
|
|
137
|
+
if (parts.length === 0 || (parts.length === 1 && parts[0] === '')) return null
|
|
138
|
+
return `${accessor}?.['${`${parts.join("']?.['")}']`}`
|
|
139
|
+
}
|
package/src/visitor.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { VisitorDepth } from './constants.ts'
|
|
2
2
|
import { visitorDepths, WALK_CONCURRENCY } from './constants.ts'
|
|
3
|
-
import {
|
|
3
|
+
import type { NodeDef } from './node.ts'
|
|
4
4
|
import type {
|
|
5
5
|
ContentNode,
|
|
6
6
|
InputNode,
|
|
7
7
|
Node,
|
|
8
|
+
NodeKind,
|
|
8
9
|
OperationNode,
|
|
9
10
|
OutputNode,
|
|
10
11
|
ParameterNode,
|
|
@@ -13,6 +14,31 @@ import type {
|
|
|
13
14
|
ResponseNode,
|
|
14
15
|
SchemaNode,
|
|
15
16
|
} from './nodes/index.ts'
|
|
17
|
+
import { nodeDefs } from './registry.ts'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
|
|
21
|
+
* Derived from each definition's `children`.
|
|
22
|
+
*/
|
|
23
|
+
const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => (def.children ? [[def.kind, def.children] as const] : []))) as Partial<
|
|
24
|
+
Record<NodeKind, ReadonlyArray<string>>
|
|
25
|
+
>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maps a node kind to the matching visitor callback name. Derived from each
|
|
29
|
+
* definition's `visitorKey`.
|
|
30
|
+
*/
|
|
31
|
+
const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => (def.visitorKey ? [[def.kind, def.visitorKey] as const] : []))) as Partial<
|
|
32
|
+
Record<NodeKind, NonNullable<NodeDef['visitorKey']>>
|
|
33
|
+
>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Per-kind builders rerun after children are rebuilt. Derived from each
|
|
37
|
+
* definition's `rebuild` flag.
|
|
38
|
+
*/
|
|
39
|
+
const nodeRebuilders = Object.fromEntries(
|
|
40
|
+
nodeDefs.flatMap((def) => (def.rebuild ? [[def.kind, def.create as unknown as (node: Node) => Node] as const] : [])),
|
|
41
|
+
) as Partial<Record<NodeKind, (node: Node) => Node>>
|
|
16
42
|
|
|
17
43
|
/**
|
|
18
44
|
* Creates a small async concurrency limiter.
|
|
@@ -76,8 +102,7 @@ type ParentNodeMap = [
|
|
|
76
102
|
/**
|
|
77
103
|
* Resolves the parent node type for a given AST node type.
|
|
78
104
|
*
|
|
79
|
-
*
|
|
80
|
-
* for each callback.
|
|
105
|
+
* Visitor context relies on this so `ctx.parent` is typed for each callback.
|
|
81
106
|
*
|
|
82
107
|
* @example
|
|
83
108
|
* ```ts
|
|
@@ -133,8 +158,7 @@ export type VisitorContext<T extends Node = Node> = {
|
|
|
133
158
|
* to leave it untouched.
|
|
134
159
|
*
|
|
135
160
|
* Plugins typically expose `transformer` so users can supply a `Visitor` that
|
|
136
|
-
* rewrites
|
|
137
|
-
* before printing.
|
|
161
|
+
* rewrites the AST before printing.
|
|
138
162
|
*
|
|
139
163
|
* @example Prefix every operationId
|
|
140
164
|
* ```ts
|
|
@@ -165,7 +189,7 @@ export type Visitor = {
|
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
/**
|
|
168
|
-
*
|
|
192
|
+
* A visitor callback result that may be sync or async.
|
|
169
193
|
*/
|
|
170
194
|
type MaybePromise<T> = T | Promise<T>
|
|
171
195
|
|
|
@@ -229,7 +253,7 @@ type CollectVisitor<T> = {
|
|
|
229
253
|
*/
|
|
230
254
|
export type TransformOptions = Visitor & {
|
|
231
255
|
/**
|
|
232
|
-
* Traversal depth
|
|
256
|
+
* Traversal depth.
|
|
233
257
|
* @default 'deep'
|
|
234
258
|
*/
|
|
235
259
|
depth?: VisitorDepth
|
|
@@ -249,7 +273,7 @@ export type TransformOptions = Visitor & {
|
|
|
249
273
|
*/
|
|
250
274
|
export type WalkOptions = AsyncVisitor & {
|
|
251
275
|
/**
|
|
252
|
-
* Traversal depth
|
|
276
|
+
* Traversal depth.
|
|
253
277
|
* @default 'deep'
|
|
254
278
|
*/
|
|
255
279
|
depth?: VisitorDepth
|
|
@@ -270,7 +294,7 @@ export type WalkOptions = AsyncVisitor & {
|
|
|
270
294
|
*/
|
|
271
295
|
export type CollectOptions<T> = CollectVisitor<T> & {
|
|
272
296
|
/**
|
|
273
|
-
* Traversal depth
|
|
297
|
+
* Traversal depth.
|
|
274
298
|
* @default 'deep'
|
|
275
299
|
*/
|
|
276
300
|
depth?: VisitorDepth
|
|
@@ -318,9 +342,9 @@ function* getChildren(node: Node, recurse: boolean): Generator<Node, void, undef
|
|
|
318
342
|
}
|
|
319
343
|
|
|
320
344
|
/**
|
|
321
|
-
*
|
|
322
|
-
* context.
|
|
323
|
-
*
|
|
345
|
+
* Runs the visitor callback that matches `node.kind` with the traversal
|
|
346
|
+
* context. The result is a replacement node, a collected value, or `undefined`
|
|
347
|
+
* when no callback is registered for the kind.
|
|
324
348
|
*
|
|
325
349
|
* Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
|
|
326
350
|
* in one place. `TResult` is the caller's expected return: the same node type
|
|
@@ -369,7 +393,7 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
|
|
|
369
393
|
|
|
370
394
|
// Visit siblings concurrently and let the shared `limit` cap how many callbacks
|
|
371
395
|
// run at once. Awaiting each child sequentially here would serialize the whole
|
|
372
|
-
// traversal and make `concurrency` inert
|
|
396
|
+
// traversal and make `concurrency` inert. Every visitor callback would run one
|
|
373
397
|
// at a time regardless of the limit.
|
|
374
398
|
const children = Array.from(getChildren(node, recurse))
|
|
375
399
|
if (children.length === 0) return
|
|
@@ -378,11 +402,11 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
|
|
|
378
402
|
}
|
|
379
403
|
|
|
380
404
|
/**
|
|
381
|
-
* Synchronous depth-first transform. Each visitor callback
|
|
382
|
-
*
|
|
405
|
+
* Synchronous depth-first transform. Each visitor callback can return a
|
|
406
|
+
* replacement node. Returning `undefined` keeps the original.
|
|
383
407
|
*
|
|
384
|
-
* The
|
|
385
|
-
*
|
|
408
|
+
* The original tree is never mutated, a new tree is returned. Pass
|
|
409
|
+
* `depth: 'shallow'` to skip recursion into children.
|
|
386
410
|
*
|
|
387
411
|
* @example Prefix every operationId
|
|
388
412
|
* ```ts
|
|
@@ -489,8 +513,8 @@ export function* collectLazy<T>(node: Node, options: CollectOptions<T>): Generat
|
|
|
489
513
|
}
|
|
490
514
|
|
|
491
515
|
/**
|
|
492
|
-
* Eager depth-first collection pass.
|
|
493
|
-
*
|
|
516
|
+
* Eager depth-first collection pass. Gathers every non-null value the visitor
|
|
517
|
+
* callbacks return into an array.
|
|
494
518
|
*
|
|
495
519
|
* @example Collect every operationId
|
|
496
520
|
* ```ts
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { n as __name } from "./chunk-CNktS9qV.js";
|
|
2
|
-
import { Ht as CodeNode } from "./ast-ClnJg9BN.js";
|
|
3
|
-
|
|
4
|
-
//#region src/utils/extractStringsFromNodes.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* Extracts all string content from a `CodeNode` tree recursively.
|
|
7
|
-
*
|
|
8
|
-
* Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
|
|
9
|
-
* and nested node content. Used to build the full source string for import filtering.
|
|
10
|
-
*/
|
|
11
|
-
declare function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): string;
|
|
12
|
-
//#endregion
|
|
13
|
-
export { extractStringsFromNodes as t };
|
|
14
|
-
//# sourceMappingURL=extractStringsFromNodes-Bn9cOos9.d.ts.map
|