@kubb/ast 5.0.0-beta.3 → 5.0.0-beta.31

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/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],
@@ -115,25 +129,40 @@ export type VisitorContext<T extends Node = Node> = {
115
129
  }
116
130
 
117
131
  /**
118
- * Synchronous visitor used by `transform`.
132
+ * Synchronous visitor consumed by `transform`. Each optional callback runs
133
+ * for the matching node type. Return a new node to replace it, or `undefined`
134
+ * to leave it untouched.
119
135
  *
120
- * @example
136
+ * Plugins typically expose `transformer` so users can supply a `Visitor` that
137
+ * rewrites operation IDs, drops descriptions, or otherwise tweaks the AST
138
+ * before printing.
139
+ *
140
+ * @example Prefix every operationId
121
141
  * ```ts
122
142
  * const visitor: Visitor = {
123
143
  * operation(node) {
124
- * return { ...node, operationId: `x_${node.operationId}` }
144
+ * return { ...node, operationId: `api_${node.operationId}` }
145
+ * },
146
+ * }
147
+ * ```
148
+ *
149
+ * @example Strip schema descriptions
150
+ * ```ts
151
+ * const visitor: Visitor = {
152
+ * schema(node) {
153
+ * return { ...node, description: undefined }
125
154
  * },
126
155
  * }
127
156
  * ```
128
157
  */
129
158
  export type Visitor = {
130
- input?(node: InputNode, context: VisitorContext<InputNode>): void | InputNode
131
- output?(node: OutputNode, context: VisitorContext<OutputNode>): void | OutputNode
132
- operation?(node: OperationNode, context: VisitorContext<OperationNode>): void | OperationNode
133
- schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): void | SchemaNode
134
- property?(node: PropertyNode, context: VisitorContext<PropertyNode>): void | PropertyNode
135
- parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): void | ParameterNode
136
- response?(node: ResponseNode, context: VisitorContext<ResponseNode>): void | ResponseNode
159
+ input?(node: InputNode, context: VisitorContext<InputNode>): undefined | null | InputNode
160
+ output?(node: OutputNode, context: VisitorContext<OutputNode>): undefined | null | OutputNode
161
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): undefined | null | OperationNode
162
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): undefined | null | SchemaNode
163
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): undefined | null | PropertyNode
164
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): undefined | null | ParameterNode
165
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): undefined | null | ResponseNode
137
166
  }
138
167
 
139
168
  /**
@@ -154,13 +183,13 @@ type MaybePromise<T> = T | Promise<T>
154
183
  * ```
155
184
  */
156
185
  export type AsyncVisitor = {
157
- input?(node: InputNode, context: VisitorContext<InputNode>): MaybePromise<void | InputNode>
158
- output?(node: OutputNode, context: VisitorContext<OutputNode>): MaybePromise<void | OutputNode>
159
- operation?(node: OperationNode, context: VisitorContext<OperationNode>): MaybePromise<void | OperationNode>
160
- schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): MaybePromise<void | SchemaNode>
161
- property?(node: PropertyNode, context: VisitorContext<PropertyNode>): MaybePromise<void | PropertyNode>
162
- parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): MaybePromise<void | ParameterNode>
163
- response?(node: ResponseNode, context: VisitorContext<ResponseNode>): MaybePromise<void | ResponseNode>
186
+ input?(node: InputNode, context: VisitorContext<InputNode>): MaybePromise<undefined | null | InputNode>
187
+ output?(node: OutputNode, context: VisitorContext<OutputNode>): MaybePromise<undefined | null | OutputNode>
188
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): MaybePromise<undefined | null | OperationNode>
189
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): MaybePromise<undefined | null | SchemaNode>
190
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): MaybePromise<undefined | null | PropertyNode>
191
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): MaybePromise<undefined | null | ParameterNode>
192
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): MaybePromise<undefined | null | ResponseNode>
164
193
  }
165
194
 
166
195
  /**
@@ -176,13 +205,13 @@ export type AsyncVisitor = {
176
205
  * ```
177
206
  */
178
207
  export type CollectVisitor<T> = {
179
- input?(node: InputNode, context: VisitorContext<InputNode>): T | undefined
180
- output?(node: OutputNode, context: VisitorContext<OutputNode>): T | undefined
181
- operation?(node: OperationNode, context: VisitorContext<OperationNode>): T | undefined
182
- schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): T | undefined
183
- property?(node: PropertyNode, context: VisitorContext<PropertyNode>): T | undefined
184
- parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): T | undefined
185
- response?(node: ResponseNode, context: VisitorContext<ResponseNode>): T | undefined
208
+ input?(node: InputNode, context: VisitorContext<InputNode>): T | null | undefined
209
+ output?(node: OutputNode, context: VisitorContext<OutputNode>): T | null | undefined
210
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): T | null | undefined
211
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): T | null | undefined
212
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): T | null | undefined
213
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): T | null | undefined
214
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): T | null | undefined
186
215
  }
187
216
 
188
217
  /**
@@ -253,60 +282,102 @@ export type CollectOptions<T> = CollectVisitor<T> & {
253
282
  }
254
283
 
255
284
  /**
256
- * Returns the immediate traversable children of `node`.
285
+ * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
257
286
  *
258
- * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
259
- * `additionalProperties`) are only included
260
- * 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.
261
315
  *
262
316
  * @example
263
317
  * ```ts
264
318
  * const children = getChildren(operationNode, true)
265
- * // returns parameters, requestBody schema (if present), and responses
319
+ * // returns parameters, the request body, and responses
266
320
  * ```
267
321
  */
268
- function getChildren(node: Node, recurse: boolean): Array<Node> {
269
- switch (node.kind) {
270
- case 'Input':
271
- return [...node.schemas, ...node.operations]
272
- case 'Output':
273
- return []
274
- case 'Operation':
275
- return [...node.parameters, ...(node.requestBody?.content?.flatMap((c) => (c.schema ? [c.schema] : [])) ?? []), ...node.responses]
276
- case 'Schema': {
277
- const children: Array<Node> = []
278
-
279
- if (!recurse) return []
280
-
281
- if ('properties' in node && node.properties.length > 0) children.push(...node.properties)
282
- if ('items' in node && node.items) children.push(...node.items)
283
- if ('members' in node && node.members) children.push(...node.members)
284
- if ('additionalProperties' in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties)
285
-
286
- return children
322
+ function* getChildren(node: Node, recurse: boolean): Generator<Node, void, undefined> {
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
287
335
  }
288
- case 'Property':
289
- return [node.schema]
290
- case 'Parameter':
291
- return [node.schema]
292
- case 'Response':
293
- return node.schema ? [node.schema] : []
294
- case 'FunctionParameter':
295
- case 'ParameterGroup':
296
- case 'FunctionParameters':
297
- case 'Type':
298
- return []
299
- default:
300
- return []
301
336
  }
302
337
  }
303
338
 
304
339
  /**
305
- * Depth-first traversal for side effects. Visitor return values are ignored.
306
- * Sibling nodes at each level are visited concurrently up to `options.concurrency`
307
- * (default: `WALK_CONCURRENCY`).
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
+ }
353
+
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).
308
358
  *
309
- * @example
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 })
370
+ }
371
+
372
+ /**
373
+ * Async depth-first traversal for side effects. Visitor return values are
374
+ * ignored. Use `transform` when you want to rewrite nodes.
375
+ *
376
+ * Sibling nodes at each depth run concurrently up to `options.concurrency`
377
+ * (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
378
+ * work; lower values reduce memory pressure.
379
+ *
380
+ * @example Log every operation
310
381
  * ```ts
311
382
  * await walk(root, {
312
383
  * operation(node) {
@@ -315,10 +386,9 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
315
386
  * })
316
387
  * ```
317
388
  *
318
- * @example
389
+ * @example Only visit the root node
319
390
  * ```ts
320
- * // Visit only the current node
321
- * await walk(root, { depth: 'shallow', root: () => {} })
391
+ * await walk(root, { depth: 'shallow', input: () => {} })
322
392
  * ```
323
393
  */
324
394
  export async function walk(node: Node, options: WalkOptions): Promise<void> {
@@ -329,41 +399,7 @@ export async function walk(node: Node, options: WalkOptions): Promise<void> {
329
399
  }
330
400
 
331
401
  async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn, parent: Node | undefined): Promise<void> {
332
- switch (node.kind) {
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> }))
338
- break
339
- case 'Operation':
340
- await limit(() =>
341
- visitor.operation?.(node, {
342
- parent: parent as ParentOf<OperationNode>,
343
- }),
344
- )
345
- break
346
- case 'Schema':
347
- await limit(() => visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> }))
348
- break
349
- case 'Property':
350
- await limit(() => visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> }))
351
- break
352
- case 'Parameter':
353
- await limit(() =>
354
- visitor.parameter?.(node, {
355
- parent: parent as ParentOf<ParameterNode>,
356
- }),
357
- )
358
- break
359
- case 'Response':
360
- await limit(() => visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> }))
361
- break
362
- case 'FunctionParameter':
363
- case 'ParameterGroup':
364
- case 'FunctionParameters':
365
- break
366
- }
402
+ await limit(() => applyVisitor(node, visitor, parent))
367
403
 
368
404
  const children = getChildren(node, recurse)
369
405
  for (const child of children) {
@@ -372,12 +408,13 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
372
408
  }
373
409
 
374
410
  /**
375
- * Runs a depth-first immutable transform.
411
+ * Synchronous depth-first transform. Each visitor callback gets a chance to
412
+ * return a replacement node; `undefined` keeps the original.
376
413
  *
377
- * If a visitor returns a node, it replaces the current node.
378
- * If it returns `undefined`, the current node stays the same.
414
+ * The transform is immutable. The original tree is not mutated; a new tree
415
+ * is returned. Use `depth: 'shallow'` to skip recursion into children.
379
416
  *
380
- * @example
417
+ * @example Prefix every operationId
381
418
  * ```ts
382
419
  * const next = transform(root, {
383
420
  * operation(node) {
@@ -386,10 +423,12 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
386
423
  * })
387
424
  * ```
388
425
  *
389
- * @example
426
+ * @example Replace only the root node
390
427
  * ```ts
391
- * // Shallow mode: only transform the input node
392
- * const next = transform(root, { depth: 'shallow', root: (node) => node })
428
+ * const next = transform(root, {
429
+ * depth: 'shallow',
430
+ * input: (node) => ({ ...node, meta: { ...node.meta, title: 'Rewritten' } }),
431
+ * })
393
432
  * ```
394
433
  */
395
434
  export function transform(node: InputNode, options: TransformOptions): InputNode
@@ -404,189 +443,104 @@ export function transform(node: Node, options: TransformOptions): Node {
404
443
  const { depth, parent, ...visitor } = options
405
444
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
406
445
 
407
- switch (node.kind) {
408
- case 'Input': {
409
- let input = node
410
- const replaced = visitor.input?.(input, {
411
- parent: parent as ParentOf<InputNode>,
412
- })
413
- if (replaced) input = replaced
446
+ const visited = applyVisitor<Node>(node, visitor, parent) ?? node
447
+ const rebuilt = transformChildren(visited, options, recurse)
414
448
 
415
- return {
416
- ...input,
417
- schemas: input.schemas.map((s) => transform(s, { ...options, parent: input })),
418
- operations: input.operations.map((op) => transform(op, { ...options, parent: input })),
419
- }
420
- }
421
- case 'Output': {
422
- let output = node
423
- const replaced = visitor.output?.(output, {
424
- parent: parent as ParentOf<OutputNode>,
425
- })
426
- if (replaced) output = replaced
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
427
453
 
428
- return output
429
- }
430
- case 'Operation': {
431
- let op = node
432
- const replaced = visitor.operation?.(op, {
433
- parent: parent as ParentOf<OperationNode>,
434
- })
435
- if (replaced) op = replaced
436
-
437
- return {
438
- ...op,
439
- parameters: op.parameters.map((p) => transform(p, { ...options, parent: op })),
440
- requestBody: op.requestBody
441
- ? {
442
- ...op.requestBody,
443
- content: op.requestBody.content?.map((c) => ({
444
- ...c,
445
- schema: c.schema ? transform(c.schema, { ...options, parent: op }) : undefined,
446
- })),
447
- }
448
- : undefined,
449
- responses: op.responses.map((r) => transform(r, { ...options, parent: op })),
450
- }
451
- }
452
- case 'Schema': {
453
- let schema = node
454
- const replaced = visitor.schema?.(schema, {
455
- parent: parent as ParentOf<SchemaNode>,
456
- })
457
- if (replaced) schema = replaced
458
-
459
- const childOptions = { ...options, parent: schema }
460
-
461
- return {
462
- ...schema,
463
- ...('properties' in schema && recurse
464
- ? {
465
- properties: schema.properties.map((p) => transform(p, childOptions)),
466
- }
467
- : {}),
468
- ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {}),
469
- ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {}),
470
- ...('additionalProperties' in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true
471
- ? {
472
- additionalProperties: transform(schema.additionalProperties, childOptions),
473
- }
474
- : {}),
475
- } as SchemaNode
476
- }
477
- case 'Property': {
478
- let prop = node
479
- const replaced = visitor.property?.(prop, {
480
- parent: parent as ParentOf<PropertyNode>,
481
- })
482
- if (replaced) prop = replaced
454
+ const finalize = nodeFinalizers[rebuilt.kind]
455
+ return finalize ? finalize(rebuilt) : rebuilt
456
+ }
483
457
 
484
- return createProperty({
485
- ...prop,
486
- schema: transform(prop.schema, { ...options, parent: prop }),
487
- })
488
- }
489
- case 'Parameter': {
490
- let param = node
491
- const replaced = visitor.parameter?.(param, {
492
- parent: parent as ParentOf<ParameterNode>,
493
- })
494
- if (replaced) param = replaced
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
+ }
495
467
 
496
- return createParameter({
497
- ...param,
498
- schema: transform(param.schema, { ...options, parent: param }),
499
- })
500
- }
501
- case 'Response': {
502
- let response = node
503
- const replaced = visitor.response?.(response, {
504
- parent: parent as ParentOf<ResponseNode>,
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
505
493
  })
506
- if (replaced) response = replaced
507
-
508
- return {
509
- ...response,
510
- schema: transform(response.schema, { ...options, parent: response }),
511
- }
494
+ if (changed) (updates ??= {})[key] = mapped
495
+ } else if (isNode(value)) {
496
+ const next = transform(value, childOptions)
497
+ if (next !== value) (updates ??= {})[key] = next
512
498
  }
513
- case 'FunctionParameter':
514
- case 'ParameterGroup':
515
- case 'FunctionParameters':
516
- case 'Type':
517
- return node
518
- default:
519
- return node
520
499
  }
500
+
501
+ return updates ? ({ ...node, ...updates } as Node) : node
521
502
  }
522
503
  /**
523
- * Runs a depth-first synchronous collection pass.
524
- *
525
- * Non-`undefined` values returned by visitor callbacks are appended to the result.
504
+ * Lazy depth-first collection pass. Yields every non-null value returned by
505
+ * the visitor callbacks. Use `collect` for the eager array form.
526
506
  *
527
- * @example
507
+ * @example Collect every operationId
528
508
  * ```ts
529
- * const ids = collect(root, {
509
+ * const ids: string[] = []
510
+ * for (const id of collectLazy<string>(root, {
530
511
  * operation(node) {
531
512
  * return node.operationId
532
513
  * },
533
- * })
534
- * ```
535
- *
536
- * @example
537
- * ```ts
538
- * // Collect from only the current node
539
- * const values = collect(root, { depth: 'shallow', root: () => 'root' })
514
+ * })) {
515
+ * ids.push(id)
516
+ * }
540
517
  * ```
541
518
  */
542
- export function collect<T>(node: Node, options: CollectOptions<T>): Array<T> {
519
+ export function* collectLazy<T>(node: Node, options: CollectOptions<T>): Generator<T, void, undefined> {
543
520
  const { depth, parent, ...visitor } = options
544
521
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
545
- const results: Array<T> = []
546
-
547
- let v: T | undefined
548
- switch (node.kind) {
549
- case 'Input':
550
- v = visitor.input?.(node, { parent: parent as ParentOf<InputNode> })
551
- break
552
- case 'Output':
553
- v = visitor.output?.(node, { parent: parent as ParentOf<OutputNode> })
554
- break
555
- case 'Operation':
556
- v = visitor.operation?.(node, {
557
- parent: parent as ParentOf<OperationNode>,
558
- })
559
- break
560
- case 'Schema':
561
- v = visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> })
562
- break
563
- case 'Property':
564
- v = visitor.property?.(node, {
565
- parent: parent as ParentOf<PropertyNode>,
566
- })
567
- break
568
- case 'Parameter':
569
- v = visitor.parameter?.(node, {
570
- parent: parent as ParentOf<ParameterNode>,
571
- })
572
- break
573
- case 'Response':
574
- v = visitor.response?.(node, {
575
- parent: parent as ParentOf<ResponseNode>,
576
- })
577
- break
578
- case 'FunctionParameter':
579
- case 'ParameterGroup':
580
- case 'FunctionParameters':
581
- break
582
- }
583
- if (v !== undefined) results.push(v)
522
+
523
+ const v = applyVisitor<T>(node, visitor, parent)
524
+ if (v != null) yield v
584
525
 
585
526
  for (const child of getChildren(node, recurse)) {
586
- for (const item of collect(child, { ...options, parent: node })) {
587
- results.push(item)
588
- }
527
+ yield* collectLazy(child, { ...options, parent: node })
589
528
  }
529
+ }
590
530
 
591
- return results
531
+ /**
532
+ * Eager depth-first collection pass. Returns an array of every non-null value
533
+ * the visitor callbacks return.
534
+ *
535
+ * @example Collect every operationId
536
+ * ```ts
537
+ * const ids = collect<string>(root, {
538
+ * operation(node) {
539
+ * return node.operationId
540
+ * },
541
+ * })
542
+ * ```
543
+ */
544
+ export function collect<T>(node: Node, options: CollectOptions<T>): Array<T> {
545
+ return Array.from(collectLazy(node, options))
592
546
  }