@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/dist/index.js CHANGED
@@ -201,6 +201,60 @@ const mediaTypes = {
201
201
  videoMp4: "video/mp4"
202
202
  };
203
203
  //#endregion
204
+ //#region src/dialect.ts
205
+ /**
206
+ * Identity helper that types a {@link SchemaDialect} for an adapter. Like
207
+ * `defineParser`, it adds no runtime behavior — it pins the dialect's type for
208
+ * inference and gives adapter authors a discoverable anchor.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * export const oasDialect = defineSchemaDialect({
213
+ * name: 'oas',
214
+ * isNullable,
215
+ * isReference,
216
+ * isDiscriminator,
217
+ * isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
218
+ * resolveRef,
219
+ * })
220
+ * ```
221
+ */
222
+ function defineSchemaDialect(dialect) {
223
+ return dialect;
224
+ }
225
+ //#endregion
226
+ //#region src/dispatch.ts
227
+ /**
228
+ * Walks an ordered list of {@link DispatchRule}s and returns the first node produced.
229
+ *
230
+ * This is the shared backbone for spec adapters (OpenAPI today, AsyncAPI and others later).
231
+ * The contract an adapter follows is intentionally minimal:
232
+ *
233
+ * context → [rule.match → rule.convert] → node
234
+ *
235
+ * An adapter derives a context from a source spec node, then declares an ordered table of
236
+ * rules mapping spec shapes onto Kubb AST nodes. To add support for a new spec, write a new
237
+ * context type and a new rules table — the traversal here is reused unchanged.
238
+ *
239
+ * Order is significant: earlier rules win, so list higher-precedence or more specific shapes
240
+ * first (e.g. composition keywords before plain `type`). A rule whose `match` returns `true`
241
+ * may still `convert` to `null` to defer to later rules. When no rule produces a node this
242
+ * returns `null`, leaving the caller to apply its own fallback.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const node = dispatch(schemaRules, schemaContext) ?? createSchema({ type: fallbackType })
247
+ * ```
248
+ */
249
+ function dispatch(rules, context) {
250
+ for (const rule of rules) {
251
+ if (!rule.match(context)) continue;
252
+ const node = rule.convert(context);
253
+ if (node !== null && node !== void 0) return node;
254
+ }
255
+ return null;
256
+ }
257
+ //#endregion
204
258
  //#region ../../internals/utils/src/casing.ts
205
259
  /**
206
260
  * Shared implementation for camelCase and PascalCase conversion.
@@ -475,6 +529,19 @@ const isOutputNode = isKind("Output");
475
529
  */
476
530
  const isOperationNode = isKind("Operation");
477
531
  /**
532
+ * Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * if (isHttpOperationNode(node)) {
537
+ * console.log(node.method, node.path)
538
+ * }
539
+ * ```
540
+ */
541
+ function isHttpOperationNode(node) {
542
+ return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
543
+ }
544
+ /**
478
545
  * Returns `true` when the input is a `SchemaNode`.
479
546
  *
480
547
  * @example
@@ -537,58 +604,84 @@ function createLimit(concurrency) {
537
604
  });
538
605
  };
539
606
  }
607
+ const visitorKeysByKind = {
608
+ Input: ["schemas", "operations"],
609
+ Operation: [
610
+ "parameters",
611
+ "requestBody",
612
+ "responses"
613
+ ],
614
+ RequestBody: ["content"],
615
+ Content: ["schema"],
616
+ Response: ["content"],
617
+ Schema: [
618
+ "properties",
619
+ "items",
620
+ "members",
621
+ "additionalProperties"
622
+ ],
623
+ Property: ["schema"],
624
+ Parameter: ["schema"]
625
+ };
540
626
  /**
541
- * Returns the immediate traversable children of `node`.
627
+ * Returns `true` when `value` is an AST node (an object carrying a `kind`).
628
+ */
629
+ function isNode(value) {
630
+ return typeof value === "object" && value !== null && "kind" in value;
631
+ }
632
+ /**
633
+ * Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
542
634
  *
543
- * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
544
- * `additionalProperties`) are only included
545
- * when `recurse` is `true`; shallow mode skips them.
635
+ * `Schema` children are only included when `recurse` is `true`; shallow mode skips them.
546
636
  *
547
637
  * @example
548
638
  * ```ts
549
639
  * const children = getChildren(operationNode, true)
550
- * // returns parameters, requestBody schema (if present), and responses
640
+ * // returns parameters, the request body, and responses
551
641
  * ```
552
642
  */
553
643
  function* getChildren(node, recurse) {
554
- if (node.kind === "Input") {
555
- yield* node.schemas;
556
- yield* node.operations;
557
- return;
558
- }
559
- if (node.kind === "Output") return;
560
- if (node.kind === "Operation") {
561
- yield* node.parameters;
562
- if (node.requestBody?.content) {
563
- for (const c of node.requestBody.content) if (c.schema) yield c.schema;
564
- }
565
- yield* node.responses;
566
- return;
567
- }
568
- if (node.kind === "Schema") {
569
- if (!recurse) return;
570
- if ("properties" in node && node.properties.length > 0) yield* node.properties;
571
- if ("items" in node && node.items) yield* node.items;
572
- if ("members" in node && node.members) yield* node.members;
573
- if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) yield node.additionalProperties;
574
- return;
575
- }
576
- if (node.kind === "Property") {
577
- yield node.schema;
578
- return;
579
- }
580
- if (node.kind === "Parameter") {
581
- yield node.schema;
582
- return;
583
- }
584
- if (node.kind === "Response") {
585
- if (node.content) {
586
- for (const c of node.content) if (c.schema) yield c.schema;
587
- }
588
- return;
644
+ if (node.kind === "Schema" && !recurse) return;
645
+ const keys = visitorKeysByKind[node.kind];
646
+ if (!keys) return;
647
+ const record = node;
648
+ for (const key of keys) {
649
+ const value = record[key];
650
+ if (Array.isArray(value)) {
651
+ for (const item of value) if (isNode(item)) yield item;
652
+ } else if (isNode(value)) yield value;
589
653
  }
590
654
  }
591
655
  /**
656
+ * Maps a node `kind` to the matching visitor callback name. Only the seven
657
+ * traversable node kinds have an entry; every other kind resolves to
658
+ * `undefined` and is skipped.
659
+ */
660
+ const VISITOR_KEY_BY_KIND = {
661
+ Input: "input",
662
+ Output: "output",
663
+ Operation: "operation",
664
+ Schema: "schema",
665
+ Property: "property",
666
+ Parameter: "parameter",
667
+ Response: "response"
668
+ };
669
+ /**
670
+ * Invokes the visitor callback that matches `node.kind`, passing the traversal
671
+ * context. Returns the callback's result (a replacement node, a collected
672
+ * value, or `undefined` when no callback is registered for the kind).
673
+ *
674
+ * Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
675
+ * in one place. `TResult` is the caller's expected return: the same node type
676
+ * for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
677
+ */
678
+ function applyVisitor(node, visitor, parent) {
679
+ const key = VISITOR_KEY_BY_KIND[node.kind];
680
+ if (!key) return void 0;
681
+ const fn = visitor[key];
682
+ return fn?.(node, { parent });
683
+ }
684
+ /**
592
685
  * Async depth-first traversal for side effects. Visitor return values are
593
686
  * ignored. Use `transform` when you want to rewrite nodes.
594
687
  *
@@ -614,122 +707,63 @@ async function walk(node, options) {
614
707
  return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
615
708
  }
616
709
  async function _walk(node, visitor, recurse, limit, parent) {
617
- switch (node.kind) {
618
- case "Input":
619
- await limit(() => visitor.input?.(node, { parent }));
620
- break;
621
- case "Output":
622
- await limit(() => visitor.output?.(node, { parent }));
623
- break;
624
- case "Operation":
625
- await limit(() => visitor.operation?.(node, { parent }));
626
- break;
627
- case "Schema":
628
- await limit(() => visitor.schema?.(node, { parent }));
629
- break;
630
- case "Property":
631
- await limit(() => visitor.property?.(node, { parent }));
632
- break;
633
- case "Parameter":
634
- await limit(() => visitor.parameter?.(node, { parent }));
635
- break;
636
- case "Response":
637
- await limit(() => visitor.response?.(node, { parent }));
638
- break;
639
- }
710
+ await limit(() => applyVisitor(node, visitor, parent));
640
711
  const children = getChildren(node, recurse);
641
712
  for (const child of children) await _walk(child, visitor, recurse, limit, node);
642
713
  }
643
714
  function transform(node, options) {
644
715
  const { depth, parent, ...visitor } = options;
645
716
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
646
- if (node.kind === "Input") {
647
- const input = visitor.input?.(node, { parent }) ?? node;
648
- return {
649
- ...input,
650
- schemas: input.schemas.map((s) => transform(s, {
651
- ...options,
652
- parent: input
653
- })),
654
- operations: input.operations.map((op) => transform(op, {
655
- ...options,
656
- parent: input
657
- }))
658
- };
659
- }
660
- if (node.kind === "Output") return visitor.output?.(node, { parent }) ?? node;
661
- if (node.kind === "Operation") {
662
- const op = visitor.operation?.(node, { parent }) ?? node;
663
- return {
664
- ...op,
665
- parameters: op.parameters.map((p) => transform(p, {
666
- ...options,
667
- parent: op
668
- })),
669
- requestBody: op.requestBody ? {
670
- ...op.requestBody,
671
- content: op.requestBody.content?.map((c) => ({
672
- ...c,
673
- schema: c.schema ? transform(c.schema, {
674
- ...options,
675
- parent: op
676
- }) : void 0
677
- }))
678
- } : void 0,
679
- responses: op.responses.map((r) => transform(r, {
680
- ...options,
681
- parent: op
682
- }))
683
- };
684
- }
685
- if (node.kind === "Schema") {
686
- const schema = visitor.schema?.(node, { parent }) ?? node;
687
- const childOptions = {
688
- ...options,
689
- parent: schema
690
- };
691
- return {
692
- ...schema,
693
- ..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
694
- ..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
695
- ..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
696
- ..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
697
- };
698
- }
699
- if (node.kind === "Property") {
700
- const prop = visitor.property?.(node, { parent }) ?? node;
701
- return createProperty({
702
- ...prop,
703
- schema: transform(prop.schema, {
704
- ...options,
705
- parent: prop
706
- })
707
- });
708
- }
709
- if (node.kind === "Parameter") {
710
- const param = visitor.parameter?.(node, { parent }) ?? node;
711
- return createParameter({
712
- ...param,
713
- schema: transform(param.schema, {
714
- ...options,
715
- parent: param
716
- })
717
- });
718
- }
719
- if (node.kind === "Response") {
720
- const response = visitor.response?.(node, { parent }) ?? node;
721
- return {
722
- ...response,
723
- content: response.content?.map((entry) => ({
724
- ...entry,
725
- schema: entry.schema ? transform(entry.schema, {
726
- ...options,
727
- parent: response
728
- }) : entry.schema
729
- }))
730
- };
717
+ const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
718
+ if (rebuilt === node) return node;
719
+ const finalize = nodeFinalizers[rebuilt.kind];
720
+ return finalize ? finalize(rebuilt) : rebuilt;
721
+ }
722
+ /**
723
+ * Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
724
+ * resync schema optionality against their `required` flag once the schema may
725
+ * have changed.
726
+ */
727
+ const nodeFinalizers = {
728
+ Property: (node) => createProperty(node),
729
+ Parameter: (node) => createParameter(node)
730
+ };
731
+ /**
732
+ * Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
733
+ * each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
734
+ * `Schema` children are skipped in shallow mode.
735
+ */
736
+ function transformChildren(node, options, recurse) {
737
+ if (node.kind === "Schema" && !recurse) return node;
738
+ const keys = visitorKeysByKind[node.kind];
739
+ if (!keys) return node;
740
+ const record = node;
741
+ const childOptions = {
742
+ ...options,
743
+ parent: node
744
+ };
745
+ let updates;
746
+ for (const key of keys) {
747
+ if (!(key in record)) continue;
748
+ const value = record[key];
749
+ if (Array.isArray(value)) {
750
+ let changed = false;
751
+ const mapped = value.map((item) => {
752
+ if (!isNode(item)) return item;
753
+ const next = transform(item, childOptions);
754
+ if (next !== item) changed = true;
755
+ return next;
756
+ });
757
+ if (changed) (updates ??= {})[key] = mapped;
758
+ } else if (isNode(value)) {
759
+ const next = transform(value, childOptions);
760
+ if (next !== value) (updates ??= {})[key] = next;
761
+ }
731
762
  }
732
- return node;
763
+ return updates ? {
764
+ ...node,
765
+ ...updates
766
+ } : node;
733
767
  }
734
768
  /**
735
769
  * Lazy depth-first collection pass. Yields every non-null value returned by
@@ -750,30 +784,7 @@ function transform(node, options) {
750
784
  function* collectLazy(node, options) {
751
785
  const { depth, parent, ...visitor } = options;
752
786
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
753
- let v;
754
- switch (node.kind) {
755
- case "Input":
756
- v = visitor.input?.(node, { parent });
757
- break;
758
- case "Output":
759
- v = visitor.output?.(node, { parent });
760
- break;
761
- case "Operation":
762
- v = visitor.operation?.(node, { parent });
763
- break;
764
- case "Schema":
765
- v = visitor.schema?.(node, { parent });
766
- break;
767
- case "Property":
768
- v = visitor.property?.(node, { parent });
769
- break;
770
- case "Parameter":
771
- v = visitor.parameter?.(node, { parent });
772
- break;
773
- case "Response":
774
- v = visitor.response?.(node, { parent });
775
- break;
776
- }
787
+ const v = applyVisitor(node, visitor, parent);
777
788
  if (v != null) yield v;
778
789
  for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
779
790
  ...options,
@@ -1135,6 +1146,16 @@ function combineSources(sources) {
1135
1146
  return [...seen.values()];
1136
1147
  }
1137
1148
  /**
1149
+ * Merges `incoming` names into `existing`, preserving order and dropping duplicates.
1150
+ *
1151
+ * Shared by `combineExports` and `combineImports` for the same-path name-merge case.
1152
+ */
1153
+ function mergeNameArrays(existing, incoming) {
1154
+ const merged = new Set(existing);
1155
+ for (const name of incoming) merged.add(name);
1156
+ return [...merged];
1157
+ }
1158
+ /**
1138
1159
  * Deduplicates and merges `ExportNode` objects by path and type.
1139
1160
  *
1140
1161
  * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
@@ -1155,11 +1176,8 @@ function combineExports(exports) {
1155
1176
  if (!name.length) continue;
1156
1177
  const key = pathTypeKey(path, isTypeOnly);
1157
1178
  const existing = namedByPath.get(key);
1158
- if (existing && Array.isArray(existing.name)) {
1159
- const merged = new Set(existing.name);
1160
- for (const n of name) merged.add(n);
1161
- existing.name = [...merged];
1162
- } else {
1179
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
1180
+ else {
1163
1181
  const newItem = {
1164
1182
  ...curr,
1165
1183
  name: [...new Set(name)]
@@ -1217,11 +1235,8 @@ function combineImports(imports, exports, source) {
1217
1235
  if (!name.length) continue;
1218
1236
  const key = pathTypeKey(path, isTypeOnly);
1219
1237
  const existing = namedByPath.get(key);
1220
- if (existing && Array.isArray(existing.name)) {
1221
- const merged = new Set(existing.name);
1222
- for (const n of name) merged.add(n);
1223
- existing.name = [...merged];
1224
- } else {
1238
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
1239
+ else {
1225
1240
  const newItem = {
1226
1241
  ...curr,
1227
1242
  name
@@ -1440,6 +1455,29 @@ function syncOptionality(schema, required) {
1440
1455
  };
1441
1456
  }
1442
1457
  /**
1458
+ * Identity-preserving node update: returns `node` unchanged when every field in
1459
+ * `changes` already equals (by reference) the current value, otherwise a new node
1460
+ * with the changes applied.
1461
+ *
1462
+ * Mirrors the TypeScript compiler's `factory.updateX` contract — pair it with the
1463
+ * structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
1464
+ * downstream passes can detect "nothing changed" by identity. Comparison is
1465
+ * shallow: a structurally-equal but newly-allocated array/object counts as a change.
1466
+ *
1467
+ * @example
1468
+ * ```ts
1469
+ * update(node, { name: node.name }) // -> same `node` reference
1470
+ * update(node, { name: 'renamed' }) // -> new node, `name` replaced
1471
+ * ```
1472
+ */
1473
+ function update(node, changes) {
1474
+ for (const key in changes) if (changes[key] !== node[key]) return {
1475
+ ...node,
1476
+ ...changes
1477
+ };
1478
+ return node;
1479
+ }
1480
+ /**
1443
1481
  * Creates an `InputNode` with stable defaults for `schemas` and `operations`.
1444
1482
  *
1445
1483
  * @example
@@ -1504,35 +1542,35 @@ function createOutput(overrides = {}) {
1504
1542
  };
1505
1543
  }
1506
1544
  /**
1507
- * Creates an `OperationNode` with default empty arrays for `tags`, `parameters`, and `responses`.
1508
- *
1509
- * @example
1510
- * ```ts
1511
- * const operation = createOperation({
1512
- * operationId: 'getPetById',
1513
- * method: 'GET',
1514
- * path: '/pet/{petId}',
1515
- * })
1516
- * // tags, parameters, and responses are []
1517
- * ```
1518
- *
1519
- * @example
1520
- * ```ts
1521
- * const operation = createOperation({
1522
- * operationId: 'findPets',
1523
- * method: 'GET',
1524
- * path: '/pet/findByStatus',
1525
- * tags: ['pet'],
1526
- * })
1527
- * ```
1545
+ * Creates a `ContentNode` for a single request-body or response content type.
1546
+ */
1547
+ function createContent(props) {
1548
+ return {
1549
+ ...props,
1550
+ kind: "Content"
1551
+ };
1552
+ }
1553
+ /**
1554
+ * Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
1528
1555
  */
1556
+ function createRequestBody(props) {
1557
+ return {
1558
+ ...props,
1559
+ kind: "RequestBody",
1560
+ content: props.content?.map(createContent)
1561
+ };
1562
+ }
1529
1563
  function createOperation(props) {
1564
+ const { requestBody, ...rest } = props;
1565
+ const isHttp = rest.method !== void 0 && rest.path !== void 0;
1530
1566
  return {
1531
1567
  tags: [],
1532
1568
  parameters: [],
1533
1569
  responses: [],
1534
- ...props,
1535
- kind: "Operation"
1570
+ ...rest,
1571
+ ...isHttp ? { protocol: "http" } : {},
1572
+ kind: "Operation",
1573
+ requestBody: requestBody ? createRequestBody(requestBody) : void 0
1536
1574
  };
1537
1575
  }
1538
1576
  /**
@@ -1660,14 +1698,15 @@ function createParameter(props) {
1660
1698
  */
1661
1699
  function createResponse(props) {
1662
1700
  const { schema, mediaType, keysToOmit, content, ...rest } = props;
1701
+ const entries = content ?? (schema ? [{
1702
+ contentType: mediaType ?? "application/json",
1703
+ schema,
1704
+ keysToOmit: keysToOmit ?? null
1705
+ }] : void 0);
1663
1706
  return {
1664
1707
  ...rest,
1665
1708
  kind: "Response",
1666
- content: content ?? (schema ? [{
1667
- contentType: mediaType ?? "application/json",
1668
- schema,
1669
- keysToOmit: keysToOmit ?? null
1670
- }] : void 0)
1709
+ content: entries?.map(createContent)
1671
1710
  };
1672
1711
  }
1673
1712
  /**
@@ -2292,6 +2331,6 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
2292
2331
  return propNode;
2293
2332
  }
2294
2333
  //#endregion
2295
- export { caseParams, childName, collect, collectImports, collectLazy, collectReferencedSchemaNames, collectUsedSchemaNames, containsCircularRef, createArrowFunction, createBreak, createConst, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter, createParameterGroup, createParamsType, createPrinterFactory, createProperty, createResponse, createSchema, createSource, createStreamInput, createText, createType, definePrinter, enumPropName, extractRefName, extractStringsFromNodes, findCircularSchemas, findDiscriminator, httpMethods, isInputNode, isOperationNode, isOutputNode, isScalarPrimitive, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, mergeAdjacentObjectsLazy, narrowSchema, nodeKinds, resolveRefName, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, walk };
2334
+ export { caseParams, childName, collect, collectImports, collectLazy, collectReferencedSchemaNames, collectUsedSchemaNames, containsCircularRef, createArrowFunction, createBreak, createConst, createContent, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter, createParameterGroup, createParamsType, createPrinterFactory, createProperty, createRequestBody, createResponse, createSchema, createSource, createStreamInput, createText, createType, definePrinter, defineSchemaDialect, dispatch, enumPropName, extractRefName, extractStringsFromNodes, findCircularSchemas, findDiscriminator, httpMethods, isHttpOperationNode, isInputNode, isOperationNode, isOutputNode, isScalarPrimitive, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, mergeAdjacentObjectsLazy, narrowSchema, nodeKinds, resolveRefName, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, update, walk };
2296
2335
 
2297
2336
  //# sourceMappingURL=index.js.map