@kubb/ast 5.0.0-beta.4 → 5.0.0-beta.40
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/README.md +17 -8
- package/dist/index.cjs +866 -395
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +33 -3310
- package/dist/index.js +859 -391
- package/dist/index.js.map +1 -1
- package/dist/types-CEIHPTfs.d.ts +3596 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/package.json +8 -4
- package/src/constants.ts +2 -52
- package/src/dedupe.ts +200 -0
- package/src/dialect.ts +58 -0
- package/src/dispatch.ts +53 -0
- package/src/factory.ts +133 -17
- package/src/guards.ts +18 -48
- package/src/index.ts +9 -5
- package/src/infer.ts +16 -14
- package/src/nodes/base.ts +2 -0
- package/src/nodes/code.ts +22 -22
- package/src/nodes/content.ts +37 -0
- package/src/nodes/file.ts +16 -14
- package/src/nodes/function.ts +11 -11
- package/src/nodes/index.ts +7 -3
- package/src/nodes/operation.ts +98 -62
- package/src/nodes/response.ts +21 -14
- package/src/nodes/root.ts +72 -10
- package/src/nodes/schema.ts +12 -9
- package/src/printer.ts +42 -36
- package/src/refs.ts +0 -54
- package/src/resolvers.ts +4 -4
- package/src/signature.ts +232 -0
- package/src/transformers.ts +20 -15
- package/src/types.ts +9 -2
- package/src/utils.ts +118 -77
- package/src/visitor.ts +237 -279
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -29,25 +29,6 @@ const visitorDepths = {
|
|
|
29
29
|
shallow: "shallow",
|
|
30
30
|
deep: "deep"
|
|
31
31
|
};
|
|
32
|
-
const nodeKinds = {
|
|
33
|
-
input: "Input",
|
|
34
|
-
output: "Output",
|
|
35
|
-
operation: "Operation",
|
|
36
|
-
schema: "Schema",
|
|
37
|
-
property: "Property",
|
|
38
|
-
parameter: "Parameter",
|
|
39
|
-
response: "Response",
|
|
40
|
-
functionParameter: "FunctionParameter",
|
|
41
|
-
parameterGroup: "ParameterGroup",
|
|
42
|
-
functionParameters: "FunctionParameters",
|
|
43
|
-
type: "Type",
|
|
44
|
-
file: "File",
|
|
45
|
-
import: "Import",
|
|
46
|
-
export: "Export",
|
|
47
|
-
source: "Source",
|
|
48
|
-
text: "Text",
|
|
49
|
-
break: "Break"
|
|
50
|
-
};
|
|
51
32
|
/**
|
|
52
33
|
* Schema type discriminators used by all AST schema nodes.
|
|
53
34
|
*
|
|
@@ -196,33 +177,6 @@ const httpMethods = {
|
|
|
196
177
|
options: "OPTIONS",
|
|
197
178
|
trace: "TRACE"
|
|
198
179
|
};
|
|
199
|
-
/**
|
|
200
|
-
* Common MIME types used in request/response content negotiation.
|
|
201
|
-
*
|
|
202
|
-
* Covers JSON, XML, form data, PDFs, images, audio, and video formats.
|
|
203
|
-
* Use these as keys when serializing request/response bodies.
|
|
204
|
-
*/
|
|
205
|
-
const mediaTypes = {
|
|
206
|
-
applicationJson: "application/json",
|
|
207
|
-
applicationXml: "application/xml",
|
|
208
|
-
applicationFormUrlEncoded: "application/x-www-form-urlencoded",
|
|
209
|
-
applicationOctetStream: "application/octet-stream",
|
|
210
|
-
applicationPdf: "application/pdf",
|
|
211
|
-
applicationZip: "application/zip",
|
|
212
|
-
applicationGraphql: "application/graphql",
|
|
213
|
-
multipartFormData: "multipart/form-data",
|
|
214
|
-
textPlain: "text/plain",
|
|
215
|
-
textHtml: "text/html",
|
|
216
|
-
textCsv: "text/csv",
|
|
217
|
-
textXml: "text/xml",
|
|
218
|
-
imagePng: "image/png",
|
|
219
|
-
imageJpeg: "image/jpeg",
|
|
220
|
-
imageGif: "image/gif",
|
|
221
|
-
imageWebp: "image/webp",
|
|
222
|
-
imageSvgXml: "image/svg+xml",
|
|
223
|
-
audioMpeg: "audio/mpeg",
|
|
224
|
-
videoMp4: "video/mp4"
|
|
225
|
-
};
|
|
226
180
|
//#endregion
|
|
227
181
|
//#region ../../internals/utils/src/casing.ts
|
|
228
182
|
/**
|
|
@@ -288,6 +242,46 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
|
288
242
|
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
289
243
|
}
|
|
290
244
|
//#endregion
|
|
245
|
+
//#region ../../internals/utils/src/promise.ts
|
|
246
|
+
/**
|
|
247
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
248
|
+
*
|
|
249
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
250
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
251
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
252
|
+
* inner (created once per outer miss) keyed by the second.
|
|
253
|
+
*
|
|
254
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
255
|
+
* cleared independently of the memoized function.
|
|
256
|
+
*
|
|
257
|
+
* @example Single WeakMap key
|
|
258
|
+
* ```ts
|
|
259
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
260
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* @example Single Map key (primitive)
|
|
264
|
+
* ```ts
|
|
265
|
+
* const cache = new Map<string, Resolver>()
|
|
266
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* @example Two-level (object + primitive)
|
|
270
|
+
* ```ts
|
|
271
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
272
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
273
|
+
* fn(params)('camelcase')
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
function memoize(store, factory) {
|
|
277
|
+
return (key) => {
|
|
278
|
+
if (store.has(key)) return store.get(key);
|
|
279
|
+
const value = factory(key);
|
|
280
|
+
store.set(key, value);
|
|
281
|
+
return value;
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
291
285
|
//#region ../../internals/utils/src/reserved.ts
|
|
292
286
|
/**
|
|
293
287
|
* JavaScript and Java reserved words.
|
|
@@ -415,11 +409,11 @@ function trimExtName(text) {
|
|
|
415
409
|
* @example
|
|
416
410
|
* ```ts
|
|
417
411
|
* const schema = createSchema({ type: 'string' })
|
|
418
|
-
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode |
|
|
412
|
+
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
|
|
419
413
|
* ```
|
|
420
414
|
*/
|
|
421
415
|
function narrowSchema(node, type) {
|
|
422
|
-
return node?.type === type ? node :
|
|
416
|
+
return node?.type === type ? node : null;
|
|
423
417
|
}
|
|
424
418
|
function isKind(kind) {
|
|
425
419
|
return (node) => node.kind === kind;
|
|
@@ -458,6 +452,19 @@ const isOutputNode = isKind("Output");
|
|
|
458
452
|
*/
|
|
459
453
|
const isOperationNode = isKind("Operation");
|
|
460
454
|
/**
|
|
455
|
+
* Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```ts
|
|
459
|
+
* if (isHttpOperationNode(node)) {
|
|
460
|
+
* console.log(node.method, node.path)
|
|
461
|
+
* }
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
function isHttpOperationNode(node) {
|
|
465
|
+
return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
461
468
|
* Returns `true` when the input is a `SchemaNode`.
|
|
462
469
|
*
|
|
463
470
|
* @example
|
|
@@ -468,12 +475,6 @@ const isOperationNode = isKind("Operation");
|
|
|
468
475
|
* ```
|
|
469
476
|
*/
|
|
470
477
|
const isSchemaNode = isKind("Schema");
|
|
471
|
-
isKind("Property");
|
|
472
|
-
isKind("Parameter");
|
|
473
|
-
isKind("Response");
|
|
474
|
-
isKind("FunctionParameter");
|
|
475
|
-
isKind("ParameterGroup");
|
|
476
|
-
isKind("FunctionParameters");
|
|
477
478
|
//#endregion
|
|
478
479
|
//#region src/refs.ts
|
|
479
480
|
/**
|
|
@@ -526,53 +527,92 @@ function createLimit(concurrency) {
|
|
|
526
527
|
});
|
|
527
528
|
};
|
|
528
529
|
}
|
|
530
|
+
const visitorKeysByKind = {
|
|
531
|
+
Input: ["schemas", "operations"],
|
|
532
|
+
Operation: [
|
|
533
|
+
"parameters",
|
|
534
|
+
"requestBody",
|
|
535
|
+
"responses"
|
|
536
|
+
],
|
|
537
|
+
RequestBody: ["content"],
|
|
538
|
+
Content: ["schema"],
|
|
539
|
+
Response: ["content"],
|
|
540
|
+
Schema: [
|
|
541
|
+
"properties",
|
|
542
|
+
"items",
|
|
543
|
+
"members",
|
|
544
|
+
"additionalProperties"
|
|
545
|
+
],
|
|
546
|
+
Property: ["schema"],
|
|
547
|
+
Parameter: ["schema"]
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* Returns `true` when `value` is an AST node (an object carrying a `kind`).
|
|
551
|
+
*/
|
|
552
|
+
function isNode(value) {
|
|
553
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
554
|
+
}
|
|
529
555
|
/**
|
|
530
|
-
* Returns the immediate traversable children of `node
|
|
556
|
+
* Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
|
|
531
557
|
*
|
|
532
|
-
*
|
|
533
|
-
* `additionalProperties`) are only included
|
|
534
|
-
* when `recurse` is `true`; shallow mode skips them.
|
|
558
|
+
* `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
|
|
535
559
|
*
|
|
536
560
|
* @example
|
|
537
561
|
* ```ts
|
|
538
562
|
* const children = getChildren(operationNode, true)
|
|
539
|
-
* // returns parameters,
|
|
540
|
-
* ```
|
|
541
|
-
*/
|
|
542
|
-
function getChildren(node, recurse) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const children = [];
|
|
553
|
-
if (!recurse) return [];
|
|
554
|
-
if ("properties" in node && node.properties.length > 0) children.push(...node.properties);
|
|
555
|
-
if ("items" in node && node.items) children.push(...node.items);
|
|
556
|
-
if ("members" in node && node.members) children.push(...node.members);
|
|
557
|
-
if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
|
|
558
|
-
return children;
|
|
559
|
-
}
|
|
560
|
-
case "Property": return [node.schema];
|
|
561
|
-
case "Parameter": return [node.schema];
|
|
562
|
-
case "Response": return node.schema ? [node.schema] : [];
|
|
563
|
-
case "FunctionParameter":
|
|
564
|
-
case "ParameterGroup":
|
|
565
|
-
case "FunctionParameters":
|
|
566
|
-
case "Type": return [];
|
|
567
|
-
default: return [];
|
|
563
|
+
* // returns parameters, the request body, and responses
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
function* getChildren(node, recurse) {
|
|
567
|
+
if (node.kind === "Schema" && !recurse) return;
|
|
568
|
+
const keys = visitorKeysByKind[node.kind];
|
|
569
|
+
if (!keys) return;
|
|
570
|
+
const record = node;
|
|
571
|
+
for (const key of keys) {
|
|
572
|
+
const value = record[key];
|
|
573
|
+
if (Array.isArray(value)) {
|
|
574
|
+
for (const item of value) if (isNode(item)) yield item;
|
|
575
|
+
} else if (isNode(value)) yield value;
|
|
568
576
|
}
|
|
569
577
|
}
|
|
570
578
|
/**
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
579
|
+
* Maps a node `kind` to the matching visitor callback name. Only the seven
|
|
580
|
+
* traversable node kinds have an entry. Every other kind resolves to
|
|
581
|
+
* `undefined` and is skipped.
|
|
582
|
+
*/
|
|
583
|
+
const VISITOR_KEY_BY_KIND = {
|
|
584
|
+
Input: "input",
|
|
585
|
+
Output: "output",
|
|
586
|
+
Operation: "operation",
|
|
587
|
+
Schema: "schema",
|
|
588
|
+
Property: "property",
|
|
589
|
+
Parameter: "parameter",
|
|
590
|
+
Response: "response"
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* Invokes the visitor callback that matches `node.kind`, passing the traversal
|
|
594
|
+
* context. Returns the callback's result (a replacement node, a collected
|
|
595
|
+
* value, or `undefined` when no callback is registered for the kind).
|
|
574
596
|
*
|
|
575
|
-
*
|
|
597
|
+
* Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
|
|
598
|
+
* in one place. `TResult` is the caller's expected return: the same node type
|
|
599
|
+
* for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
|
|
600
|
+
*/
|
|
601
|
+
function applyVisitor(node, visitor, parent) {
|
|
602
|
+
const key = VISITOR_KEY_BY_KIND[node.kind];
|
|
603
|
+
if (!key) return void 0;
|
|
604
|
+
const fn = visitor[key];
|
|
605
|
+
return fn?.(node, { parent });
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Async depth-first traversal for side effects. Visitor return values are
|
|
609
|
+
* ignored. Use `transform` when you want to rewrite nodes.
|
|
610
|
+
*
|
|
611
|
+
* Sibling nodes at each depth run concurrently up to `options.concurrency`
|
|
612
|
+
* (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
|
|
613
|
+
* work. Lower values reduce memory pressure.
|
|
614
|
+
*
|
|
615
|
+
* @example Log every operation
|
|
576
616
|
* ```ts
|
|
577
617
|
* await walk(root, {
|
|
578
618
|
* operation(node) {
|
|
@@ -581,213 +621,115 @@ function getChildren(node, recurse) {
|
|
|
581
621
|
* })
|
|
582
622
|
* ```
|
|
583
623
|
*
|
|
584
|
-
* @example
|
|
624
|
+
* @example Only visit the root node
|
|
585
625
|
* ```ts
|
|
586
|
-
*
|
|
587
|
-
* await walk(root, { depth: 'shallow', root: () => {} })
|
|
626
|
+
* await walk(root, { depth: 'shallow', input: () => {} })
|
|
588
627
|
* ```
|
|
589
628
|
*/
|
|
590
629
|
async function walk(node, options) {
|
|
591
630
|
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
592
631
|
}
|
|
593
632
|
async function _walk(node, visitor, recurse, limit, parent) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
case "Output":
|
|
599
|
-
await limit(() => visitor.output?.(node, { parent }));
|
|
600
|
-
break;
|
|
601
|
-
case "Operation":
|
|
602
|
-
await limit(() => visitor.operation?.(node, { parent }));
|
|
603
|
-
break;
|
|
604
|
-
case "Schema":
|
|
605
|
-
await limit(() => visitor.schema?.(node, { parent }));
|
|
606
|
-
break;
|
|
607
|
-
case "Property":
|
|
608
|
-
await limit(() => visitor.property?.(node, { parent }));
|
|
609
|
-
break;
|
|
610
|
-
case "Parameter":
|
|
611
|
-
await limit(() => visitor.parameter?.(node, { parent }));
|
|
612
|
-
break;
|
|
613
|
-
case "Response":
|
|
614
|
-
await limit(() => visitor.response?.(node, { parent }));
|
|
615
|
-
break;
|
|
616
|
-
case "FunctionParameter":
|
|
617
|
-
case "ParameterGroup":
|
|
618
|
-
case "FunctionParameters": break;
|
|
619
|
-
}
|
|
620
|
-
const children = getChildren(node, recurse);
|
|
621
|
-
for (const child of children) await _walk(child, visitor, recurse, limit, node);
|
|
633
|
+
await limit(() => applyVisitor(node, visitor, parent));
|
|
634
|
+
const children = Array.from(getChildren(node, recurse));
|
|
635
|
+
if (children.length === 0) return;
|
|
636
|
+
await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
|
|
622
637
|
}
|
|
623
638
|
function transform(node, options) {
|
|
624
639
|
const { depth, parent, ...visitor } = options;
|
|
625
640
|
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
parent: op
|
|
666
|
-
}) : void 0
|
|
667
|
-
}))
|
|
668
|
-
} : void 0,
|
|
669
|
-
responses: op.responses.map((r) => transform(r, {
|
|
670
|
-
...options,
|
|
671
|
-
parent: op
|
|
672
|
-
}))
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
case "Schema": {
|
|
676
|
-
let schema = node;
|
|
677
|
-
const replaced = visitor.schema?.(schema, { parent });
|
|
678
|
-
if (replaced) schema = replaced;
|
|
679
|
-
const childOptions = {
|
|
680
|
-
...options,
|
|
681
|
-
parent: schema
|
|
682
|
-
};
|
|
683
|
-
return {
|
|
684
|
-
...schema,
|
|
685
|
-
..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
|
|
686
|
-
..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
|
|
687
|
-
..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
|
|
688
|
-
..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
case "Property": {
|
|
692
|
-
let prop = node;
|
|
693
|
-
const replaced = visitor.property?.(prop, { parent });
|
|
694
|
-
if (replaced) prop = replaced;
|
|
695
|
-
return createProperty({
|
|
696
|
-
...prop,
|
|
697
|
-
schema: transform(prop.schema, {
|
|
698
|
-
...options,
|
|
699
|
-
parent: prop
|
|
700
|
-
})
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
case "Parameter": {
|
|
704
|
-
let param = node;
|
|
705
|
-
const replaced = visitor.parameter?.(param, { parent });
|
|
706
|
-
if (replaced) param = replaced;
|
|
707
|
-
return createParameter({
|
|
708
|
-
...param,
|
|
709
|
-
schema: transform(param.schema, {
|
|
710
|
-
...options,
|
|
711
|
-
parent: param
|
|
712
|
-
})
|
|
641
|
+
const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
|
|
642
|
+
if (rebuilt === node) return node;
|
|
643
|
+
const finalize = nodeFinalizers[rebuilt.kind];
|
|
644
|
+
return finalize ? finalize(rebuilt) : rebuilt;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
|
|
648
|
+
* resync schema optionality against their `required` flag once the schema may
|
|
649
|
+
* have changed.
|
|
650
|
+
*/
|
|
651
|
+
const nodeFinalizers = {
|
|
652
|
+
Property: (node) => createProperty(node),
|
|
653
|
+
Parameter: (node) => createParameter(node)
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
|
|
657
|
+
* each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
|
|
658
|
+
* `Schema` children are skipped in shallow mode.
|
|
659
|
+
*/
|
|
660
|
+
function transformChildren(node, options, recurse) {
|
|
661
|
+
if (node.kind === "Schema" && !recurse) return node;
|
|
662
|
+
const keys = visitorKeysByKind[node.kind];
|
|
663
|
+
if (!keys) return node;
|
|
664
|
+
const record = node;
|
|
665
|
+
const childOptions = {
|
|
666
|
+
...options,
|
|
667
|
+
parent: node
|
|
668
|
+
};
|
|
669
|
+
let updates;
|
|
670
|
+
for (const key of keys) {
|
|
671
|
+
if (!(key in record)) continue;
|
|
672
|
+
const value = record[key];
|
|
673
|
+
if (Array.isArray(value)) {
|
|
674
|
+
let changed = false;
|
|
675
|
+
const mapped = value.map((item) => {
|
|
676
|
+
if (!isNode(item)) return item;
|
|
677
|
+
const next = transform(item, childOptions);
|
|
678
|
+
if (next !== item) changed = true;
|
|
679
|
+
return next;
|
|
713
680
|
});
|
|
681
|
+
if (changed) (updates ??= {})[key] = mapped;
|
|
682
|
+
} else if (isNode(value)) {
|
|
683
|
+
const next = transform(value, childOptions);
|
|
684
|
+
if (next !== value) (updates ??= {})[key] = next;
|
|
714
685
|
}
|
|
715
|
-
case "Response": {
|
|
716
|
-
let response = node;
|
|
717
|
-
const replaced = visitor.response?.(response, { parent });
|
|
718
|
-
if (replaced) response = replaced;
|
|
719
|
-
return {
|
|
720
|
-
...response,
|
|
721
|
-
schema: transform(response.schema, {
|
|
722
|
-
...options,
|
|
723
|
-
parent: response
|
|
724
|
-
})
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
case "FunctionParameter":
|
|
728
|
-
case "ParameterGroup":
|
|
729
|
-
case "FunctionParameters":
|
|
730
|
-
case "Type": return node;
|
|
731
|
-
default: return node;
|
|
732
686
|
}
|
|
687
|
+
return updates ? {
|
|
688
|
+
...node,
|
|
689
|
+
...updates
|
|
690
|
+
} : node;
|
|
733
691
|
}
|
|
734
692
|
/**
|
|
735
|
-
*
|
|
693
|
+
* Lazy depth-first collection pass. Yields every non-null value returned by
|
|
694
|
+
* the visitor callbacks. Use `collect` for the eager array form.
|
|
736
695
|
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
* @example
|
|
696
|
+
* @example Collect every operationId
|
|
740
697
|
* ```ts
|
|
741
|
-
* const ids =
|
|
698
|
+
* const ids: string[] = []
|
|
699
|
+
* for (const id of collectLazy<string>(root, {
|
|
742
700
|
* operation(node) {
|
|
743
701
|
* return node.operationId
|
|
744
702
|
* },
|
|
745
|
-
* })
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* @example
|
|
749
|
-
* ```ts
|
|
750
|
-
* // Collect from only the current node
|
|
751
|
-
* const values = collect(root, { depth: 'shallow', root: () => 'root' })
|
|
703
|
+
* })) {
|
|
704
|
+
* ids.push(id)
|
|
705
|
+
* }
|
|
752
706
|
* ```
|
|
753
707
|
*/
|
|
754
|
-
function
|
|
708
|
+
function* collectLazy(node, options) {
|
|
755
709
|
const { depth, parent, ...visitor } = options;
|
|
756
710
|
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
case "Input":
|
|
761
|
-
v = visitor.input?.(node, { parent });
|
|
762
|
-
break;
|
|
763
|
-
case "Output":
|
|
764
|
-
v = visitor.output?.(node, { parent });
|
|
765
|
-
break;
|
|
766
|
-
case "Operation":
|
|
767
|
-
v = visitor.operation?.(node, { parent });
|
|
768
|
-
break;
|
|
769
|
-
case "Schema":
|
|
770
|
-
v = visitor.schema?.(node, { parent });
|
|
771
|
-
break;
|
|
772
|
-
case "Property":
|
|
773
|
-
v = visitor.property?.(node, { parent });
|
|
774
|
-
break;
|
|
775
|
-
case "Parameter":
|
|
776
|
-
v = visitor.parameter?.(node, { parent });
|
|
777
|
-
break;
|
|
778
|
-
case "Response":
|
|
779
|
-
v = visitor.response?.(node, { parent });
|
|
780
|
-
break;
|
|
781
|
-
case "FunctionParameter":
|
|
782
|
-
case "ParameterGroup":
|
|
783
|
-
case "FunctionParameters": break;
|
|
784
|
-
}
|
|
785
|
-
if (v !== void 0) results.push(v);
|
|
786
|
-
for (const child of getChildren(node, recurse)) for (const item of collect(child, {
|
|
711
|
+
const v = applyVisitor(node, visitor, parent);
|
|
712
|
+
if (v != null) yield v;
|
|
713
|
+
for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
|
|
787
714
|
...options,
|
|
788
715
|
parent: node
|
|
789
|
-
})
|
|
790
|
-
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Eager depth-first collection pass. Returns an array of every non-null value
|
|
720
|
+
* the visitor callbacks return.
|
|
721
|
+
*
|
|
722
|
+
* @example Collect every operationId
|
|
723
|
+
* ```ts
|
|
724
|
+
* const ids = collect<string>(root, {
|
|
725
|
+
* operation(node) {
|
|
726
|
+
* return node.operationId
|
|
727
|
+
* },
|
|
728
|
+
* })
|
|
729
|
+
* ```
|
|
730
|
+
*/
|
|
731
|
+
function collect(node, options) {
|
|
732
|
+
return Array.from(collectLazy(node, options));
|
|
791
733
|
}
|
|
792
734
|
//#endregion
|
|
793
735
|
//#region src/utils.ts
|
|
@@ -841,15 +783,16 @@ function isStringType(node) {
|
|
|
841
783
|
* the desired casing while preserving `OperationNode.parameters` for other consumers.
|
|
842
784
|
* The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
|
|
843
785
|
*/
|
|
786
|
+
const caseParamsMemo = memoize(/* @__PURE__ */ new WeakMap(), (params) => memoize(/* @__PURE__ */ new Map(), (casing) => params.map((param) => {
|
|
787
|
+
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
788
|
+
return {
|
|
789
|
+
...param,
|
|
790
|
+
name: transformed
|
|
791
|
+
};
|
|
792
|
+
})));
|
|
844
793
|
function caseParams(params, casing) {
|
|
845
794
|
if (!casing) return params;
|
|
846
|
-
return params
|
|
847
|
-
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
848
|
-
return {
|
|
849
|
-
...param,
|
|
850
|
-
name: transformed
|
|
851
|
-
};
|
|
852
|
-
});
|
|
795
|
+
return caseParamsMemo(params)(casing);
|
|
853
796
|
}
|
|
854
797
|
/**
|
|
855
798
|
* Creates a single-property object schema used as a discriminator literal.
|
|
@@ -978,7 +921,7 @@ function createOperationParams(node, options) {
|
|
|
978
921
|
}));
|
|
979
922
|
} else {
|
|
980
923
|
if (pathParams.length) if (pathParamsType === "inlineSpread") {
|
|
981
|
-
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0])
|
|
924
|
+
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]);
|
|
982
925
|
params.push(createFunctionParameter({
|
|
983
926
|
name: pathName,
|
|
984
927
|
type: spreadType ? wrapType(spreadType) : void 0,
|
|
@@ -1054,13 +997,13 @@ function buildGroupParam({ name, node, params, groupType, resolver, wrapType })
|
|
|
1054
997
|
}
|
|
1055
998
|
/**
|
|
1056
999
|
* Derives a {@link ParamGroupType} from the resolver's group method.
|
|
1057
|
-
* Returns `
|
|
1000
|
+
* Returns `null` when the group name equals the individual param name (no real group).
|
|
1058
1001
|
*/
|
|
1059
1002
|
function resolveGroupType({ node, params, groupMethod, resolver }) {
|
|
1060
|
-
if (!params.length) return;
|
|
1003
|
+
if (!params.length) return null;
|
|
1061
1004
|
const firstParam = params[0];
|
|
1062
1005
|
const groupName = groupMethod.call(resolver, node, firstParam);
|
|
1063
|
-
if (groupName === resolver.resolveParamName(node, firstParam)) return;
|
|
1006
|
+
if (groupName === resolver.resolveParamName(node, firstParam)) return null;
|
|
1064
1007
|
const allOptional = params.every((p) => !p.required);
|
|
1065
1008
|
return {
|
|
1066
1009
|
type: createParamsType({
|
|
@@ -1104,7 +1047,7 @@ function importKey(path, name, isTypeOnly) {
|
|
|
1104
1047
|
}
|
|
1105
1048
|
/**
|
|
1106
1049
|
* Computes a multi-level sort key for exports and imports:
|
|
1107
|
-
* non-array names first (wildcards/namespace aliases)
|
|
1050
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
1108
1051
|
*/
|
|
1109
1052
|
function sortKey(node) {
|
|
1110
1053
|
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
@@ -1127,6 +1070,16 @@ function combineSources(sources) {
|
|
|
1127
1070
|
return [...seen.values()];
|
|
1128
1071
|
}
|
|
1129
1072
|
/**
|
|
1073
|
+
* Merges `incoming` names into `existing`, preserving order and dropping duplicates.
|
|
1074
|
+
*
|
|
1075
|
+
* Shared by `combineExports` and `combineImports` for the same-path name-merge case.
|
|
1076
|
+
*/
|
|
1077
|
+
function mergeNameArrays(existing, incoming) {
|
|
1078
|
+
const merged = new Set(existing);
|
|
1079
|
+
for (const name of incoming) merged.add(name);
|
|
1080
|
+
return [...merged];
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1130
1083
|
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
1131
1084
|
*
|
|
1132
1085
|
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
@@ -1147,11 +1100,8 @@ function combineExports(exports) {
|
|
|
1147
1100
|
if (!name.length) continue;
|
|
1148
1101
|
const key = pathTypeKey(path, isTypeOnly);
|
|
1149
1102
|
const existing = namedByPath.get(key);
|
|
1150
|
-
if (existing && Array.isArray(existing.name))
|
|
1151
|
-
|
|
1152
|
-
for (const n of name) merged.add(n);
|
|
1153
|
-
existing.name = [...merged];
|
|
1154
|
-
} else {
|
|
1103
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
1104
|
+
else {
|
|
1155
1105
|
const newItem = {
|
|
1156
1106
|
...curr,
|
|
1157
1107
|
name: [...new Set(name)]
|
|
@@ -1187,6 +1137,11 @@ function combineImports(imports, exports, source) {
|
|
|
1187
1137
|
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
1188
1138
|
return importNameMemo.get(key);
|
|
1189
1139
|
};
|
|
1140
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
1141
|
+
for (const node of imports) {
|
|
1142
|
+
if (!Array.isArray(node.name)) continue;
|
|
1143
|
+
if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
|
|
1144
|
+
}
|
|
1190
1145
|
const result = [];
|
|
1191
1146
|
const namedByPath = /* @__PURE__ */ new Map();
|
|
1192
1147
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1204,11 +1159,8 @@ function combineImports(imports, exports, source) {
|
|
|
1204
1159
|
if (!name.length) continue;
|
|
1205
1160
|
const key = pathTypeKey(path, isTypeOnly);
|
|
1206
1161
|
const existing = namedByPath.get(key);
|
|
1207
|
-
if (existing && Array.isArray(existing.name))
|
|
1208
|
-
|
|
1209
|
-
for (const n of name) merged.add(n);
|
|
1210
|
-
existing.name = [...merged];
|
|
1211
|
-
} else {
|
|
1162
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
1163
|
+
else {
|
|
1212
1164
|
const newItem = {
|
|
1213
1165
|
...curr,
|
|
1214
1166
|
name
|
|
@@ -1217,7 +1169,7 @@ function combineImports(imports, exports, source) {
|
|
|
1217
1169
|
namedByPath.set(key, newItem);
|
|
1218
1170
|
}
|
|
1219
1171
|
} else {
|
|
1220
|
-
if (name && !isUsed(name)) continue;
|
|
1172
|
+
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
1221
1173
|
const key = importKey(path, name, isTypeOnly);
|
|
1222
1174
|
if (!seen.has(key)) {
|
|
1223
1175
|
result.push(curr);
|
|
@@ -1253,7 +1205,7 @@ function extractStringsFromNodes(nodes) {
|
|
|
1253
1205
|
/**
|
|
1254
1206
|
* Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
|
|
1255
1207
|
*
|
|
1256
|
-
* Returns `
|
|
1208
|
+
* Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
|
|
1257
1209
|
* identifier for type definitions or error messages.
|
|
1258
1210
|
*
|
|
1259
1211
|
* @example
|
|
@@ -1263,14 +1215,14 @@ function extractStringsFromNodes(nodes) {
|
|
|
1263
1215
|
* ```
|
|
1264
1216
|
*/
|
|
1265
1217
|
function resolveRefName(node) {
|
|
1266
|
-
if (!node || node.type !== "ref") return
|
|
1267
|
-
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ??
|
|
1268
|
-
return node.name ?? node.schema?.name ??
|
|
1218
|
+
if (!node || node.type !== "ref") return null;
|
|
1219
|
+
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null;
|
|
1220
|
+
return node.name ?? node.schema?.name ?? null;
|
|
1269
1221
|
}
|
|
1270
1222
|
/**
|
|
1271
1223
|
* Collects every named schema referenced (transitively) from a node via ref edges.
|
|
1272
1224
|
*
|
|
1273
|
-
* Refs are followed by name only
|
|
1225
|
+
* Refs are followed by name only, the resolved `node.schema` is not traversed inline.
|
|
1274
1226
|
* Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
|
|
1275
1227
|
*
|
|
1276
1228
|
* @example Collect refs from a single schema
|
|
@@ -1287,21 +1239,26 @@ function resolveRefName(node) {
|
|
|
1287
1239
|
* }
|
|
1288
1240
|
* ```
|
|
1289
1241
|
*/
|
|
1290
|
-
|
|
1291
|
-
|
|
1242
|
+
const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
|
|
1243
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1292
1244
|
collect(node, { schema(child) {
|
|
1293
1245
|
if (child.type === "ref") {
|
|
1294
1246
|
const name = resolveRefName(child);
|
|
1295
|
-
if (name)
|
|
1247
|
+
if (name) refs.add(name);
|
|
1296
1248
|
}
|
|
1297
1249
|
} });
|
|
1250
|
+
return refs;
|
|
1251
|
+
});
|
|
1252
|
+
function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
1253
|
+
if (!node) return out;
|
|
1254
|
+
for (const name of collectSchemaRefs(node)) out.add(name);
|
|
1298
1255
|
return out;
|
|
1299
1256
|
}
|
|
1300
1257
|
/**
|
|
1301
1258
|
* Collects the names of all top-level schemas transitively used by a set of operations.
|
|
1302
1259
|
*
|
|
1303
1260
|
* An operation uses a schema when any of its parameters, request body content, or responses
|
|
1304
|
-
* reference it
|
|
1261
|
+
* reference it, directly or indirectly through other named schemas.
|
|
1305
1262
|
* The walk is iterative and safe against reference cycles.
|
|
1306
1263
|
*
|
|
1307
1264
|
* Use this together with `include` filters to determine which schemas from `components/schemas`
|
|
@@ -1310,10 +1267,10 @@ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
|
1310
1267
|
*
|
|
1311
1268
|
* @example Only generate schemas referenced by included operations
|
|
1312
1269
|
* ```ts
|
|
1313
|
-
* const includedOps =
|
|
1314
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
1270
|
+
* const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
|
|
1271
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1315
1272
|
*
|
|
1316
|
-
* for (const schema of
|
|
1273
|
+
* for (const schema of schemas) {
|
|
1317
1274
|
* if (schema.name && !allowed.has(schema.name)) continue
|
|
1318
1275
|
* // … generate schema
|
|
1319
1276
|
* }
|
|
@@ -1321,11 +1278,12 @@ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
|
1321
1278
|
*
|
|
1322
1279
|
* @example Check whether a specific schema is needed
|
|
1323
1280
|
* ```ts
|
|
1324
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
1281
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1325
1282
|
* allowed.has('OrderStatus') // false when no included operation references OrderStatus
|
|
1326
1283
|
* ```
|
|
1327
1284
|
*/
|
|
1328
|
-
|
|
1285
|
+
const collectUsedSchemaNamesMemo = memoize(/* @__PURE__ */ new WeakMap(), (ops) => memoize(/* @__PURE__ */ new WeakMap(), (schemas) => computeUsedSchemaNames(ops, schemas)));
|
|
1286
|
+
function computeUsedSchemaNames(operations, schemas) {
|
|
1329
1287
|
const schemaMap = /* @__PURE__ */ new Map();
|
|
1330
1288
|
for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
|
|
1331
1289
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -1337,22 +1295,17 @@ function collectUsedSchemaNames(operations, schemas) {
|
|
|
1337
1295
|
if (namedSchema) visitSchema(namedSchema);
|
|
1338
1296
|
}
|
|
1339
1297
|
}
|
|
1340
|
-
for (const op of operations) for (const schema of
|
|
1298
|
+
for (const op of operations) for (const schema of collectLazy(op, {
|
|
1341
1299
|
depth: "shallow",
|
|
1342
1300
|
schema: (node) => node
|
|
1343
1301
|
})) visitSchema(schema);
|
|
1344
1302
|
return result;
|
|
1345
1303
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
1352
|
-
*
|
|
1353
|
-
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
1354
|
-
*/
|
|
1355
|
-
function findCircularSchemas(schemas) {
|
|
1304
|
+
function collectUsedSchemaNames(operations, schemas) {
|
|
1305
|
+
return collectUsedSchemaNamesMemo(operations)(schemas);
|
|
1306
|
+
}
|
|
1307
|
+
const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
|
|
1308
|
+
const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
|
|
1356
1309
|
const graph = /* @__PURE__ */ new Map();
|
|
1357
1310
|
for (const schema of schemas) {
|
|
1358
1311
|
if (!schema.name) continue;
|
|
@@ -1375,6 +1328,19 @@ function findCircularSchemas(schemas) {
|
|
|
1375
1328
|
}
|
|
1376
1329
|
}
|
|
1377
1330
|
return circular;
|
|
1331
|
+
});
|
|
1332
|
+
/**
|
|
1333
|
+
* Identifies all schemas that participate in circular dependency chains, including direct self-loops.
|
|
1334
|
+
*
|
|
1335
|
+
* Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
|
|
1336
|
+
* in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
|
|
1337
|
+
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
1338
|
+
*
|
|
1339
|
+
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
1340
|
+
*/
|
|
1341
|
+
function findCircularSchemas(schemas) {
|
|
1342
|
+
if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
|
|
1343
|
+
return findCircularSchemasMemo(schemas);
|
|
1378
1344
|
}
|
|
1379
1345
|
/**
|
|
1380
1346
|
* Type guard returning `true` when a schema or anything nested within it contains a ref to a circular schema.
|
|
@@ -1382,23 +1348,27 @@ function findCircularSchemas(schemas) {
|
|
|
1382
1348
|
* Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
|
|
1383
1349
|
* Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
|
|
1384
1350
|
*
|
|
1385
|
-
* @note Returns `true` for the first matching circular ref found
|
|
1351
|
+
* @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
|
|
1386
1352
|
*/
|
|
1387
1353
|
function containsCircularRef(node, { circularSchemas, excludeName }) {
|
|
1388
1354
|
if (!node || circularSchemas.size === 0) return false;
|
|
1389
|
-
|
|
1390
|
-
if (child.type !== "ref") return
|
|
1355
|
+
for (const _ of collectLazy(node, { schema(child) {
|
|
1356
|
+
if (child.type !== "ref") return null;
|
|
1391
1357
|
const name = resolveRefName(child);
|
|
1392
|
-
return name && name !== excludeName && circularSchemas.has(name) ? true :
|
|
1393
|
-
} })
|
|
1358
|
+
return name && name !== excludeName && circularSchemas.has(name) ? true : null;
|
|
1359
|
+
} })) return true;
|
|
1360
|
+
return false;
|
|
1394
1361
|
}
|
|
1395
1362
|
//#endregion
|
|
1396
1363
|
//#region src/factory.ts
|
|
1397
1364
|
/**
|
|
1398
|
-
*
|
|
1365
|
+
* Updates a schema's `optional` and `nullish` flags from a parent's `required`
|
|
1366
|
+
* value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
|
|
1367
|
+
* object properties combine "required" and "nullable" into a single AST.
|
|
1399
1368
|
*
|
|
1400
|
-
* -
|
|
1401
|
-
* -
|
|
1369
|
+
* - Non-required + non-nullable → `optional: true`.
|
|
1370
|
+
* - Non-required + nullable → `nullish: true`.
|
|
1371
|
+
* - Required → both flags cleared.
|
|
1402
1372
|
*/
|
|
1403
1373
|
function syncOptionality(schema, required) {
|
|
1404
1374
|
const nullable = schema.nullable ?? false;
|
|
@@ -1409,6 +1379,29 @@ function syncOptionality(schema, required) {
|
|
|
1409
1379
|
};
|
|
1410
1380
|
}
|
|
1411
1381
|
/**
|
|
1382
|
+
* Identity-preserving node update: returns `node` unchanged when every field in
|
|
1383
|
+
* `changes` already equals (by reference) the current value, otherwise a new node
|
|
1384
|
+
* with the changes applied.
|
|
1385
|
+
*
|
|
1386
|
+
* Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
|
|
1387
|
+
* structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
|
|
1388
|
+
* downstream passes can detect "nothing changed" by identity. Comparison is
|
|
1389
|
+
* shallow: a structurally-equal but newly-allocated array/object counts as a change.
|
|
1390
|
+
*
|
|
1391
|
+
* @example
|
|
1392
|
+
* ```ts
|
|
1393
|
+
* update(node, { name: node.name }) // -> same `node` reference
|
|
1394
|
+
* update(node, { name: 'renamed' }) // -> new node, `name` replaced
|
|
1395
|
+
* ```
|
|
1396
|
+
*/
|
|
1397
|
+
function update(node, changes) {
|
|
1398
|
+
for (const key in changes) if (changes[key] !== node[key]) return {
|
|
1399
|
+
...node,
|
|
1400
|
+
...changes
|
|
1401
|
+
};
|
|
1402
|
+
return node;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1412
1405
|
* Creates an `InputNode` with stable defaults for `schemas` and `operations`.
|
|
1413
1406
|
*
|
|
1414
1407
|
* @example
|
|
@@ -1427,11 +1420,31 @@ function createInput(overrides = {}) {
|
|
|
1427
1420
|
return {
|
|
1428
1421
|
schemas: [],
|
|
1429
1422
|
operations: [],
|
|
1423
|
+
meta: {
|
|
1424
|
+
circularNames: [],
|
|
1425
|
+
enumNames: []
|
|
1426
|
+
},
|
|
1430
1427
|
...overrides,
|
|
1431
1428
|
kind: "Input"
|
|
1432
1429
|
};
|
|
1433
1430
|
}
|
|
1434
1431
|
/**
|
|
1432
|
+
* Creates an `InputStreamNode` from pre-built `AsyncIterable` sources.
|
|
1433
|
+
*
|
|
1434
|
+
* @example
|
|
1435
|
+
* ```ts
|
|
1436
|
+
* const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
|
|
1437
|
+
* ```
|
|
1438
|
+
*/
|
|
1439
|
+
function createStreamInput(schemas, operations, meta) {
|
|
1440
|
+
return {
|
|
1441
|
+
kind: "Input",
|
|
1442
|
+
schemas,
|
|
1443
|
+
operations,
|
|
1444
|
+
meta
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1435
1448
|
* Creates an `OutputNode` with a stable default for `files`.
|
|
1436
1449
|
*
|
|
1437
1450
|
* @example
|
|
@@ -1453,40 +1466,40 @@ function createOutput(overrides = {}) {
|
|
|
1453
1466
|
};
|
|
1454
1467
|
}
|
|
1455
1468
|
/**
|
|
1456
|
-
* Creates
|
|
1457
|
-
*
|
|
1458
|
-
* @example
|
|
1459
|
-
* ```ts
|
|
1460
|
-
* const operation = createOperation({
|
|
1461
|
-
* operationId: 'getPetById',
|
|
1462
|
-
* method: 'GET',
|
|
1463
|
-
* path: '/pet/{petId}',
|
|
1464
|
-
* })
|
|
1465
|
-
* // tags, parameters, and responses are []
|
|
1466
|
-
* ```
|
|
1467
|
-
*
|
|
1468
|
-
* @example
|
|
1469
|
-
* ```ts
|
|
1470
|
-
* const operation = createOperation({
|
|
1471
|
-
* operationId: 'findPets',
|
|
1472
|
-
* method: 'GET',
|
|
1473
|
-
* path: '/pet/findByStatus',
|
|
1474
|
-
* tags: ['pet'],
|
|
1475
|
-
* })
|
|
1476
|
-
* ```
|
|
1469
|
+
* Creates a `ContentNode` for a single request-body or response content type.
|
|
1477
1470
|
*/
|
|
1471
|
+
function createContent(props) {
|
|
1472
|
+
return {
|
|
1473
|
+
...props,
|
|
1474
|
+
kind: "Content"
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
|
|
1479
|
+
*/
|
|
1480
|
+
function createRequestBody(props) {
|
|
1481
|
+
return {
|
|
1482
|
+
...props,
|
|
1483
|
+
kind: "RequestBody",
|
|
1484
|
+
content: props.content?.map(createContent)
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1478
1487
|
function createOperation(props) {
|
|
1488
|
+
const { requestBody, ...rest } = props;
|
|
1489
|
+
const isHttp = rest.method !== void 0 && rest.path !== void 0;
|
|
1479
1490
|
return {
|
|
1480
1491
|
tags: [],
|
|
1481
1492
|
parameters: [],
|
|
1482
1493
|
responses: [],
|
|
1483
|
-
...
|
|
1484
|
-
|
|
1494
|
+
...rest,
|
|
1495
|
+
...isHttp ? { protocol: "http" } : {},
|
|
1496
|
+
kind: "Operation",
|
|
1497
|
+
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
1485
1498
|
};
|
|
1486
1499
|
}
|
|
1487
1500
|
/**
|
|
1488
1501
|
* Maps schema `type` to its underlying `primitive`.
|
|
1489
|
-
* Primitive types map to themselves
|
|
1502
|
+
* Primitive types map to themselves. Special string formats map to `'string'`.
|
|
1490
1503
|
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
1491
1504
|
*/
|
|
1492
1505
|
const TYPE_TO_PRIMITIVE = {
|
|
@@ -1595,19 +1608,29 @@ function createParameter(props) {
|
|
|
1595
1608
|
/**
|
|
1596
1609
|
* Creates a `ResponseNode`.
|
|
1597
1610
|
*
|
|
1611
|
+
* Response body schemas live inside `content`. For convenience a single legacy `schema`
|
|
1612
|
+
* (with optional `mediaType`/`keysToOmit`) is normalized into one `content` entry, so the same
|
|
1613
|
+
* schema is never stored both at the node root and inside `content`.
|
|
1614
|
+
*
|
|
1598
1615
|
* @example
|
|
1599
1616
|
* ```ts
|
|
1600
1617
|
* const response = createResponse({
|
|
1601
1618
|
* statusCode: '200',
|
|
1602
|
-
*
|
|
1603
|
-
* schema: createSchema({ type: 'object', properties: [] }),
|
|
1619
|
+
* content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
|
|
1604
1620
|
* })
|
|
1605
1621
|
* ```
|
|
1606
1622
|
*/
|
|
1607
1623
|
function createResponse(props) {
|
|
1624
|
+
const { schema, mediaType, keysToOmit, content, ...rest } = props;
|
|
1625
|
+
const entries = content ?? (schema ? [{
|
|
1626
|
+
contentType: mediaType ?? "application/json",
|
|
1627
|
+
schema,
|
|
1628
|
+
keysToOmit: keysToOmit ?? null
|
|
1629
|
+
}] : void 0);
|
|
1608
1630
|
return {
|
|
1609
|
-
...
|
|
1610
|
-
kind: "Response"
|
|
1631
|
+
...rest,
|
|
1632
|
+
kind: "Response",
|
|
1633
|
+
content: entries?.map(createContent)
|
|
1611
1634
|
};
|
|
1612
1635
|
}
|
|
1613
1636
|
/**
|
|
@@ -1627,7 +1650,7 @@ function createResponse(props) {
|
|
|
1627
1650
|
* // → params?: QueryParams
|
|
1628
1651
|
* ```
|
|
1629
1652
|
*
|
|
1630
|
-
* @example Param with default (implicitly optional
|
|
1653
|
+
* @example Param with default (implicitly optional. Cannot combine with `optional: true`)
|
|
1631
1654
|
* ```ts
|
|
1632
1655
|
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
|
|
1633
1656
|
* // → config: RequestConfig = {}
|
|
@@ -1684,7 +1707,7 @@ function createParamsType(props) {
|
|
|
1684
1707
|
* // call → { id, name }
|
|
1685
1708
|
* ```
|
|
1686
1709
|
*
|
|
1687
|
-
* @example Inline (spread)
|
|
1710
|
+
* @example Inline (spread), children emitted as individual top-level parameters
|
|
1688
1711
|
* ```ts
|
|
1689
1712
|
* createParameterGroup({
|
|
1690
1713
|
* properties: [createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false })],
|
|
@@ -1786,9 +1809,9 @@ function createSource(props) {
|
|
|
1786
1809
|
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1787
1810
|
*
|
|
1788
1811
|
* Computes:
|
|
1789
|
-
* - `id`
|
|
1790
|
-
* - `name`
|
|
1791
|
-
* - `extname`
|
|
1812
|
+
* - `id` SHA256 hash of the file path
|
|
1813
|
+
* - `name` `baseName` without extension
|
|
1814
|
+
* - `extname` extension extracted from `baseName`
|
|
1792
1815
|
*
|
|
1793
1816
|
* Deduplicates:
|
|
1794
1817
|
* - `sources` via `combineSources`
|
|
@@ -2016,24 +2039,466 @@ function createJsx(value) {
|
|
|
2016
2039
|
};
|
|
2017
2040
|
}
|
|
2018
2041
|
//#endregion
|
|
2019
|
-
//#region src/
|
|
2042
|
+
//#region src/signature.ts
|
|
2043
|
+
/**
|
|
2044
|
+
* The shape-affecting flags shared by every node kind: base primitive, format, and `nullable`.
|
|
2045
|
+
* Documentation and usage-slot flags (`optional`/`nullish`/`readOnly`/`writeOnly`) are
|
|
2046
|
+
* intentionally excluded, they describe the property slot, not the type.
|
|
2047
|
+
*/
|
|
2048
|
+
function flagsDescriptor(node) {
|
|
2049
|
+
return `${node.primitive ?? ""};${node.format ?? ""};${node.nullable ? 1 : 0}`;
|
|
2050
|
+
}
|
|
2051
|
+
function refTargetName(node) {
|
|
2052
|
+
if (node.ref) return extractRefName(node.ref);
|
|
2053
|
+
return node.name ?? "";
|
|
2054
|
+
}
|
|
2055
|
+
const arrayTupleFields = [
|
|
2056
|
+
{
|
|
2057
|
+
kind: "children",
|
|
2058
|
+
key: "items",
|
|
2059
|
+
prefix: "i"
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
kind: "child",
|
|
2063
|
+
key: "rest",
|
|
2064
|
+
prefix: "r"
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
kind: "scalar",
|
|
2068
|
+
key: "min",
|
|
2069
|
+
prefix: "mn"
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
kind: "scalar",
|
|
2073
|
+
key: "max",
|
|
2074
|
+
prefix: "mx"
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
kind: "bool",
|
|
2078
|
+
key: "unique",
|
|
2079
|
+
prefix: "u"
|
|
2080
|
+
}
|
|
2081
|
+
];
|
|
2082
|
+
const numericFields = [
|
|
2083
|
+
{
|
|
2084
|
+
kind: "scalar",
|
|
2085
|
+
key: "min",
|
|
2086
|
+
prefix: "mn"
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
kind: "scalar",
|
|
2090
|
+
key: "max",
|
|
2091
|
+
prefix: "mx"
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
kind: "scalar",
|
|
2095
|
+
key: "exclusiveMinimum",
|
|
2096
|
+
prefix: "emn"
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
kind: "scalar",
|
|
2100
|
+
key: "exclusiveMaximum",
|
|
2101
|
+
prefix: "emx"
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
kind: "scalar",
|
|
2105
|
+
key: "multipleOf",
|
|
2106
|
+
prefix: "mo"
|
|
2107
|
+
}
|
|
2108
|
+
];
|
|
2109
|
+
const rangeFields = [{
|
|
2110
|
+
kind: "scalar",
|
|
2111
|
+
key: "min",
|
|
2112
|
+
prefix: "mn"
|
|
2113
|
+
}, {
|
|
2114
|
+
kind: "scalar",
|
|
2115
|
+
key: "max",
|
|
2116
|
+
prefix: "mx"
|
|
2117
|
+
}];
|
|
2118
|
+
/**
|
|
2119
|
+
* Maps each schema node `type` to the ordered list of shape-contributing fields.
|
|
2120
|
+
* Node types absent from this map (scalar types like boolean, null, any, etc.) fall
|
|
2121
|
+
* back to `${type}|${flags}` with no additional fields.
|
|
2122
|
+
*/
|
|
2123
|
+
const SHAPE_KEYS = {
|
|
2124
|
+
object: [
|
|
2125
|
+
{ kind: "objectProps" },
|
|
2126
|
+
{ kind: "additionalProps" },
|
|
2127
|
+
{ kind: "patternProps" },
|
|
2128
|
+
{
|
|
2129
|
+
kind: "scalar",
|
|
2130
|
+
key: "minProperties",
|
|
2131
|
+
prefix: "mn"
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
kind: "scalar",
|
|
2135
|
+
key: "maxProperties",
|
|
2136
|
+
prefix: "mx"
|
|
2137
|
+
}
|
|
2138
|
+
],
|
|
2139
|
+
array: arrayTupleFields,
|
|
2140
|
+
tuple: arrayTupleFields,
|
|
2141
|
+
union: [
|
|
2142
|
+
{
|
|
2143
|
+
kind: "scalar",
|
|
2144
|
+
key: "strategy",
|
|
2145
|
+
prefix: "s"
|
|
2146
|
+
},
|
|
2147
|
+
{
|
|
2148
|
+
kind: "scalar",
|
|
2149
|
+
key: "discriminatorPropertyName",
|
|
2150
|
+
prefix: "d"
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
kind: "children",
|
|
2154
|
+
key: "members",
|
|
2155
|
+
prefix: "m"
|
|
2156
|
+
}
|
|
2157
|
+
],
|
|
2158
|
+
intersection: [{
|
|
2159
|
+
kind: "children",
|
|
2160
|
+
key: "members",
|
|
2161
|
+
prefix: "m"
|
|
2162
|
+
}],
|
|
2163
|
+
enum: [{ kind: "enumValues" }],
|
|
2164
|
+
ref: [{ kind: "refTarget" }],
|
|
2165
|
+
string: [
|
|
2166
|
+
{
|
|
2167
|
+
kind: "scalar",
|
|
2168
|
+
key: "min",
|
|
2169
|
+
prefix: "mn"
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
kind: "scalar",
|
|
2173
|
+
key: "max",
|
|
2174
|
+
prefix: "mx"
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
kind: "scalar",
|
|
2178
|
+
key: "pattern",
|
|
2179
|
+
prefix: "pt"
|
|
2180
|
+
}
|
|
2181
|
+
],
|
|
2182
|
+
number: numericFields,
|
|
2183
|
+
integer: numericFields,
|
|
2184
|
+
bigint: numericFields,
|
|
2185
|
+
url: [
|
|
2186
|
+
{
|
|
2187
|
+
kind: "scalar",
|
|
2188
|
+
key: "path",
|
|
2189
|
+
prefix: "path"
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
kind: "scalar",
|
|
2193
|
+
key: "min",
|
|
2194
|
+
prefix: "mn"
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
kind: "scalar",
|
|
2198
|
+
key: "max",
|
|
2199
|
+
prefix: "mx"
|
|
2200
|
+
}
|
|
2201
|
+
],
|
|
2202
|
+
uuid: rangeFields,
|
|
2203
|
+
email: rangeFields,
|
|
2204
|
+
datetime: [{
|
|
2205
|
+
kind: "bool",
|
|
2206
|
+
key: "offset",
|
|
2207
|
+
prefix: "o"
|
|
2208
|
+
}, {
|
|
2209
|
+
kind: "bool",
|
|
2210
|
+
key: "local",
|
|
2211
|
+
prefix: "l"
|
|
2212
|
+
}],
|
|
2213
|
+
date: [{
|
|
2214
|
+
kind: "scalar",
|
|
2215
|
+
key: "representation",
|
|
2216
|
+
prefix: "rep"
|
|
2217
|
+
}],
|
|
2218
|
+
time: [{
|
|
2219
|
+
kind: "scalar",
|
|
2220
|
+
key: "representation",
|
|
2221
|
+
prefix: "rep"
|
|
2222
|
+
}]
|
|
2223
|
+
};
|
|
2224
|
+
function serializeShapeField(field, node, record) {
|
|
2225
|
+
switch (field.kind) {
|
|
2226
|
+
case "scalar": return `${field.prefix}:${record[field.key] ?? ""}`;
|
|
2227
|
+
case "bool": return `${field.prefix}:${record[field.key] ? 1 : 0}`;
|
|
2228
|
+
case "child": {
|
|
2229
|
+
const child = record[field.key];
|
|
2230
|
+
return `${field.prefix}:${child ? signatureOf(child) : ""}`;
|
|
2231
|
+
}
|
|
2232
|
+
case "children": {
|
|
2233
|
+
const children = record[field.key] ?? [];
|
|
2234
|
+
return `${field.prefix}[${children.map((c) => signatureOf(c)).join(",")}]`;
|
|
2235
|
+
}
|
|
2236
|
+
case "objectProps": return `p[${(node.properties ?? []).map((prop) => `${prop.name}${prop.required ? "!" : "?"}${signatureOf(prop.schema)}`).join(",")}]`;
|
|
2237
|
+
case "additionalProps": {
|
|
2238
|
+
const obj = node;
|
|
2239
|
+
if (typeof obj.additionalProperties === "boolean") return `ab:${obj.additionalProperties}`;
|
|
2240
|
+
if (obj.additionalProperties) return `as:${signatureOf(obj.additionalProperties)}`;
|
|
2241
|
+
return "";
|
|
2242
|
+
}
|
|
2243
|
+
case "patternProps": {
|
|
2244
|
+
const obj = node;
|
|
2245
|
+
return `pp[${obj.patternProperties ? Object.keys(obj.patternProperties).sort().map((key) => `${key}=${signatureOf(obj.patternProperties[key])}`).join(",") : ""}]`;
|
|
2246
|
+
}
|
|
2247
|
+
case "enumValues": {
|
|
2248
|
+
const en = node;
|
|
2249
|
+
let values = "";
|
|
2250
|
+
if (en.namedEnumValues?.length) values = en.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(",");
|
|
2251
|
+
else if (en.enumValues?.length) values = en.enumValues.map((value) => `${value === null ? "null" : typeof value}:${String(value)}`).join(",");
|
|
2252
|
+
return `v[${values}]`;
|
|
2253
|
+
}
|
|
2254
|
+
case "refTarget": return `->${refTargetName(node)}`;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Builds the local, shape-only descriptor for a node: its kind, flags, constraints, and its
|
|
2259
|
+
* children's signatures. {@link signatureOf} hashes this string. Children contribute their
|
|
2260
|
+
* fixed-length signature rather than their own full descriptor, which keeps the result bounded.
|
|
2261
|
+
*/
|
|
2262
|
+
function describeShape(node) {
|
|
2263
|
+
const flags = flagsDescriptor(node);
|
|
2264
|
+
const fields = SHAPE_KEYS[node.type];
|
|
2265
|
+
if (!fields) return `${node.type}|${flags}`;
|
|
2266
|
+
const record = node;
|
|
2267
|
+
const parts = [`${node.type}|${flags}`];
|
|
2268
|
+
for (const field of fields) parts.push(serializeShapeField(field, node, record));
|
|
2269
|
+
return parts.join("|");
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Persistent hash-consing cache: `SchemaNode` → signature digest, keyed by node identity.
|
|
2273
|
+
*
|
|
2274
|
+
* A `WeakMap` so entries are released once the node is garbage-collected, and so a node hashed
|
|
2275
|
+
* during dedupe planning is not re-hashed when the same tree is rewritten during streaming
|
|
2276
|
+
* (where `schemaSignature` and `applyDedupe` would otherwise each walk it from scratch). Reuse
|
|
2277
|
+
* across calls is sound because a signature depends only on a node's content, and schema nodes
|
|
2278
|
+
* are immutable once created, transforms allocate new objects rather than mutating in place.
|
|
2279
|
+
*/
|
|
2280
|
+
const signatureCache = /* @__PURE__ */ new WeakMap();
|
|
2281
|
+
/**
|
|
2282
|
+
* Hash-consing: each node's signature is a fixed-length digest of its local shape plus its
|
|
2283
|
+
* children's digests (a Merkle hash). Children contribute their 64-char hash instead of their
|
|
2284
|
+
* full nested descriptor, so a signature stays bounded regardless of subtree depth, and the
|
|
2285
|
+
* digest is identical across calls because it depends only on content, never on traversal
|
|
2286
|
+
* order. This keeps the keys built during planning consistent with the ones recomputed later
|
|
2287
|
+
* during streaming. {@link signatureCache} memoizes node → digest across every computation.
|
|
2288
|
+
*/
|
|
2289
|
+
function signatureOf(node) {
|
|
2290
|
+
const cached = signatureCache.get(node);
|
|
2291
|
+
if (cached !== void 0) return cached;
|
|
2292
|
+
const signature = (0, node_crypto.createHash)("sha256").update(describeShape(node)).digest("hex");
|
|
2293
|
+
signatureCache.set(node, signature);
|
|
2294
|
+
return signature;
|
|
2295
|
+
}
|
|
2020
2296
|
/**
|
|
2021
|
-
*
|
|
2297
|
+
* Computes a deterministic, shape-only signature (a fixed-length content hash) for a schema node.
|
|
2022
2298
|
*
|
|
2023
|
-
*
|
|
2299
|
+
* Two schemas share a signature when they are structurally identical, ignoring
|
|
2300
|
+
* documentation (`name`, `title`, `description`, `example`, `default`, `deprecated`)
|
|
2301
|
+
* and usage-slot flags (`optional`, `nullish`, `readOnly`, `writeOnly`). `nullable`
|
|
2302
|
+
* is kept because it changes the produced type. `ref` nodes compare by target name,
|
|
2303
|
+
* which also keeps the algorithm terminating on circular shapes.
|
|
2304
|
+
*
|
|
2305
|
+
* @example Two enums with different descriptions share a signature
|
|
2306
|
+
* ```ts
|
|
2307
|
+
* schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
|
|
2308
|
+
* schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
|
|
2309
|
+
* ```
|
|
2310
|
+
*/
|
|
2311
|
+
function schemaSignature(node) {
|
|
2312
|
+
return signatureOf(node);
|
|
2313
|
+
}
|
|
2314
|
+
//#endregion
|
|
2315
|
+
//#region src/dedupe.ts
|
|
2316
|
+
/**
|
|
2317
|
+
* Builds the shared `ref` replacement for a duplicate occurrence, carrying the
|
|
2318
|
+
* usage-slot and documentation fields that are not part of the canonical type.
|
|
2319
|
+
*/
|
|
2320
|
+
function createRefNode(node, canonical) {
|
|
2321
|
+
return createSchema({
|
|
2322
|
+
type: "ref",
|
|
2323
|
+
name: canonical.name,
|
|
2324
|
+
ref: canonical.ref,
|
|
2325
|
+
optional: node.optional,
|
|
2326
|
+
nullish: node.nullish,
|
|
2327
|
+
readOnly: node.readOnly,
|
|
2328
|
+
writeOnly: node.writeOnly,
|
|
2329
|
+
deprecated: node.deprecated,
|
|
2330
|
+
description: node.description,
|
|
2331
|
+
default: node.default,
|
|
2332
|
+
example: node.example
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
function applyDedupe(node, canonicalBySignature, skipRootMatch = false) {
|
|
2336
|
+
if (canonicalBySignature.size === 0) return node;
|
|
2337
|
+
const root = node;
|
|
2338
|
+
return transform(node, { schema(schemaNode) {
|
|
2339
|
+
const signature = signatureOf(schemaNode);
|
|
2340
|
+
if (skipRootMatch && schemaNode === root) return void 0;
|
|
2341
|
+
const canonical = canonicalBySignature.get(signature);
|
|
2342
|
+
if (!canonical) return void 0;
|
|
2343
|
+
return createRefNode(schemaNode, canonical);
|
|
2344
|
+
} });
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Strips usage-slot flags from a hoisted definition and applies its canonical name.
|
|
2348
|
+
* A standalone definition is never optional, so `optional`/`nullish` are cleared.
|
|
2349
|
+
*/
|
|
2350
|
+
function cleanDefinition(node, name) {
|
|
2351
|
+
return {
|
|
2352
|
+
...node,
|
|
2353
|
+
name,
|
|
2354
|
+
optional: void 0,
|
|
2355
|
+
nullish: void 0
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Scans a forest of schema and operation nodes and produces a {@link DedupePlan}.
|
|
2360
|
+
*
|
|
2361
|
+
* A shape that occurs at least `minOccurrences` times is deduplicated: if any occurrence
|
|
2362
|
+
* is a named top-level schema, that name becomes the canonical (so other top-level duplicates
|
|
2363
|
+
* and inline copies turn into references to it). Otherwise a new definition is hoisted using
|
|
2364
|
+
* `nameFor`. The plan is then applied per node with {@link applyDedupe}.
|
|
2365
|
+
*
|
|
2366
|
+
* @example
|
|
2367
|
+
* ```ts
|
|
2368
|
+
* const plan = buildDedupePlan([...schemaNodes, ...operationNodes], {
|
|
2369
|
+
* isCandidate: (node) => node.type === 'enum' || node.type === 'object',
|
|
2370
|
+
* nameFor: (node) => node.name ?? null,
|
|
2371
|
+
* refFor: (name) => `#/components/schemas/${name}`,
|
|
2372
|
+
* })
|
|
2373
|
+
* ```
|
|
2374
|
+
*/
|
|
2375
|
+
function buildDedupePlan(roots, options) {
|
|
2376
|
+
const { isCandidate, nameFor, refFor, minOccurrences = 2 } = options;
|
|
2377
|
+
const topLevelNodes = /* @__PURE__ */ new Set();
|
|
2378
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2379
|
+
function record(schemaNode) {
|
|
2380
|
+
const signature = signatureOf(schemaNode);
|
|
2381
|
+
if (!isCandidate(schemaNode)) return;
|
|
2382
|
+
const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
|
|
2383
|
+
const group = groups.get(signature);
|
|
2384
|
+
if (group) {
|
|
2385
|
+
group.count++;
|
|
2386
|
+
if (isTopLevel && !group.topLevelName) group.topLevelName = schemaNode.name;
|
|
2387
|
+
} else groups.set(signature, {
|
|
2388
|
+
count: 1,
|
|
2389
|
+
representative: schemaNode,
|
|
2390
|
+
topLevelName: isTopLevel ? schemaNode.name : void 0
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
for (const root of roots) {
|
|
2394
|
+
if (root.kind === "Schema") topLevelNodes.add(root);
|
|
2395
|
+
for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
|
|
2396
|
+
}
|
|
2397
|
+
const canonicalBySignature = /* @__PURE__ */ new Map();
|
|
2398
|
+
const pendingHoists = [];
|
|
2399
|
+
for (const [signature, group] of groups) {
|
|
2400
|
+
if (group.count < minOccurrences) continue;
|
|
2401
|
+
if (group.topLevelName) {
|
|
2402
|
+
canonicalBySignature.set(signature, {
|
|
2403
|
+
name: group.topLevelName,
|
|
2404
|
+
ref: refFor(group.topLevelName)
|
|
2405
|
+
});
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2408
|
+
const name = nameFor(group.representative, signature);
|
|
2409
|
+
if (!name) continue;
|
|
2410
|
+
canonicalBySignature.set(signature, {
|
|
2411
|
+
name,
|
|
2412
|
+
ref: refFor(name)
|
|
2413
|
+
});
|
|
2414
|
+
pendingHoists.push({
|
|
2415
|
+
name,
|
|
2416
|
+
representative: group.representative
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
return {
|
|
2420
|
+
canonicalBySignature,
|
|
2421
|
+
hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, canonicalBySignature, true), name))
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
//#endregion
|
|
2425
|
+
//#region src/dialect.ts
|
|
2426
|
+
/**
|
|
2427
|
+
* Identity helper that types a {@link SchemaDialect} for an adapter. Like
|
|
2428
|
+
* `defineParser`, it adds no runtime behavior, it pins the dialect's type for
|
|
2429
|
+
* inference and gives adapter authors a discoverable anchor.
|
|
2430
|
+
*
|
|
2431
|
+
* @example
|
|
2432
|
+
* ```ts
|
|
2433
|
+
* export const oasDialect = defineSchemaDialect({
|
|
2434
|
+
* name: 'oas',
|
|
2435
|
+
* isNullable,
|
|
2436
|
+
* isReference,
|
|
2437
|
+
* isDiscriminator,
|
|
2438
|
+
* isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
|
|
2439
|
+
* resolveRef,
|
|
2440
|
+
* })
|
|
2441
|
+
* ```
|
|
2442
|
+
*/
|
|
2443
|
+
function defineSchemaDialect(dialect) {
|
|
2444
|
+
return dialect;
|
|
2445
|
+
}
|
|
2446
|
+
//#endregion
|
|
2447
|
+
//#region src/dispatch.ts
|
|
2448
|
+
/**
|
|
2449
|
+
* Walks an ordered list of {@link DispatchRule}s and returns the first node produced.
|
|
2450
|
+
*
|
|
2451
|
+
* This is the shared backbone for spec adapters (OpenAPI today, AsyncAPI and others later).
|
|
2452
|
+
* The contract an adapter follows is intentionally minimal:
|
|
2453
|
+
*
|
|
2454
|
+
* context → [rule.match → rule.convert] → node
|
|
2455
|
+
*
|
|
2456
|
+
* An adapter derives a context from a source spec node, then declares an ordered table of
|
|
2457
|
+
* rules mapping spec shapes onto Kubb AST nodes. To add support for a new spec, write a new
|
|
2458
|
+
* context type and a new rules table, the traversal here is reused unchanged.
|
|
2459
|
+
*
|
|
2460
|
+
* Order is significant: earlier rules win, so list higher-precedence or more specific shapes
|
|
2461
|
+
* first (e.g. composition keywords before plain `type`). A rule whose `match` returns `true`
|
|
2462
|
+
* may still `convert` to `null` to defer to later rules. When no rule produces a node this
|
|
2463
|
+
* returns `null`, leaving the caller to apply its own fallback.
|
|
2464
|
+
*
|
|
2465
|
+
* @example
|
|
2466
|
+
* ```ts
|
|
2467
|
+
* const node = dispatch(schemaRules, schemaContext) ?? createSchema({ type: fallbackType })
|
|
2468
|
+
* ```
|
|
2469
|
+
*/
|
|
2470
|
+
function dispatch(rules, context) {
|
|
2471
|
+
for (const rule of rules) {
|
|
2472
|
+
if (!rule.match(context)) continue;
|
|
2473
|
+
const node = rule.convert(context);
|
|
2474
|
+
if (node !== null && node !== void 0) return node;
|
|
2475
|
+
}
|
|
2476
|
+
return null;
|
|
2477
|
+
}
|
|
2478
|
+
//#endregion
|
|
2479
|
+
//#region src/printer.ts
|
|
2480
|
+
/**
|
|
2481
|
+
* Defines a schema printer: a function that takes a `SchemaNode` and emits
|
|
2482
|
+
* code in your target language. Each plugin that produces code from schemas
|
|
2483
|
+
* (TypeScript types, Zod schemas, Faker factories) ships a printer built
|
|
2484
|
+
* with this helper.
|
|
2024
2485
|
*
|
|
2025
2486
|
* The builder receives resolved options and returns:
|
|
2026
|
-
* - `name` — a unique identifier for the printer
|
|
2027
|
-
* - `options` — options stored on the returned printer instance
|
|
2028
|
-
* - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
|
|
2029
|
-
* - `print` _(optional)_ — top-level override exposed as `printer.print`
|
|
2030
|
-
* - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
|
|
2031
|
-
* - This keeps recursion safe and avoids self-calls
|
|
2032
2487
|
*
|
|
2033
|
-
*
|
|
2488
|
+
* - `name` unique identifier for the printer.
|
|
2489
|
+
* - `options` stored on the returned printer instance.
|
|
2490
|
+
* - `nodes` map of `SchemaType` → handler. Handlers return the rendered
|
|
2491
|
+
* output (a string, a TypeScript AST node, ...) for that schema type.
|
|
2492
|
+
* - `print` (optional), top-level override exposed as `printer.print`.
|
|
2493
|
+
* Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
|
|
2494
|
+
*
|
|
2495
|
+
* Without a `print` override, `printer.print` falls back to `printer.transform`
|
|
2496
|
+
* (the node-level dispatcher).
|
|
2034
2497
|
*
|
|
2035
|
-
* @example
|
|
2498
|
+
* @example Tiny Zod printer
|
|
2036
2499
|
* ```ts
|
|
2500
|
+
* import { definePrinter, type PrinterFactoryOptions } from '@kubb/ast'
|
|
2501
|
+
*
|
|
2037
2502
|
* type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
|
|
2038
2503
|
*
|
|
2039
2504
|
* export const zodPrinter = definePrinter<PrinterZod>((options) => ({
|
|
@@ -2042,7 +2507,9 @@ function createJsx(value) {
|
|
|
2042
2507
|
* nodes: {
|
|
2043
2508
|
* string: () => 'z.string()',
|
|
2044
2509
|
* object(node) {
|
|
2045
|
-
* const props = node.properties
|
|
2510
|
+
* const props = node.properties
|
|
2511
|
+
* .map((p) => `${p.name}: ${this.transform(p.schema)}`)
|
|
2512
|
+
* .join(', ')
|
|
2046
2513
|
* return `z.object({ ${props} })`
|
|
2047
2514
|
* },
|
|
2048
2515
|
* },
|
|
@@ -2070,7 +2537,7 @@ function createPrinterFactory(getKey) {
|
|
|
2070
2537
|
options: resolvedOptions,
|
|
2071
2538
|
transform: (node) => {
|
|
2072
2539
|
const key = getKey(node);
|
|
2073
|
-
if (key ===
|
|
2540
|
+
if (key === null) return null;
|
|
2074
2541
|
const handler = nodes[key];
|
|
2075
2542
|
if (!handler) return null;
|
|
2076
2543
|
return handler.call(context, node);
|
|
@@ -2107,10 +2574,10 @@ function enumPropName(parentName, propName, enumSuffix) {
|
|
|
2107
2574
|
function collectImports({ node, nameMapping, resolve }) {
|
|
2108
2575
|
return collect(node, { schema(schemaNode) {
|
|
2109
2576
|
const schemaRef = narrowSchema(schemaNode, "ref");
|
|
2110
|
-
if (!schemaRef?.ref) return;
|
|
2577
|
+
if (!schemaRef?.ref) return null;
|
|
2111
2578
|
const rawName = extractRefName(schemaRef.ref);
|
|
2112
2579
|
const result = resolve(nameMapping.get(rawName) ?? rawName);
|
|
2113
|
-
if (!result) return;
|
|
2580
|
+
if (!result) return null;
|
|
2114
2581
|
return result;
|
|
2115
2582
|
} });
|
|
2116
2583
|
}
|
|
@@ -2164,23 +2631,24 @@ function setDiscriminatorEnum({ node, propertyName, values, enumName }) {
|
|
|
2164
2631
|
* ])
|
|
2165
2632
|
* ```
|
|
2166
2633
|
*/
|
|
2167
|
-
function
|
|
2168
|
-
|
|
2634
|
+
function* mergeAdjacentObjectsLazy(members) {
|
|
2635
|
+
let acc;
|
|
2636
|
+
for (const member of members) {
|
|
2169
2637
|
const objectMember = narrowSchema(member, "object");
|
|
2170
|
-
if (objectMember && !objectMember.name) {
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
...
|
|
2176
|
-
properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
|
|
2638
|
+
if (objectMember && !objectMember.name && acc !== void 0) {
|
|
2639
|
+
const accObject = narrowSchema(acc, "object");
|
|
2640
|
+
if (accObject && !accObject.name) {
|
|
2641
|
+
acc = createSchema({
|
|
2642
|
+
...accObject,
|
|
2643
|
+
properties: [...accObject.properties ?? [], ...objectMember.properties ?? []]
|
|
2177
2644
|
});
|
|
2178
|
-
|
|
2645
|
+
continue;
|
|
2179
2646
|
}
|
|
2180
2647
|
}
|
|
2181
|
-
acc
|
|
2182
|
-
|
|
2183
|
-
}
|
|
2648
|
+
if (acc !== void 0) yield acc;
|
|
2649
|
+
acc = member;
|
|
2650
|
+
}
|
|
2651
|
+
if (acc !== void 0) yield acc;
|
|
2184
2652
|
}
|
|
2185
2653
|
/**
|
|
2186
2654
|
* Removes enum members that are covered by broader scalar primitives in the same union.
|
|
@@ -2212,7 +2680,7 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
|
2212
2680
|
const enumNode = narrowSchema(propNode, "enum");
|
|
2213
2681
|
if (enumNode?.primitive === "boolean") return {
|
|
2214
2682
|
...propNode,
|
|
2215
|
-
name:
|
|
2683
|
+
name: null
|
|
2216
2684
|
};
|
|
2217
2685
|
if (enumNode) return {
|
|
2218
2686
|
...propNode,
|
|
@@ -2221,11 +2689,12 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
|
2221
2689
|
return propNode;
|
|
2222
2690
|
}
|
|
2223
2691
|
//#endregion
|
|
2692
|
+
exports.applyDedupe = applyDedupe;
|
|
2693
|
+
exports.buildDedupePlan = buildDedupePlan;
|
|
2224
2694
|
exports.caseParams = caseParams;
|
|
2225
2695
|
exports.childName = childName;
|
|
2226
2696
|
exports.collect = collect;
|
|
2227
2697
|
exports.collectImports = collectImports;
|
|
2228
|
-
exports.collectReferencedSchemaNames = collectReferencedSchemaNames;
|
|
2229
2698
|
exports.collectUsedSchemaNames = collectUsedSchemaNames;
|
|
2230
2699
|
exports.containsCircularRef = containsCircularRef;
|
|
2231
2700
|
exports.createArrowFunction = createArrowFunction;
|
|
@@ -2251,26 +2720,27 @@ exports.createProperty = createProperty;
|
|
|
2251
2720
|
exports.createResponse = createResponse;
|
|
2252
2721
|
exports.createSchema = createSchema;
|
|
2253
2722
|
exports.createSource = createSource;
|
|
2723
|
+
exports.createStreamInput = createStreamInput;
|
|
2254
2724
|
exports.createText = createText;
|
|
2255
2725
|
exports.createType = createType;
|
|
2256
2726
|
exports.definePrinter = definePrinter;
|
|
2727
|
+
exports.defineSchemaDialect = defineSchemaDialect;
|
|
2728
|
+
exports.dispatch = dispatch;
|
|
2257
2729
|
exports.enumPropName = enumPropName;
|
|
2258
2730
|
exports.extractRefName = extractRefName;
|
|
2259
2731
|
exports.extractStringsFromNodes = extractStringsFromNodes;
|
|
2260
2732
|
exports.findCircularSchemas = findCircularSchemas;
|
|
2261
2733
|
exports.findDiscriminator = findDiscriminator;
|
|
2262
2734
|
exports.httpMethods = httpMethods;
|
|
2735
|
+
exports.isHttpOperationNode = isHttpOperationNode;
|
|
2263
2736
|
exports.isInputNode = isInputNode;
|
|
2264
2737
|
exports.isOperationNode = isOperationNode;
|
|
2265
2738
|
exports.isOutputNode = isOutputNode;
|
|
2266
|
-
exports.isScalarPrimitive = isScalarPrimitive;
|
|
2267
2739
|
exports.isSchemaNode = isSchemaNode;
|
|
2268
2740
|
exports.isStringType = isStringType;
|
|
2269
|
-
exports.
|
|
2270
|
-
exports.mergeAdjacentObjects = mergeAdjacentObjects;
|
|
2741
|
+
exports.mergeAdjacentObjectsLazy = mergeAdjacentObjectsLazy;
|
|
2271
2742
|
exports.narrowSchema = narrowSchema;
|
|
2272
|
-
exports.
|
|
2273
|
-
exports.resolveRefName = resolveRefName;
|
|
2743
|
+
exports.schemaSignature = schemaSignature;
|
|
2274
2744
|
exports.schemaTypes = schemaTypes;
|
|
2275
2745
|
exports.setDiscriminatorEnum = setDiscriminatorEnum;
|
|
2276
2746
|
exports.setEnumName = setEnumName;
|
|
@@ -2278,6 +2748,7 @@ exports.simplifyUnion = simplifyUnion;
|
|
|
2278
2748
|
exports.syncOptionality = syncOptionality;
|
|
2279
2749
|
exports.syncSchemaRef = syncSchemaRef;
|
|
2280
2750
|
exports.transform = transform;
|
|
2751
|
+
exports.update = update;
|
|
2281
2752
|
exports.walk = walk;
|
|
2282
2753
|
|
|
2283
2754
|
//# sourceMappingURL=index.cjs.map
|