@kubb/ast 5.0.0-beta.55 → 5.0.0-beta.57

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/ast.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { camelCase, isValidVarName, memoize } from '@internals/utils'
2
2
 
3
- import { createFunctionParameter, createFunctionParameters, createParameterGroup, createParamsType, createProperty, createSchema } from '../factory.ts'
4
3
  import { narrowSchema } from '../guards.ts'
4
+ import { createFunctionParameter, createFunctionParameters, createIndexedAccessType, createTypeLiteral } from '../nodes/function.ts'
5
+ import { createProperty } from '../nodes/property.ts'
6
+ import { createSchema } from '../nodes/schema.ts'
5
7
  import type {
6
8
  CodeNode,
7
9
  ExportNode,
@@ -9,11 +11,11 @@ import type {
9
11
  FunctionParametersNode,
10
12
  ImportNode,
11
13
  OperationNode,
12
- ParameterGroupNode,
13
14
  ParameterNode,
14
- ParamsTypeNode,
15
15
  SchemaNode,
16
16
  SourceNode,
17
+ TypeExpression,
18
+ TypeLiteralNode,
17
19
  } from '../nodes/index.ts'
18
20
  import type { SchemaType } from '../nodes/schema.ts'
19
21
  import { extractRefName } from './index.ts'
@@ -120,15 +122,24 @@ export function createDiscriminantNode({ propertyName, value }: { propertyName:
120
122
  */
121
123
  type ParamGroupType = {
122
124
  /**
123
- * TypeNode for the group type.
125
+ * Type expression for the group, a plain group-name reference.
124
126
  */
125
- type: ParamsTypeNode
127
+ type: TypeExpression
126
128
  /**
127
129
  * Whether the parameter group is optional.
128
130
  */
129
131
  optional: boolean
130
132
  }
131
133
 
134
+ /**
135
+ * A single member of a destructured parameter group, fed to `createFunctionParameter({ properties })`.
136
+ */
137
+ type GroupProperty = {
138
+ name: string
139
+ type: TypeExpression
140
+ optional?: boolean
141
+ }
142
+
132
143
  /**
133
144
  * Resolver interface for {@link createOperationParams}.
134
145
  *
@@ -215,7 +226,7 @@ export type CreateOperationParamsOptions = {
215
226
  * extraParams: [createFunctionParameter({ name: 'options', type: 'Partial<RequestOptions>', default: '{}' })]
216
227
  * ```
217
228
  */
218
- extraParams?: Array<FunctionParameterNode | ParameterGroupNode>
229
+ extraParams?: Array<FunctionParameterNode>
219
230
  /**
220
231
  * Override the default parameter names used for body, query, header, and rest-path groups.
221
232
  *
@@ -255,7 +266,14 @@ export type CreateOperationParamsOptions = {
255
266
  typeWrapper?: (type: string) => string
256
267
  }
257
268
 
258
- function resolveParamsType({
269
+ /**
270
+ * Resolves the {@link TypeExpression} for an individual parameter.
271
+ *
272
+ * Without a resolver, falls back to the schema primitive (a plain type-name string).
273
+ * When the parameter belongs to a named group, emits an {@link IndexedAccessTypeNode}
274
+ * (`GroupParams['petId']`); otherwise the resolved individual name as a plain string.
275
+ */
276
+ export function resolveParamType({
259
277
  node,
260
278
  param,
261
279
  resolver,
@@ -263,12 +281,9 @@ function resolveParamsType({
263
281
  node: OperationNode
264
282
  param: ParameterNode
265
283
  resolver: OperationParamsResolver | undefined
266
- }): ParamsTypeNode {
284
+ }): TypeExpression {
267
285
  if (!resolver) {
268
- return createParamsType({
269
- variant: 'reference',
270
- name: param.schema.primitive ?? 'unknown',
271
- })
286
+ return param.schema.primitive ?? 'unknown'
272
287
  }
273
288
 
274
289
  const individualName = resolver.resolveParamName(node, param)
@@ -284,23 +299,19 @@ function resolveParamsType({
284
299
  const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : undefined
285
300
 
286
301
  if (groupName && groupName !== individualName) {
287
- return createParamsType({
288
- variant: 'member',
289
- base: groupName,
290
- key: param.name,
291
- })
302
+ return createIndexedAccessType({ objectType: groupName, indexType: param.name })
292
303
  }
293
304
 
294
- return createParamsType({ variant: 'reference', name: individualName })
305
+ return individualName
295
306
  }
296
307
 
297
308
  /**
298
309
  * Converts an `OperationNode` into function parameters for code generation.
299
310
  *
300
- * Centralizes parameter grouping logic for all plugins. Provide a `resolver` for type name resolution
301
- * and `extraParams` for plugin-specific trailing parameters (e.g., `options` objects).
302
- * Supports three grouping modes: `object` (single destructured param), `inline` (separate params),
303
- * and `inlineSpread` (rest parameter). Use `CreateOperationParamsOptions` to fine-tune output.
311
+ * Centralizes parameter grouping logic for all plugins. `paramsType` chooses between one
312
+ * destructured object parameter (`object`) and separate top-level parameters (`inline`), while
313
+ * `pathParamsType` controls how path params render in inline mode. Provide a `resolver` for type
314
+ * name resolution and `extraParams` for plugin-specific trailing parameters such as an `options` object.
304
315
  */
305
316
  export function createOperationParams(node: OperationNode, options: CreateOperationParamsOptions): FunctionParametersNode {
306
317
  const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options
@@ -310,147 +321,58 @@ export function createOperationParams(node: OperationNode, options: CreateOperat
310
321
  const headersName = paramNames?.headers ?? 'headers'
311
322
  const pathName = paramNames?.path ?? 'pathParams'
312
323
 
313
- const wrapType = (type: string): ParamsTypeNode =>
314
- createParamsType({
315
- variant: 'reference',
316
- name: typeWrapper ? typeWrapper(type) : type,
317
- })
318
- // Only reference-variant TypeNodes are wrapped, they hold a plain type name string that needs casing applied.
319
- // Member and struct TypeNodes are pre-resolved structured expressions and are passed through unchanged.
320
- const wrapTypeNode = (type: ParamsTypeNode): ParamsTypeNode => (type.kind === 'ParamsType' && type.variant === 'reference' ? wrapType(type.name) : type)
324
+ const wrapType = (type: string): string => (typeWrapper ? typeWrapper(type) : type)
325
+ // Only plain type-name references are wrapped, they need casing applied.
326
+ // TypeLiteral and IndexedAccessType expressions are pre-resolved and pass through unchanged.
327
+ const wrapTypeExpression = (type: TypeExpression): TypeExpression => (typeof type === 'string' ? wrapType(type) : type)
321
328
 
322
329
  const casedParams = caseParams(node.parameters, paramsCasing)
323
330
  const pathParams = casedParams.filter((p) => p.in === 'path')
324
331
  const queryParams = casedParams.filter((p) => p.in === 'query')
325
332
  const headerParams = casedParams.filter((p) => p.in === 'header')
326
333
 
334
+ const toProperty = (param: ParameterNode): GroupProperty => ({
335
+ name: param.name,
336
+ type: wrapTypeExpression(resolveParamType({ node, param, resolver })),
337
+ optional: !param.required,
338
+ })
339
+ const emptyObjectDefault = (props: Array<GroupProperty>): string | undefined => (props.every((p) => p.optional) ? '{}' : undefined)
340
+
327
341
  const bodyType = node.requestBody?.content?.[0]?.schema ? wrapType(resolver?.resolveDataName(node) ?? 'unknown') : undefined
328
- const bodyRequired = node.requestBody?.required ?? false
329
-
330
- const queryGroupType = resolver
331
- ? resolveGroupType({
332
- node,
333
- params: queryParams,
334
- groupMethod: resolver.resolveQueryParamsName,
335
- resolver,
336
- })
337
- : undefined
338
- const headerGroupType = resolver
339
- ? resolveGroupType({
340
- node,
341
- params: headerParams,
342
- groupMethod: resolver.resolveHeaderParamsName,
343
- resolver,
344
- })
345
- : undefined
346
-
347
- const params: Array<FunctionParameterNode | ParameterGroupNode> = []
342
+ const bodyProperty: Array<GroupProperty> = bodyType ? [{ name: dataName, type: bodyType, optional: !(node.requestBody?.required ?? false) }] : []
343
+
344
+ const trailingGroups: Array<BuildGroupArgs> = [
345
+ { name: paramsName, node, params: queryParams, groupType: resolveGroupType({ node, params: queryParams, group: 'query', resolver }), resolver, wrapType },
346
+ {
347
+ name: headersName,
348
+ node,
349
+ params: headerParams,
350
+ groupType: resolveGroupType({ node, params: headerParams, group: 'header', resolver }),
351
+ resolver,
352
+ wrapType,
353
+ },
354
+ ]
348
355
 
349
- if (paramsType === 'object') {
350
- const children: Array<FunctionParameterNode> = [
351
- ...pathParams.map((p) => {
352
- const type = resolveParamsType({ node, param: p, resolver })
353
- return createFunctionParameter({
354
- name: p.name,
355
- type: wrapTypeNode(type),
356
- optional: !p.required,
357
- })
358
- }),
359
- ...(bodyType
360
- ? [
361
- createFunctionParameter({
362
- name: dataName,
363
- type: bodyType,
364
- optional: !bodyRequired,
365
- }),
366
- ]
367
- : []),
368
- ...buildGroupParam({
369
- name: paramsName,
370
- node,
371
- params: queryParams,
372
- groupType: queryGroupType,
373
- resolver,
374
- wrapType,
375
- }),
376
- ...buildGroupParam({
377
- name: headersName,
378
- node,
379
- params: headerParams,
380
- groupType: headerGroupType,
381
- resolver,
382
- wrapType,
383
- }),
384
- ]
356
+ const params: Array<FunctionParameterNode> = []
385
357
 
358
+ if (paramsType === 'object') {
359
+ const children = [...pathParams.map(toProperty), ...bodyProperty, ...trailingGroups.flatMap(buildGroupProperty)]
386
360
  if (children.length) {
387
- params.push(
388
- createParameterGroup({
389
- properties: children,
390
- default: children.every((c) => c.optional) ? '{}' : undefined,
391
- }),
392
- )
361
+ params.push(createFunctionParameter({ properties: children, default: emptyObjectDefault(children) }))
393
362
  }
394
363
  } else {
395
- if (pathParams.length) {
396
- if (pathParamsType === 'inlineSpread') {
397
- const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]!)
398
- params.push(
399
- createFunctionParameter({
400
- name: pathName,
401
- type: spreadType ? wrapType(spreadType) : undefined,
402
- rest: true,
403
- }),
404
- )
405
- } else {
406
- const pathChildren = pathParams.map((p) => {
407
- const type = resolveParamsType({ node, param: p, resolver })
408
- return createFunctionParameter({
409
- name: p.name,
410
- type: wrapTypeNode(type),
411
- optional: !p.required,
412
- })
413
- })
414
- params.push(
415
- createParameterGroup({
416
- properties: pathChildren,
417
- inline: pathParamsType === 'inline',
418
- default: pathParamsDefault ?? (pathChildren.every((c) => c.optional) ? '{}' : undefined),
419
- }),
420
- )
421
- }
422
- }
423
-
424
- if (bodyType) {
425
- params.push(
426
- createFunctionParameter({
427
- name: dataName,
428
- type: bodyType,
429
- optional: !bodyRequired,
430
- }),
431
- )
364
+ if (pathParamsType === 'inlineSpread' && pathParams.length) {
365
+ const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]!)
366
+ params.push(createFunctionParameter({ name: pathName, type: spreadType ? wrapType(spreadType) : undefined, rest: true }))
367
+ } else if (pathParamsType === 'inline') {
368
+ params.push(...pathParams.map((p) => createFunctionParameter(toProperty(p))))
369
+ } else if (pathParams.length) {
370
+ const pathChildren = pathParams.map(toProperty)
371
+ params.push(createFunctionParameter({ properties: pathChildren, default: pathParamsDefault ?? emptyObjectDefault(pathChildren) }))
432
372
  }
433
373
 
434
- params.push(
435
- ...buildGroupParam({
436
- name: paramsName,
437
- node,
438
- params: queryParams,
439
- groupType: queryGroupType,
440
- resolver,
441
- wrapType,
442
- }),
443
- )
444
- params.push(
445
- ...buildGroupParam({
446
- name: headersName,
447
- node,
448
- params: headerParams,
449
- groupType: headerGroupType,
450
- resolver,
451
- wrapType,
452
- }),
453
- )
374
+ params.push(...bodyProperty.map((p) => createFunctionParameter(p)))
375
+ params.push(...trailingGroups.flatMap(buildGroupParam))
454
376
  }
455
377
 
456
378
  params.push(...extraParams)
@@ -459,80 +381,82 @@ export function createOperationParams(node: OperationNode, options: CreateOperat
459
381
  }
460
382
 
461
383
  /**
462
- * Builds a single {@link FunctionParameterNode} for a query or header group.
463
- * Returns an empty array when there are no params to emit.
464
- *
465
- * If a pre-resolved `groupType` is provided it emits `name: GroupType`.
466
- * Otherwise, it builds an inline struct from the individual params.
384
+ * Shared arguments for building a query or header parameter group.
467
385
  */
468
- function buildGroupParam({
469
- name,
470
- node,
471
- params,
472
- groupType,
473
- resolver,
474
- wrapType,
475
- }: {
386
+ type BuildGroupArgs = {
476
387
  name: string
477
388
  node: OperationNode
478
389
  params: Array<ParameterNode>
479
- groupType: ParamGroupType | null | undefined
390
+ groupType: ParamGroupType | null
480
391
  resolver: OperationParamsResolver | undefined
481
- wrapType: (type: string) => ParamsTypeNode
482
- }): Array<FunctionParameterNode> {
392
+ wrapType: (type: string) => string
393
+ }
394
+
395
+ /**
396
+ * Builds the property descriptor for a query or header group.
397
+ * Returns an empty array when there are no params to emit.
398
+ *
399
+ * A pre-resolved `groupType` emits `name: GroupType`. Otherwise it builds an inline
400
+ * {@link TypeLiteralNode} from the individual params.
401
+ */
402
+ function buildGroupProperty({ name, node, params, groupType, resolver, wrapType }: BuildGroupArgs): Array<GroupProperty> {
483
403
  if (groupType) {
484
- const type = groupType.type.kind === 'ParamsType' && groupType.type.variant === 'reference' ? wrapType(groupType.type.name) : groupType.type
485
- return [createFunctionParameter({ name, type, optional: groupType.optional })]
404
+ const type = typeof groupType.type === 'string' ? wrapType(groupType.type) : groupType.type
405
+ return [{ name, type, optional: groupType.optional }]
486
406
  }
487
407
  if (params.length) {
488
- return [
489
- createFunctionParameter({
490
- name,
491
- type: toStructType({ node, params, resolver }),
492
- optional: params.every((p) => !p.required),
493
- }),
494
- ]
408
+ return [{ name, type: buildTypeLiteral({ node, params, resolver }), optional: params.every((p) => !p.required) }]
495
409
  }
496
410
  return []
497
411
  }
498
412
 
499
413
  /**
500
- * Derives a {@link ParamGroupType} from the resolver's group method.
501
- * Returns `null` when the group name equals the individual param name (no real group).
414
+ * Builds a single {@link FunctionParameterNode} for a query or header group.
415
+ * Returns an empty array when there are no params to emit.
416
+ *
417
+ * A pre-resolved `groupType` emits `name: GroupType`. Otherwise it builds an inline
418
+ * {@link TypeLiteralNode} from the individual params.
419
+ */
420
+ export function buildGroupParam(args: BuildGroupArgs): Array<FunctionParameterNode> {
421
+ return buildGroupProperty(args).map((p) => createFunctionParameter(p))
422
+ }
423
+
424
+ /**
425
+ * Derives a {@link ParamGroupType} for a query or header group from the resolver.
426
+ *
427
+ * Returns `null` when there is no resolver, no params, or the group name equals the
428
+ * individual param name (so there is no real group to emit).
502
429
  */
503
- function resolveGroupType({
430
+ export function resolveGroupType({
504
431
  node,
505
432
  params,
506
- groupMethod,
433
+ group,
507
434
  resolver,
508
435
  }: {
509
436
  node: OperationNode
510
437
  params: Array<ParameterNode>
511
- groupMethod: (_node: OperationNode, _param: ParameterNode) => string
512
- resolver: OperationParamsResolver
438
+ group: 'query' | 'header'
439
+ resolver: OperationParamsResolver | undefined
513
440
  }): ParamGroupType | null {
514
- if (!params.length) {
441
+ if (!resolver || !params.length) {
515
442
  return null
516
443
  }
517
444
  const firstParam = params[0]!
445
+ const groupMethod = group === 'query' ? resolver.resolveQueryParamsName : resolver.resolveHeaderParamsName
518
446
  const groupName = groupMethod.call(resolver, node, firstParam)
519
447
  if (groupName === resolver.resolveParamName(node, firstParam)) {
520
448
  return null
521
449
  }
522
- const allOptional = params.every((p) => !p.required)
523
- return {
524
- type: createParamsType({ variant: 'reference', name: groupName }),
525
- optional: allOptional,
526
- }
450
+ return { type: groupName, optional: params.every((p) => !p.required) }
527
451
  }
528
452
 
529
453
  /**
530
- * Builds a {@link TypeNode} with `variant: 'struct'` for an inline anonymous type grouping named fields.
454
+ * Builds a {@link TypeLiteralNode} for an inline anonymous type grouping named fields.
531
455
  *
532
456
  * Used when query or header parameters have no dedicated group type name.
533
457
  * Each language printer renders this appropriately (TypeScript: `{ petId: string; name?: string }`).
534
458
  */
535
- function toStructType({
459
+ export function buildTypeLiteral({
536
460
  node,
537
461
  params,
538
462
  resolver,
@@ -540,13 +464,12 @@ function toStructType({
540
464
  node: OperationNode
541
465
  params: Array<ParameterNode>
542
466
  resolver: OperationParamsResolver | undefined
543
- }): ParamsTypeNode {
544
- return createParamsType({
545
- variant: 'struct',
546
- properties: params.map((p) => ({
467
+ }): TypeLiteralNode {
468
+ return createTypeLiteral({
469
+ members: params.map((p) => ({
547
470
  name: p.name,
471
+ type: resolveParamType({ node, param: p, resolver }),
548
472
  optional: !p.required,
549
- type: resolveParamsType({ node, param: p, resolver }),
550
473
  })),
551
474
  })
552
475
  }
package/src/visitor.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import type { VisitorDepth } from './constants.ts'
2
2
  import { visitorDepths, WALK_CONCURRENCY } from './constants.ts'
3
- import { createParameter, createProperty } from './factory.ts'
3
+ import { nodeRebuilders, VISITOR_KEY_BY_KIND, VISITOR_KEYS } from './registry.ts'
4
4
  import type {
5
5
  ContentNode,
6
6
  InputNode,
7
7
  Node,
8
- NodeKind,
9
8
  OperationNode,
10
9
  OutputNode,
11
10
  ParameterNode,
@@ -281,24 +280,6 @@ export type CollectOptions<T> = CollectVisitor<T> & {
281
280
  parent?: Node
282
281
  }
283
282
 
284
- /**
285
- * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
286
- *
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
283
  const visitorKeysByKind = VISITOR_KEYS as Record<string, ReadonlyArray<string> | undefined>
303
284
 
304
285
  /**
@@ -336,21 +317,6 @@ function* getChildren(node: Node, recurse: boolean): Generator<Node, void, undef
336
317
  }
337
318
  }
338
319
 
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
- }
353
-
354
320
  /**
355
321
  * Invokes the visitor callback that matches `node.kind`, passing the traversal
356
322
  * context. Returns the callback's result (a replacement node, a collected
@@ -455,18 +421,8 @@ export function transform(node: Node, options: TransformOptions): Node {
455
421
  // changed" by identity and ancestors can avoid reallocating.
456
422
  if (rebuilt === node) return node
457
423
 
458
- const finalize = nodeFinalizers[rebuilt.kind]
459
- return finalize ? finalize(rebuilt) : rebuilt
460
- }
461
-
462
- /**
463
- * Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
464
- * resync schema optionality against their `required` flag once the schema may
465
- * have changed.
466
- */
467
- const nodeFinalizers: Partial<Record<NodeKind, (node: Node) => Node>> = {
468
- Property: (node) => createProperty(node as PropertyNode),
469
- Parameter: (node) => createParameter(node as ParameterNode),
424
+ const rebuild = nodeRebuilders[rebuilt.kind]
425
+ return rebuild ? rebuild(rebuilt) : rebuilt
470
426
  }
471
427
 
472
428
  /**