@kubb/ast 5.0.0-beta.29 → 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/src/utils.ts CHANGED
@@ -594,6 +594,17 @@ export function combineSources(sources: Array<SourceNode>): Array<SourceNode> {
594
594
  return [...seen.values()]
595
595
  }
596
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
+
597
608
  /**
598
609
  * Deduplicates and merges `ExportNode` objects by path and type.
599
610
  *
@@ -621,9 +632,7 @@ export function combineExports(exports: Array<ExportNode>): Array<ExportNode> {
621
632
  const existing = namedByPath.get(key)
622
633
 
623
634
  if (existing && Array.isArray(existing.name)) {
624
- const merged = new Set(existing.name)
625
- for (const n of name) merged.add(n)
626
- existing.name = [...merged]
635
+ existing.name = mergeNameArrays(existing.name, name)
627
636
  } else {
628
637
  const newItem: ExportNode = { ...curr, name: [...new Set(name)] }
629
638
  result.push(newItem)
@@ -699,9 +708,7 @@ export function combineImports(imports: Array<ImportNode>, exports: Array<Export
699
708
  const existing = namedByPath.get(key)
700
709
 
701
710
  if (existing && Array.isArray(existing.name)) {
702
- const merged = new Set(existing.name)
703
- for (const n of name) merged.add(n)
704
- existing.name = [...merged]
711
+ existing.name = mergeNameArrays(existing.name, name)
705
712
  } else {
706
713
  const newItem: ImportNode = { ...curr, name }
707
714
  result.push(newItem)
package/src/visitor.ts CHANGED
@@ -1,7 +1,19 @@
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 { InputNode, Node, OperationNode, OutputNode, ParameterNode, PropertyNode, ResponseNode, SchemaNode } from './nodes/index.ts'
4
+ import type {
5
+ ContentNode,
6
+ InputNode,
7
+ Node,
8
+ NodeKind,
9
+ OperationNode,
10
+ OutputNode,
11
+ ParameterNode,
12
+ PropertyNode,
13
+ RequestBodyNode,
14
+ ResponseNode,
15
+ SchemaNode,
16
+ } from './nodes/index.ts'
5
17
 
6
18
  /**
7
19
  * Creates a small async concurrency limiter.
@@ -54,7 +66,9 @@ type ParentNodeMap = [
54
66
  [InputNode, undefined],
55
67
  [OutputNode, undefined],
56
68
  [OperationNode, InputNode],
57
- [SchemaNode, InputNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode],
69
+ [RequestBodyNode, OperationNode],
70
+ [ContentNode, RequestBodyNode | ResponseNode],
71
+ [SchemaNode, InputNode | ContentNode | SchemaNode | PropertyNode | ParameterNode],
58
72
  [PropertyNode, SchemaNode],
59
73
  [ParameterNode, OperationNode],
60
74
  [ResponseNode, OperationNode],
@@ -268,64 +282,91 @@ export type CollectOptions<T> = CollectVisitor<T> & {
268
282
  }
269
283
 
270
284
  /**
271
- * Returns the immediate traversable children of `node`.
285
+ * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
272
286
  *
273
- * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
274
- * `additionalProperties`) are only included
275
- * when `recurse` is `true`; shallow mode skips them.
287
+ * Each listed property holds a child node, an array of child nodes, or — for
288
+ * `additionalProperties` — a node or the literal `true` (skipped). Every value
289
+ * in a child slot is a node, so one table drives both `getChildren` and `transform`.
290
+ */
291
+ const VISITOR_KEYS = {
292
+ Input: ['schemas', 'operations'],
293
+ Operation: ['parameters', 'requestBody', 'responses'],
294
+ RequestBody: ['content'],
295
+ Content: ['schema'],
296
+ Response: ['content'],
297
+ Schema: ['properties', 'items', 'members', 'additionalProperties'],
298
+ Property: ['schema'],
299
+ Parameter: ['schema'],
300
+ } as const satisfies Partial<Record<NodeKind, ReadonlyArray<string>>>
301
+
302
+ const visitorKeysByKind = VISITOR_KEYS as Record<string, ReadonlyArray<string> | undefined>
303
+
304
+ /**
305
+ * Returns `true` when `value` is an AST node (an object carrying a `kind`).
306
+ */
307
+ function isNode(value: unknown): value is Node {
308
+ return typeof value === 'object' && value !== null && 'kind' in value
309
+ }
310
+
311
+ /**
312
+ * Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
313
+ *
314
+ * `Schema` children are only included when `recurse` is `true`; shallow mode skips them.
276
315
  *
277
316
  * @example
278
317
  * ```ts
279
318
  * const children = getChildren(operationNode, true)
280
- * // returns parameters, requestBody schema (if present), and responses
319
+ * // returns parameters, the request body, and responses
281
320
  * ```
282
321
  */
283
322
  function* getChildren(node: Node, recurse: boolean): Generator<Node, void, undefined> {
284
- if (node.kind === 'Input') {
285
- yield* node.schemas
286
- yield* node.operations
287
-
288
- return
289
- }
290
- if (node.kind === 'Output') return
291
- if (node.kind === 'Operation') {
292
- yield* node.parameters
293
- if (node.requestBody?.content) {
294
- for (const c of node.requestBody.content) {
295
- if (c.schema) yield c.schema
296
- }
323
+ if (node.kind === 'Schema' && !recurse) return
324
+
325
+ const keys = visitorKeysByKind[node.kind]
326
+ if (!keys) return
327
+
328
+ const record = node as unknown as Record<string, unknown>
329
+ for (const key of keys) {
330
+ const value = record[key]
331
+ if (Array.isArray(value)) {
332
+ for (const item of value) if (isNode(item)) yield item
333
+ } else if (isNode(value)) {
334
+ yield value
297
335
  }
298
- yield* node.responses
299
-
300
- return
301
- }
302
- if (node.kind === 'Schema') {
303
- if (!recurse) return
304
- if ('properties' in node && node.properties.length > 0) yield* node.properties
305
- if ('items' in node && node.items) yield* node.items
306
- if ('members' in node && node.members) yield* node.members
307
- if ('additionalProperties' in node && node.additionalProperties && node.additionalProperties !== true) yield node.additionalProperties
308
-
309
- return
310
336
  }
311
- if (node.kind === 'Property') {
312
- yield node.schema
337
+ }
313
338
 
314
- return
315
- }
316
- if (node.kind === 'Parameter') {
317
- yield node.schema
339
+ /**
340
+ * Maps a node `kind` to the matching visitor callback name. Only the seven
341
+ * traversable node kinds have an entry; every other kind resolves to
342
+ * `undefined` and is skipped.
343
+ */
344
+ const VISITOR_KEY_BY_KIND: Partial<Record<NodeKind, keyof Visitor>> = {
345
+ Input: 'input',
346
+ Output: 'output',
347
+ Operation: 'operation',
348
+ Schema: 'schema',
349
+ Property: 'property',
350
+ Parameter: 'parameter',
351
+ Response: 'response',
352
+ }
318
353
 
319
- return
320
- }
321
- if (node.kind === 'Response') {
322
- if (node.content) {
323
- for (const c of node.content) {
324
- if (c.schema) yield c.schema
325
- }
326
- }
327
- return
328
- }
354
+ /**
355
+ * Invokes the visitor callback that matches `node.kind`, passing the traversal
356
+ * context. Returns the callback's result (a replacement node, a collected
357
+ * value, or `undefined` when no callback is registered for the kind).
358
+ *
359
+ * Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
360
+ * in one place. `TResult` is the caller's expected return: the same node type
361
+ * for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
362
+ */
363
+ function applyVisitor<TResult>(node: Node, visitor: Visitor | AsyncVisitor | CollectVisitor<unknown>, parent: Node | undefined): TResult | null | undefined {
364
+ const key = VISITOR_KEY_BY_KIND[node.kind]
365
+ if (!key) return undefined
366
+
367
+ const fn = visitor[key] as ((node: Node, context: VisitorContext) => TResult | null | undefined) | undefined
368
+
369
+ return fn?.(node, { parent })
329
370
  }
330
371
 
331
372
  /**
@@ -358,29 +399,7 @@ export async function walk(node: Node, options: WalkOptions): Promise<void> {
358
399
  }
359
400
 
360
401
  async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn, parent: Node | undefined): Promise<void> {
361
- switch (node.kind) {
362
- case 'Input':
363
- await limit(() => visitor.input?.(node, { parent: parent as ParentOf<InputNode> }))
364
- break
365
- case 'Output':
366
- await limit(() => visitor.output?.(node, { parent: parent as ParentOf<OutputNode> }))
367
- break
368
- case 'Operation':
369
- await limit(() => visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> }))
370
- break
371
- case 'Schema':
372
- await limit(() => visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> }))
373
- break
374
- case 'Property':
375
- await limit(() => visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> }))
376
- break
377
- case 'Parameter':
378
- await limit(() => visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> }))
379
- break
380
- case 'Response':
381
- await limit(() => visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> }))
382
- break
383
- }
402
+ await limit(() => applyVisitor(node, visitor, parent))
384
403
 
385
404
  const children = getChildren(node, recurse)
386
405
  for (const child of children) {
@@ -424,92 +443,62 @@ export function transform(node: Node, options: TransformOptions): Node {
424
443
  const { depth, parent, ...visitor } = options
425
444
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
426
445
 
427
- if (node.kind === 'Input') {
428
- const input = visitor.input?.(node, { parent: parent as ParentOf<InputNode> }) ?? node
446
+ const visited = applyVisitor<Node>(node, visitor, parent) ?? node
447
+ const rebuilt = transformChildren(visited, options, recurse)
429
448
 
430
- return {
431
- ...input,
432
- schemas: input.schemas.map((s) => transform(s, { ...options, parent: input })),
433
- operations: input.operations.map((op) => transform(op, { ...options, parent: input })),
434
- }
435
- }
449
+ // Structural sharing: when the visitor and child rebuild both left this node
450
+ // untouched, return the original reference so callers can detect "nothing
451
+ // changed" by identity and ancestors can avoid reallocating.
452
+ if (rebuilt === node) return node
436
453
 
437
- if (node.kind === 'Output') {
438
- return visitor.output?.(node, { parent: parent as ParentOf<OutputNode> }) ?? node
439
- }
440
-
441
- if (node.kind === 'Operation') {
442
- const op = visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> }) ?? node
443
-
444
- return {
445
- ...op,
446
- parameters: op.parameters.map((p) => transform(p, { ...options, parent: op })),
447
- requestBody: op.requestBody
448
- ? {
449
- ...op.requestBody,
450
- content: op.requestBody.content?.map((c) => ({
451
- ...c,
452
- schema: c.schema ? transform(c.schema, { ...options, parent: op }) : undefined,
453
- })),
454
- }
455
- : undefined,
456
- responses: op.responses.map((r) => transform(r, { ...options, parent: op })),
457
- }
458
- }
459
-
460
- if (node.kind === 'Schema') {
461
- const schema = visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> }) ?? node
462
-
463
- const childOptions = { ...options, parent: schema }
464
-
465
- return {
466
- ...schema,
467
- ...('properties' in schema && recurse
468
- ? {
469
- properties: schema.properties.map((p) => transform(p, childOptions)),
470
- }
471
- : {}),
472
- ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {}),
473
- ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {}),
474
- ...('additionalProperties' in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true
475
- ? {
476
- additionalProperties: transform(schema.additionalProperties, childOptions),
477
- }
478
- : {}),
479
- } as SchemaNode
480
- }
481
-
482
- if (node.kind === 'Property') {
483
- const prop = visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> }) ?? node
484
-
485
- return createProperty({
486
- ...prop,
487
- schema: transform(prop.schema, { ...options, parent: prop }),
488
- })
489
- }
490
-
491
- if (node.kind === 'Parameter') {
492
- const param = visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> }) ?? node
493
-
494
- return createParameter({
495
- ...param,
496
- schema: transform(param.schema, { ...options, parent: param }),
497
- })
498
- }
454
+ const finalize = nodeFinalizers[rebuilt.kind]
455
+ return finalize ? finalize(rebuilt) : rebuilt
456
+ }
499
457
 
500
- if (node.kind === 'Response') {
501
- const response = visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> }) ?? node
458
+ /**
459
+ * Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
460
+ * resync schema optionality against their `required` flag once the schema may
461
+ * have changed.
462
+ */
463
+ const nodeFinalizers: Partial<Record<NodeKind, (node: Node) => Node>> = {
464
+ Property: (node) => createProperty(node as PropertyNode),
465
+ Parameter: (node) => createParameter(node as ParameterNode),
466
+ }
502
467
 
503
- return {
504
- ...response,
505
- content: response.content?.map((entry) => ({
506
- ...entry,
507
- schema: entry.schema ? transform(entry.schema, { ...options, parent: response }) : entry.schema,
508
- })),
468
+ /**
469
+ * Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
470
+ * each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
471
+ * `Schema` children are skipped in shallow mode.
472
+ */
473
+ function transformChildren(node: Node, options: TransformOptions, recurse: boolean): Node {
474
+ if (node.kind === 'Schema' && !recurse) return node
475
+
476
+ const keys = visitorKeysByKind[node.kind]
477
+ if (!keys) return node
478
+
479
+ const record = node as unknown as Record<string, unknown>
480
+ const childOptions = { ...options, parent: node }
481
+ let updates: Record<string, unknown> | undefined
482
+
483
+ for (const key of keys) {
484
+ if (!(key in record)) continue
485
+ const value = record[key]
486
+ if (Array.isArray(value)) {
487
+ let changed = false
488
+ const mapped = value.map((item) => {
489
+ if (!isNode(item)) return item
490
+ const next = transform(item, childOptions)
491
+ if (next !== item) changed = true
492
+ return next
493
+ })
494
+ if (changed) (updates ??= {})[key] = mapped
495
+ } else if (isNode(value)) {
496
+ const next = transform(value, childOptions)
497
+ if (next !== value) (updates ??= {})[key] = next
509
498
  }
510
499
  }
511
500
 
512
- return node
501
+ return updates ? ({ ...node, ...updates } as Node) : node
513
502
  }
514
503
  /**
515
504
  * Lazy depth-first collection pass. Yields every non-null value returned by
@@ -531,30 +520,7 @@ export function* collectLazy<T>(node: Node, options: CollectOptions<T>): Generat
531
520
  const { depth, parent, ...visitor } = options
532
521
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
533
522
 
534
- let v: T | null | undefined
535
- switch (node.kind) {
536
- case 'Input':
537
- v = visitor.input?.(node, { parent: parent as ParentOf<InputNode> })
538
- break
539
- case 'Output':
540
- v = visitor.output?.(node, { parent: parent as ParentOf<OutputNode> })
541
- break
542
- case 'Operation':
543
- v = visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> })
544
- break
545
- case 'Schema':
546
- v = visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> })
547
- break
548
- case 'Property':
549
- v = visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> })
550
- break
551
- case 'Parameter':
552
- v = visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> })
553
- break
554
- case 'Response':
555
- v = visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> })
556
- break
557
- }
523
+ const v = applyVisitor<T>(node, visitor, parent)
558
524
  if (v != null) yield v
559
525
 
560
526
  for (const child of getChildren(node, recurse)) {