@kubb/ast 5.0.0-alpha.31 → 5.0.0-alpha.33
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/dist/index.cjs +1632 -1066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +33 -3
- package/dist/index.js +1594 -1066
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/{visitor-z-5U8NoF.d.ts → visitor-CJMIoAE3.d.ts} +893 -61
- package/package.json +1 -1
- package/src/constants.ts +9 -1
- package/src/factory.ts +350 -24
- package/src/guards.ts +17 -4
- package/src/index.ts +24 -5
- package/src/mocks.ts +6 -6
- package/src/nodes/base.ts +14 -2
- package/src/nodes/code.ts +304 -0
- package/src/nodes/file.ts +230 -0
- package/src/nodes/function.ts +35 -17
- package/src/nodes/index.ts +32 -7
- package/src/nodes/output.ts +26 -0
- package/src/nodes/root.ts +9 -8
- package/src/refs.ts +5 -5
- package/src/types.ts +17 -2
- package/src/utils.ts +208 -19
- package/src/visitor.ts +52 -25
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BaseNode } from './base.ts'
|
|
2
|
+
import type { FileNode } from './file.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Output AST node that groups all generated file output for one API document.
|
|
6
|
+
*
|
|
7
|
+
* Produced by generators and consumed by the build pipeline to write files.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const output: OutputNode = {
|
|
12
|
+
* kind: 'Output',
|
|
13
|
+
* files: [],
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export type OutputNode = BaseNode & {
|
|
18
|
+
/**
|
|
19
|
+
* Node kind.
|
|
20
|
+
*/
|
|
21
|
+
kind: 'Output'
|
|
22
|
+
/**
|
|
23
|
+
* Generated file nodes.
|
|
24
|
+
*/
|
|
25
|
+
files: Array<FileNode>
|
|
26
|
+
}
|
package/src/nodes/root.ts
CHANGED
|
@@ -8,10 +8,10 @@ import type { SchemaNode } from './schema.ts'
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* const meta:
|
|
11
|
+
* const meta: InputMeta = { title: 'Pet API', version: '1.0.0' }
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
|
-
export type
|
|
14
|
+
export type InputMeta = {
|
|
15
15
|
/**
|
|
16
16
|
* API title (from `info.title` in OAS/AsyncAPI).
|
|
17
17
|
*/
|
|
@@ -32,22 +32,23 @@ export type RootMeta = {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Input AST node that contains all schemas and operations for one API document.
|
|
36
|
+
* Produced by the adapter and consumed by all Kubb plugins.
|
|
36
37
|
*
|
|
37
38
|
* @example
|
|
38
39
|
* ```ts
|
|
39
|
-
* const
|
|
40
|
-
* kind: '
|
|
40
|
+
* const input: InputNode = {
|
|
41
|
+
* kind: 'Input',
|
|
41
42
|
* schemas: [],
|
|
42
43
|
* operations: [],
|
|
43
44
|
* }
|
|
44
45
|
* ```
|
|
45
46
|
*/
|
|
46
|
-
export type
|
|
47
|
+
export type InputNode = BaseNode & {
|
|
47
48
|
/**
|
|
48
49
|
* Node kind.
|
|
49
50
|
*/
|
|
50
|
-
kind: '
|
|
51
|
+
kind: 'Input'
|
|
51
52
|
/**
|
|
52
53
|
* All schema nodes in the document.
|
|
53
54
|
*/
|
|
@@ -59,5 +60,5 @@ export type RootNode = BaseNode & {
|
|
|
59
60
|
/**
|
|
60
61
|
* Optional document metadata populated by the adapter.
|
|
61
62
|
*/
|
|
62
|
-
meta?:
|
|
63
|
+
meta?: InputMeta
|
|
63
64
|
}
|
package/src/refs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { InputNode } from './nodes/root.ts'
|
|
2
2
|
import type { SchemaNode } from './nodes/schema.ts'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -21,20 +21,20 @@ export function extractRefName(ref: string): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Builds a `RefMap` from `
|
|
24
|
+
* Builds a `RefMap` from `input.schemas` using each schema's `name`.
|
|
25
25
|
*
|
|
26
26
|
* Unnamed schemas are skipped.
|
|
27
27
|
*
|
|
28
28
|
* @example
|
|
29
29
|
* ```ts
|
|
30
|
-
* const refMap = buildRefMap(
|
|
30
|
+
* const refMap = buildRefMap(input)
|
|
31
31
|
* const pet = refMap.get('Pet')
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
|
-
export function buildRefMap(
|
|
34
|
+
export function buildRefMap(input: InputNode): RefMap {
|
|
35
35
|
const map: RefMap = new Map()
|
|
36
36
|
|
|
37
|
-
for (const schema of
|
|
37
|
+
for (const schema of input.schemas) {
|
|
38
38
|
if (schema.name) {
|
|
39
39
|
map.set(schema.name, schema)
|
|
40
40
|
}
|
package/src/types.ts
CHANGED
|
@@ -3,46 +3,61 @@ export type { DistributiveOmit } from './factory.ts'
|
|
|
3
3
|
export type { InferSchema, InferSchemaNode, ParserOptions } from './infer.ts'
|
|
4
4
|
export type {
|
|
5
5
|
ArraySchemaNode,
|
|
6
|
+
ArrowFunctionNode,
|
|
6
7
|
BaseNode,
|
|
8
|
+
BreakNode,
|
|
9
|
+
CodeNode,
|
|
7
10
|
ComplexSchemaType,
|
|
11
|
+
ConstNode,
|
|
8
12
|
DateSchemaNode,
|
|
9
13
|
DatetimeSchemaNode,
|
|
10
14
|
EnumSchemaNode,
|
|
11
15
|
EnumValueNode,
|
|
16
|
+
ExportNode,
|
|
17
|
+
FileNode,
|
|
12
18
|
FormatStringSchemaNode,
|
|
13
19
|
FunctionNode,
|
|
14
20
|
FunctionNodeType,
|
|
15
21
|
FunctionParameterNode,
|
|
16
22
|
FunctionParametersNode,
|
|
23
|
+
FunctionParamNode,
|
|
17
24
|
HttpMethod,
|
|
18
25
|
HttpStatusCode,
|
|
26
|
+
ImportNode,
|
|
27
|
+
InputMeta,
|
|
28
|
+
InputNode,
|
|
19
29
|
IntersectionSchemaNode,
|
|
20
30
|
Ipv4SchemaNode,
|
|
21
31
|
Ipv6SchemaNode,
|
|
32
|
+
JSDocNode,
|
|
33
|
+
JsxNode,
|
|
22
34
|
MediaType,
|
|
23
35
|
Node,
|
|
24
36
|
NodeKind,
|
|
25
37
|
NumberSchemaNode,
|
|
26
38
|
ObjectSchemaNode,
|
|
27
39
|
OperationNode,
|
|
40
|
+
OutputNode,
|
|
28
41
|
ParameterGroupNode,
|
|
29
42
|
ParameterLocation,
|
|
30
43
|
ParameterNode,
|
|
44
|
+
ParamsTypeNode,
|
|
31
45
|
PrimitiveSchemaType,
|
|
32
46
|
PropertyNode,
|
|
33
47
|
RefSchemaNode,
|
|
34
48
|
ResponseNode,
|
|
35
|
-
RootMeta,
|
|
36
|
-
RootNode,
|
|
37
49
|
ScalarSchemaNode,
|
|
38
50
|
ScalarSchemaType,
|
|
39
51
|
SchemaNode,
|
|
40
52
|
SchemaNodeByType,
|
|
41
53
|
SchemaType,
|
|
54
|
+
SourceNode,
|
|
42
55
|
SpecialSchemaType,
|
|
43
56
|
StatusCode,
|
|
44
57
|
StringSchemaNode,
|
|
58
|
+
TextNode,
|
|
45
59
|
TimeSchemaNode,
|
|
60
|
+
TypeDeclarationNode,
|
|
46
61
|
TypeNode,
|
|
47
62
|
UnionSchemaNode,
|
|
48
63
|
UrlSchemaNode,
|
package/src/utils.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { camelCase, isValidVarName } from '@internals/utils'
|
|
2
2
|
|
|
3
|
-
import { createFunctionParameter, createFunctionParameters, createParameterGroup, createProperty, createSchema
|
|
3
|
+
import { createFunctionParameter, createFunctionParameters, createParameterGroup, createParamsType, createProperty, createSchema } from './factory.ts'
|
|
4
4
|
import { narrowSchema } from './guards.ts'
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
CodeNode,
|
|
7
|
+
ExportNode,
|
|
8
|
+
FunctionParameterNode,
|
|
9
|
+
FunctionParametersNode,
|
|
10
|
+
ImportNode,
|
|
11
|
+
OperationNode,
|
|
12
|
+
ParameterGroupNode,
|
|
13
|
+
ParameterNode,
|
|
14
|
+
ParamsTypeNode,
|
|
15
|
+
SchemaNode,
|
|
16
|
+
SourceNode,
|
|
17
|
+
} from './nodes/index.ts'
|
|
6
18
|
import type { SchemaType } from './nodes/schema.ts'
|
|
7
19
|
|
|
8
20
|
const plainStringTypes = new Set<SchemaType>(['string', 'uuid', 'email', 'url', 'datetime'] as const)
|
|
@@ -118,7 +130,7 @@ export type ParamGroupType = {
|
|
|
118
130
|
/**
|
|
119
131
|
* TypeNode for the group type.
|
|
120
132
|
*/
|
|
121
|
-
type:
|
|
133
|
+
type: ParamsTypeNode
|
|
122
134
|
/**
|
|
123
135
|
* Whether the parameter group is optional.
|
|
124
136
|
*/
|
|
@@ -251,9 +263,17 @@ export type CreateOperationParamsOptions = {
|
|
|
251
263
|
typeWrapper?: (type: string) => string
|
|
252
264
|
}
|
|
253
265
|
|
|
254
|
-
function
|
|
266
|
+
function resolveParamsType({
|
|
267
|
+
node,
|
|
268
|
+
param,
|
|
269
|
+
resolver,
|
|
270
|
+
}: {
|
|
271
|
+
node: OperationNode
|
|
272
|
+
param: ParameterNode
|
|
273
|
+
resolver: OperationParamsResolver | undefined
|
|
274
|
+
}): ParamsTypeNode {
|
|
255
275
|
if (!resolver) {
|
|
256
|
-
return
|
|
276
|
+
return createParamsType({ variant: 'reference', name: param.schema.primitive ?? 'unknown' })
|
|
257
277
|
}
|
|
258
278
|
|
|
259
279
|
const individualName = resolver.resolveParamName(node, param)
|
|
@@ -269,10 +289,10 @@ function resolveType({ node, param, resolver }: { node: OperationNode; param: Pa
|
|
|
269
289
|
const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : undefined
|
|
270
290
|
|
|
271
291
|
if (groupName && groupName !== individualName) {
|
|
272
|
-
return
|
|
292
|
+
return createParamsType({ variant: 'member', base: groupName, key: param.name })
|
|
273
293
|
}
|
|
274
294
|
|
|
275
|
-
return
|
|
295
|
+
return createParamsType({ variant: 'reference', name: individualName })
|
|
276
296
|
}
|
|
277
297
|
|
|
278
298
|
/**
|
|
@@ -287,7 +307,7 @@ function resolveType({ node, param, resolver }: { node: OperationNode; param: Pa
|
|
|
287
307
|
* paramsType: 'inline',
|
|
288
308
|
* pathParamsType: 'inline',
|
|
289
309
|
* resolver: tsResolver,
|
|
290
|
-
* extraParams: [createFunctionParameter({ name: 'options', type:
|
|
310
|
+
* extraParams: [createFunctionParameter({ name: 'options', type: createParamsType({ variant: 'reference', name: 'Partial<RequestOptions>' }), default: '{}' })],
|
|
291
311
|
* })
|
|
292
312
|
* ```
|
|
293
313
|
*/
|
|
@@ -299,10 +319,10 @@ export function createOperationParams(node: OperationNode, options: CreateOperat
|
|
|
299
319
|
const headersName = paramNames?.headers ?? 'headers'
|
|
300
320
|
const pathName = paramNames?.path ?? 'pathParams'
|
|
301
321
|
|
|
302
|
-
const wrapType = (type: string):
|
|
303
|
-
// Only reference TypeNodes are wrapped
|
|
322
|
+
const wrapType = (type: string): ParamsTypeNode => createParamsType({ variant: 'reference', name: typeWrapper ? typeWrapper(type) : type })
|
|
323
|
+
// Only reference-variant TypeNodes are wrapped — they hold a plain type name string that needs casing applied.
|
|
304
324
|
// Member and struct TypeNodes are pre-resolved structured expressions and are passed through unchanged.
|
|
305
|
-
const wrapTypeNode = (type:
|
|
325
|
+
const wrapTypeNode = (type: ParamsTypeNode): ParamsTypeNode => (type.kind === 'ParamsType' && type.variant === 'reference' ? wrapType(type.name) : type)
|
|
306
326
|
|
|
307
327
|
const casedParams = caseParams(node.parameters, paramsCasing)
|
|
308
328
|
const pathParams = casedParams.filter((p) => p.in === 'path')
|
|
@@ -320,7 +340,7 @@ export function createOperationParams(node: OperationNode, options: CreateOperat
|
|
|
320
340
|
if (paramsType === 'object') {
|
|
321
341
|
const children: Array<FunctionParameterNode> = [
|
|
322
342
|
...pathParams.map((p) => {
|
|
323
|
-
const type =
|
|
343
|
+
const type = resolveParamsType({ node, param: p, resolver })
|
|
324
344
|
return createFunctionParameter({ name: p.name, type: wrapTypeNode(type), optional: !p.required })
|
|
325
345
|
}),
|
|
326
346
|
...(bodyType ? [createFunctionParameter({ name: dataName, type: bodyType, optional: !bodyRequired })] : []),
|
|
@@ -338,7 +358,7 @@ export function createOperationParams(node: OperationNode, options: CreateOperat
|
|
|
338
358
|
params.push(createFunctionParameter({ name: pathName, type: spreadType ? wrapType(spreadType) : undefined, rest: true }))
|
|
339
359
|
} else {
|
|
340
360
|
const pathChildren = pathParams.map((p) => {
|
|
341
|
-
const type =
|
|
361
|
+
const type = resolveParamsType({ node, param: p, resolver })
|
|
342
362
|
return createFunctionParameter({ name: p.name, type: wrapTypeNode(type), optional: !p.required })
|
|
343
363
|
})
|
|
344
364
|
params.push(
|
|
@@ -384,10 +404,10 @@ function buildGroupParam({
|
|
|
384
404
|
params: Array<ParameterNode>
|
|
385
405
|
groupType: ParamGroupType | undefined
|
|
386
406
|
resolver: OperationParamsResolver | undefined
|
|
387
|
-
wrapType: (type: string) =>
|
|
407
|
+
wrapType: (type: string) => ParamsTypeNode
|
|
388
408
|
}): Array<FunctionParameterNode> {
|
|
389
409
|
if (groupType) {
|
|
390
|
-
const type = groupType.type.variant === 'reference' ? wrapType(groupType.type.name) : groupType.type
|
|
410
|
+
const type = groupType.type.kind === 'ParamsType' && groupType.type.variant === 'reference' ? wrapType(groupType.type.name) : groupType.type
|
|
391
411
|
return [createFunctionParameter({ name, type, optional: groupType.optional })]
|
|
392
412
|
}
|
|
393
413
|
if (params.length) {
|
|
@@ -426,7 +446,7 @@ function resolveGroupType({
|
|
|
426
446
|
return undefined
|
|
427
447
|
}
|
|
428
448
|
const allOptional = params.every((p) => !p.required)
|
|
429
|
-
return { type:
|
|
449
|
+
return { type: createParamsType({ variant: 'reference', name: groupName }), optional: allOptional }
|
|
430
450
|
}
|
|
431
451
|
|
|
432
452
|
/**
|
|
@@ -443,9 +463,178 @@ function toStructType({
|
|
|
443
463
|
node: OperationNode
|
|
444
464
|
params: Array<ParameterNode>
|
|
445
465
|
resolver: OperationParamsResolver | undefined
|
|
446
|
-
}):
|
|
447
|
-
return
|
|
466
|
+
}): ParamsTypeNode {
|
|
467
|
+
return createParamsType({
|
|
448
468
|
variant: 'struct',
|
|
449
|
-
properties: params.map((p) => ({ name: p.name, optional: !p.required, type:
|
|
469
|
+
properties: params.map((p) => ({ name: p.name, optional: !p.required, type: resolveParamsType({ node, param: p, resolver }) })),
|
|
450
470
|
})
|
|
451
471
|
}
|
|
472
|
+
|
|
473
|
+
function sourceKey(source: SourceNode): string {
|
|
474
|
+
const nameKey = source.name ?? extractStringsFromNodes(source.nodes)
|
|
475
|
+
return `${nameKey}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function pathTypeKey(path: string, isTypeOnly: boolean | undefined): string {
|
|
479
|
+
return `${path}:${isTypeOnly ?? false}`
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function exportKey(path: string, name: string | undefined, isTypeOnly: boolean | undefined, asAlias: boolean | undefined): string {
|
|
483
|
+
return `${path}:${name ?? ''}:${isTypeOnly ?? false}:${asAlias ?? ''}`
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function importKey(path: string, name: string | undefined, isTypeOnly: boolean | undefined): string {
|
|
487
|
+
return `${path}:${name ?? ''}:${isTypeOnly ?? false}`
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Computes a multi-level sort key for exports and imports:
|
|
492
|
+
* non-array names first (wildcards/namespace aliases); type-only before value; alphabetical path; unnamed before named.
|
|
493
|
+
*/
|
|
494
|
+
function sortKey(node: { name?: string | Array<unknown>; isTypeOnly?: boolean; path: string }): string {
|
|
495
|
+
const isArray = Array.isArray(node.name) ? '1' : '0'
|
|
496
|
+
const typeOnly = node.isTypeOnly ? '0' : '1'
|
|
497
|
+
const hasName = node.name != null ? '1' : '0'
|
|
498
|
+
const name = Array.isArray(node.name) ? [...node.name].sort().join('\0') : (node.name ?? '')
|
|
499
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Deduplicates an array of `SourceNode` objects.
|
|
504
|
+
* Named sources are deduplicated by `name + isExportable + isTypeOnly`.
|
|
505
|
+
* Unnamed sources are deduplicated by object reference.
|
|
506
|
+
*/
|
|
507
|
+
export function combineSources(sources: Array<SourceNode>): Array<SourceNode> {
|
|
508
|
+
const seen = new Map<string, SourceNode>()
|
|
509
|
+
for (const source of sources) {
|
|
510
|
+
const key = sourceKey(source)
|
|
511
|
+
if (!seen.has(key)) seen.set(key, source)
|
|
512
|
+
}
|
|
513
|
+
return [...seen.values()]
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Deduplicates and merges an array of `ExportNode` objects.
|
|
518
|
+
* Exports with the same path and `isTypeOnly` flag have their names merged.
|
|
519
|
+
*/
|
|
520
|
+
export function combineExports(exports: Array<ExportNode>): Array<ExportNode> {
|
|
521
|
+
const result: Array<ExportNode> = []
|
|
522
|
+
// Accumulates array-named exports keyed by `path:isTypeOnly` for name-merging
|
|
523
|
+
const namedByPath = new Map<string, ExportNode>()
|
|
524
|
+
// Deduplicates non-array exports by their exact identity
|
|
525
|
+
const seen = new Set<string>()
|
|
526
|
+
|
|
527
|
+
for (const curr of [...exports].sort((a, b) => {
|
|
528
|
+
const ka = sortKey(a)
|
|
529
|
+
const kb = sortKey(b)
|
|
530
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0
|
|
531
|
+
})) {
|
|
532
|
+
const { name, path, isTypeOnly, asAlias } = curr
|
|
533
|
+
|
|
534
|
+
if (Array.isArray(name)) {
|
|
535
|
+
if (!name.length) continue
|
|
536
|
+
|
|
537
|
+
const key = pathTypeKey(path, isTypeOnly)
|
|
538
|
+
const existing = namedByPath.get(key)
|
|
539
|
+
|
|
540
|
+
if (existing && Array.isArray(existing.name)) {
|
|
541
|
+
existing.name = [...new Set([...existing.name, ...name])]
|
|
542
|
+
} else {
|
|
543
|
+
const newItem: ExportNode = { ...curr, name: [...new Set(name)] }
|
|
544
|
+
result.push(newItem)
|
|
545
|
+
namedByPath.set(key, newItem)
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
const key = exportKey(path, name, isTypeOnly, asAlias)
|
|
549
|
+
if (!seen.has(key)) {
|
|
550
|
+
result.push(curr)
|
|
551
|
+
seen.add(key)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return result
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Deduplicates and merges an array of `ImportNode` objects.
|
|
561
|
+
* Filters out unused imports (names not referenced in `source` or re-exported).
|
|
562
|
+
* Imports with the same path and `isTypeOnly` flag have their names merged.
|
|
563
|
+
*/
|
|
564
|
+
export function combineImports(imports: Array<ImportNode>, exports: Array<ExportNode>, source?: string): Array<ImportNode> {
|
|
565
|
+
// Build a lookup of all exported names to retain imports that are re-exported
|
|
566
|
+
const exportedNames = new Set(exports.flatMap((e) => (Array.isArray(e.name) ? e.name : e.name ? [e.name] : [])))
|
|
567
|
+
const isUsed = (importName: string): boolean => !source || source.includes(importName) || exportedNames.has(importName)
|
|
568
|
+
|
|
569
|
+
const result: Array<ImportNode> = []
|
|
570
|
+
// Accumulates array-named imports keyed by `path:isTypeOnly` for name-merging
|
|
571
|
+
const namedByPath = new Map<string, ImportNode>()
|
|
572
|
+
// Deduplicates non-array imports by their exact identity
|
|
573
|
+
const seen = new Set<string>()
|
|
574
|
+
|
|
575
|
+
for (const curr of [...imports].sort((a, b) => {
|
|
576
|
+
const ka = sortKey(a)
|
|
577
|
+
const kb = sortKey(b)
|
|
578
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0
|
|
579
|
+
})) {
|
|
580
|
+
if (curr.path === curr.root) continue
|
|
581
|
+
|
|
582
|
+
const { path, isTypeOnly } = curr
|
|
583
|
+
let { name } = curr
|
|
584
|
+
|
|
585
|
+
if (Array.isArray(name)) {
|
|
586
|
+
name = [...new Set(name)].filter((item) => (typeof item === 'string' ? isUsed(item) : isUsed(item.propertyName)))
|
|
587
|
+
if (!name.length) continue
|
|
588
|
+
|
|
589
|
+
const key = pathTypeKey(path, isTypeOnly)
|
|
590
|
+
const existing = namedByPath.get(key)
|
|
591
|
+
|
|
592
|
+
if (existing && Array.isArray(existing.name)) {
|
|
593
|
+
existing.name = [...new Set([...existing.name, ...name])]
|
|
594
|
+
} else {
|
|
595
|
+
const newItem: ImportNode = { ...curr, name }
|
|
596
|
+
result.push(newItem)
|
|
597
|
+
namedByPath.set(key, newItem)
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
if (name && !isUsed(name)) continue
|
|
601
|
+
|
|
602
|
+
const key = importKey(path, name, isTypeOnly)
|
|
603
|
+
if (!seen.has(key)) {
|
|
604
|
+
result.push(curr)
|
|
605
|
+
seen.add(key)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return result
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Recursively extracts all string content embedded in a {@link CodeNode} tree.
|
|
615
|
+
*
|
|
616
|
+
* Includes text node values, and string attribute fields (`params`, `generics`,
|
|
617
|
+
* `returnType`, `type`) that may reference identifiers needing imports.
|
|
618
|
+
* Used by `createFile` to build the full source string for import filtering.
|
|
619
|
+
*/
|
|
620
|
+
export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): string {
|
|
621
|
+
if (!nodes?.length) return ''
|
|
622
|
+
return nodes
|
|
623
|
+
.map((node) => {
|
|
624
|
+
// Backward-compat: compiled plugins may still pass bare strings at runtime
|
|
625
|
+
if (typeof node === 'string') return node as string
|
|
626
|
+
if (node.kind === 'Text') return node.value
|
|
627
|
+
if (node.kind === 'Break') return ''
|
|
628
|
+
if (node.kind === 'Jsx') return node.value
|
|
629
|
+
const parts: string[] = []
|
|
630
|
+
if ('params' in node && node.params) parts.push(node.params)
|
|
631
|
+
if ('generics' in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(', ') : node.generics)
|
|
632
|
+
if ('returnType' in node && node.returnType) parts.push(node.returnType)
|
|
633
|
+
if ('type' in node && typeof node.type === 'string') parts.push(node.type)
|
|
634
|
+
const nested = extractStringsFromNodes(node.nodes)
|
|
635
|
+
if (nested) parts.push(nested)
|
|
636
|
+
return parts.join('\n')
|
|
637
|
+
})
|
|
638
|
+
.filter(Boolean)
|
|
639
|
+
.join('\n')
|
|
640
|
+
}
|
package/src/visitor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { VisitorDepth } from './constants.ts'
|
|
2
2
|
import { visitorDepths, WALK_CONCURRENCY } from './constants.ts'
|
|
3
3
|
import { createParameter, createProperty } from './factory.ts'
|
|
4
|
-
import type { Node, OperationNode, ParameterNode, PropertyNode, ResponseNode,
|
|
4
|
+
import type { InputNode, Node, OperationNode, OutputNode, ParameterNode, PropertyNode, ResponseNode, SchemaNode } from './nodes/index.ts'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Creates a small async concurrency limiter.
|
|
@@ -51,9 +51,10 @@ type LimitFn = ReturnType<typeof createLimit>
|
|
|
51
51
|
* `ParentOf` uses this map to find parent types.
|
|
52
52
|
*/
|
|
53
53
|
type ParentNodeMap = [
|
|
54
|
-
[
|
|
55
|
-
[
|
|
56
|
-
[
|
|
54
|
+
[InputNode, undefined],
|
|
55
|
+
[OutputNode, undefined],
|
|
56
|
+
[OperationNode, InputNode],
|
|
57
|
+
[SchemaNode, InputNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode],
|
|
57
58
|
[PropertyNode, SchemaNode],
|
|
58
59
|
[ParameterNode, OperationNode],
|
|
59
60
|
[ResponseNode, OperationNode],
|
|
@@ -67,7 +68,7 @@ type ParentNodeMap = [
|
|
|
67
68
|
*
|
|
68
69
|
* @example
|
|
69
70
|
* ```ts
|
|
70
|
-
* type
|
|
71
|
+
* type InputParent = ParentOf<InputNode>
|
|
71
72
|
* // undefined
|
|
72
73
|
* ```
|
|
73
74
|
*
|
|
@@ -80,7 +81,7 @@ type ParentNodeMap = [
|
|
|
80
81
|
* @example
|
|
81
82
|
* ```ts
|
|
82
83
|
* type SchemaParent = ParentOf<SchemaNode>
|
|
83
|
-
* //
|
|
84
|
+
* // InputNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode
|
|
84
85
|
* ```
|
|
85
86
|
*/
|
|
86
87
|
export type ParentOf<T extends Node, TEntries extends ReadonlyArray<[Node, unknown]> = ParentNodeMap> = TEntries extends [
|
|
@@ -108,7 +109,7 @@ export type ParentOf<T extends Node, TEntries extends ReadonlyArray<[Node, unkno
|
|
|
108
109
|
export type VisitorContext<T extends Node = Node> = {
|
|
109
110
|
/**
|
|
110
111
|
* Parent node of the currently visited node.
|
|
111
|
-
* For `
|
|
112
|
+
* For `InputNode`, this is `undefined`.
|
|
112
113
|
*/
|
|
113
114
|
parent?: ParentOf<T>
|
|
114
115
|
}
|
|
@@ -126,7 +127,8 @@ export type VisitorContext<T extends Node = Node> = {
|
|
|
126
127
|
* ```
|
|
127
128
|
*/
|
|
128
129
|
export type Visitor = {
|
|
129
|
-
|
|
130
|
+
input?(node: InputNode, context: VisitorContext<InputNode>): void | InputNode
|
|
131
|
+
output?(node: OutputNode, context: VisitorContext<OutputNode>): void | OutputNode
|
|
130
132
|
operation?(node: OperationNode, context: VisitorContext<OperationNode>): void | OperationNode
|
|
131
133
|
schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): void | SchemaNode
|
|
132
134
|
property?(node: PropertyNode, context: VisitorContext<PropertyNode>): void | PropertyNode
|
|
@@ -152,7 +154,8 @@ type MaybePromise<T> = T | Promise<T>
|
|
|
152
154
|
* ```
|
|
153
155
|
*/
|
|
154
156
|
export type AsyncVisitor = {
|
|
155
|
-
|
|
157
|
+
input?(node: InputNode, context: VisitorContext<InputNode>): MaybePromise<void | InputNode>
|
|
158
|
+
output?(node: OutputNode, context: VisitorContext<OutputNode>): MaybePromise<void | OutputNode>
|
|
156
159
|
operation?(node: OperationNode, context: VisitorContext<OperationNode>): MaybePromise<void | OperationNode>
|
|
157
160
|
schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): MaybePromise<void | SchemaNode>
|
|
158
161
|
property?(node: PropertyNode, context: VisitorContext<PropertyNode>): MaybePromise<void | PropertyNode>
|
|
@@ -173,7 +176,8 @@ export type AsyncVisitor = {
|
|
|
173
176
|
* ```
|
|
174
177
|
*/
|
|
175
178
|
export type CollectVisitor<T> = {
|
|
176
|
-
|
|
179
|
+
input?(node: InputNode, context: VisitorContext<InputNode>): T | undefined
|
|
180
|
+
output?(node: OutputNode, context: VisitorContext<OutputNode>): T | undefined
|
|
177
181
|
operation?(node: OperationNode, context: VisitorContext<OperationNode>): T | undefined
|
|
178
182
|
schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): T | undefined
|
|
179
183
|
property?(node: PropertyNode, context: VisitorContext<PropertyNode>): T | undefined
|
|
@@ -263,8 +267,10 @@ export type CollectOptions<T> = CollectVisitor<T> & {
|
|
|
263
267
|
*/
|
|
264
268
|
function getChildren(node: Node, recurse: boolean): Array<Node> {
|
|
265
269
|
switch (node.kind) {
|
|
266
|
-
case '
|
|
270
|
+
case 'Input':
|
|
267
271
|
return [...node.schemas, ...node.operations]
|
|
272
|
+
case 'Output':
|
|
273
|
+
return []
|
|
268
274
|
case 'Operation':
|
|
269
275
|
return [...node.parameters, ...(node.requestBody?.schema ? [node.requestBody.schema] : []), ...node.responses]
|
|
270
276
|
case 'Schema': {
|
|
@@ -290,6 +296,8 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
|
|
|
290
296
|
case 'FunctionParameters':
|
|
291
297
|
case 'Type':
|
|
292
298
|
return []
|
|
299
|
+
default:
|
|
300
|
+
return []
|
|
293
301
|
}
|
|
294
302
|
}
|
|
295
303
|
|
|
@@ -322,8 +330,11 @@ export async function walk(node: Node, options: WalkOptions): Promise<void> {
|
|
|
322
330
|
|
|
323
331
|
async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn, parent: Node | undefined): Promise<void> {
|
|
324
332
|
switch (node.kind) {
|
|
325
|
-
case '
|
|
326
|
-
await limit(() => visitor.
|
|
333
|
+
case 'Input':
|
|
334
|
+
await limit(() => visitor.input?.(node, { parent: parent as ParentOf<InputNode> }))
|
|
335
|
+
break
|
|
336
|
+
case 'Output':
|
|
337
|
+
await limit(() => visitor.output?.(node, { parent: parent as ParentOf<OutputNode> }))
|
|
327
338
|
break
|
|
328
339
|
case 'Operation':
|
|
329
340
|
await limit(() => visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> }))
|
|
@@ -373,7 +384,8 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
|
|
|
373
384
|
* const next = transform(root, { depth: 'shallow', root: (node) => node })
|
|
374
385
|
* ```
|
|
375
386
|
*/
|
|
376
|
-
export function transform(node:
|
|
387
|
+
export function transform(node: InputNode, options: TransformOptions): InputNode
|
|
388
|
+
export function transform(node: OutputNode, options: TransformOptions): OutputNode
|
|
377
389
|
export function transform(node: OperationNode, options: TransformOptions): OperationNode
|
|
378
390
|
export function transform(node: SchemaNode, options: TransformOptions): SchemaNode
|
|
379
391
|
export function transform(node: PropertyNode, options: TransformOptions): PropertyNode
|
|
@@ -385,17 +397,24 @@ export function transform(node: Node, options: TransformOptions): Node {
|
|
|
385
397
|
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
|
|
386
398
|
|
|
387
399
|
switch (node.kind) {
|
|
388
|
-
case '
|
|
389
|
-
let
|
|
390
|
-
const replaced = visitor.
|
|
391
|
-
if (replaced)
|
|
400
|
+
case 'Input': {
|
|
401
|
+
let input = node
|
|
402
|
+
const replaced = visitor.input?.(input, { parent: parent as ParentOf<InputNode> })
|
|
403
|
+
if (replaced) input = replaced
|
|
392
404
|
|
|
393
405
|
return {
|
|
394
|
-
...
|
|
395
|
-
schemas:
|
|
396
|
-
operations:
|
|
406
|
+
...input,
|
|
407
|
+
schemas: input.schemas.map((s) => transform(s, { ...options, parent: input })),
|
|
408
|
+
operations: input.operations.map((op) => transform(op, { ...options, parent: input })),
|
|
397
409
|
}
|
|
398
410
|
}
|
|
411
|
+
case 'Output': {
|
|
412
|
+
let output = node
|
|
413
|
+
const replaced = visitor.output?.(output, { parent: parent as ParentOf<OutputNode> })
|
|
414
|
+
if (replaced) output = replaced
|
|
415
|
+
|
|
416
|
+
return output
|
|
417
|
+
}
|
|
399
418
|
case 'Operation': {
|
|
400
419
|
let op = node
|
|
401
420
|
const replaced = visitor.operation?.(op, { parent: parent as ParentOf<OperationNode> })
|
|
@@ -462,6 +481,8 @@ export function transform(node: Node, options: TransformOptions): Node {
|
|
|
462
481
|
case 'FunctionParameters':
|
|
463
482
|
case 'Type':
|
|
464
483
|
return node
|
|
484
|
+
default:
|
|
485
|
+
return node
|
|
465
486
|
}
|
|
466
487
|
}
|
|
467
488
|
|
|
@@ -481,8 +502,11 @@ export function transform(node: Node, options: TransformOptions): Node {
|
|
|
481
502
|
*/
|
|
482
503
|
export function composeTransformers(...visitors: Array<Visitor>): Visitor {
|
|
483
504
|
return {
|
|
484
|
-
|
|
485
|
-
return visitors.reduce<
|
|
505
|
+
input(node, context) {
|
|
506
|
+
return visitors.reduce<InputNode>((acc, v) => v.input?.(acc, context) ?? acc, node)
|
|
507
|
+
},
|
|
508
|
+
output(node, context) {
|
|
509
|
+
return visitors.reduce<OutputNode>((acc, v) => v.output?.(acc, context) ?? acc, node)
|
|
486
510
|
},
|
|
487
511
|
operation(node, context) {
|
|
488
512
|
return visitors.reduce<OperationNode>((acc, v) => v.operation?.(acc, context) ?? acc, node)
|
|
@@ -529,8 +553,11 @@ export function collect<T>(node: Node, options: CollectOptions<T>): Array<T> {
|
|
|
529
553
|
|
|
530
554
|
let v: T | undefined
|
|
531
555
|
switch (node.kind) {
|
|
532
|
-
case '
|
|
533
|
-
v = visitor.
|
|
556
|
+
case 'Input':
|
|
557
|
+
v = visitor.input?.(node, { parent: parent as ParentOf<InputNode> })
|
|
558
|
+
break
|
|
559
|
+
case 'Output':
|
|
560
|
+
v = visitor.output?.(node, { parent: parent as ParentOf<OutputNode> })
|
|
534
561
|
break
|
|
535
562
|
case 'Operation':
|
|
536
563
|
v = visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> })
|