@kubb/ast 5.0.0-beta.4 → 5.0.0-beta.41
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 +28 -19
- 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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./chunk
|
|
1
|
+
import "./chunk-C0LytTxp.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
//#region src/constants.ts
|
|
@@ -6,25 +6,6 @@ const visitorDepths = {
|
|
|
6
6
|
shallow: "shallow",
|
|
7
7
|
deep: "deep"
|
|
8
8
|
};
|
|
9
|
-
const nodeKinds = {
|
|
10
|
-
input: "Input",
|
|
11
|
-
output: "Output",
|
|
12
|
-
operation: "Operation",
|
|
13
|
-
schema: "Schema",
|
|
14
|
-
property: "Property",
|
|
15
|
-
parameter: "Parameter",
|
|
16
|
-
response: "Response",
|
|
17
|
-
functionParameter: "FunctionParameter",
|
|
18
|
-
parameterGroup: "ParameterGroup",
|
|
19
|
-
functionParameters: "FunctionParameters",
|
|
20
|
-
type: "Type",
|
|
21
|
-
file: "File",
|
|
22
|
-
import: "Import",
|
|
23
|
-
export: "Export",
|
|
24
|
-
source: "Source",
|
|
25
|
-
text: "Text",
|
|
26
|
-
break: "Break"
|
|
27
|
-
};
|
|
28
9
|
/**
|
|
29
10
|
* Schema type discriminators used by all AST schema nodes.
|
|
30
11
|
*
|
|
@@ -173,33 +154,6 @@ const httpMethods = {
|
|
|
173
154
|
options: "OPTIONS",
|
|
174
155
|
trace: "TRACE"
|
|
175
156
|
};
|
|
176
|
-
/**
|
|
177
|
-
* Common MIME types used in request/response content negotiation.
|
|
178
|
-
*
|
|
179
|
-
* Covers JSON, XML, form data, PDFs, images, audio, and video formats.
|
|
180
|
-
* Use these as keys when serializing request/response bodies.
|
|
181
|
-
*/
|
|
182
|
-
const mediaTypes = {
|
|
183
|
-
applicationJson: "application/json",
|
|
184
|
-
applicationXml: "application/xml",
|
|
185
|
-
applicationFormUrlEncoded: "application/x-www-form-urlencoded",
|
|
186
|
-
applicationOctetStream: "application/octet-stream",
|
|
187
|
-
applicationPdf: "application/pdf",
|
|
188
|
-
applicationZip: "application/zip",
|
|
189
|
-
applicationGraphql: "application/graphql",
|
|
190
|
-
multipartFormData: "multipart/form-data",
|
|
191
|
-
textPlain: "text/plain",
|
|
192
|
-
textHtml: "text/html",
|
|
193
|
-
textCsv: "text/csv",
|
|
194
|
-
textXml: "text/xml",
|
|
195
|
-
imagePng: "image/png",
|
|
196
|
-
imageJpeg: "image/jpeg",
|
|
197
|
-
imageGif: "image/gif",
|
|
198
|
-
imageWebp: "image/webp",
|
|
199
|
-
imageSvgXml: "image/svg+xml",
|
|
200
|
-
audioMpeg: "audio/mpeg",
|
|
201
|
-
videoMp4: "video/mp4"
|
|
202
|
-
};
|
|
203
157
|
//#endregion
|
|
204
158
|
//#region ../../internals/utils/src/casing.ts
|
|
205
159
|
/**
|
|
@@ -265,6 +219,46 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
|
265
219
|
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
266
220
|
}
|
|
267
221
|
//#endregion
|
|
222
|
+
//#region ../../internals/utils/src/promise.ts
|
|
223
|
+
/**
|
|
224
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
225
|
+
*
|
|
226
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
227
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
228
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
229
|
+
* inner (created once per outer miss) keyed by the second.
|
|
230
|
+
*
|
|
231
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
232
|
+
* cleared independently of the memoized function.
|
|
233
|
+
*
|
|
234
|
+
* @example Single WeakMap key
|
|
235
|
+
* ```ts
|
|
236
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
237
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* @example Single Map key (primitive)
|
|
241
|
+
* ```ts
|
|
242
|
+
* const cache = new Map<string, Resolver>()
|
|
243
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
244
|
+
* ```
|
|
245
|
+
*
|
|
246
|
+
* @example Two-level (object + primitive)
|
|
247
|
+
* ```ts
|
|
248
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
249
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
250
|
+
* fn(params)('camelcase')
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
function memoize(store, factory) {
|
|
254
|
+
return (key) => {
|
|
255
|
+
if (store.has(key)) return store.get(key);
|
|
256
|
+
const value = factory(key);
|
|
257
|
+
store.set(key, value);
|
|
258
|
+
return value;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
268
262
|
//#region ../../internals/utils/src/reserved.ts
|
|
269
263
|
/**
|
|
270
264
|
* JavaScript and Java reserved words.
|
|
@@ -392,11 +386,11 @@ function trimExtName(text) {
|
|
|
392
386
|
* @example
|
|
393
387
|
* ```ts
|
|
394
388
|
* const schema = createSchema({ type: 'string' })
|
|
395
|
-
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode |
|
|
389
|
+
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
|
|
396
390
|
* ```
|
|
397
391
|
*/
|
|
398
392
|
function narrowSchema(node, type) {
|
|
399
|
-
return node?.type === type ? node :
|
|
393
|
+
return node?.type === type ? node : null;
|
|
400
394
|
}
|
|
401
395
|
function isKind(kind) {
|
|
402
396
|
return (node) => node.kind === kind;
|
|
@@ -435,6 +429,19 @@ const isOutputNode = isKind("Output");
|
|
|
435
429
|
*/
|
|
436
430
|
const isOperationNode = isKind("Operation");
|
|
437
431
|
/**
|
|
432
|
+
* Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* if (isHttpOperationNode(node)) {
|
|
437
|
+
* console.log(node.method, node.path)
|
|
438
|
+
* }
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
function isHttpOperationNode(node) {
|
|
442
|
+
return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
438
445
|
* Returns `true` when the input is a `SchemaNode`.
|
|
439
446
|
*
|
|
440
447
|
* @example
|
|
@@ -445,12 +452,6 @@ const isOperationNode = isKind("Operation");
|
|
|
445
452
|
* ```
|
|
446
453
|
*/
|
|
447
454
|
const isSchemaNode = isKind("Schema");
|
|
448
|
-
isKind("Property");
|
|
449
|
-
isKind("Parameter");
|
|
450
|
-
isKind("Response");
|
|
451
|
-
isKind("FunctionParameter");
|
|
452
|
-
isKind("ParameterGroup");
|
|
453
|
-
isKind("FunctionParameters");
|
|
454
455
|
//#endregion
|
|
455
456
|
//#region src/refs.ts
|
|
456
457
|
/**
|
|
@@ -503,53 +504,92 @@ function createLimit(concurrency) {
|
|
|
503
504
|
});
|
|
504
505
|
};
|
|
505
506
|
}
|
|
507
|
+
const visitorKeysByKind = {
|
|
508
|
+
Input: ["schemas", "operations"],
|
|
509
|
+
Operation: [
|
|
510
|
+
"parameters",
|
|
511
|
+
"requestBody",
|
|
512
|
+
"responses"
|
|
513
|
+
],
|
|
514
|
+
RequestBody: ["content"],
|
|
515
|
+
Content: ["schema"],
|
|
516
|
+
Response: ["content"],
|
|
517
|
+
Schema: [
|
|
518
|
+
"properties",
|
|
519
|
+
"items",
|
|
520
|
+
"members",
|
|
521
|
+
"additionalProperties"
|
|
522
|
+
],
|
|
523
|
+
Property: ["schema"],
|
|
524
|
+
Parameter: ["schema"]
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* Returns `true` when `value` is an AST node (an object carrying a `kind`).
|
|
528
|
+
*/
|
|
529
|
+
function isNode(value) {
|
|
530
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
531
|
+
}
|
|
506
532
|
/**
|
|
507
|
-
* Returns the immediate traversable children of `node
|
|
533
|
+
* Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
|
|
508
534
|
*
|
|
509
|
-
*
|
|
510
|
-
* `additionalProperties`) are only included
|
|
511
|
-
* when `recurse` is `true`; shallow mode skips them.
|
|
535
|
+
* `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
|
|
512
536
|
*
|
|
513
537
|
* @example
|
|
514
538
|
* ```ts
|
|
515
539
|
* const children = getChildren(operationNode, true)
|
|
516
|
-
* // returns parameters,
|
|
517
|
-
* ```
|
|
518
|
-
*/
|
|
519
|
-
function getChildren(node, recurse) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const children = [];
|
|
530
|
-
if (!recurse) return [];
|
|
531
|
-
if ("properties" in node && node.properties.length > 0) children.push(...node.properties);
|
|
532
|
-
if ("items" in node && node.items) children.push(...node.items);
|
|
533
|
-
if ("members" in node && node.members) children.push(...node.members);
|
|
534
|
-
if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
|
|
535
|
-
return children;
|
|
536
|
-
}
|
|
537
|
-
case "Property": return [node.schema];
|
|
538
|
-
case "Parameter": return [node.schema];
|
|
539
|
-
case "Response": return node.schema ? [node.schema] : [];
|
|
540
|
-
case "FunctionParameter":
|
|
541
|
-
case "ParameterGroup":
|
|
542
|
-
case "FunctionParameters":
|
|
543
|
-
case "Type": return [];
|
|
544
|
-
default: return [];
|
|
540
|
+
* // returns parameters, the request body, and responses
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
function* getChildren(node, recurse) {
|
|
544
|
+
if (node.kind === "Schema" && !recurse) return;
|
|
545
|
+
const keys = visitorKeysByKind[node.kind];
|
|
546
|
+
if (!keys) return;
|
|
547
|
+
const record = node;
|
|
548
|
+
for (const key of keys) {
|
|
549
|
+
const value = record[key];
|
|
550
|
+
if (Array.isArray(value)) {
|
|
551
|
+
for (const item of value) if (isNode(item)) yield item;
|
|
552
|
+
} else if (isNode(value)) yield value;
|
|
545
553
|
}
|
|
546
554
|
}
|
|
547
555
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
556
|
+
* Maps a node `kind` to the matching visitor callback name. Only the seven
|
|
557
|
+
* traversable node kinds have an entry. Every other kind resolves to
|
|
558
|
+
* `undefined` and is skipped.
|
|
559
|
+
*/
|
|
560
|
+
const VISITOR_KEY_BY_KIND = {
|
|
561
|
+
Input: "input",
|
|
562
|
+
Output: "output",
|
|
563
|
+
Operation: "operation",
|
|
564
|
+
Schema: "schema",
|
|
565
|
+
Property: "property",
|
|
566
|
+
Parameter: "parameter",
|
|
567
|
+
Response: "response"
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* Invokes the visitor callback that matches `node.kind`, passing the traversal
|
|
571
|
+
* context. Returns the callback's result (a replacement node, a collected
|
|
572
|
+
* value, or `undefined` when no callback is registered for the kind).
|
|
551
573
|
*
|
|
552
|
-
*
|
|
574
|
+
* Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
|
|
575
|
+
* in one place. `TResult` is the caller's expected return: the same node type
|
|
576
|
+
* for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
|
|
577
|
+
*/
|
|
578
|
+
function applyVisitor(node, visitor, parent) {
|
|
579
|
+
const key = VISITOR_KEY_BY_KIND[node.kind];
|
|
580
|
+
if (!key) return void 0;
|
|
581
|
+
const fn = visitor[key];
|
|
582
|
+
return fn?.(node, { parent });
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Async depth-first traversal for side effects. Visitor return values are
|
|
586
|
+
* ignored. Use `transform` when you want to rewrite nodes.
|
|
587
|
+
*
|
|
588
|
+
* Sibling nodes at each depth run concurrently up to `options.concurrency`
|
|
589
|
+
* (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
|
|
590
|
+
* work. Lower values reduce memory pressure.
|
|
591
|
+
*
|
|
592
|
+
* @example Log every operation
|
|
553
593
|
* ```ts
|
|
554
594
|
* await walk(root, {
|
|
555
595
|
* operation(node) {
|
|
@@ -558,213 +598,115 @@ function getChildren(node, recurse) {
|
|
|
558
598
|
* })
|
|
559
599
|
* ```
|
|
560
600
|
*
|
|
561
|
-
* @example
|
|
601
|
+
* @example Only visit the root node
|
|
562
602
|
* ```ts
|
|
563
|
-
*
|
|
564
|
-
* await walk(root, { depth: 'shallow', root: () => {} })
|
|
603
|
+
* await walk(root, { depth: 'shallow', input: () => {} })
|
|
565
604
|
* ```
|
|
566
605
|
*/
|
|
567
606
|
async function walk(node, options) {
|
|
568
607
|
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
569
608
|
}
|
|
570
609
|
async function _walk(node, visitor, recurse, limit, parent) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
case "Output":
|
|
576
|
-
await limit(() => visitor.output?.(node, { parent }));
|
|
577
|
-
break;
|
|
578
|
-
case "Operation":
|
|
579
|
-
await limit(() => visitor.operation?.(node, { parent }));
|
|
580
|
-
break;
|
|
581
|
-
case "Schema":
|
|
582
|
-
await limit(() => visitor.schema?.(node, { parent }));
|
|
583
|
-
break;
|
|
584
|
-
case "Property":
|
|
585
|
-
await limit(() => visitor.property?.(node, { parent }));
|
|
586
|
-
break;
|
|
587
|
-
case "Parameter":
|
|
588
|
-
await limit(() => visitor.parameter?.(node, { parent }));
|
|
589
|
-
break;
|
|
590
|
-
case "Response":
|
|
591
|
-
await limit(() => visitor.response?.(node, { parent }));
|
|
592
|
-
break;
|
|
593
|
-
case "FunctionParameter":
|
|
594
|
-
case "ParameterGroup":
|
|
595
|
-
case "FunctionParameters": break;
|
|
596
|
-
}
|
|
597
|
-
const children = getChildren(node, recurse);
|
|
598
|
-
for (const child of children) await _walk(child, visitor, recurse, limit, node);
|
|
610
|
+
await limit(() => applyVisitor(node, visitor, parent));
|
|
611
|
+
const children = Array.from(getChildren(node, recurse));
|
|
612
|
+
if (children.length === 0) return;
|
|
613
|
+
await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
|
|
599
614
|
}
|
|
600
615
|
function transform(node, options) {
|
|
601
616
|
const { depth, parent, ...visitor } = options;
|
|
602
617
|
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
parent: op
|
|
643
|
-
}) : void 0
|
|
644
|
-
}))
|
|
645
|
-
} : void 0,
|
|
646
|
-
responses: op.responses.map((r) => transform(r, {
|
|
647
|
-
...options,
|
|
648
|
-
parent: op
|
|
649
|
-
}))
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
case "Schema": {
|
|
653
|
-
let schema = node;
|
|
654
|
-
const replaced = visitor.schema?.(schema, { parent });
|
|
655
|
-
if (replaced) schema = replaced;
|
|
656
|
-
const childOptions = {
|
|
657
|
-
...options,
|
|
658
|
-
parent: schema
|
|
659
|
-
};
|
|
660
|
-
return {
|
|
661
|
-
...schema,
|
|
662
|
-
..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
|
|
663
|
-
..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
|
|
664
|
-
..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
|
|
665
|
-
..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
case "Property": {
|
|
669
|
-
let prop = node;
|
|
670
|
-
const replaced = visitor.property?.(prop, { parent });
|
|
671
|
-
if (replaced) prop = replaced;
|
|
672
|
-
return createProperty({
|
|
673
|
-
...prop,
|
|
674
|
-
schema: transform(prop.schema, {
|
|
675
|
-
...options,
|
|
676
|
-
parent: prop
|
|
677
|
-
})
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
case "Parameter": {
|
|
681
|
-
let param = node;
|
|
682
|
-
const replaced = visitor.parameter?.(param, { parent });
|
|
683
|
-
if (replaced) param = replaced;
|
|
684
|
-
return createParameter({
|
|
685
|
-
...param,
|
|
686
|
-
schema: transform(param.schema, {
|
|
687
|
-
...options,
|
|
688
|
-
parent: param
|
|
689
|
-
})
|
|
618
|
+
const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
|
|
619
|
+
if (rebuilt === node) return node;
|
|
620
|
+
const finalize = nodeFinalizers[rebuilt.kind];
|
|
621
|
+
return finalize ? finalize(rebuilt) : rebuilt;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
|
|
625
|
+
* resync schema optionality against their `required` flag once the schema may
|
|
626
|
+
* have changed.
|
|
627
|
+
*/
|
|
628
|
+
const nodeFinalizers = {
|
|
629
|
+
Property: (node) => createProperty(node),
|
|
630
|
+
Parameter: (node) => createParameter(node)
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
|
|
634
|
+
* each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
|
|
635
|
+
* `Schema` children are skipped in shallow mode.
|
|
636
|
+
*/
|
|
637
|
+
function transformChildren(node, options, recurse) {
|
|
638
|
+
if (node.kind === "Schema" && !recurse) return node;
|
|
639
|
+
const keys = visitorKeysByKind[node.kind];
|
|
640
|
+
if (!keys) return node;
|
|
641
|
+
const record = node;
|
|
642
|
+
const childOptions = {
|
|
643
|
+
...options,
|
|
644
|
+
parent: node
|
|
645
|
+
};
|
|
646
|
+
let updates;
|
|
647
|
+
for (const key of keys) {
|
|
648
|
+
if (!(key in record)) continue;
|
|
649
|
+
const value = record[key];
|
|
650
|
+
if (Array.isArray(value)) {
|
|
651
|
+
let changed = false;
|
|
652
|
+
const mapped = value.map((item) => {
|
|
653
|
+
if (!isNode(item)) return item;
|
|
654
|
+
const next = transform(item, childOptions);
|
|
655
|
+
if (next !== item) changed = true;
|
|
656
|
+
return next;
|
|
690
657
|
});
|
|
658
|
+
if (changed) (updates ??= {})[key] = mapped;
|
|
659
|
+
} else if (isNode(value)) {
|
|
660
|
+
const next = transform(value, childOptions);
|
|
661
|
+
if (next !== value) (updates ??= {})[key] = next;
|
|
691
662
|
}
|
|
692
|
-
case "Response": {
|
|
693
|
-
let response = node;
|
|
694
|
-
const replaced = visitor.response?.(response, { parent });
|
|
695
|
-
if (replaced) response = replaced;
|
|
696
|
-
return {
|
|
697
|
-
...response,
|
|
698
|
-
schema: transform(response.schema, {
|
|
699
|
-
...options,
|
|
700
|
-
parent: response
|
|
701
|
-
})
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
case "FunctionParameter":
|
|
705
|
-
case "ParameterGroup":
|
|
706
|
-
case "FunctionParameters":
|
|
707
|
-
case "Type": return node;
|
|
708
|
-
default: return node;
|
|
709
663
|
}
|
|
664
|
+
return updates ? {
|
|
665
|
+
...node,
|
|
666
|
+
...updates
|
|
667
|
+
} : node;
|
|
710
668
|
}
|
|
711
669
|
/**
|
|
712
|
-
*
|
|
670
|
+
* Lazy depth-first collection pass. Yields every non-null value returned by
|
|
671
|
+
* the visitor callbacks. Use `collect` for the eager array form.
|
|
713
672
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
* @example
|
|
673
|
+
* @example Collect every operationId
|
|
717
674
|
* ```ts
|
|
718
|
-
* const ids =
|
|
675
|
+
* const ids: string[] = []
|
|
676
|
+
* for (const id of collectLazy<string>(root, {
|
|
719
677
|
* operation(node) {
|
|
720
678
|
* return node.operationId
|
|
721
679
|
* },
|
|
722
|
-
* })
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* @example
|
|
726
|
-
* ```ts
|
|
727
|
-
* // Collect from only the current node
|
|
728
|
-
* const values = collect(root, { depth: 'shallow', root: () => 'root' })
|
|
680
|
+
* })) {
|
|
681
|
+
* ids.push(id)
|
|
682
|
+
* }
|
|
729
683
|
* ```
|
|
730
684
|
*/
|
|
731
|
-
function
|
|
685
|
+
function* collectLazy(node, options) {
|
|
732
686
|
const { depth, parent, ...visitor } = options;
|
|
733
687
|
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
case "Input":
|
|
738
|
-
v = visitor.input?.(node, { parent });
|
|
739
|
-
break;
|
|
740
|
-
case "Output":
|
|
741
|
-
v = visitor.output?.(node, { parent });
|
|
742
|
-
break;
|
|
743
|
-
case "Operation":
|
|
744
|
-
v = visitor.operation?.(node, { parent });
|
|
745
|
-
break;
|
|
746
|
-
case "Schema":
|
|
747
|
-
v = visitor.schema?.(node, { parent });
|
|
748
|
-
break;
|
|
749
|
-
case "Property":
|
|
750
|
-
v = visitor.property?.(node, { parent });
|
|
751
|
-
break;
|
|
752
|
-
case "Parameter":
|
|
753
|
-
v = visitor.parameter?.(node, { parent });
|
|
754
|
-
break;
|
|
755
|
-
case "Response":
|
|
756
|
-
v = visitor.response?.(node, { parent });
|
|
757
|
-
break;
|
|
758
|
-
case "FunctionParameter":
|
|
759
|
-
case "ParameterGroup":
|
|
760
|
-
case "FunctionParameters": break;
|
|
761
|
-
}
|
|
762
|
-
if (v !== void 0) results.push(v);
|
|
763
|
-
for (const child of getChildren(node, recurse)) for (const item of collect(child, {
|
|
688
|
+
const v = applyVisitor(node, visitor, parent);
|
|
689
|
+
if (v != null) yield v;
|
|
690
|
+
for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
|
|
764
691
|
...options,
|
|
765
692
|
parent: node
|
|
766
|
-
})
|
|
767
|
-
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Eager depth-first collection pass. Returns an array of every non-null value
|
|
697
|
+
* the visitor callbacks return.
|
|
698
|
+
*
|
|
699
|
+
* @example Collect every operationId
|
|
700
|
+
* ```ts
|
|
701
|
+
* const ids = collect<string>(root, {
|
|
702
|
+
* operation(node) {
|
|
703
|
+
* return node.operationId
|
|
704
|
+
* },
|
|
705
|
+
* })
|
|
706
|
+
* ```
|
|
707
|
+
*/
|
|
708
|
+
function collect(node, options) {
|
|
709
|
+
return Array.from(collectLazy(node, options));
|
|
768
710
|
}
|
|
769
711
|
//#endregion
|
|
770
712
|
//#region src/utils.ts
|
|
@@ -818,15 +760,16 @@ function isStringType(node) {
|
|
|
818
760
|
* the desired casing while preserving `OperationNode.parameters` for other consumers.
|
|
819
761
|
* The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
|
|
820
762
|
*/
|
|
763
|
+
const caseParamsMemo = memoize(/* @__PURE__ */ new WeakMap(), (params) => memoize(/* @__PURE__ */ new Map(), (casing) => params.map((param) => {
|
|
764
|
+
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
765
|
+
return {
|
|
766
|
+
...param,
|
|
767
|
+
name: transformed
|
|
768
|
+
};
|
|
769
|
+
})));
|
|
821
770
|
function caseParams(params, casing) {
|
|
822
771
|
if (!casing) return params;
|
|
823
|
-
return params
|
|
824
|
-
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
825
|
-
return {
|
|
826
|
-
...param,
|
|
827
|
-
name: transformed
|
|
828
|
-
};
|
|
829
|
-
});
|
|
772
|
+
return caseParamsMemo(params)(casing);
|
|
830
773
|
}
|
|
831
774
|
/**
|
|
832
775
|
* Creates a single-property object schema used as a discriminator literal.
|
|
@@ -955,7 +898,7 @@ function createOperationParams(node, options) {
|
|
|
955
898
|
}));
|
|
956
899
|
} else {
|
|
957
900
|
if (pathParams.length) if (pathParamsType === "inlineSpread") {
|
|
958
|
-
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0])
|
|
901
|
+
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]);
|
|
959
902
|
params.push(createFunctionParameter({
|
|
960
903
|
name: pathName,
|
|
961
904
|
type: spreadType ? wrapType(spreadType) : void 0,
|
|
@@ -1031,13 +974,13 @@ function buildGroupParam({ name, node, params, groupType, resolver, wrapType })
|
|
|
1031
974
|
}
|
|
1032
975
|
/**
|
|
1033
976
|
* Derives a {@link ParamGroupType} from the resolver's group method.
|
|
1034
|
-
* Returns `
|
|
977
|
+
* Returns `null` when the group name equals the individual param name (no real group).
|
|
1035
978
|
*/
|
|
1036
979
|
function resolveGroupType({ node, params, groupMethod, resolver }) {
|
|
1037
|
-
if (!params.length) return;
|
|
980
|
+
if (!params.length) return null;
|
|
1038
981
|
const firstParam = params[0];
|
|
1039
982
|
const groupName = groupMethod.call(resolver, node, firstParam);
|
|
1040
|
-
if (groupName === resolver.resolveParamName(node, firstParam)) return;
|
|
983
|
+
if (groupName === resolver.resolveParamName(node, firstParam)) return null;
|
|
1041
984
|
const allOptional = params.every((p) => !p.required);
|
|
1042
985
|
return {
|
|
1043
986
|
type: createParamsType({
|
|
@@ -1081,7 +1024,7 @@ function importKey(path, name, isTypeOnly) {
|
|
|
1081
1024
|
}
|
|
1082
1025
|
/**
|
|
1083
1026
|
* Computes a multi-level sort key for exports and imports:
|
|
1084
|
-
* non-array names first (wildcards/namespace aliases)
|
|
1027
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
1085
1028
|
*/
|
|
1086
1029
|
function sortKey(node) {
|
|
1087
1030
|
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
@@ -1104,6 +1047,16 @@ function combineSources(sources) {
|
|
|
1104
1047
|
return [...seen.values()];
|
|
1105
1048
|
}
|
|
1106
1049
|
/**
|
|
1050
|
+
* Merges `incoming` names into `existing`, preserving order and dropping duplicates.
|
|
1051
|
+
*
|
|
1052
|
+
* Shared by `combineExports` and `combineImports` for the same-path name-merge case.
|
|
1053
|
+
*/
|
|
1054
|
+
function mergeNameArrays(existing, incoming) {
|
|
1055
|
+
const merged = new Set(existing);
|
|
1056
|
+
for (const name of incoming) merged.add(name);
|
|
1057
|
+
return [...merged];
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1107
1060
|
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
1108
1061
|
*
|
|
1109
1062
|
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
@@ -1124,11 +1077,8 @@ function combineExports(exports) {
|
|
|
1124
1077
|
if (!name.length) continue;
|
|
1125
1078
|
const key = pathTypeKey(path, isTypeOnly);
|
|
1126
1079
|
const existing = namedByPath.get(key);
|
|
1127
|
-
if (existing && Array.isArray(existing.name))
|
|
1128
|
-
|
|
1129
|
-
for (const n of name) merged.add(n);
|
|
1130
|
-
existing.name = [...merged];
|
|
1131
|
-
} else {
|
|
1080
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
1081
|
+
else {
|
|
1132
1082
|
const newItem = {
|
|
1133
1083
|
...curr,
|
|
1134
1084
|
name: [...new Set(name)]
|
|
@@ -1164,6 +1114,11 @@ function combineImports(imports, exports, source) {
|
|
|
1164
1114
|
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
1165
1115
|
return importNameMemo.get(key);
|
|
1166
1116
|
};
|
|
1117
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
1118
|
+
for (const node of imports) {
|
|
1119
|
+
if (!Array.isArray(node.name)) continue;
|
|
1120
|
+
if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
|
|
1121
|
+
}
|
|
1167
1122
|
const result = [];
|
|
1168
1123
|
const namedByPath = /* @__PURE__ */ new Map();
|
|
1169
1124
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1181,11 +1136,8 @@ function combineImports(imports, exports, source) {
|
|
|
1181
1136
|
if (!name.length) continue;
|
|
1182
1137
|
const key = pathTypeKey(path, isTypeOnly);
|
|
1183
1138
|
const existing = namedByPath.get(key);
|
|
1184
|
-
if (existing && Array.isArray(existing.name))
|
|
1185
|
-
|
|
1186
|
-
for (const n of name) merged.add(n);
|
|
1187
|
-
existing.name = [...merged];
|
|
1188
|
-
} else {
|
|
1139
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
1140
|
+
else {
|
|
1189
1141
|
const newItem = {
|
|
1190
1142
|
...curr,
|
|
1191
1143
|
name
|
|
@@ -1194,7 +1146,7 @@ function combineImports(imports, exports, source) {
|
|
|
1194
1146
|
namedByPath.set(key, newItem);
|
|
1195
1147
|
}
|
|
1196
1148
|
} else {
|
|
1197
|
-
if (name && !isUsed(name)) continue;
|
|
1149
|
+
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
1198
1150
|
const key = importKey(path, name, isTypeOnly);
|
|
1199
1151
|
if (!seen.has(key)) {
|
|
1200
1152
|
result.push(curr);
|
|
@@ -1230,7 +1182,7 @@ function extractStringsFromNodes(nodes) {
|
|
|
1230
1182
|
/**
|
|
1231
1183
|
* Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
|
|
1232
1184
|
*
|
|
1233
|
-
* Returns `
|
|
1185
|
+
* Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
|
|
1234
1186
|
* identifier for type definitions or error messages.
|
|
1235
1187
|
*
|
|
1236
1188
|
* @example
|
|
@@ -1240,14 +1192,14 @@ function extractStringsFromNodes(nodes) {
|
|
|
1240
1192
|
* ```
|
|
1241
1193
|
*/
|
|
1242
1194
|
function resolveRefName(node) {
|
|
1243
|
-
if (!node || node.type !== "ref") return
|
|
1244
|
-
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ??
|
|
1245
|
-
return node.name ?? node.schema?.name ??
|
|
1195
|
+
if (!node || node.type !== "ref") return null;
|
|
1196
|
+
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null;
|
|
1197
|
+
return node.name ?? node.schema?.name ?? null;
|
|
1246
1198
|
}
|
|
1247
1199
|
/**
|
|
1248
1200
|
* Collects every named schema referenced (transitively) from a node via ref edges.
|
|
1249
1201
|
*
|
|
1250
|
-
* Refs are followed by name only
|
|
1202
|
+
* Refs are followed by name only, the resolved `node.schema` is not traversed inline.
|
|
1251
1203
|
* Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
|
|
1252
1204
|
*
|
|
1253
1205
|
* @example Collect refs from a single schema
|
|
@@ -1264,21 +1216,26 @@ function resolveRefName(node) {
|
|
|
1264
1216
|
* }
|
|
1265
1217
|
* ```
|
|
1266
1218
|
*/
|
|
1267
|
-
|
|
1268
|
-
|
|
1219
|
+
const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
|
|
1220
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1269
1221
|
collect(node, { schema(child) {
|
|
1270
1222
|
if (child.type === "ref") {
|
|
1271
1223
|
const name = resolveRefName(child);
|
|
1272
|
-
if (name)
|
|
1224
|
+
if (name) refs.add(name);
|
|
1273
1225
|
}
|
|
1274
1226
|
} });
|
|
1227
|
+
return refs;
|
|
1228
|
+
});
|
|
1229
|
+
function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
1230
|
+
if (!node) return out;
|
|
1231
|
+
for (const name of collectSchemaRefs(node)) out.add(name);
|
|
1275
1232
|
return out;
|
|
1276
1233
|
}
|
|
1277
1234
|
/**
|
|
1278
1235
|
* Collects the names of all top-level schemas transitively used by a set of operations.
|
|
1279
1236
|
*
|
|
1280
1237
|
* An operation uses a schema when any of its parameters, request body content, or responses
|
|
1281
|
-
* reference it
|
|
1238
|
+
* reference it, directly or indirectly through other named schemas.
|
|
1282
1239
|
* The walk is iterative and safe against reference cycles.
|
|
1283
1240
|
*
|
|
1284
1241
|
* Use this together with `include` filters to determine which schemas from `components/schemas`
|
|
@@ -1287,10 +1244,10 @@ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
|
1287
1244
|
*
|
|
1288
1245
|
* @example Only generate schemas referenced by included operations
|
|
1289
1246
|
* ```ts
|
|
1290
|
-
* const includedOps =
|
|
1291
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
1247
|
+
* const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
|
|
1248
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1292
1249
|
*
|
|
1293
|
-
* for (const schema of
|
|
1250
|
+
* for (const schema of schemas) {
|
|
1294
1251
|
* if (schema.name && !allowed.has(schema.name)) continue
|
|
1295
1252
|
* // … generate schema
|
|
1296
1253
|
* }
|
|
@@ -1298,11 +1255,12 @@ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
|
1298
1255
|
*
|
|
1299
1256
|
* @example Check whether a specific schema is needed
|
|
1300
1257
|
* ```ts
|
|
1301
|
-
* const allowed = collectUsedSchemaNames(includedOps,
|
|
1258
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1302
1259
|
* allowed.has('OrderStatus') // false when no included operation references OrderStatus
|
|
1303
1260
|
* ```
|
|
1304
1261
|
*/
|
|
1305
|
-
|
|
1262
|
+
const collectUsedSchemaNamesMemo = memoize(/* @__PURE__ */ new WeakMap(), (ops) => memoize(/* @__PURE__ */ new WeakMap(), (schemas) => computeUsedSchemaNames(ops, schemas)));
|
|
1263
|
+
function computeUsedSchemaNames(operations, schemas) {
|
|
1306
1264
|
const schemaMap = /* @__PURE__ */ new Map();
|
|
1307
1265
|
for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
|
|
1308
1266
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -1314,22 +1272,17 @@ function collectUsedSchemaNames(operations, schemas) {
|
|
|
1314
1272
|
if (namedSchema) visitSchema(namedSchema);
|
|
1315
1273
|
}
|
|
1316
1274
|
}
|
|
1317
|
-
for (const op of operations) for (const schema of
|
|
1275
|
+
for (const op of operations) for (const schema of collectLazy(op, {
|
|
1318
1276
|
depth: "shallow",
|
|
1319
1277
|
schema: (node) => node
|
|
1320
1278
|
})) visitSchema(schema);
|
|
1321
1279
|
return result;
|
|
1322
1280
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
1329
|
-
*
|
|
1330
|
-
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
1331
|
-
*/
|
|
1332
|
-
function findCircularSchemas(schemas) {
|
|
1281
|
+
function collectUsedSchemaNames(operations, schemas) {
|
|
1282
|
+
return collectUsedSchemaNamesMemo(operations)(schemas);
|
|
1283
|
+
}
|
|
1284
|
+
const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
|
|
1285
|
+
const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
|
|
1333
1286
|
const graph = /* @__PURE__ */ new Map();
|
|
1334
1287
|
for (const schema of schemas) {
|
|
1335
1288
|
if (!schema.name) continue;
|
|
@@ -1352,6 +1305,19 @@ function findCircularSchemas(schemas) {
|
|
|
1352
1305
|
}
|
|
1353
1306
|
}
|
|
1354
1307
|
return circular;
|
|
1308
|
+
});
|
|
1309
|
+
/**
|
|
1310
|
+
* Identifies all schemas that participate in circular dependency chains, including direct self-loops.
|
|
1311
|
+
*
|
|
1312
|
+
* Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
|
|
1313
|
+
* in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
|
|
1314
|
+
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
1315
|
+
*
|
|
1316
|
+
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
1317
|
+
*/
|
|
1318
|
+
function findCircularSchemas(schemas) {
|
|
1319
|
+
if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
|
|
1320
|
+
return findCircularSchemasMemo(schemas);
|
|
1355
1321
|
}
|
|
1356
1322
|
/**
|
|
1357
1323
|
* Type guard returning `true` when a schema or anything nested within it contains a ref to a circular schema.
|
|
@@ -1359,23 +1325,27 @@ function findCircularSchemas(schemas) {
|
|
|
1359
1325
|
* Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
|
|
1360
1326
|
* Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
|
|
1361
1327
|
*
|
|
1362
|
-
* @note Returns `true` for the first matching circular ref found
|
|
1328
|
+
* @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
|
|
1363
1329
|
*/
|
|
1364
1330
|
function containsCircularRef(node, { circularSchemas, excludeName }) {
|
|
1365
1331
|
if (!node || circularSchemas.size === 0) return false;
|
|
1366
|
-
|
|
1367
|
-
if (child.type !== "ref") return
|
|
1332
|
+
for (const _ of collectLazy(node, { schema(child) {
|
|
1333
|
+
if (child.type !== "ref") return null;
|
|
1368
1334
|
const name = resolveRefName(child);
|
|
1369
|
-
return name && name !== excludeName && circularSchemas.has(name) ? true :
|
|
1370
|
-
} })
|
|
1335
|
+
return name && name !== excludeName && circularSchemas.has(name) ? true : null;
|
|
1336
|
+
} })) return true;
|
|
1337
|
+
return false;
|
|
1371
1338
|
}
|
|
1372
1339
|
//#endregion
|
|
1373
1340
|
//#region src/factory.ts
|
|
1374
1341
|
/**
|
|
1375
|
-
*
|
|
1342
|
+
* Updates a schema's `optional` and `nullish` flags from a parent's `required`
|
|
1343
|
+
* value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
|
|
1344
|
+
* object properties combine "required" and "nullable" into a single AST.
|
|
1376
1345
|
*
|
|
1377
|
-
* -
|
|
1378
|
-
* -
|
|
1346
|
+
* - Non-required + non-nullable → `optional: true`.
|
|
1347
|
+
* - Non-required + nullable → `nullish: true`.
|
|
1348
|
+
* - Required → both flags cleared.
|
|
1379
1349
|
*/
|
|
1380
1350
|
function syncOptionality(schema, required) {
|
|
1381
1351
|
const nullable = schema.nullable ?? false;
|
|
@@ -1386,6 +1356,29 @@ function syncOptionality(schema, required) {
|
|
|
1386
1356
|
};
|
|
1387
1357
|
}
|
|
1388
1358
|
/**
|
|
1359
|
+
* Identity-preserving node update: returns `node` unchanged when every field in
|
|
1360
|
+
* `changes` already equals (by reference) the current value, otherwise a new node
|
|
1361
|
+
* with the changes applied.
|
|
1362
|
+
*
|
|
1363
|
+
* Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
|
|
1364
|
+
* structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
|
|
1365
|
+
* downstream passes can detect "nothing changed" by identity. Comparison is
|
|
1366
|
+
* shallow: a structurally-equal but newly-allocated array/object counts as a change.
|
|
1367
|
+
*
|
|
1368
|
+
* @example
|
|
1369
|
+
* ```ts
|
|
1370
|
+
* update(node, { name: node.name }) // -> same `node` reference
|
|
1371
|
+
* update(node, { name: 'renamed' }) // -> new node, `name` replaced
|
|
1372
|
+
* ```
|
|
1373
|
+
*/
|
|
1374
|
+
function update(node, changes) {
|
|
1375
|
+
for (const key in changes) if (changes[key] !== node[key]) return {
|
|
1376
|
+
...node,
|
|
1377
|
+
...changes
|
|
1378
|
+
};
|
|
1379
|
+
return node;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1389
1382
|
* Creates an `InputNode` with stable defaults for `schemas` and `operations`.
|
|
1390
1383
|
*
|
|
1391
1384
|
* @example
|
|
@@ -1404,11 +1397,31 @@ function createInput(overrides = {}) {
|
|
|
1404
1397
|
return {
|
|
1405
1398
|
schemas: [],
|
|
1406
1399
|
operations: [],
|
|
1400
|
+
meta: {
|
|
1401
|
+
circularNames: [],
|
|
1402
|
+
enumNames: []
|
|
1403
|
+
},
|
|
1407
1404
|
...overrides,
|
|
1408
1405
|
kind: "Input"
|
|
1409
1406
|
};
|
|
1410
1407
|
}
|
|
1411
1408
|
/**
|
|
1409
|
+
* Creates an `InputStreamNode` from pre-built `AsyncIterable` sources.
|
|
1410
|
+
*
|
|
1411
|
+
* @example
|
|
1412
|
+
* ```ts
|
|
1413
|
+
* const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
|
|
1414
|
+
* ```
|
|
1415
|
+
*/
|
|
1416
|
+
function createStreamInput(schemas, operations, meta) {
|
|
1417
|
+
return {
|
|
1418
|
+
kind: "Input",
|
|
1419
|
+
schemas,
|
|
1420
|
+
operations,
|
|
1421
|
+
meta
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1412
1425
|
* Creates an `OutputNode` with a stable default for `files`.
|
|
1413
1426
|
*
|
|
1414
1427
|
* @example
|
|
@@ -1430,40 +1443,40 @@ function createOutput(overrides = {}) {
|
|
|
1430
1443
|
};
|
|
1431
1444
|
}
|
|
1432
1445
|
/**
|
|
1433
|
-
* Creates
|
|
1434
|
-
*
|
|
1435
|
-
* @example
|
|
1436
|
-
* ```ts
|
|
1437
|
-
* const operation = createOperation({
|
|
1438
|
-
* operationId: 'getPetById',
|
|
1439
|
-
* method: 'GET',
|
|
1440
|
-
* path: '/pet/{petId}',
|
|
1441
|
-
* })
|
|
1442
|
-
* // tags, parameters, and responses are []
|
|
1443
|
-
* ```
|
|
1444
|
-
*
|
|
1445
|
-
* @example
|
|
1446
|
-
* ```ts
|
|
1447
|
-
* const operation = createOperation({
|
|
1448
|
-
* operationId: 'findPets',
|
|
1449
|
-
* method: 'GET',
|
|
1450
|
-
* path: '/pet/findByStatus',
|
|
1451
|
-
* tags: ['pet'],
|
|
1452
|
-
* })
|
|
1453
|
-
* ```
|
|
1446
|
+
* Creates a `ContentNode` for a single request-body or response content type.
|
|
1454
1447
|
*/
|
|
1448
|
+
function createContent(props) {
|
|
1449
|
+
return {
|
|
1450
|
+
...props,
|
|
1451
|
+
kind: "Content"
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
|
|
1456
|
+
*/
|
|
1457
|
+
function createRequestBody(props) {
|
|
1458
|
+
return {
|
|
1459
|
+
...props,
|
|
1460
|
+
kind: "RequestBody",
|
|
1461
|
+
content: props.content?.map(createContent)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1455
1464
|
function createOperation(props) {
|
|
1465
|
+
const { requestBody, ...rest } = props;
|
|
1466
|
+
const isHttp = rest.method !== void 0 && rest.path !== void 0;
|
|
1456
1467
|
return {
|
|
1457
1468
|
tags: [],
|
|
1458
1469
|
parameters: [],
|
|
1459
1470
|
responses: [],
|
|
1460
|
-
...
|
|
1461
|
-
|
|
1471
|
+
...rest,
|
|
1472
|
+
...isHttp ? { protocol: "http" } : {},
|
|
1473
|
+
kind: "Operation",
|
|
1474
|
+
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
1462
1475
|
};
|
|
1463
1476
|
}
|
|
1464
1477
|
/**
|
|
1465
1478
|
* Maps schema `type` to its underlying `primitive`.
|
|
1466
|
-
* Primitive types map to themselves
|
|
1479
|
+
* Primitive types map to themselves. Special string formats map to `'string'`.
|
|
1467
1480
|
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
1468
1481
|
*/
|
|
1469
1482
|
const TYPE_TO_PRIMITIVE = {
|
|
@@ -1572,19 +1585,29 @@ function createParameter(props) {
|
|
|
1572
1585
|
/**
|
|
1573
1586
|
* Creates a `ResponseNode`.
|
|
1574
1587
|
*
|
|
1588
|
+
* Response body schemas live inside `content`. For convenience a single legacy `schema`
|
|
1589
|
+
* (with optional `mediaType`/`keysToOmit`) is normalized into one `content` entry, so the same
|
|
1590
|
+
* schema is never stored both at the node root and inside `content`.
|
|
1591
|
+
*
|
|
1575
1592
|
* @example
|
|
1576
1593
|
* ```ts
|
|
1577
1594
|
* const response = createResponse({
|
|
1578
1595
|
* statusCode: '200',
|
|
1579
|
-
*
|
|
1580
|
-
* schema: createSchema({ type: 'object', properties: [] }),
|
|
1596
|
+
* content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
|
|
1581
1597
|
* })
|
|
1582
1598
|
* ```
|
|
1583
1599
|
*/
|
|
1584
1600
|
function createResponse(props) {
|
|
1601
|
+
const { schema, mediaType, keysToOmit, content, ...rest } = props;
|
|
1602
|
+
const entries = content ?? (schema ? [{
|
|
1603
|
+
contentType: mediaType ?? "application/json",
|
|
1604
|
+
schema,
|
|
1605
|
+
keysToOmit: keysToOmit ?? null
|
|
1606
|
+
}] : void 0);
|
|
1585
1607
|
return {
|
|
1586
|
-
...
|
|
1587
|
-
kind: "Response"
|
|
1608
|
+
...rest,
|
|
1609
|
+
kind: "Response",
|
|
1610
|
+
content: entries?.map(createContent)
|
|
1588
1611
|
};
|
|
1589
1612
|
}
|
|
1590
1613
|
/**
|
|
@@ -1604,7 +1627,7 @@ function createResponse(props) {
|
|
|
1604
1627
|
* // → params?: QueryParams
|
|
1605
1628
|
* ```
|
|
1606
1629
|
*
|
|
1607
|
-
* @example Param with default (implicitly optional
|
|
1630
|
+
* @example Param with default (implicitly optional. Cannot combine with `optional: true`)
|
|
1608
1631
|
* ```ts
|
|
1609
1632
|
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
|
|
1610
1633
|
* // → config: RequestConfig = {}
|
|
@@ -1661,7 +1684,7 @@ function createParamsType(props) {
|
|
|
1661
1684
|
* // call → { id, name }
|
|
1662
1685
|
* ```
|
|
1663
1686
|
*
|
|
1664
|
-
* @example Inline (spread)
|
|
1687
|
+
* @example Inline (spread), children emitted as individual top-level parameters
|
|
1665
1688
|
* ```ts
|
|
1666
1689
|
* createParameterGroup({
|
|
1667
1690
|
* properties: [createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false })],
|
|
@@ -1763,9 +1786,9 @@ function createSource(props) {
|
|
|
1763
1786
|
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1764
1787
|
*
|
|
1765
1788
|
* Computes:
|
|
1766
|
-
* - `id`
|
|
1767
|
-
* - `name`
|
|
1768
|
-
* - `extname`
|
|
1789
|
+
* - `id` SHA256 hash of the file path
|
|
1790
|
+
* - `name` `baseName` without extension
|
|
1791
|
+
* - `extname` extension extracted from `baseName`
|
|
1769
1792
|
*
|
|
1770
1793
|
* Deduplicates:
|
|
1771
1794
|
* - `sources` via `combineSources`
|
|
@@ -1993,24 +2016,466 @@ function createJsx(value) {
|
|
|
1993
2016
|
};
|
|
1994
2017
|
}
|
|
1995
2018
|
//#endregion
|
|
1996
|
-
//#region src/
|
|
2019
|
+
//#region src/signature.ts
|
|
2020
|
+
/**
|
|
2021
|
+
* The shape-affecting flags shared by every node kind: base primitive, format, and `nullable`.
|
|
2022
|
+
* Documentation and usage-slot flags (`optional`/`nullish`/`readOnly`/`writeOnly`) are
|
|
2023
|
+
* intentionally excluded, they describe the property slot, not the type.
|
|
2024
|
+
*/
|
|
2025
|
+
function flagsDescriptor(node) {
|
|
2026
|
+
return `${node.primitive ?? ""};${node.format ?? ""};${node.nullable ? 1 : 0}`;
|
|
2027
|
+
}
|
|
2028
|
+
function refTargetName(node) {
|
|
2029
|
+
if (node.ref) return extractRefName(node.ref);
|
|
2030
|
+
return node.name ?? "";
|
|
2031
|
+
}
|
|
2032
|
+
const arrayTupleFields = [
|
|
2033
|
+
{
|
|
2034
|
+
kind: "children",
|
|
2035
|
+
key: "items",
|
|
2036
|
+
prefix: "i"
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
kind: "child",
|
|
2040
|
+
key: "rest",
|
|
2041
|
+
prefix: "r"
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
kind: "scalar",
|
|
2045
|
+
key: "min",
|
|
2046
|
+
prefix: "mn"
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
kind: "scalar",
|
|
2050
|
+
key: "max",
|
|
2051
|
+
prefix: "mx"
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
kind: "bool",
|
|
2055
|
+
key: "unique",
|
|
2056
|
+
prefix: "u"
|
|
2057
|
+
}
|
|
2058
|
+
];
|
|
2059
|
+
const numericFields = [
|
|
2060
|
+
{
|
|
2061
|
+
kind: "scalar",
|
|
2062
|
+
key: "min",
|
|
2063
|
+
prefix: "mn"
|
|
2064
|
+
},
|
|
2065
|
+
{
|
|
2066
|
+
kind: "scalar",
|
|
2067
|
+
key: "max",
|
|
2068
|
+
prefix: "mx"
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
kind: "scalar",
|
|
2072
|
+
key: "exclusiveMinimum",
|
|
2073
|
+
prefix: "emn"
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
kind: "scalar",
|
|
2077
|
+
key: "exclusiveMaximum",
|
|
2078
|
+
prefix: "emx"
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
kind: "scalar",
|
|
2082
|
+
key: "multipleOf",
|
|
2083
|
+
prefix: "mo"
|
|
2084
|
+
}
|
|
2085
|
+
];
|
|
2086
|
+
const rangeFields = [{
|
|
2087
|
+
kind: "scalar",
|
|
2088
|
+
key: "min",
|
|
2089
|
+
prefix: "mn"
|
|
2090
|
+
}, {
|
|
2091
|
+
kind: "scalar",
|
|
2092
|
+
key: "max",
|
|
2093
|
+
prefix: "mx"
|
|
2094
|
+
}];
|
|
2095
|
+
/**
|
|
2096
|
+
* Maps each schema node `type` to the ordered list of shape-contributing fields.
|
|
2097
|
+
* Node types absent from this map (scalar types like boolean, null, any, etc.) fall
|
|
2098
|
+
* back to `${type}|${flags}` with no additional fields.
|
|
2099
|
+
*/
|
|
2100
|
+
const SHAPE_KEYS = {
|
|
2101
|
+
object: [
|
|
2102
|
+
{ kind: "objectProps" },
|
|
2103
|
+
{ kind: "additionalProps" },
|
|
2104
|
+
{ kind: "patternProps" },
|
|
2105
|
+
{
|
|
2106
|
+
kind: "scalar",
|
|
2107
|
+
key: "minProperties",
|
|
2108
|
+
prefix: "mn"
|
|
2109
|
+
},
|
|
2110
|
+
{
|
|
2111
|
+
kind: "scalar",
|
|
2112
|
+
key: "maxProperties",
|
|
2113
|
+
prefix: "mx"
|
|
2114
|
+
}
|
|
2115
|
+
],
|
|
2116
|
+
array: arrayTupleFields,
|
|
2117
|
+
tuple: arrayTupleFields,
|
|
2118
|
+
union: [
|
|
2119
|
+
{
|
|
2120
|
+
kind: "scalar",
|
|
2121
|
+
key: "strategy",
|
|
2122
|
+
prefix: "s"
|
|
2123
|
+
},
|
|
2124
|
+
{
|
|
2125
|
+
kind: "scalar",
|
|
2126
|
+
key: "discriminatorPropertyName",
|
|
2127
|
+
prefix: "d"
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
kind: "children",
|
|
2131
|
+
key: "members",
|
|
2132
|
+
prefix: "m"
|
|
2133
|
+
}
|
|
2134
|
+
],
|
|
2135
|
+
intersection: [{
|
|
2136
|
+
kind: "children",
|
|
2137
|
+
key: "members",
|
|
2138
|
+
prefix: "m"
|
|
2139
|
+
}],
|
|
2140
|
+
enum: [{ kind: "enumValues" }],
|
|
2141
|
+
ref: [{ kind: "refTarget" }],
|
|
2142
|
+
string: [
|
|
2143
|
+
{
|
|
2144
|
+
kind: "scalar",
|
|
2145
|
+
key: "min",
|
|
2146
|
+
prefix: "mn"
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
kind: "scalar",
|
|
2150
|
+
key: "max",
|
|
2151
|
+
prefix: "mx"
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
kind: "scalar",
|
|
2155
|
+
key: "pattern",
|
|
2156
|
+
prefix: "pt"
|
|
2157
|
+
}
|
|
2158
|
+
],
|
|
2159
|
+
number: numericFields,
|
|
2160
|
+
integer: numericFields,
|
|
2161
|
+
bigint: numericFields,
|
|
2162
|
+
url: [
|
|
2163
|
+
{
|
|
2164
|
+
kind: "scalar",
|
|
2165
|
+
key: "path",
|
|
2166
|
+
prefix: "path"
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
kind: "scalar",
|
|
2170
|
+
key: "min",
|
|
2171
|
+
prefix: "mn"
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
kind: "scalar",
|
|
2175
|
+
key: "max",
|
|
2176
|
+
prefix: "mx"
|
|
2177
|
+
}
|
|
2178
|
+
],
|
|
2179
|
+
uuid: rangeFields,
|
|
2180
|
+
email: rangeFields,
|
|
2181
|
+
datetime: [{
|
|
2182
|
+
kind: "bool",
|
|
2183
|
+
key: "offset",
|
|
2184
|
+
prefix: "o"
|
|
2185
|
+
}, {
|
|
2186
|
+
kind: "bool",
|
|
2187
|
+
key: "local",
|
|
2188
|
+
prefix: "l"
|
|
2189
|
+
}],
|
|
2190
|
+
date: [{
|
|
2191
|
+
kind: "scalar",
|
|
2192
|
+
key: "representation",
|
|
2193
|
+
prefix: "rep"
|
|
2194
|
+
}],
|
|
2195
|
+
time: [{
|
|
2196
|
+
kind: "scalar",
|
|
2197
|
+
key: "representation",
|
|
2198
|
+
prefix: "rep"
|
|
2199
|
+
}]
|
|
2200
|
+
};
|
|
2201
|
+
function serializeShapeField(field, node, record) {
|
|
2202
|
+
switch (field.kind) {
|
|
2203
|
+
case "scalar": return `${field.prefix}:${record[field.key] ?? ""}`;
|
|
2204
|
+
case "bool": return `${field.prefix}:${record[field.key] ? 1 : 0}`;
|
|
2205
|
+
case "child": {
|
|
2206
|
+
const child = record[field.key];
|
|
2207
|
+
return `${field.prefix}:${child ? signatureOf(child) : ""}`;
|
|
2208
|
+
}
|
|
2209
|
+
case "children": {
|
|
2210
|
+
const children = record[field.key] ?? [];
|
|
2211
|
+
return `${field.prefix}[${children.map((c) => signatureOf(c)).join(",")}]`;
|
|
2212
|
+
}
|
|
2213
|
+
case "objectProps": return `p[${(node.properties ?? []).map((prop) => `${prop.name}${prop.required ? "!" : "?"}${signatureOf(prop.schema)}`).join(",")}]`;
|
|
2214
|
+
case "additionalProps": {
|
|
2215
|
+
const obj = node;
|
|
2216
|
+
if (typeof obj.additionalProperties === "boolean") return `ab:${obj.additionalProperties}`;
|
|
2217
|
+
if (obj.additionalProperties) return `as:${signatureOf(obj.additionalProperties)}`;
|
|
2218
|
+
return "";
|
|
2219
|
+
}
|
|
2220
|
+
case "patternProps": {
|
|
2221
|
+
const obj = node;
|
|
2222
|
+
return `pp[${obj.patternProperties ? Object.keys(obj.patternProperties).sort().map((key) => `${key}=${signatureOf(obj.patternProperties[key])}`).join(",") : ""}]`;
|
|
2223
|
+
}
|
|
2224
|
+
case "enumValues": {
|
|
2225
|
+
const en = node;
|
|
2226
|
+
let values = "";
|
|
2227
|
+
if (en.namedEnumValues?.length) values = en.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(",");
|
|
2228
|
+
else if (en.enumValues?.length) values = en.enumValues.map((value) => `${value === null ? "null" : typeof value}:${String(value)}`).join(",");
|
|
2229
|
+
return `v[${values}]`;
|
|
2230
|
+
}
|
|
2231
|
+
case "refTarget": return `->${refTargetName(node)}`;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Builds the local, shape-only descriptor for a node: its kind, flags, constraints, and its
|
|
2236
|
+
* children's signatures. {@link signatureOf} hashes this string. Children contribute their
|
|
2237
|
+
* fixed-length signature rather than their own full descriptor, which keeps the result bounded.
|
|
2238
|
+
*/
|
|
2239
|
+
function describeShape(node) {
|
|
2240
|
+
const flags = flagsDescriptor(node);
|
|
2241
|
+
const fields = SHAPE_KEYS[node.type];
|
|
2242
|
+
if (!fields) return `${node.type}|${flags}`;
|
|
2243
|
+
const record = node;
|
|
2244
|
+
const parts = [`${node.type}|${flags}`];
|
|
2245
|
+
for (const field of fields) parts.push(serializeShapeField(field, node, record));
|
|
2246
|
+
return parts.join("|");
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Persistent hash-consing cache: `SchemaNode` → signature digest, keyed by node identity.
|
|
2250
|
+
*
|
|
2251
|
+
* A `WeakMap` so entries are released once the node is garbage-collected, and so a node hashed
|
|
2252
|
+
* during dedupe planning is not re-hashed when the same tree is rewritten during streaming
|
|
2253
|
+
* (where `schemaSignature` and `applyDedupe` would otherwise each walk it from scratch). Reuse
|
|
2254
|
+
* across calls is sound because a signature depends only on a node's content, and schema nodes
|
|
2255
|
+
* are immutable once created, transforms allocate new objects rather than mutating in place.
|
|
2256
|
+
*/
|
|
2257
|
+
const signatureCache = /* @__PURE__ */ new WeakMap();
|
|
2258
|
+
/**
|
|
2259
|
+
* Hash-consing: each node's signature is a fixed-length digest of its local shape plus its
|
|
2260
|
+
* children's digests (a Merkle hash). Children contribute their 64-char hash instead of their
|
|
2261
|
+
* full nested descriptor, so a signature stays bounded regardless of subtree depth, and the
|
|
2262
|
+
* digest is identical across calls because it depends only on content, never on traversal
|
|
2263
|
+
* order. This keeps the keys built during planning consistent with the ones recomputed later
|
|
2264
|
+
* during streaming. {@link signatureCache} memoizes node → digest across every computation.
|
|
2265
|
+
*/
|
|
2266
|
+
function signatureOf(node) {
|
|
2267
|
+
const cached = signatureCache.get(node);
|
|
2268
|
+
if (cached !== void 0) return cached;
|
|
2269
|
+
const signature = createHash("sha256").update(describeShape(node)).digest("hex");
|
|
2270
|
+
signatureCache.set(node, signature);
|
|
2271
|
+
return signature;
|
|
2272
|
+
}
|
|
1997
2273
|
/**
|
|
1998
|
-
*
|
|
2274
|
+
* Computes a deterministic, shape-only signature (a fixed-length content hash) for a schema node.
|
|
1999
2275
|
*
|
|
2000
|
-
*
|
|
2276
|
+
* Two schemas share a signature when they are structurally identical, ignoring
|
|
2277
|
+
* documentation (`name`, `title`, `description`, `example`, `default`, `deprecated`)
|
|
2278
|
+
* and usage-slot flags (`optional`, `nullish`, `readOnly`, `writeOnly`). `nullable`
|
|
2279
|
+
* is kept because it changes the produced type. `ref` nodes compare by target name,
|
|
2280
|
+
* which also keeps the algorithm terminating on circular shapes.
|
|
2281
|
+
*
|
|
2282
|
+
* @example Two enums with different descriptions share a signature
|
|
2283
|
+
* ```ts
|
|
2284
|
+
* schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
|
|
2285
|
+
* schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
|
|
2286
|
+
* ```
|
|
2287
|
+
*/
|
|
2288
|
+
function schemaSignature(node) {
|
|
2289
|
+
return signatureOf(node);
|
|
2290
|
+
}
|
|
2291
|
+
//#endregion
|
|
2292
|
+
//#region src/dedupe.ts
|
|
2293
|
+
/**
|
|
2294
|
+
* Builds the shared `ref` replacement for a duplicate occurrence, carrying the
|
|
2295
|
+
* usage-slot and documentation fields that are not part of the canonical type.
|
|
2296
|
+
*/
|
|
2297
|
+
function createRefNode(node, canonical) {
|
|
2298
|
+
return createSchema({
|
|
2299
|
+
type: "ref",
|
|
2300
|
+
name: canonical.name,
|
|
2301
|
+
ref: canonical.ref,
|
|
2302
|
+
optional: node.optional,
|
|
2303
|
+
nullish: node.nullish,
|
|
2304
|
+
readOnly: node.readOnly,
|
|
2305
|
+
writeOnly: node.writeOnly,
|
|
2306
|
+
deprecated: node.deprecated,
|
|
2307
|
+
description: node.description,
|
|
2308
|
+
default: node.default,
|
|
2309
|
+
example: node.example
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
function applyDedupe(node, canonicalBySignature, skipRootMatch = false) {
|
|
2313
|
+
if (canonicalBySignature.size === 0) return node;
|
|
2314
|
+
const root = node;
|
|
2315
|
+
return transform(node, { schema(schemaNode) {
|
|
2316
|
+
const signature = signatureOf(schemaNode);
|
|
2317
|
+
if (skipRootMatch && schemaNode === root) return void 0;
|
|
2318
|
+
const canonical = canonicalBySignature.get(signature);
|
|
2319
|
+
if (!canonical) return void 0;
|
|
2320
|
+
return createRefNode(schemaNode, canonical);
|
|
2321
|
+
} });
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Strips usage-slot flags from a hoisted definition and applies its canonical name.
|
|
2325
|
+
* A standalone definition is never optional, so `optional`/`nullish` are cleared.
|
|
2326
|
+
*/
|
|
2327
|
+
function cleanDefinition(node, name) {
|
|
2328
|
+
return {
|
|
2329
|
+
...node,
|
|
2330
|
+
name,
|
|
2331
|
+
optional: void 0,
|
|
2332
|
+
nullish: void 0
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Scans a forest of schema and operation nodes and produces a {@link DedupePlan}.
|
|
2337
|
+
*
|
|
2338
|
+
* A shape that occurs at least `minOccurrences` times is deduplicated: if any occurrence
|
|
2339
|
+
* is a named top-level schema, that name becomes the canonical (so other top-level duplicates
|
|
2340
|
+
* and inline copies turn into references to it). Otherwise a new definition is hoisted using
|
|
2341
|
+
* `nameFor`. The plan is then applied per node with {@link applyDedupe}.
|
|
2342
|
+
*
|
|
2343
|
+
* @example
|
|
2344
|
+
* ```ts
|
|
2345
|
+
* const plan = buildDedupePlan([...schemaNodes, ...operationNodes], {
|
|
2346
|
+
* isCandidate: (node) => node.type === 'enum' || node.type === 'object',
|
|
2347
|
+
* nameFor: (node) => node.name ?? null,
|
|
2348
|
+
* refFor: (name) => `#/components/schemas/${name}`,
|
|
2349
|
+
* })
|
|
2350
|
+
* ```
|
|
2351
|
+
*/
|
|
2352
|
+
function buildDedupePlan(roots, options) {
|
|
2353
|
+
const { isCandidate, nameFor, refFor, minOccurrences = 2 } = options;
|
|
2354
|
+
const topLevelNodes = /* @__PURE__ */ new Set();
|
|
2355
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2356
|
+
function record(schemaNode) {
|
|
2357
|
+
const signature = signatureOf(schemaNode);
|
|
2358
|
+
if (!isCandidate(schemaNode)) return;
|
|
2359
|
+
const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
|
|
2360
|
+
const group = groups.get(signature);
|
|
2361
|
+
if (group) {
|
|
2362
|
+
group.count++;
|
|
2363
|
+
if (isTopLevel && !group.topLevelName) group.topLevelName = schemaNode.name;
|
|
2364
|
+
} else groups.set(signature, {
|
|
2365
|
+
count: 1,
|
|
2366
|
+
representative: schemaNode,
|
|
2367
|
+
topLevelName: isTopLevel ? schemaNode.name : void 0
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
for (const root of roots) {
|
|
2371
|
+
if (root.kind === "Schema") topLevelNodes.add(root);
|
|
2372
|
+
for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
|
|
2373
|
+
}
|
|
2374
|
+
const canonicalBySignature = /* @__PURE__ */ new Map();
|
|
2375
|
+
const pendingHoists = [];
|
|
2376
|
+
for (const [signature, group] of groups) {
|
|
2377
|
+
if (group.count < minOccurrences) continue;
|
|
2378
|
+
if (group.topLevelName) {
|
|
2379
|
+
canonicalBySignature.set(signature, {
|
|
2380
|
+
name: group.topLevelName,
|
|
2381
|
+
ref: refFor(group.topLevelName)
|
|
2382
|
+
});
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
const name = nameFor(group.representative, signature);
|
|
2386
|
+
if (!name) continue;
|
|
2387
|
+
canonicalBySignature.set(signature, {
|
|
2388
|
+
name,
|
|
2389
|
+
ref: refFor(name)
|
|
2390
|
+
});
|
|
2391
|
+
pendingHoists.push({
|
|
2392
|
+
name,
|
|
2393
|
+
representative: group.representative
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
return {
|
|
2397
|
+
canonicalBySignature,
|
|
2398
|
+
hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, canonicalBySignature, true), name))
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
//#endregion
|
|
2402
|
+
//#region src/dialect.ts
|
|
2403
|
+
/**
|
|
2404
|
+
* Identity helper that types a {@link SchemaDialect} for an adapter. Like
|
|
2405
|
+
* `defineParser`, it adds no runtime behavior, it pins the dialect's type for
|
|
2406
|
+
* inference and gives adapter authors a discoverable anchor.
|
|
2407
|
+
*
|
|
2408
|
+
* @example
|
|
2409
|
+
* ```ts
|
|
2410
|
+
* export const oasDialect = defineSchemaDialect({
|
|
2411
|
+
* name: 'oas',
|
|
2412
|
+
* isNullable,
|
|
2413
|
+
* isReference,
|
|
2414
|
+
* isDiscriminator,
|
|
2415
|
+
* isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
|
|
2416
|
+
* resolveRef,
|
|
2417
|
+
* })
|
|
2418
|
+
* ```
|
|
2419
|
+
*/
|
|
2420
|
+
function defineSchemaDialect(dialect) {
|
|
2421
|
+
return dialect;
|
|
2422
|
+
}
|
|
2423
|
+
//#endregion
|
|
2424
|
+
//#region src/dispatch.ts
|
|
2425
|
+
/**
|
|
2426
|
+
* Walks an ordered list of {@link DispatchRule}s and returns the first node produced.
|
|
2427
|
+
*
|
|
2428
|
+
* This is the shared backbone for spec adapters (OpenAPI today, AsyncAPI and others later).
|
|
2429
|
+
* The contract an adapter follows is intentionally minimal:
|
|
2430
|
+
*
|
|
2431
|
+
* context → [rule.match → rule.convert] → node
|
|
2432
|
+
*
|
|
2433
|
+
* An adapter derives a context from a source spec node, then declares an ordered table of
|
|
2434
|
+
* rules mapping spec shapes onto Kubb AST nodes. To add support for a new spec, write a new
|
|
2435
|
+
* context type and a new rules table, the traversal here is reused unchanged.
|
|
2436
|
+
*
|
|
2437
|
+
* Order is significant: earlier rules win, so list higher-precedence or more specific shapes
|
|
2438
|
+
* first (e.g. composition keywords before plain `type`). A rule whose `match` returns `true`
|
|
2439
|
+
* may still `convert` to `null` to defer to later rules. When no rule produces a node this
|
|
2440
|
+
* returns `null`, leaving the caller to apply its own fallback.
|
|
2441
|
+
*
|
|
2442
|
+
* @example
|
|
2443
|
+
* ```ts
|
|
2444
|
+
* const node = dispatch(schemaRules, schemaContext) ?? createSchema({ type: fallbackType })
|
|
2445
|
+
* ```
|
|
2446
|
+
*/
|
|
2447
|
+
function dispatch(rules, context) {
|
|
2448
|
+
for (const rule of rules) {
|
|
2449
|
+
if (!rule.match(context)) continue;
|
|
2450
|
+
const node = rule.convert(context);
|
|
2451
|
+
if (node !== null && node !== void 0) return node;
|
|
2452
|
+
}
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
//#endregion
|
|
2456
|
+
//#region src/printer.ts
|
|
2457
|
+
/**
|
|
2458
|
+
* Defines a schema printer: a function that takes a `SchemaNode` and emits
|
|
2459
|
+
* code in your target language. Each plugin that produces code from schemas
|
|
2460
|
+
* (TypeScript types, Zod schemas, Faker factories) ships a printer built
|
|
2461
|
+
* with this helper.
|
|
2001
2462
|
*
|
|
2002
2463
|
* The builder receives resolved options and returns:
|
|
2003
|
-
* - `name` — a unique identifier for the printer
|
|
2004
|
-
* - `options` — options stored on the returned printer instance
|
|
2005
|
-
* - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
|
|
2006
|
-
* - `print` _(optional)_ — top-level override exposed as `printer.print`
|
|
2007
|
-
* - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
|
|
2008
|
-
* - This keeps recursion safe and avoids self-calls
|
|
2009
2464
|
*
|
|
2010
|
-
*
|
|
2465
|
+
* - `name` unique identifier for the printer.
|
|
2466
|
+
* - `options` stored on the returned printer instance.
|
|
2467
|
+
* - `nodes` map of `SchemaType` → handler. Handlers return the rendered
|
|
2468
|
+
* output (a string, a TypeScript AST node, ...) for that schema type.
|
|
2469
|
+
* - `print` (optional), top-level override exposed as `printer.print`.
|
|
2470
|
+
* Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
|
|
2471
|
+
*
|
|
2472
|
+
* Without a `print` override, `printer.print` falls back to `printer.transform`
|
|
2473
|
+
* (the node-level dispatcher).
|
|
2011
2474
|
*
|
|
2012
|
-
* @example
|
|
2475
|
+
* @example Tiny Zod printer
|
|
2013
2476
|
* ```ts
|
|
2477
|
+
* import { definePrinter, type PrinterFactoryOptions } from '@kubb/ast'
|
|
2478
|
+
*
|
|
2014
2479
|
* type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
|
|
2015
2480
|
*
|
|
2016
2481
|
* export const zodPrinter = definePrinter<PrinterZod>((options) => ({
|
|
@@ -2019,7 +2484,9 @@ function createJsx(value) {
|
|
|
2019
2484
|
* nodes: {
|
|
2020
2485
|
* string: () => 'z.string()',
|
|
2021
2486
|
* object(node) {
|
|
2022
|
-
* const props = node.properties
|
|
2487
|
+
* const props = node.properties
|
|
2488
|
+
* .map((p) => `${p.name}: ${this.transform(p.schema)}`)
|
|
2489
|
+
* .join(', ')
|
|
2023
2490
|
* return `z.object({ ${props} })`
|
|
2024
2491
|
* },
|
|
2025
2492
|
* },
|
|
@@ -2047,7 +2514,7 @@ function createPrinterFactory(getKey) {
|
|
|
2047
2514
|
options: resolvedOptions,
|
|
2048
2515
|
transform: (node) => {
|
|
2049
2516
|
const key = getKey(node);
|
|
2050
|
-
if (key ===
|
|
2517
|
+
if (key === null) return null;
|
|
2051
2518
|
const handler = nodes[key];
|
|
2052
2519
|
if (!handler) return null;
|
|
2053
2520
|
return handler.call(context, node);
|
|
@@ -2084,10 +2551,10 @@ function enumPropName(parentName, propName, enumSuffix) {
|
|
|
2084
2551
|
function collectImports({ node, nameMapping, resolve }) {
|
|
2085
2552
|
return collect(node, { schema(schemaNode) {
|
|
2086
2553
|
const schemaRef = narrowSchema(schemaNode, "ref");
|
|
2087
|
-
if (!schemaRef?.ref) return;
|
|
2554
|
+
if (!schemaRef?.ref) return null;
|
|
2088
2555
|
const rawName = extractRefName(schemaRef.ref);
|
|
2089
2556
|
const result = resolve(nameMapping.get(rawName) ?? rawName);
|
|
2090
|
-
if (!result) return;
|
|
2557
|
+
if (!result) return null;
|
|
2091
2558
|
return result;
|
|
2092
2559
|
} });
|
|
2093
2560
|
}
|
|
@@ -2141,23 +2608,24 @@ function setDiscriminatorEnum({ node, propertyName, values, enumName }) {
|
|
|
2141
2608
|
* ])
|
|
2142
2609
|
* ```
|
|
2143
2610
|
*/
|
|
2144
|
-
function
|
|
2145
|
-
|
|
2611
|
+
function* mergeAdjacentObjectsLazy(members) {
|
|
2612
|
+
let acc;
|
|
2613
|
+
for (const member of members) {
|
|
2146
2614
|
const objectMember = narrowSchema(member, "object");
|
|
2147
|
-
if (objectMember && !objectMember.name) {
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
...
|
|
2153
|
-
properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
|
|
2615
|
+
if (objectMember && !objectMember.name && acc !== void 0) {
|
|
2616
|
+
const accObject = narrowSchema(acc, "object");
|
|
2617
|
+
if (accObject && !accObject.name) {
|
|
2618
|
+
acc = createSchema({
|
|
2619
|
+
...accObject,
|
|
2620
|
+
properties: [...accObject.properties ?? [], ...objectMember.properties ?? []]
|
|
2154
2621
|
});
|
|
2155
|
-
|
|
2622
|
+
continue;
|
|
2156
2623
|
}
|
|
2157
2624
|
}
|
|
2158
|
-
acc
|
|
2159
|
-
|
|
2160
|
-
}
|
|
2625
|
+
if (acc !== void 0) yield acc;
|
|
2626
|
+
acc = member;
|
|
2627
|
+
}
|
|
2628
|
+
if (acc !== void 0) yield acc;
|
|
2161
2629
|
}
|
|
2162
2630
|
/**
|
|
2163
2631
|
* Removes enum members that are covered by broader scalar primitives in the same union.
|
|
@@ -2189,7 +2657,7 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
|
2189
2657
|
const enumNode = narrowSchema(propNode, "enum");
|
|
2190
2658
|
if (enumNode?.primitive === "boolean") return {
|
|
2191
2659
|
...propNode,
|
|
2192
|
-
name:
|
|
2660
|
+
name: null
|
|
2193
2661
|
};
|
|
2194
2662
|
if (enumNode) return {
|
|
2195
2663
|
...propNode,
|
|
@@ -2198,6 +2666,6 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
|
2198
2666
|
return propNode;
|
|
2199
2667
|
}
|
|
2200
2668
|
//#endregion
|
|
2201
|
-
export { caseParams, childName, collect, collectImports,
|
|
2669
|
+
export { applyDedupe, buildDedupePlan, caseParams, childName, collect, collectImports, 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, defineSchemaDialect, dispatch, enumPropName, extractRefName, extractStringsFromNodes, findCircularSchemas, findDiscriminator, httpMethods, isHttpOperationNode, isInputNode, isOperationNode, isOutputNode, isSchemaNode, isStringType, mergeAdjacentObjectsLazy, narrowSchema, schemaSignature, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, update, walk };
|
|
2202
2670
|
|
|
2203
2671
|
//# sourceMappingURL=index.js.map
|