@kubb/ast 5.0.0-beta.55 → 5.0.0-beta.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.cjs +1710 -1745
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -46
- package/dist/index.js +1682 -1740
- package/dist/index.js.map +1 -1
- package/dist/{types-BL7RpQAE.d.ts → types-C5aVnRE1.d.ts} +1730 -1789
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/dedupe.ts +1 -1
- package/src/factory.ts +3 -763
- package/src/guards.ts +1 -53
- package/src/index.ts +35 -20
- package/src/mocks.ts +6 -1
- package/src/node.ts +128 -0
- package/src/nodes/base.ts +3 -2
- package/src/nodes/code.ts +115 -0
- package/src/nodes/content.ts +19 -0
- package/src/nodes/file.ts +54 -0
- package/src/nodes/function.ts +221 -146
- package/src/nodes/index.ts +11 -3
- package/src/nodes/input.ts +36 -0
- package/src/nodes/operation.ts +59 -1
- package/src/nodes/output.ts +23 -0
- package/src/nodes/parameter.ts +33 -0
- package/src/nodes/property.ts +36 -0
- package/src/nodes/requestBody.ts +23 -1
- package/src/nodes/response.ts +39 -1
- package/src/nodes/schema.ts +72 -0
- package/src/registry.ts +70 -0
- package/src/transformers.ts +2 -2
- package/src/types.ts +5 -3
- package/src/utils/ast.ts +116 -193
- package/src/visitor.ts +3 -47
package/dist/index.js
CHANGED
|
@@ -2,6 +2,131 @@ import "./chunk-C0LytTxp.js";
|
|
|
2
2
|
import { _ as httpMethods, a as enumPropName, b as visitorDepths, g as camelCase, h as isValidVarName, o as extractRefName, v as isScalarPrimitive, y as schemaTypes } from "./utils-0p8ZO287.js";
|
|
3
3
|
import { hash } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
//#region src/node.ts
|
|
6
|
+
/**
|
|
7
|
+
* Builds a type guard that matches nodes of the given `kind`.
|
|
8
|
+
*/
|
|
9
|
+
function isKind(kind) {
|
|
10
|
+
return (node) => node.kind === kind;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Updates a schema's `optional` and `nullish` flags from a parent's `required`
|
|
14
|
+
* value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
|
|
15
|
+
* object properties combine "required" and "nullable" into a single AST.
|
|
16
|
+
*
|
|
17
|
+
* - Non-required + non-nullable → `optional: true`.
|
|
18
|
+
* - Non-required + nullable → `nullish: true`.
|
|
19
|
+
* - Required → both flags cleared.
|
|
20
|
+
*/
|
|
21
|
+
function syncOptionality(schema, required) {
|
|
22
|
+
const nullable = schema.nullable ?? false;
|
|
23
|
+
return {
|
|
24
|
+
...schema,
|
|
25
|
+
optional: !required && !nullable ? true : void 0,
|
|
26
|
+
nullish: !required && nullable ? true : void 0
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Defines a node once and derives its `create` builder, `is` guard, and traversal
|
|
31
|
+
* metadata. `create` merges `defaults`, the `build` hook (or the raw input), and the
|
|
32
|
+
* `kind`, so node construction lives in one place without scattered `as` casts.
|
|
33
|
+
*
|
|
34
|
+
* Set `rebuild: true` when the `build` hook derives fields from children. After a
|
|
35
|
+
* transform rewrites those children, the registry reruns `create` so the derived
|
|
36
|
+
* fields stay correct.
|
|
37
|
+
*
|
|
38
|
+
* @example Simple node
|
|
39
|
+
* ```ts
|
|
40
|
+
* const importDef = defineNode<ImportNode>({ kind: 'Import' })
|
|
41
|
+
* const createImport = importDef.create
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Node with a build hook that is rerun on transform
|
|
45
|
+
* ```ts
|
|
46
|
+
* const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
|
|
47
|
+
* kind: 'Property',
|
|
48
|
+
* build: (props) => ({ ...props, required: props.required ?? false }),
|
|
49
|
+
* children: ['schema'],
|
|
50
|
+
* visitorKey: 'property',
|
|
51
|
+
* rebuild: true,
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function defineNode(config) {
|
|
56
|
+
const { kind, defaults, build, children, visitorKey, rebuild } = config;
|
|
57
|
+
function create(input) {
|
|
58
|
+
const base = build ? build(input) : input;
|
|
59
|
+
return {
|
|
60
|
+
...defaults,
|
|
61
|
+
...base,
|
|
62
|
+
kind
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
kind,
|
|
67
|
+
create,
|
|
68
|
+
is: isKind(kind),
|
|
69
|
+
children,
|
|
70
|
+
visitorKey,
|
|
71
|
+
rebuild
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/nodes/schema.ts
|
|
76
|
+
/**
|
|
77
|
+
* Maps schema `type` to its underlying `primitive`.
|
|
78
|
+
* Primitive types map to themselves. Special string formats map to `'string'`.
|
|
79
|
+
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
80
|
+
*/
|
|
81
|
+
const TYPE_TO_PRIMITIVE = {
|
|
82
|
+
string: "string",
|
|
83
|
+
number: "number",
|
|
84
|
+
integer: "integer",
|
|
85
|
+
bigint: "bigint",
|
|
86
|
+
boolean: "boolean",
|
|
87
|
+
null: "null",
|
|
88
|
+
any: "any",
|
|
89
|
+
unknown: "unknown",
|
|
90
|
+
void: "void",
|
|
91
|
+
never: "never",
|
|
92
|
+
object: "object",
|
|
93
|
+
array: "array",
|
|
94
|
+
date: "date",
|
|
95
|
+
uuid: "string",
|
|
96
|
+
email: "string",
|
|
97
|
+
url: "string",
|
|
98
|
+
datetime: "string",
|
|
99
|
+
time: "string"
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Definition for the {@link SchemaNode}. Object schemas default `properties` to an
|
|
103
|
+
* empty array, and `primitive` is inferred from `type` when not explicitly provided.
|
|
104
|
+
*/
|
|
105
|
+
const schemaDef = defineNode({
|
|
106
|
+
kind: "Schema",
|
|
107
|
+
build: (props) => {
|
|
108
|
+
if (props.type === "object") return {
|
|
109
|
+
properties: [],
|
|
110
|
+
primitive: "object",
|
|
111
|
+
...props
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
primitive: TYPE_TO_PRIMITIVE[props.type],
|
|
115
|
+
...props
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
children: [
|
|
119
|
+
"properties",
|
|
120
|
+
"items",
|
|
121
|
+
"members",
|
|
122
|
+
"additionalProperties"
|
|
123
|
+
],
|
|
124
|
+
visitorKey: "schema"
|
|
125
|
+
});
|
|
126
|
+
function createSchema(props) {
|
|
127
|
+
return schemaDef.create(props);
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
5
130
|
//#region ../../internals/utils/src/fs.ts
|
|
6
131
|
/**
|
|
7
132
|
* Strips the file extension from a path or file name.
|
|
@@ -59,2040 +184,1857 @@ function memoize(store, factory) {
|
|
|
59
184
|
};
|
|
60
185
|
}
|
|
61
186
|
//#endregion
|
|
62
|
-
//#region src/
|
|
187
|
+
//#region src/signature.ts
|
|
63
188
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```ts
|
|
68
|
-
* const schema = createSchema({ type: 'string' })
|
|
69
|
-
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
|
|
70
|
-
* ```
|
|
189
|
+
* The flags shared by every node kind that affect its type: `primitive`, `format`, `nullable`.
|
|
71
190
|
*/
|
|
72
|
-
function
|
|
73
|
-
return node
|
|
74
|
-
}
|
|
75
|
-
function isKind(kind) {
|
|
76
|
-
return (node) => node.kind === kind;
|
|
191
|
+
function flagsDescriptor(node) {
|
|
192
|
+
return `${node.primitive ?? ""};${node.format ?? ""};${node.nullable ? 1 : 0}`;
|
|
77
193
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
* @example
|
|
82
|
-
* ```ts
|
|
83
|
-
* if (isInputNode(node)) {
|
|
84
|
-
* console.log(node.schemas.length)
|
|
85
|
-
* }
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
const isInputNode = isKind("Input");
|
|
89
|
-
/**
|
|
90
|
-
* Returns `true` when the input is an `OutputNode`.
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* ```ts
|
|
94
|
-
* if (isOutputNode(node)) {
|
|
95
|
-
* console.log(node.files.length)
|
|
96
|
-
* }
|
|
97
|
-
* ```
|
|
98
|
-
*/
|
|
99
|
-
const isOutputNode = isKind("Output");
|
|
100
|
-
/**
|
|
101
|
-
* Returns `true` when the input is an `OperationNode`.
|
|
102
|
-
*
|
|
103
|
-
* @example
|
|
104
|
-
* ```ts
|
|
105
|
-
* if (isOperationNode(node)) {
|
|
106
|
-
* console.log(node.operationId)
|
|
107
|
-
* }
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
const isOperationNode = isKind("Operation");
|
|
111
|
-
/**
|
|
112
|
-
* Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* ```ts
|
|
116
|
-
* if (isHttpOperationNode(node)) {
|
|
117
|
-
* console.log(node.method, node.path)
|
|
118
|
-
* }
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
function isHttpOperationNode(node) {
|
|
122
|
-
return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
|
|
194
|
+
function refTargetName(node) {
|
|
195
|
+
if (node.ref) return extractRefName(node.ref);
|
|
196
|
+
return node.name ?? "";
|
|
123
197
|
}
|
|
198
|
+
const arrayTupleFields = [
|
|
199
|
+
{
|
|
200
|
+
kind: "children",
|
|
201
|
+
key: "items",
|
|
202
|
+
prefix: "i"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
kind: "child",
|
|
206
|
+
key: "rest",
|
|
207
|
+
prefix: "r"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
kind: "scalar",
|
|
211
|
+
key: "min",
|
|
212
|
+
prefix: "mn"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
kind: "scalar",
|
|
216
|
+
key: "max",
|
|
217
|
+
prefix: "mx"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
kind: "bool",
|
|
221
|
+
key: "unique",
|
|
222
|
+
prefix: "u"
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
const numericFields = [
|
|
226
|
+
{
|
|
227
|
+
kind: "scalar",
|
|
228
|
+
key: "min",
|
|
229
|
+
prefix: "mn"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
kind: "scalar",
|
|
233
|
+
key: "max",
|
|
234
|
+
prefix: "mx"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
kind: "scalar",
|
|
238
|
+
key: "exclusiveMinimum",
|
|
239
|
+
prefix: "emn"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
kind: "scalar",
|
|
243
|
+
key: "exclusiveMaximum",
|
|
244
|
+
prefix: "emx"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
kind: "scalar",
|
|
248
|
+
key: "multipleOf",
|
|
249
|
+
prefix: "mo"
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
const rangeFields = [{
|
|
253
|
+
kind: "scalar",
|
|
254
|
+
key: "min",
|
|
255
|
+
prefix: "mn"
|
|
256
|
+
}, {
|
|
257
|
+
kind: "scalar",
|
|
258
|
+
key: "max",
|
|
259
|
+
prefix: "mx"
|
|
260
|
+
}];
|
|
124
261
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* ```ts
|
|
129
|
-
* if (isSchemaNode(node)) {
|
|
130
|
-
* console.log(node.type)
|
|
131
|
-
* }
|
|
132
|
-
* ```
|
|
133
|
-
*/
|
|
134
|
-
const isSchemaNode = isKind("Schema");
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/visitor.ts
|
|
137
|
-
/**
|
|
138
|
-
* Creates a small async concurrency limiter.
|
|
139
|
-
*
|
|
140
|
-
* At most `concurrency` tasks are in flight at once. Extra tasks are queued.
|
|
141
|
-
*
|
|
142
|
-
* @example
|
|
143
|
-
* ```ts
|
|
144
|
-
* const limit = createLimit(2)
|
|
145
|
-
* for (const task of [taskA, taskB, taskC]) {
|
|
146
|
-
* await limit(() => task())
|
|
147
|
-
* }
|
|
148
|
-
* // only 2 tasks run at the same time
|
|
149
|
-
* ```
|
|
262
|
+
* Maps each node `type` to its ordered shape-contributing fields. Types absent from the map
|
|
263
|
+
* (boolean, null, any, and other scalars) fall back to `${type}|${flags}`.
|
|
150
264
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
265
|
+
const SHAPE_KEYS = {
|
|
266
|
+
object: [
|
|
267
|
+
{ kind: "objectProps" },
|
|
268
|
+
{ kind: "additionalProps" },
|
|
269
|
+
{ kind: "patternProps" },
|
|
270
|
+
{
|
|
271
|
+
kind: "scalar",
|
|
272
|
+
key: "minProperties",
|
|
273
|
+
prefix: "mn"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
kind: "scalar",
|
|
277
|
+
key: "maxProperties",
|
|
278
|
+
prefix: "mx"
|
|
158
279
|
}
|
|
159
|
-
}
|
|
160
|
-
return function limit(fn) {
|
|
161
|
-
return new Promise((resolve, reject) => {
|
|
162
|
-
queue.push(() => {
|
|
163
|
-
Promise.resolve(fn()).then(resolve, reject).finally(() => {
|
|
164
|
-
active--;
|
|
165
|
-
next();
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
next();
|
|
169
|
-
});
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
const visitorKeysByKind = {
|
|
173
|
-
Input: ["schemas", "operations"],
|
|
174
|
-
Operation: [
|
|
175
|
-
"parameters",
|
|
176
|
-
"requestBody",
|
|
177
|
-
"responses"
|
|
178
280
|
],
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
281
|
+
array: arrayTupleFields,
|
|
282
|
+
tuple: arrayTupleFields,
|
|
283
|
+
union: [
|
|
284
|
+
{
|
|
285
|
+
kind: "scalar",
|
|
286
|
+
key: "strategy",
|
|
287
|
+
prefix: "s"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
kind: "scalar",
|
|
291
|
+
key: "discriminatorPropertyName",
|
|
292
|
+
prefix: "d"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
kind: "children",
|
|
296
|
+
key: "members",
|
|
297
|
+
prefix: "m"
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
intersection: [{
|
|
301
|
+
kind: "children",
|
|
302
|
+
key: "members",
|
|
303
|
+
prefix: "m"
|
|
304
|
+
}],
|
|
305
|
+
enum: [{ kind: "enumValues" }],
|
|
306
|
+
ref: [{ kind: "refTarget" }],
|
|
307
|
+
string: [
|
|
308
|
+
{
|
|
309
|
+
kind: "scalar",
|
|
310
|
+
key: "min",
|
|
311
|
+
prefix: "mn"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
kind: "scalar",
|
|
315
|
+
key: "max",
|
|
316
|
+
prefix: "mx"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
kind: "scalar",
|
|
320
|
+
key: "pattern",
|
|
321
|
+
prefix: "pt"
|
|
322
|
+
}
|
|
323
|
+
],
|
|
324
|
+
number: numericFields,
|
|
325
|
+
integer: numericFields,
|
|
326
|
+
bigint: numericFields,
|
|
327
|
+
url: [
|
|
328
|
+
{
|
|
329
|
+
kind: "scalar",
|
|
330
|
+
key: "path",
|
|
331
|
+
prefix: "path"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
kind: "scalar",
|
|
335
|
+
key: "min",
|
|
336
|
+
prefix: "mn"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
kind: "scalar",
|
|
340
|
+
key: "max",
|
|
341
|
+
prefix: "mx"
|
|
342
|
+
}
|
|
187
343
|
],
|
|
188
|
-
|
|
189
|
-
|
|
344
|
+
uuid: rangeFields,
|
|
345
|
+
email: rangeFields,
|
|
346
|
+
datetime: [{
|
|
347
|
+
kind: "bool",
|
|
348
|
+
key: "offset",
|
|
349
|
+
prefix: "o"
|
|
350
|
+
}, {
|
|
351
|
+
kind: "bool",
|
|
352
|
+
key: "local",
|
|
353
|
+
prefix: "l"
|
|
354
|
+
}],
|
|
355
|
+
date: [{
|
|
356
|
+
kind: "scalar",
|
|
357
|
+
key: "representation",
|
|
358
|
+
prefix: "rep"
|
|
359
|
+
}],
|
|
360
|
+
time: [{
|
|
361
|
+
kind: "scalar",
|
|
362
|
+
key: "representation",
|
|
363
|
+
prefix: "rep"
|
|
364
|
+
}]
|
|
190
365
|
};
|
|
366
|
+
function serializeShapeField(field, node, record) {
|
|
367
|
+
switch (field.kind) {
|
|
368
|
+
case "scalar": return `${field.prefix}:${record[field.key] ?? ""}`;
|
|
369
|
+
case "bool": return `${field.prefix}:${record[field.key] ? 1 : 0}`;
|
|
370
|
+
case "child": {
|
|
371
|
+
const child = record[field.key];
|
|
372
|
+
return `${field.prefix}:${child ? signatureOf(child) : ""}`;
|
|
373
|
+
}
|
|
374
|
+
case "children": {
|
|
375
|
+
const children = record[field.key] ?? [];
|
|
376
|
+
return `${field.prefix}[${children.map((c) => signatureOf(c)).join(",")}]`;
|
|
377
|
+
}
|
|
378
|
+
case "objectProps": return `p[${(node.properties ?? []).map((prop) => `${prop.name}${prop.required ? "!" : "?"}${signatureOf(prop.schema)}`).join(",")}]`;
|
|
379
|
+
case "additionalProps": {
|
|
380
|
+
const obj = node;
|
|
381
|
+
if (typeof obj.additionalProperties === "boolean") return `ab:${obj.additionalProperties}`;
|
|
382
|
+
if (obj.additionalProperties) return `as:${signatureOf(obj.additionalProperties)}`;
|
|
383
|
+
return "";
|
|
384
|
+
}
|
|
385
|
+
case "patternProps": {
|
|
386
|
+
const obj = node;
|
|
387
|
+
return `pp[${obj.patternProperties ? Object.keys(obj.patternProperties).sort().map((key) => `${key}=${signatureOf(obj.patternProperties[key])}`).join(",") : ""}]`;
|
|
388
|
+
}
|
|
389
|
+
case "enumValues": {
|
|
390
|
+
const en = node;
|
|
391
|
+
let values = "";
|
|
392
|
+
if (en.namedEnumValues?.length) values = en.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(",");
|
|
393
|
+
else if (en.enumValues?.length) values = en.enumValues.map((value) => `${value === null ? "null" : typeof value}:${String(value)}`).join(",");
|
|
394
|
+
return `v[${values}]`;
|
|
395
|
+
}
|
|
396
|
+
case "refTarget": return `->${refTargetName(node)}`;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
191
399
|
/**
|
|
192
|
-
*
|
|
400
|
+
* Builds the local shape descriptor that {@link signatureOf} hashes: the node's kind, flags,
|
|
401
|
+
* constraints, and its children's signatures.
|
|
193
402
|
*/
|
|
194
|
-
function
|
|
195
|
-
|
|
403
|
+
function describeShape(node) {
|
|
404
|
+
const flags = flagsDescriptor(node);
|
|
405
|
+
const fields = SHAPE_KEYS[node.type];
|
|
406
|
+
if (!fields) return `${node.type}|${flags}`;
|
|
407
|
+
const record = node;
|
|
408
|
+
const parts = [`${node.type}|${flags}`];
|
|
409
|
+
for (const field of fields) parts.push(serializeShapeField(field, node, record));
|
|
410
|
+
return parts.join("|");
|
|
196
411
|
}
|
|
197
412
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
413
|
+
* Node → digest cache, keyed by identity. A `WeakMap` so entries die with the node, and so a tree
|
|
414
|
+
* hashed during dedupe planning is not walked again when it is rewritten during streaming. Reuse
|
|
415
|
+
* is safe because a signature depends only on content, and nodes are immutable once created.
|
|
416
|
+
*/
|
|
417
|
+
const signatureCache = /* @__PURE__ */ new WeakMap();
|
|
418
|
+
/**
|
|
419
|
+
* Computes a deterministic, shape-only signature (a content hash) for a schema node. Two schemas
|
|
420
|
+
* share a signature when they are structurally identical, ignoring documentation (`name`, `title`,
|
|
421
|
+
* `description`, `example`, `default`, `deprecated`) and usage-slot flags (`optional`, `nullish`,
|
|
422
|
+
* `readOnly`, `writeOnly`). `nullable` is kept because it changes the produced type, and `ref`
|
|
423
|
+
* nodes compare by target name, which also terminates on circular shapes.
|
|
201
424
|
*
|
|
202
|
-
* @example
|
|
425
|
+
* @example Two enums with different descriptions share a signature
|
|
203
426
|
* ```ts
|
|
204
|
-
*
|
|
205
|
-
*
|
|
427
|
+
* signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
|
|
428
|
+
* signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
|
|
206
429
|
* ```
|
|
207
430
|
*/
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const value = record[key];
|
|
215
|
-
if (Array.isArray(value)) {
|
|
216
|
-
for (const item of value) if (isNode(item)) yield item;
|
|
217
|
-
} else if (isNode(value)) yield value;
|
|
218
|
-
}
|
|
431
|
+
function signatureOf(node) {
|
|
432
|
+
const cached = signatureCache.get(node);
|
|
433
|
+
if (cached !== void 0) return cached;
|
|
434
|
+
const signature = hash("sha256", describeShape(node), "hex");
|
|
435
|
+
signatureCache.set(node, signature);
|
|
436
|
+
return signature;
|
|
219
437
|
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/nodes/code.ts
|
|
220
440
|
/**
|
|
221
|
-
*
|
|
222
|
-
* traversable node kinds have an entry. Every other kind resolves to
|
|
223
|
-
* `undefined` and is skipped.
|
|
441
|
+
* Definition for the {@link ConstNode}.
|
|
224
442
|
*/
|
|
225
|
-
const
|
|
226
|
-
Input: "input",
|
|
227
|
-
Output: "output",
|
|
228
|
-
Operation: "operation",
|
|
229
|
-
Schema: "schema",
|
|
230
|
-
Property: "property",
|
|
231
|
-
Parameter: "parameter",
|
|
232
|
-
Response: "response"
|
|
233
|
-
};
|
|
443
|
+
const constDef = defineNode({ kind: "Const" });
|
|
234
444
|
/**
|
|
235
|
-
*
|
|
236
|
-
* context. Returns the callback's result (a replacement node, a collected
|
|
237
|
-
* value, or `undefined` when no callback is registered for the kind).
|
|
445
|
+
* Creates a `ConstNode` representing a TypeScript `const` declaration.
|
|
238
446
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
447
|
+
* @example Exported constant with type and `as const`
|
|
448
|
+
* ```ts
|
|
449
|
+
* createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
|
|
450
|
+
* // export const pets: Pet[] = ... as const
|
|
451
|
+
* ```
|
|
242
452
|
*/
|
|
243
|
-
|
|
244
|
-
const key = VISITOR_KEY_BY_KIND[node.kind];
|
|
245
|
-
if (!key) return void 0;
|
|
246
|
-
const fn = visitor[key];
|
|
247
|
-
return fn?.(node, { parent });
|
|
248
|
-
}
|
|
453
|
+
const createConst = constDef.create;
|
|
249
454
|
/**
|
|
250
|
-
*
|
|
251
|
-
|
|
455
|
+
* Definition for the {@link TypeNode}.
|
|
456
|
+
*/
|
|
457
|
+
const typeDef = defineNode({ kind: "Type" });
|
|
458
|
+
/**
|
|
459
|
+
* Creates a `TypeNode` representing a TypeScript `type` alias declaration.
|
|
252
460
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```ts
|
|
463
|
+
* createType({ name: 'Pet', export: true })
|
|
464
|
+
* // export type Pet = ...
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
const createType = typeDef.create;
|
|
468
|
+
/**
|
|
469
|
+
* Definition for the {@link FunctionNode}.
|
|
470
|
+
*/
|
|
471
|
+
const functionDef = defineNode({ kind: "Function" });
|
|
472
|
+
/**
|
|
473
|
+
* Creates a `FunctionNode` representing a TypeScript `function` declaration.
|
|
256
474
|
*
|
|
257
|
-
* @example
|
|
475
|
+
* @example
|
|
258
476
|
* ```ts
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* console.log(node.operationId)
|
|
262
|
-
* },
|
|
263
|
-
* })
|
|
477
|
+
* createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
|
|
478
|
+
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
264
479
|
* ```
|
|
480
|
+
*/
|
|
481
|
+
const createFunction = functionDef.create;
|
|
482
|
+
/**
|
|
483
|
+
* Definition for the {@link ArrowFunctionNode}.
|
|
484
|
+
*/
|
|
485
|
+
const arrowFunctionDef = defineNode({ kind: "ArrowFunction" });
|
|
486
|
+
/**
|
|
487
|
+
* Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
|
|
265
488
|
*
|
|
266
|
-
* @example
|
|
489
|
+
* @example
|
|
267
490
|
* ```ts
|
|
268
|
-
*
|
|
491
|
+
* createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
|
|
492
|
+
* // export const double = (n: number) => ...
|
|
269
493
|
* ```
|
|
270
494
|
*/
|
|
271
|
-
|
|
272
|
-
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
273
|
-
}
|
|
274
|
-
async function _walk(node, visitor, recurse, limit, parent) {
|
|
275
|
-
await limit(() => applyVisitor(node, visitor, parent));
|
|
276
|
-
const children = Array.from(getChildren(node, recurse));
|
|
277
|
-
if (children.length === 0) return;
|
|
278
|
-
await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
|
|
279
|
-
}
|
|
280
|
-
function transform(node, options) {
|
|
281
|
-
const { depth, parent, ...visitor } = options;
|
|
282
|
-
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
283
|
-
const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
|
|
284
|
-
if (rebuilt === node) return node;
|
|
285
|
-
const finalize = nodeFinalizers[rebuilt.kind];
|
|
286
|
-
return finalize ? finalize(rebuilt) : rebuilt;
|
|
287
|
-
}
|
|
495
|
+
const createArrowFunction = arrowFunctionDef.create;
|
|
288
496
|
/**
|
|
289
|
-
*
|
|
290
|
-
* resync schema optionality against their `required` flag once the schema may
|
|
291
|
-
* have changed.
|
|
497
|
+
* Definition for the {@link TextNode}.
|
|
292
498
|
*/
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
};
|
|
499
|
+
const textDef = defineNode({
|
|
500
|
+
kind: "Text",
|
|
501
|
+
build: (value) => ({ value })
|
|
502
|
+
});
|
|
297
503
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
504
|
+
* Creates a {@link TextNode} representing a raw string fragment in the source output.
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* createText('return fetch(id)')
|
|
509
|
+
* // { kind: 'Text', value: 'return fetch(id)' }
|
|
510
|
+
* ```
|
|
301
511
|
*/
|
|
302
|
-
|
|
303
|
-
if (node.kind === "Schema" && !recurse) return node;
|
|
304
|
-
const keys = visitorKeysByKind[node.kind];
|
|
305
|
-
if (!keys) return node;
|
|
306
|
-
const record = node;
|
|
307
|
-
const childOptions = {
|
|
308
|
-
...options,
|
|
309
|
-
parent: node
|
|
310
|
-
};
|
|
311
|
-
let updates;
|
|
312
|
-
for (const key of keys) {
|
|
313
|
-
if (!(key in record)) continue;
|
|
314
|
-
const value = record[key];
|
|
315
|
-
if (Array.isArray(value)) {
|
|
316
|
-
let changed = false;
|
|
317
|
-
const mapped = value.map((item) => {
|
|
318
|
-
if (!isNode(item)) return item;
|
|
319
|
-
const next = transform(item, childOptions);
|
|
320
|
-
if (next !== item) changed = true;
|
|
321
|
-
return next;
|
|
322
|
-
});
|
|
323
|
-
if (changed) (updates ??= {})[key] = mapped;
|
|
324
|
-
} else if (isNode(value)) {
|
|
325
|
-
const next = transform(value, childOptions);
|
|
326
|
-
if (next !== value) (updates ??= {})[key] = next;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return updates ? {
|
|
330
|
-
...node,
|
|
331
|
-
...updates
|
|
332
|
-
} : node;
|
|
333
|
-
}
|
|
512
|
+
const createText = textDef.create;
|
|
334
513
|
/**
|
|
335
|
-
*
|
|
336
|
-
|
|
514
|
+
* Definition for the {@link BreakNode}.
|
|
515
|
+
*/
|
|
516
|
+
const breakDef = defineNode({
|
|
517
|
+
kind: "Break",
|
|
518
|
+
build: () => ({})
|
|
519
|
+
});
|
|
520
|
+
/**
|
|
521
|
+
* Creates a {@link BreakNode} representing a line break in the source output.
|
|
337
522
|
*
|
|
338
|
-
* @example
|
|
523
|
+
* @example
|
|
339
524
|
* ```ts
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* operation(node) {
|
|
343
|
-
* return node.operationId
|
|
344
|
-
* },
|
|
345
|
-
* })) {
|
|
346
|
-
* ids.push(id)
|
|
347
|
-
* }
|
|
525
|
+
* createBreak()
|
|
526
|
+
* // { kind: 'Break' }
|
|
348
527
|
* ```
|
|
349
528
|
*/
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
353
|
-
const v = applyVisitor(node, visitor, parent);
|
|
354
|
-
if (v != null) yield v;
|
|
355
|
-
for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
|
|
356
|
-
...options,
|
|
357
|
-
parent: node
|
|
358
|
-
});
|
|
529
|
+
function createBreak() {
|
|
530
|
+
return breakDef.create();
|
|
359
531
|
}
|
|
360
532
|
/**
|
|
361
|
-
*
|
|
362
|
-
|
|
533
|
+
* Definition for the {@link JsxNode}.
|
|
534
|
+
*/
|
|
535
|
+
const jsxDef = defineNode({
|
|
536
|
+
kind: "Jsx",
|
|
537
|
+
build: (value) => ({ value })
|
|
538
|
+
});
|
|
539
|
+
/**
|
|
540
|
+
* Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
|
|
363
541
|
*
|
|
364
|
-
* @example
|
|
542
|
+
* @example
|
|
365
543
|
* ```ts
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* return node.operationId
|
|
369
|
-
* },
|
|
370
|
-
* })
|
|
544
|
+
* createJsx('<>\n <a href={href}>Open</a>\n</>')
|
|
545
|
+
* // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
|
|
371
546
|
* ```
|
|
372
547
|
*/
|
|
373
|
-
|
|
374
|
-
return Array.from(collectLazy(node, options));
|
|
375
|
-
}
|
|
548
|
+
const createJsx = jsxDef.create;
|
|
376
549
|
//#endregion
|
|
377
|
-
//#region src/
|
|
378
|
-
const plainStringTypes = new Set([
|
|
379
|
-
"string",
|
|
380
|
-
"uuid",
|
|
381
|
-
"email",
|
|
382
|
-
"url",
|
|
383
|
-
"datetime"
|
|
384
|
-
]);
|
|
550
|
+
//#region src/nodes/content.ts
|
|
385
551
|
/**
|
|
386
|
-
*
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
552
|
+
* Definition for the {@link ContentNode}.
|
|
553
|
+
*/
|
|
554
|
+
const contentDef = defineNode({
|
|
555
|
+
kind: "Content",
|
|
556
|
+
children: ["schema"]
|
|
557
|
+
});
|
|
558
|
+
/**
|
|
559
|
+
* Creates a `ContentNode` for a single request-body or response content type.
|
|
560
|
+
*/
|
|
561
|
+
const createContent = contentDef.create;
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/nodes/file.ts
|
|
564
|
+
/**
|
|
565
|
+
* Definition for the {@link ImportNode}.
|
|
566
|
+
*/
|
|
567
|
+
const importDef = defineNode({ kind: "Import" });
|
|
568
|
+
/**
|
|
569
|
+
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
390
570
|
*
|
|
391
|
-
* @example
|
|
571
|
+
* @example Named import
|
|
392
572
|
* ```ts
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* const merged = syncSchemaRef(ref) // merges with resolved Pet schema
|
|
573
|
+
* createImport({ name: ['useState'], path: 'react' })
|
|
574
|
+
* // import { useState } from 'react'
|
|
396
575
|
* ```
|
|
397
576
|
*/
|
|
398
|
-
|
|
399
|
-
const ref = narrowSchema(node, "ref");
|
|
400
|
-
if (!ref) return node;
|
|
401
|
-
if (!ref.schema) return node;
|
|
402
|
-
const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref;
|
|
403
|
-
const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
|
|
404
|
-
return createSchema({
|
|
405
|
-
...ref.schema,
|
|
406
|
-
...definedOverrides
|
|
407
|
-
});
|
|
408
|
-
}
|
|
577
|
+
const createImport = importDef.create;
|
|
409
578
|
/**
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* Covers `string`, `uuid`, `email`, `url`, and `datetime` types. For `date` and `time`
|
|
413
|
-
* types, returns `true` only when `representation` is `'string'` rather than `'date'`.
|
|
579
|
+
* Definition for the {@link ExportNode}.
|
|
414
580
|
*/
|
|
415
|
-
|
|
416
|
-
if (plainStringTypes.has(node.type)) return true;
|
|
417
|
-
const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
|
|
418
|
-
if (temporal) return temporal.representation !== "date";
|
|
419
|
-
return false;
|
|
420
|
-
}
|
|
581
|
+
const exportDef = defineNode({ kind: "Export" });
|
|
421
582
|
/**
|
|
422
|
-
*
|
|
583
|
+
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
423
584
|
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
585
|
+
* @example Named export
|
|
586
|
+
* ```ts
|
|
587
|
+
* createExport({ name: ['Pet'], path: './Pet' })
|
|
588
|
+
* // export { Pet } from './Pet'
|
|
589
|
+
* ```
|
|
427
590
|
*/
|
|
428
|
-
const
|
|
429
|
-
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
430
|
-
return {
|
|
431
|
-
...param,
|
|
432
|
-
name: transformed
|
|
433
|
-
};
|
|
434
|
-
})));
|
|
435
|
-
function caseParams(params, casing) {
|
|
436
|
-
if (!casing) return params;
|
|
437
|
-
return caseParamsMemo(params)(casing);
|
|
438
|
-
}
|
|
591
|
+
const createExport = exportDef.create;
|
|
439
592
|
/**
|
|
440
|
-
*
|
|
593
|
+
* Definition for the {@link SourceNode}.
|
|
594
|
+
*/
|
|
595
|
+
const sourceDef = defineNode({ kind: "Source" });
|
|
596
|
+
/**
|
|
597
|
+
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
441
598
|
*
|
|
442
599
|
* @example
|
|
443
600
|
* ```ts
|
|
444
|
-
*
|
|
445
|
-
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
601
|
+
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
446
602
|
* ```
|
|
447
603
|
*/
|
|
448
|
-
|
|
449
|
-
return createSchema({
|
|
450
|
-
type: "object",
|
|
451
|
-
primitive: "object",
|
|
452
|
-
properties: [createProperty({
|
|
453
|
-
name: propertyName,
|
|
454
|
-
schema: createSchema({
|
|
455
|
-
type: "enum",
|
|
456
|
-
primitive: "string",
|
|
457
|
-
enumValues: [value]
|
|
458
|
-
}),
|
|
459
|
-
required: true
|
|
460
|
-
})]
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
function resolveParamsType({ node, param, resolver }) {
|
|
464
|
-
if (!resolver) return createParamsType({
|
|
465
|
-
variant: "reference",
|
|
466
|
-
name: param.schema.primitive ?? "unknown"
|
|
467
|
-
});
|
|
468
|
-
const individualName = resolver.resolveParamName(node, param);
|
|
469
|
-
const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
|
|
470
|
-
const groupResolvers = {
|
|
471
|
-
path: resolver.resolvePathParamsName,
|
|
472
|
-
query: resolver.resolveQueryParamsName,
|
|
473
|
-
header: resolver.resolveHeaderParamsName
|
|
474
|
-
};
|
|
475
|
-
const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
|
|
476
|
-
if (groupName && groupName !== individualName) return createParamsType({
|
|
477
|
-
variant: "member",
|
|
478
|
-
base: groupName,
|
|
479
|
-
key: param.name
|
|
480
|
-
});
|
|
481
|
-
return createParamsType({
|
|
482
|
-
variant: "reference",
|
|
483
|
-
name: individualName
|
|
484
|
-
});
|
|
485
|
-
}
|
|
604
|
+
const createSource = sourceDef.create;
|
|
486
605
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* Centralizes parameter grouping logic for all plugins. Provide a `resolver` for type name resolution
|
|
490
|
-
* and `extraParams` for plugin-specific trailing parameters (e.g., `options` objects).
|
|
491
|
-
* Supports three grouping modes: `object` (single destructured param), `inline` (separate params),
|
|
492
|
-
* and `inlineSpread` (rest parameter). Use `CreateOperationParamsOptions` to fine-tune output.
|
|
606
|
+
* Definition for the {@link FileNode}. The fully resolved builder lives in
|
|
607
|
+
* `createFile`, so this definition only supplies the guard.
|
|
493
608
|
*/
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const paramsName = paramNames?.params ?? "params";
|
|
498
|
-
const headersName = paramNames?.headers ?? "headers";
|
|
499
|
-
const pathName = paramNames?.path ?? "pathParams";
|
|
500
|
-
const wrapType = (type) => createParamsType({
|
|
501
|
-
variant: "reference",
|
|
502
|
-
name: typeWrapper ? typeWrapper(type) : type
|
|
503
|
-
});
|
|
504
|
-
const wrapTypeNode = (type) => type.kind === "ParamsType" && type.variant === "reference" ? wrapType(type.name) : type;
|
|
505
|
-
const casedParams = caseParams(node.parameters, paramsCasing);
|
|
506
|
-
const pathParams = casedParams.filter((p) => p.in === "path");
|
|
507
|
-
const queryParams = casedParams.filter((p) => p.in === "query");
|
|
508
|
-
const headerParams = casedParams.filter((p) => p.in === "header");
|
|
509
|
-
const bodyType = node.requestBody?.content?.[0]?.schema ? wrapType(resolver?.resolveDataName(node) ?? "unknown") : void 0;
|
|
510
|
-
const bodyRequired = node.requestBody?.required ?? false;
|
|
511
|
-
const queryGroupType = resolver ? resolveGroupType({
|
|
512
|
-
node,
|
|
513
|
-
params: queryParams,
|
|
514
|
-
groupMethod: resolver.resolveQueryParamsName,
|
|
515
|
-
resolver
|
|
516
|
-
}) : void 0;
|
|
517
|
-
const headerGroupType = resolver ? resolveGroupType({
|
|
518
|
-
node,
|
|
519
|
-
params: headerParams,
|
|
520
|
-
groupMethod: resolver.resolveHeaderParamsName,
|
|
521
|
-
resolver
|
|
522
|
-
}) : void 0;
|
|
523
|
-
const params = [];
|
|
524
|
-
if (paramsType === "object") {
|
|
525
|
-
const children = [
|
|
526
|
-
...pathParams.map((p) => {
|
|
527
|
-
const type = resolveParamsType({
|
|
528
|
-
node,
|
|
529
|
-
param: p,
|
|
530
|
-
resolver
|
|
531
|
-
});
|
|
532
|
-
return createFunctionParameter({
|
|
533
|
-
name: p.name,
|
|
534
|
-
type: wrapTypeNode(type),
|
|
535
|
-
optional: !p.required
|
|
536
|
-
});
|
|
537
|
-
}),
|
|
538
|
-
...bodyType ? [createFunctionParameter({
|
|
539
|
-
name: dataName,
|
|
540
|
-
type: bodyType,
|
|
541
|
-
optional: !bodyRequired
|
|
542
|
-
})] : [],
|
|
543
|
-
...buildGroupParam({
|
|
544
|
-
name: paramsName,
|
|
545
|
-
node,
|
|
546
|
-
params: queryParams,
|
|
547
|
-
groupType: queryGroupType,
|
|
548
|
-
resolver,
|
|
549
|
-
wrapType
|
|
550
|
-
}),
|
|
551
|
-
...buildGroupParam({
|
|
552
|
-
name: headersName,
|
|
553
|
-
node,
|
|
554
|
-
params: headerParams,
|
|
555
|
-
groupType: headerGroupType,
|
|
556
|
-
resolver,
|
|
557
|
-
wrapType
|
|
558
|
-
})
|
|
559
|
-
];
|
|
560
|
-
if (children.length) params.push(createParameterGroup({
|
|
561
|
-
properties: children,
|
|
562
|
-
default: children.every((c) => c.optional) ? "{}" : void 0
|
|
563
|
-
}));
|
|
564
|
-
} else {
|
|
565
|
-
if (pathParams.length) if (pathParamsType === "inlineSpread") {
|
|
566
|
-
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]);
|
|
567
|
-
params.push(createFunctionParameter({
|
|
568
|
-
name: pathName,
|
|
569
|
-
type: spreadType ? wrapType(spreadType) : void 0,
|
|
570
|
-
rest: true
|
|
571
|
-
}));
|
|
572
|
-
} else {
|
|
573
|
-
const pathChildren = pathParams.map((p) => {
|
|
574
|
-
const type = resolveParamsType({
|
|
575
|
-
node,
|
|
576
|
-
param: p,
|
|
577
|
-
resolver
|
|
578
|
-
});
|
|
579
|
-
return createFunctionParameter({
|
|
580
|
-
name: p.name,
|
|
581
|
-
type: wrapTypeNode(type),
|
|
582
|
-
optional: !p.required
|
|
583
|
-
});
|
|
584
|
-
});
|
|
585
|
-
params.push(createParameterGroup({
|
|
586
|
-
properties: pathChildren,
|
|
587
|
-
inline: pathParamsType === "inline",
|
|
588
|
-
default: pathParamsDefault ?? (pathChildren.every((c) => c.optional) ? "{}" : void 0)
|
|
589
|
-
}));
|
|
590
|
-
}
|
|
591
|
-
if (bodyType) params.push(createFunctionParameter({
|
|
592
|
-
name: dataName,
|
|
593
|
-
type: bodyType,
|
|
594
|
-
optional: !bodyRequired
|
|
595
|
-
}));
|
|
596
|
-
params.push(...buildGroupParam({
|
|
597
|
-
name: paramsName,
|
|
598
|
-
node,
|
|
599
|
-
params: queryParams,
|
|
600
|
-
groupType: queryGroupType,
|
|
601
|
-
resolver,
|
|
602
|
-
wrapType
|
|
603
|
-
}));
|
|
604
|
-
params.push(...buildGroupParam({
|
|
605
|
-
name: headersName,
|
|
606
|
-
node,
|
|
607
|
-
params: headerParams,
|
|
608
|
-
groupType: headerGroupType,
|
|
609
|
-
resolver,
|
|
610
|
-
wrapType
|
|
611
|
-
}));
|
|
612
|
-
}
|
|
613
|
-
params.push(...extraParams);
|
|
614
|
-
return createFunctionParameters({ params });
|
|
615
|
-
}
|
|
609
|
+
const fileDef = defineNode({ kind: "File" });
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/nodes/function.ts
|
|
616
612
|
/**
|
|
617
|
-
*
|
|
618
|
-
|
|
613
|
+
* Definition for the {@link TypeLiteralNode}.
|
|
614
|
+
*/
|
|
615
|
+
const typeLiteralDef = defineNode({ kind: "TypeLiteral" });
|
|
616
|
+
/**
|
|
617
|
+
* Creates a {@link TypeLiteralNode} representing an inline anonymous object type.
|
|
619
618
|
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
619
|
+
* @example
|
|
620
|
+
* ```ts
|
|
621
|
+
* createTypeLiteral({ members: [{ name: 'petId', type: 'string', optional: false }] })
|
|
622
|
+
* // { petId: string }
|
|
623
|
+
* ```
|
|
622
624
|
*/
|
|
623
|
-
|
|
624
|
-
if (groupType) return [createFunctionParameter({
|
|
625
|
-
name,
|
|
626
|
-
type: groupType.type.kind === "ParamsType" && groupType.type.variant === "reference" ? wrapType(groupType.type.name) : groupType.type,
|
|
627
|
-
optional: groupType.optional
|
|
628
|
-
})];
|
|
629
|
-
if (params.length) return [createFunctionParameter({
|
|
630
|
-
name,
|
|
631
|
-
type: toStructType({
|
|
632
|
-
node,
|
|
633
|
-
params,
|
|
634
|
-
resolver
|
|
635
|
-
}),
|
|
636
|
-
optional: params.every((p) => !p.required)
|
|
637
|
-
})];
|
|
638
|
-
return [];
|
|
639
|
-
}
|
|
625
|
+
const createTypeLiteral = typeLiteralDef.create;
|
|
640
626
|
/**
|
|
641
|
-
*
|
|
642
|
-
* Returns `null` when the group name equals the individual param name (no real group).
|
|
627
|
+
* Definition for the {@link IndexedAccessTypeNode}.
|
|
643
628
|
*/
|
|
644
|
-
|
|
645
|
-
if (!params.length) return null;
|
|
646
|
-
const firstParam = params[0];
|
|
647
|
-
const groupName = groupMethod.call(resolver, node, firstParam);
|
|
648
|
-
if (groupName === resolver.resolveParamName(node, firstParam)) return null;
|
|
649
|
-
const allOptional = params.every((p) => !p.required);
|
|
650
|
-
return {
|
|
651
|
-
type: createParamsType({
|
|
652
|
-
variant: "reference",
|
|
653
|
-
name: groupName
|
|
654
|
-
}),
|
|
655
|
-
optional: allOptional
|
|
656
|
-
};
|
|
657
|
-
}
|
|
629
|
+
const indexedAccessTypeDef = defineNode({ kind: "IndexedAccessType" });
|
|
658
630
|
/**
|
|
659
|
-
*
|
|
631
|
+
* Creates an {@link IndexedAccessTypeNode} representing a single field accessed from a named type.
|
|
660
632
|
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
variant: "struct",
|
|
667
|
-
properties: params.map((p) => ({
|
|
668
|
-
name: p.name,
|
|
669
|
-
optional: !p.required,
|
|
670
|
-
type: resolveParamsType({
|
|
671
|
-
node,
|
|
672
|
-
param: p,
|
|
673
|
-
resolver
|
|
674
|
-
})
|
|
675
|
-
}))
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
function sourceKey(source) {
|
|
679
|
-
return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
|
|
680
|
-
}
|
|
681
|
-
function pathTypeKey(path, isTypeOnly) {
|
|
682
|
-
return `${path}:${isTypeOnly ?? false}`;
|
|
683
|
-
}
|
|
684
|
-
function exportKey(path, name, isTypeOnly, asAlias) {
|
|
685
|
-
return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
686
|
-
}
|
|
687
|
-
function importKey(path, name, isTypeOnly) {
|
|
688
|
-
return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Computes a multi-level sort key for exports and imports:
|
|
692
|
-
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
633
|
+
* @example
|
|
634
|
+
* ```ts
|
|
635
|
+
* createIndexedAccessType({ objectType: 'DeletePetPathParams', indexType: 'petId' })
|
|
636
|
+
* // DeletePetPathParams['petId']
|
|
637
|
+
* ```
|
|
693
638
|
*/
|
|
694
|
-
|
|
695
|
-
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
696
|
-
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
697
|
-
const hasName = node.name != null ? "1" : "0";
|
|
698
|
-
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
699
|
-
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
700
|
-
}
|
|
639
|
+
const createIndexedAccessType = indexedAccessTypeDef.create;
|
|
701
640
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
* Unnamed sources are deduplicated by object reference. Returns a deduplicated array in original order.
|
|
641
|
+
* Definition for the {@link ObjectBindingPatternNode}.
|
|
705
642
|
*/
|
|
706
|
-
|
|
707
|
-
const seen = /* @__PURE__ */ new Map();
|
|
708
|
-
for (const source of sources) {
|
|
709
|
-
const key = sourceKey(source);
|
|
710
|
-
if (!seen.has(key)) seen.set(key, source);
|
|
711
|
-
}
|
|
712
|
-
return [...seen.values()];
|
|
713
|
-
}
|
|
643
|
+
const objectBindingPatternDef = defineNode({ kind: "ObjectBindingPattern" });
|
|
714
644
|
/**
|
|
715
|
-
*
|
|
645
|
+
* Creates an {@link ObjectBindingPatternNode} for a destructured parameter binding.
|
|
716
646
|
*
|
|
717
|
-
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```ts
|
|
649
|
+
* createObjectBindingPattern({ elements: [{ name: 'id' }, { name: 'name' }] })
|
|
650
|
+
* // { id, name }
|
|
651
|
+
* ```
|
|
718
652
|
*/
|
|
719
|
-
|
|
720
|
-
const merged = new Set(existing);
|
|
721
|
-
for (const name of incoming) merged.add(name);
|
|
722
|
-
return [...merged];
|
|
723
|
-
}
|
|
653
|
+
const createObjectBindingPattern = objectBindingPatternDef.create;
|
|
724
654
|
/**
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
655
|
+
* Definition for the {@link FunctionParameterNode}. `optional` defaults to `false`.
|
|
656
|
+
* Passing `properties` builds a destructured group: an {@link ObjectBindingPatternNode} name
|
|
657
|
+
* paired with a {@link TypeLiteralNode} type.
|
|
729
658
|
*/
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const newItem = {
|
|
748
|
-
...curr,
|
|
749
|
-
name: [...new Set(name)]
|
|
750
|
-
};
|
|
751
|
-
result.push(newItem);
|
|
752
|
-
namedByPath.set(key, newItem);
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
const key = exportKey(path, name, isTypeOnly, asAlias);
|
|
756
|
-
if (!seen.has(key)) {
|
|
757
|
-
result.push(curr);
|
|
758
|
-
seen.add(key);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
659
|
+
const functionParameterDef = defineNode({
|
|
660
|
+
kind: "FunctionParameter",
|
|
661
|
+
build: (input) => {
|
|
662
|
+
if ("properties" in input) return {
|
|
663
|
+
name: createObjectBindingPattern({ elements: input.properties.map((p) => ({ name: p.name })) }),
|
|
664
|
+
type: createTypeLiteral({ members: input.properties.map((p) => ({
|
|
665
|
+
name: p.name,
|
|
666
|
+
type: p.type,
|
|
667
|
+
optional: p.optional ?? false
|
|
668
|
+
})) }),
|
|
669
|
+
optional: input.optional ?? false,
|
|
670
|
+
...input.default !== void 0 ? { default: input.default } : {}
|
|
671
|
+
};
|
|
672
|
+
return {
|
|
673
|
+
optional: false,
|
|
674
|
+
...input
|
|
675
|
+
};
|
|
761
676
|
}
|
|
762
|
-
|
|
763
|
-
}
|
|
677
|
+
});
|
|
764
678
|
/**
|
|
765
|
-
*
|
|
679
|
+
* Creates a `FunctionParameterNode`. `optional` defaults to `false`.
|
|
766
680
|
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
681
|
+
* @example Optional param
|
|
682
|
+
* ```ts
|
|
683
|
+
* createFunctionParameter({ name: 'params', type: 'QueryParams', optional: true })
|
|
684
|
+
* // → params?: QueryParams
|
|
685
|
+
* ```
|
|
769
686
|
*
|
|
770
|
-
* @
|
|
687
|
+
* @example Destructured group
|
|
688
|
+
* ```ts
|
|
689
|
+
* createFunctionParameter({ properties: [{ name: 'id', type: 'string' }, { name: 'name', type: 'string', optional: true }], default: '{}' })
|
|
690
|
+
* // → { id, name }: { id: string; name?: string } = {}
|
|
691
|
+
* ```
|
|
771
692
|
*/
|
|
772
|
-
|
|
773
|
-
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
774
|
-
const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
|
|
775
|
-
const importNameMemo = /* @__PURE__ */ new Map();
|
|
776
|
-
const canonicalizeName = (n) => {
|
|
777
|
-
if (typeof n === "string") return n;
|
|
778
|
-
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
779
|
-
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
780
|
-
return importNameMemo.get(key);
|
|
781
|
-
};
|
|
782
|
-
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
783
|
-
for (const node of imports) {
|
|
784
|
-
if (!Array.isArray(node.name)) continue;
|
|
785
|
-
if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
|
|
786
|
-
}
|
|
787
|
-
const result = [];
|
|
788
|
-
const namedByPath = /* @__PURE__ */ new Map();
|
|
789
|
-
const seen = /* @__PURE__ */ new Set();
|
|
790
|
-
const keyed = imports.map((node) => ({
|
|
791
|
-
node,
|
|
792
|
-
key: sortKey(node)
|
|
793
|
-
}));
|
|
794
|
-
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
795
|
-
for (const { node: curr } of keyed) {
|
|
796
|
-
if (curr.path === curr.root) continue;
|
|
797
|
-
const { path, isTypeOnly } = curr;
|
|
798
|
-
let { name } = curr;
|
|
799
|
-
if (Array.isArray(name)) {
|
|
800
|
-
name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
|
|
801
|
-
if (!name.length) continue;
|
|
802
|
-
const key = pathTypeKey(path, isTypeOnly);
|
|
803
|
-
const existing = namedByPath.get(key);
|
|
804
|
-
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
805
|
-
else {
|
|
806
|
-
const newItem = {
|
|
807
|
-
...curr,
|
|
808
|
-
name
|
|
809
|
-
};
|
|
810
|
-
result.push(newItem);
|
|
811
|
-
namedByPath.set(key, newItem);
|
|
812
|
-
}
|
|
813
|
-
} else {
|
|
814
|
-
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
815
|
-
const key = importKey(path, name, isTypeOnly);
|
|
816
|
-
if (!seen.has(key)) {
|
|
817
|
-
result.push(curr);
|
|
818
|
-
seen.add(key);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
return result;
|
|
823
|
-
}
|
|
693
|
+
const createFunctionParameter = functionParameterDef.create;
|
|
824
694
|
/**
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
* Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
|
|
828
|
-
* and nested node content. Used internally to build the full source string for import filtering.
|
|
695
|
+
* Definition for the {@link FunctionParametersNode}.
|
|
829
696
|
*/
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
if (node.kind === "Text") return node.value;
|
|
835
|
-
if (node.kind === "Break") return "";
|
|
836
|
-
if (node.kind === "Jsx") return node.value;
|
|
837
|
-
const parts = [];
|
|
838
|
-
if ("params" in node && node.params) parts.push(node.params);
|
|
839
|
-
if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
|
|
840
|
-
if ("returnType" in node && node.returnType) parts.push(node.returnType);
|
|
841
|
-
if ("type" in node && typeof node.type === "string") parts.push(node.type);
|
|
842
|
-
const nested = extractStringsFromNodes(node.nodes);
|
|
843
|
-
if (nested) parts.push(nested);
|
|
844
|
-
return parts.join("\n");
|
|
845
|
-
}).filter(Boolean).join("\n");
|
|
846
|
-
}
|
|
697
|
+
const functionParametersDef = defineNode({
|
|
698
|
+
kind: "FunctionParameters",
|
|
699
|
+
defaults: { params: [] }
|
|
700
|
+
});
|
|
847
701
|
/**
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
* Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
|
|
851
|
-
* identifier for type definitions or error messages.
|
|
702
|
+
* Creates a `FunctionParametersNode` from an ordered list of parameters.
|
|
852
703
|
*
|
|
853
704
|
* @example
|
|
854
705
|
* ```ts
|
|
855
|
-
*
|
|
856
|
-
* //
|
|
706
|
+
* const empty = createFunctionParameters()
|
|
707
|
+
* // { kind: 'FunctionParameters', params: [] }
|
|
857
708
|
* ```
|
|
858
709
|
*/
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null;
|
|
862
|
-
return node.name ?? node.schema?.name ?? null;
|
|
710
|
+
function createFunctionParameters(props = {}) {
|
|
711
|
+
return functionParametersDef.create(props);
|
|
863
712
|
}
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/nodes/input.ts
|
|
864
715
|
/**
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
* Refs are followed by name only, the resolved `node.schema` is not traversed inline.
|
|
868
|
-
* Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
|
|
869
|
-
*
|
|
870
|
-
* @example Collect refs from a single schema
|
|
871
|
-
* ```ts
|
|
872
|
-
* const names = collectReferencedSchemaNames(petSchema)
|
|
873
|
-
* // → Set { 'Category', 'Tag' }
|
|
874
|
-
* ```
|
|
875
|
-
*
|
|
876
|
-
* @example Accumulate refs from multiple schemas into one set
|
|
877
|
-
* ```ts
|
|
878
|
-
* const out = new Set<string>()
|
|
879
|
-
* for (const schema of schemas) {
|
|
880
|
-
* collectReferencedSchemaNames(schema, out)
|
|
881
|
-
* }
|
|
882
|
-
* ```
|
|
716
|
+
* Definition for the {@link InputNode}.
|
|
883
717
|
*/
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
718
|
+
const inputDef = defineNode({
|
|
719
|
+
kind: "Input",
|
|
720
|
+
defaults: {
|
|
721
|
+
schemas: [],
|
|
722
|
+
operations: [],
|
|
723
|
+
meta: {
|
|
724
|
+
circularNames: [],
|
|
725
|
+
enumNames: []
|
|
890
726
|
}
|
|
891
|
-
}
|
|
892
|
-
|
|
727
|
+
},
|
|
728
|
+
children: ["schemas", "operations"],
|
|
729
|
+
visitorKey: "input"
|
|
893
730
|
});
|
|
894
|
-
function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
895
|
-
if (!node) return out;
|
|
896
|
-
for (const name of collectSchemaRefs(node)) out.add(name);
|
|
897
|
-
return out;
|
|
898
|
-
}
|
|
899
731
|
/**
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
* An operation uses a schema when any of its parameters, request body content, or responses
|
|
903
|
-
* reference it, directly or indirectly through other named schemas.
|
|
904
|
-
* The walk is iterative and safe against reference cycles.
|
|
905
|
-
*
|
|
906
|
-
* Use this together with `include` filters to determine which schemas from `components/schemas`
|
|
907
|
-
* are reachable from the allowed operations, so that schemas used only by excluded operations
|
|
908
|
-
* are not generated.
|
|
732
|
+
* Creates an `InputNode` with stable defaults for `schemas` and `operations`.
|
|
909
733
|
*
|
|
910
|
-
* @example
|
|
734
|
+
* @example
|
|
911
735
|
* ```ts
|
|
912
|
-
* const
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
* for (const schema of schemas) {
|
|
916
|
-
* if (schema.name && !allowed.has(schema.name)) continue
|
|
917
|
-
* // … generate schema
|
|
918
|
-
* }
|
|
736
|
+
* const input = createInput()
|
|
737
|
+
* // { kind: 'Input', schemas: [], operations: [] }
|
|
919
738
|
* ```
|
|
739
|
+
*/
|
|
740
|
+
function createInput(overrides = {}) {
|
|
741
|
+
return inputDef.create(overrides);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Creates a streaming `InputNode<true>` from pre-built `AsyncIterable` sources.
|
|
920
745
|
*
|
|
921
|
-
* @example
|
|
746
|
+
* @example
|
|
922
747
|
* ```ts
|
|
923
|
-
* const
|
|
924
|
-
* allowed.has('OrderStatus') // false when no included operation references OrderStatus
|
|
748
|
+
* const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
|
|
925
749
|
* ```
|
|
926
750
|
*/
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
for (const name of directRefs) if (!result.has(name)) {
|
|
935
|
-
result.add(name);
|
|
936
|
-
const namedSchema = schemaMap.get(name);
|
|
937
|
-
if (namedSchema) visitSchema(namedSchema);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
for (const op of operations) for (const schema of collectLazy(op, {
|
|
941
|
-
depth: "shallow",
|
|
942
|
-
schema: (node) => node
|
|
943
|
-
})) visitSchema(schema);
|
|
944
|
-
return result;
|
|
945
|
-
}
|
|
946
|
-
function collectUsedSchemaNames(operations, schemas) {
|
|
947
|
-
return collectUsedSchemaNamesMemo(operations)(schemas);
|
|
751
|
+
function createStreamInput(schemas, operations, meta) {
|
|
752
|
+
return {
|
|
753
|
+
kind: "Input",
|
|
754
|
+
schemas,
|
|
755
|
+
operations,
|
|
756
|
+
meta
|
|
757
|
+
};
|
|
948
758
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const graph = /* @__PURE__ */ new Map();
|
|
952
|
-
for (const schema of schemas) {
|
|
953
|
-
if (!schema.name) continue;
|
|
954
|
-
graph.set(schema.name, collectReferencedSchemaNames(schema));
|
|
955
|
-
}
|
|
956
|
-
const circular = /* @__PURE__ */ new Set();
|
|
957
|
-
for (const start of graph.keys()) {
|
|
958
|
-
const visited = /* @__PURE__ */ new Set();
|
|
959
|
-
const stack = [...graph.get(start) ?? []];
|
|
960
|
-
while (stack.length > 0) {
|
|
961
|
-
const node = stack.pop();
|
|
962
|
-
if (node === start) {
|
|
963
|
-
circular.add(start);
|
|
964
|
-
break;
|
|
965
|
-
}
|
|
966
|
-
if (visited.has(node)) continue;
|
|
967
|
-
visited.add(node);
|
|
968
|
-
const next = graph.get(node);
|
|
969
|
-
if (next) for (const r of next) stack.push(r);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
return circular;
|
|
973
|
-
});
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/nodes/requestBody.ts
|
|
974
761
|
/**
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
* Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
|
|
978
|
-
* in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
|
|
979
|
-
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
980
|
-
*
|
|
981
|
-
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
762
|
+
* Definition for the {@link RequestBodyNode}, normalizing each content entry into a `ContentNode`.
|
|
982
763
|
*/
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
764
|
+
const requestBodyDef = defineNode({
|
|
765
|
+
kind: "RequestBody",
|
|
766
|
+
build: (props) => ({
|
|
767
|
+
...props,
|
|
768
|
+
content: props.content?.map(createContent)
|
|
769
|
+
}),
|
|
770
|
+
children: ["content"]
|
|
771
|
+
});
|
|
987
772
|
/**
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
* Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
|
|
991
|
-
* Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
|
|
992
|
-
*
|
|
993
|
-
* @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
|
|
773
|
+
* Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
|
|
994
774
|
*/
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
775
|
+
const createRequestBody = requestBodyDef.create;
|
|
776
|
+
//#endregion
|
|
777
|
+
//#region src/nodes/operation.ts
|
|
778
|
+
/**
|
|
779
|
+
* Definition for the {@link OperationNode}. HTTP operations (those carrying both
|
|
780
|
+
* `method` and `path`) are tagged with `protocol: 'http'`, and the request body is
|
|
781
|
+
* normalized into a `RequestBodyNode`.
|
|
782
|
+
*/
|
|
783
|
+
const operationDef = defineNode({
|
|
784
|
+
kind: "Operation",
|
|
785
|
+
build: (props) => {
|
|
786
|
+
const { requestBody, ...rest } = props;
|
|
787
|
+
const isHttp = rest.method !== void 0 && rest.path !== void 0;
|
|
788
|
+
return {
|
|
789
|
+
tags: [],
|
|
790
|
+
parameters: [],
|
|
791
|
+
responses: [],
|
|
792
|
+
...rest,
|
|
793
|
+
...isHttp ? { protocol: "http" } : {},
|
|
794
|
+
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
795
|
+
};
|
|
796
|
+
},
|
|
797
|
+
children: [
|
|
798
|
+
"parameters",
|
|
799
|
+
"requestBody",
|
|
800
|
+
"responses"
|
|
801
|
+
],
|
|
802
|
+
visitorKey: "operation"
|
|
803
|
+
});
|
|
804
|
+
function createOperation(props) {
|
|
805
|
+
return operationDef.create(props);
|
|
1003
806
|
}
|
|
1004
807
|
//#endregion
|
|
1005
|
-
//#region src/
|
|
808
|
+
//#region src/nodes/output.ts
|
|
1006
809
|
/**
|
|
1007
|
-
*
|
|
1008
|
-
* value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
|
|
1009
|
-
* object properties combine "required" and "nullable" into a single AST.
|
|
1010
|
-
*
|
|
1011
|
-
* - Non-required + non-nullable → `optional: true`.
|
|
1012
|
-
* - Non-required + nullable → `nullish: true`.
|
|
1013
|
-
* - Required → both flags cleared.
|
|
810
|
+
* Definition for the {@link OutputNode}.
|
|
1014
811
|
*/
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
nullish: !required && nullable ? true : void 0
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
812
|
+
const outputDef = defineNode({
|
|
813
|
+
kind: "Output",
|
|
814
|
+
defaults: { files: [] },
|
|
815
|
+
visitorKey: "output"
|
|
816
|
+
});
|
|
1023
817
|
/**
|
|
1024
|
-
*
|
|
1025
|
-
* `changes` already equals (by reference) the current value, otherwise a new node
|
|
1026
|
-
* with the changes applied.
|
|
1027
|
-
*
|
|
1028
|
-
* Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
|
|
1029
|
-
* structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
|
|
1030
|
-
* downstream passes can detect "nothing changed" by identity. Comparison is
|
|
1031
|
-
* shallow: a structurally-equal but newly-allocated array/object counts as a change.
|
|
818
|
+
* Creates an `OutputNode` with a stable default for `files`.
|
|
1032
819
|
*
|
|
1033
820
|
* @example
|
|
1034
821
|
* ```ts
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
822
|
+
* const output = createOutput()
|
|
823
|
+
* // { kind: 'Output', files: [] }
|
|
1037
824
|
* ```
|
|
1038
825
|
*/
|
|
1039
|
-
function
|
|
1040
|
-
|
|
1041
|
-
...node,
|
|
1042
|
-
...changes
|
|
1043
|
-
};
|
|
1044
|
-
return node;
|
|
826
|
+
function createOutput(overrides = {}) {
|
|
827
|
+
return outputDef.create(overrides);
|
|
1045
828
|
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/nodes/parameter.ts
|
|
831
|
+
/**
|
|
832
|
+
* Definition for the {@link ParameterNode}. `required` defaults to `false` and the
|
|
833
|
+
* schema's `optional`/`nullish` flags are kept in sync with it.
|
|
834
|
+
*/
|
|
835
|
+
const parameterDef = defineNode({
|
|
836
|
+
kind: "Parameter",
|
|
837
|
+
build: (props) => {
|
|
838
|
+
const required = props.required ?? false;
|
|
839
|
+
return {
|
|
840
|
+
...props,
|
|
841
|
+
required,
|
|
842
|
+
schema: syncOptionality(props.schema, required)
|
|
843
|
+
};
|
|
844
|
+
},
|
|
845
|
+
children: ["schema"],
|
|
846
|
+
visitorKey: "parameter",
|
|
847
|
+
rebuild: true
|
|
848
|
+
});
|
|
1046
849
|
/**
|
|
1047
|
-
* Creates
|
|
850
|
+
* Creates a `ParameterNode`.
|
|
1048
851
|
*
|
|
1049
852
|
* @example
|
|
1050
853
|
* ```ts
|
|
1051
|
-
* const
|
|
1052
|
-
*
|
|
854
|
+
* const param = createParameter({
|
|
855
|
+
* name: 'petId',
|
|
856
|
+
* in: 'path',
|
|
857
|
+
* required: true,
|
|
858
|
+
* schema: createSchema({ type: 'string' }),
|
|
859
|
+
* })
|
|
1053
860
|
* ```
|
|
861
|
+
*/
|
|
862
|
+
const createParameter = parameterDef.create;
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/nodes/property.ts
|
|
865
|
+
/**
|
|
866
|
+
* Definition for the {@link PropertyNode}. `required` defaults to `false` and the
|
|
867
|
+
* schema's `optional`/`nullish` flags are kept in sync with it.
|
|
868
|
+
*/
|
|
869
|
+
const propertyDef = defineNode({
|
|
870
|
+
kind: "Property",
|
|
871
|
+
build: (props) => {
|
|
872
|
+
const required = props.required ?? false;
|
|
873
|
+
return {
|
|
874
|
+
...props,
|
|
875
|
+
required,
|
|
876
|
+
schema: syncOptionality(props.schema, required)
|
|
877
|
+
};
|
|
878
|
+
},
|
|
879
|
+
children: ["schema"],
|
|
880
|
+
visitorKey: "property",
|
|
881
|
+
rebuild: true
|
|
882
|
+
});
|
|
883
|
+
/**
|
|
884
|
+
* Creates a `PropertyNode`.
|
|
1054
885
|
*
|
|
1055
886
|
* @example
|
|
1056
887
|
* ```ts
|
|
1057
|
-
* const
|
|
1058
|
-
*
|
|
888
|
+
* const property = createProperty({
|
|
889
|
+
* name: 'status',
|
|
890
|
+
* required: true,
|
|
891
|
+
* schema: createSchema({ type: 'string', nullable: true }),
|
|
892
|
+
* })
|
|
893
|
+
* // required=true, no optional/nullish
|
|
1059
894
|
* ```
|
|
1060
895
|
*/
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
896
|
+
const createProperty = propertyDef.create;
|
|
897
|
+
//#endregion
|
|
898
|
+
//#region src/nodes/response.ts
|
|
899
|
+
/**
|
|
900
|
+
* Definition for the {@link ResponseNode}. A single legacy `schema` (with optional
|
|
901
|
+
* `mediaType`/`keysToOmit`) is normalized into one `content` entry.
|
|
902
|
+
*/
|
|
903
|
+
const responseDef = defineNode({
|
|
904
|
+
kind: "Response",
|
|
905
|
+
build: (props) => {
|
|
906
|
+
const { schema, mediaType, keysToOmit, content, ...rest } = props;
|
|
907
|
+
const entries = content ?? (schema ? [{
|
|
908
|
+
contentType: mediaType ?? "application/json",
|
|
909
|
+
schema,
|
|
910
|
+
keysToOmit: keysToOmit ?? null
|
|
911
|
+
}] : void 0);
|
|
912
|
+
return {
|
|
913
|
+
...rest,
|
|
914
|
+
content: entries?.map(createContent)
|
|
915
|
+
};
|
|
916
|
+
},
|
|
917
|
+
children: ["content"],
|
|
918
|
+
visitorKey: "response"
|
|
919
|
+
});
|
|
1073
920
|
/**
|
|
1074
|
-
* Creates a
|
|
921
|
+
* Creates a `ResponseNode`.
|
|
1075
922
|
*
|
|
1076
923
|
* @example
|
|
1077
924
|
* ```ts
|
|
1078
|
-
* const
|
|
925
|
+
* const response = createResponse({
|
|
926
|
+
* statusCode: '200',
|
|
927
|
+
* content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
|
|
928
|
+
* })
|
|
1079
929
|
* ```
|
|
1080
930
|
*/
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
931
|
+
const createResponse = responseDef.create;
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/registry.ts
|
|
934
|
+
/**
|
|
935
|
+
* Every node definition. Adding a node means adding its `defineNode` to one
|
|
936
|
+
* `nodes/*.ts` file and listing it here. The visitor tables below derive from it.
|
|
937
|
+
*/
|
|
938
|
+
const nodeDefs = [
|
|
939
|
+
inputDef,
|
|
940
|
+
outputDef,
|
|
941
|
+
operationDef,
|
|
942
|
+
requestBodyDef,
|
|
943
|
+
contentDef,
|
|
944
|
+
responseDef,
|
|
945
|
+
schemaDef,
|
|
946
|
+
propertyDef,
|
|
947
|
+
parameterDef,
|
|
948
|
+
functionParameterDef,
|
|
949
|
+
functionParametersDef,
|
|
950
|
+
typeLiteralDef,
|
|
951
|
+
indexedAccessTypeDef,
|
|
952
|
+
objectBindingPatternDef,
|
|
953
|
+
constDef,
|
|
954
|
+
typeDef,
|
|
955
|
+
functionDef,
|
|
956
|
+
arrowFunctionDef,
|
|
957
|
+
textDef,
|
|
958
|
+
breakDef,
|
|
959
|
+
jsxDef,
|
|
960
|
+
importDef,
|
|
961
|
+
exportDef,
|
|
962
|
+
sourceDef,
|
|
963
|
+
fileDef
|
|
964
|
+
];
|
|
1089
965
|
/**
|
|
1090
|
-
*
|
|
966
|
+
* Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
|
|
967
|
+
* Derived from each definition's `children`.
|
|
968
|
+
*/
|
|
969
|
+
const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => def.children ? [[def.kind, def.children]] : []));
|
|
970
|
+
/**
|
|
971
|
+
* Maps a node kind to the matching visitor callback name. Derived from each
|
|
972
|
+
* definition's `visitorKey`.
|
|
973
|
+
*/
|
|
974
|
+
const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => def.visitorKey ? [[def.kind, def.visitorKey]] : []));
|
|
975
|
+
/**
|
|
976
|
+
* Per-kind builders rerun after children are rebuilt. Derived from each
|
|
977
|
+
* definition's `rebuild` flag.
|
|
978
|
+
*/
|
|
979
|
+
const nodeRebuilders = Object.fromEntries(nodeDefs.flatMap((def) => def.rebuild ? [[def.kind, def.create]] : []));
|
|
980
|
+
//#endregion
|
|
981
|
+
//#region src/visitor.ts
|
|
982
|
+
/**
|
|
983
|
+
* Creates a small async concurrency limiter.
|
|
1091
984
|
*
|
|
1092
|
-
*
|
|
1093
|
-
* ```ts
|
|
1094
|
-
* const output = createOutput()
|
|
1095
|
-
* // { kind: 'Output', files: [] }
|
|
1096
|
-
* ```
|
|
985
|
+
* At most `concurrency` tasks are in flight at once. Extra tasks are queued.
|
|
1097
986
|
*
|
|
1098
987
|
* @example
|
|
1099
988
|
* ```ts
|
|
1100
|
-
* const
|
|
989
|
+
* const limit = createLimit(2)
|
|
990
|
+
* for (const task of [taskA, taskB, taskC]) {
|
|
991
|
+
* await limit(() => task())
|
|
992
|
+
* }
|
|
993
|
+
* // only 2 tasks run at the same time
|
|
1101
994
|
* ```
|
|
1102
995
|
*/
|
|
1103
|
-
function
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
996
|
+
function createLimit(concurrency) {
|
|
997
|
+
let active = 0;
|
|
998
|
+
const queue = [];
|
|
999
|
+
function next() {
|
|
1000
|
+
if (active < concurrency && queue.length > 0) {
|
|
1001
|
+
active++;
|
|
1002
|
+
queue.shift()();
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return function limit(fn) {
|
|
1006
|
+
return new Promise((resolve, reject) => {
|
|
1007
|
+
queue.push(() => {
|
|
1008
|
+
Promise.resolve(fn()).then(resolve, reject).finally(() => {
|
|
1009
|
+
active--;
|
|
1010
|
+
next();
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
next();
|
|
1014
|
+
});
|
|
1108
1015
|
};
|
|
1109
1016
|
}
|
|
1017
|
+
const visitorKeysByKind = VISITOR_KEYS;
|
|
1110
1018
|
/**
|
|
1111
|
-
*
|
|
1019
|
+
* Returns `true` when `value` is an AST node (an object carrying a `kind`).
|
|
1112
1020
|
*/
|
|
1113
|
-
function
|
|
1114
|
-
return
|
|
1115
|
-
...props,
|
|
1116
|
-
kind: "Content"
|
|
1117
|
-
};
|
|
1021
|
+
function isNode(value) {
|
|
1022
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
1118
1023
|
}
|
|
1119
1024
|
/**
|
|
1120
|
-
*
|
|
1025
|
+
* Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
|
|
1026
|
+
*
|
|
1027
|
+
* `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* ```ts
|
|
1031
|
+
* const children = getChildren(operationNode, true)
|
|
1032
|
+
* // returns parameters, the request body, and responses
|
|
1033
|
+
* ```
|
|
1121
1034
|
*/
|
|
1122
|
-
function
|
|
1123
|
-
return
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
tags: [],
|
|
1134
|
-
parameters: [],
|
|
1135
|
-
responses: [],
|
|
1136
|
-
...rest,
|
|
1137
|
-
...isHttp ? { protocol: "http" } : {},
|
|
1138
|
-
kind: "Operation",
|
|
1139
|
-
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Maps schema `type` to its underlying `primitive`.
|
|
1144
|
-
* Primitive types map to themselves. Special string formats map to `'string'`.
|
|
1145
|
-
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
1146
|
-
*/
|
|
1147
|
-
const TYPE_TO_PRIMITIVE = {
|
|
1148
|
-
string: "string",
|
|
1149
|
-
number: "number",
|
|
1150
|
-
integer: "integer",
|
|
1151
|
-
bigint: "bigint",
|
|
1152
|
-
boolean: "boolean",
|
|
1153
|
-
null: "null",
|
|
1154
|
-
any: "any",
|
|
1155
|
-
unknown: "unknown",
|
|
1156
|
-
void: "void",
|
|
1157
|
-
never: "never",
|
|
1158
|
-
object: "object",
|
|
1159
|
-
array: "array",
|
|
1160
|
-
date: "date",
|
|
1161
|
-
uuid: "string",
|
|
1162
|
-
email: "string",
|
|
1163
|
-
url: "string",
|
|
1164
|
-
datetime: "string",
|
|
1165
|
-
time: "string"
|
|
1166
|
-
};
|
|
1167
|
-
function createSchema(props) {
|
|
1168
|
-
const inferredPrimitive = TYPE_TO_PRIMITIVE[props.type];
|
|
1169
|
-
if (props["type"] === "object") return {
|
|
1170
|
-
properties: [],
|
|
1171
|
-
primitive: "object",
|
|
1172
|
-
...props,
|
|
1173
|
-
kind: "Schema"
|
|
1174
|
-
};
|
|
1175
|
-
return {
|
|
1176
|
-
primitive: inferredPrimitive,
|
|
1177
|
-
...props,
|
|
1178
|
-
kind: "Schema"
|
|
1179
|
-
};
|
|
1035
|
+
function* getChildren(node, recurse) {
|
|
1036
|
+
if (node.kind === "Schema" && !recurse) return;
|
|
1037
|
+
const keys = visitorKeysByKind[node.kind];
|
|
1038
|
+
if (!keys) return;
|
|
1039
|
+
const record = node;
|
|
1040
|
+
for (const key of keys) {
|
|
1041
|
+
const value = record[key];
|
|
1042
|
+
if (Array.isArray(value)) {
|
|
1043
|
+
for (const item of value) if (isNode(item)) yield item;
|
|
1044
|
+
} else if (isNode(value)) yield value;
|
|
1045
|
+
}
|
|
1180
1046
|
}
|
|
1181
1047
|
/**
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
* `
|
|
1185
|
-
* `schema.optional` and `schema.nullish` are derived from `required` and `schema.nullable`.
|
|
1186
|
-
*
|
|
1187
|
-
* @example
|
|
1188
|
-
* ```ts
|
|
1189
|
-
* const property = createProperty({
|
|
1190
|
-
* name: 'status',
|
|
1191
|
-
* schema: createSchema({ type: 'string' }),
|
|
1192
|
-
* })
|
|
1193
|
-
* // required=false, schema.optional=true
|
|
1194
|
-
* ```
|
|
1048
|
+
* Invokes the visitor callback that matches `node.kind`, passing the traversal
|
|
1049
|
+
* context. Returns the callback's result (a replacement node, a collected
|
|
1050
|
+
* value, or `undefined` when no callback is registered for the kind).
|
|
1195
1051
|
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
* name: 'status',
|
|
1200
|
-
* required: true,
|
|
1201
|
-
* schema: createSchema({ type: 'string', nullable: true }),
|
|
1202
|
-
* })
|
|
1203
|
-
* // required=true, no optional/nullish
|
|
1204
|
-
* ```
|
|
1052
|
+
* Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
|
|
1053
|
+
* in one place. `TResult` is the caller's expected return: the same node type
|
|
1054
|
+
* for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
|
|
1205
1055
|
*/
|
|
1206
|
-
function
|
|
1207
|
-
const
|
|
1208
|
-
return
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
required,
|
|
1212
|
-
schema: syncOptionality(props.schema, required)
|
|
1213
|
-
};
|
|
1056
|
+
function applyVisitor(node, visitor, parent) {
|
|
1057
|
+
const key = VISITOR_KEY_BY_KIND[node.kind];
|
|
1058
|
+
if (!key) return void 0;
|
|
1059
|
+
const fn = visitor[key];
|
|
1060
|
+
return fn?.(node, { parent });
|
|
1214
1061
|
}
|
|
1215
1062
|
/**
|
|
1216
|
-
*
|
|
1063
|
+
* Async depth-first traversal for side effects. Visitor return values are
|
|
1064
|
+
* ignored. Use `transform` when you want to rewrite nodes.
|
|
1217
1065
|
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1066
|
+
* Sibling nodes at each depth run concurrently up to `options.concurrency`
|
|
1067
|
+
* (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
|
|
1068
|
+
* work. Lower values reduce memory pressure.
|
|
1220
1069
|
*
|
|
1221
|
-
* @example
|
|
1070
|
+
* @example Log every operation
|
|
1222
1071
|
* ```ts
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* schema: createSchema({ type: 'string' }),
|
|
1072
|
+
* await walk(root, {
|
|
1073
|
+
* operation(node) {
|
|
1074
|
+
* console.log(node.operationId)
|
|
1075
|
+
* },
|
|
1228
1076
|
* })
|
|
1229
1077
|
* ```
|
|
1230
1078
|
*
|
|
1231
|
-
* @example
|
|
1079
|
+
* @example Only visit the root node
|
|
1232
1080
|
* ```ts
|
|
1233
|
-
*
|
|
1234
|
-
* name: 'status',
|
|
1235
|
-
* in: 'query',
|
|
1236
|
-
* schema: createSchema({ type: 'string', nullable: true }),
|
|
1237
|
-
* })
|
|
1238
|
-
* // required=false, schema.nullish=true
|
|
1081
|
+
* await walk(root, { depth: 'shallow', input: () => {} })
|
|
1239
1082
|
* ```
|
|
1240
1083
|
*/
|
|
1241
|
-
function
|
|
1242
|
-
|
|
1243
|
-
return {
|
|
1244
|
-
...props,
|
|
1245
|
-
kind: "Parameter",
|
|
1246
|
-
required,
|
|
1247
|
-
schema: syncOptionality(props.schema, required)
|
|
1248
|
-
};
|
|
1084
|
+
async function walk(node, options) {
|
|
1085
|
+
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
1249
1086
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
* schema is never stored both at the node root and inside `content`.
|
|
1256
|
-
*
|
|
1257
|
-
* @example
|
|
1258
|
-
* ```ts
|
|
1259
|
-
* const response = createResponse({
|
|
1260
|
-
* statusCode: '200',
|
|
1261
|
-
* content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
|
|
1262
|
-
* })
|
|
1263
|
-
* ```
|
|
1264
|
-
*/
|
|
1265
|
-
function createResponse(props) {
|
|
1266
|
-
const { schema, mediaType, keysToOmit, content, ...rest } = props;
|
|
1267
|
-
const entries = content ?? (schema ? [{
|
|
1268
|
-
contentType: mediaType ?? "application/json",
|
|
1269
|
-
schema,
|
|
1270
|
-
keysToOmit: keysToOmit ?? null
|
|
1271
|
-
}] : void 0);
|
|
1272
|
-
return {
|
|
1273
|
-
...rest,
|
|
1274
|
-
kind: "Response",
|
|
1275
|
-
content: entries?.map(createContent)
|
|
1276
|
-
};
|
|
1087
|
+
async function _walk(node, visitor, recurse, limit, parent) {
|
|
1088
|
+
await limit(() => applyVisitor(node, visitor, parent));
|
|
1089
|
+
const children = Array.from(getChildren(node, recurse));
|
|
1090
|
+
if (children.length === 0) return;
|
|
1091
|
+
await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
|
|
1277
1092
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
* createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }) })
|
|
1286
|
-
* // → petId: string
|
|
1287
|
-
* ```
|
|
1288
|
-
*
|
|
1289
|
-
* @example Optional param
|
|
1290
|
-
* ```ts
|
|
1291
|
-
* createFunctionParameter({ name: 'params', type: createParamsType({ variant: 'reference', name: 'QueryParams' }), optional: true })
|
|
1292
|
-
* // → params?: QueryParams
|
|
1293
|
-
* ```
|
|
1294
|
-
*
|
|
1295
|
-
* @example Param with default (implicitly optional. Cannot combine with `optional: true`)
|
|
1296
|
-
* ```ts
|
|
1297
|
-
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
|
|
1298
|
-
* // → config: RequestConfig = {}
|
|
1299
|
-
* ```
|
|
1300
|
-
*/
|
|
1301
|
-
function createFunctionParameter(props) {
|
|
1302
|
-
return {
|
|
1303
|
-
optional: false,
|
|
1304
|
-
...props,
|
|
1305
|
-
kind: "FunctionParameter"
|
|
1306
|
-
};
|
|
1093
|
+
function transform(node, options) {
|
|
1094
|
+
const { depth, parent, ...visitor } = options;
|
|
1095
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
1096
|
+
const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
|
|
1097
|
+
if (rebuilt === node) return node;
|
|
1098
|
+
const rebuild = nodeRebuilders[rebuilt.kind];
|
|
1099
|
+
return rebuild ? rebuild(rebuilt) : rebuilt;
|
|
1307
1100
|
}
|
|
1308
1101
|
/**
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
* named field accessed from a group type. Each language's printer renders the variant
|
|
1313
|
-
* into its own syntax (TypeScript, Python, C#, Kotlin, …).
|
|
1314
|
-
*
|
|
1315
|
-
* @example Reference type (TypeScript: `QueryParams`)
|
|
1316
|
-
* ```ts
|
|
1317
|
-
* createParamsType({ variant: 'reference', name: 'QueryParams' })
|
|
1318
|
-
* ```
|
|
1319
|
-
*
|
|
1320
|
-
* @example Struct type (TypeScript: `{ petId: string }`)
|
|
1321
|
-
* ```ts
|
|
1322
|
-
* createParamsType({ variant: 'struct', properties: [{ name: 'petId', optional: false, type: createParamsType({ variant: 'reference', name: 'string' }) }] })
|
|
1323
|
-
* ```
|
|
1324
|
-
*
|
|
1325
|
-
* @example Member type (TypeScript: `DeletePetPathParams['petId']`)
|
|
1326
|
-
* ```ts
|
|
1327
|
-
* createParamsType({ variant: 'member', base: 'DeletePetPathParams', key: 'petId' })
|
|
1328
|
-
* ```
|
|
1102
|
+
* Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
|
|
1103
|
+
* each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
|
|
1104
|
+
* `Schema` children are skipped in shallow mode.
|
|
1329
1105
|
*/
|
|
1330
|
-
function
|
|
1331
|
-
return
|
|
1332
|
-
|
|
1333
|
-
|
|
1106
|
+
function transformChildren(node, options, recurse) {
|
|
1107
|
+
if (node.kind === "Schema" && !recurse) return node;
|
|
1108
|
+
const keys = visitorKeysByKind[node.kind];
|
|
1109
|
+
if (!keys) return node;
|
|
1110
|
+
const record = node;
|
|
1111
|
+
const childOptions = {
|
|
1112
|
+
...options,
|
|
1113
|
+
parent: node
|
|
1334
1114
|
};
|
|
1115
|
+
let updates;
|
|
1116
|
+
for (const key of keys) {
|
|
1117
|
+
if (!(key in record)) continue;
|
|
1118
|
+
const value = record[key];
|
|
1119
|
+
if (Array.isArray(value)) {
|
|
1120
|
+
let changed = false;
|
|
1121
|
+
const mapped = value.map((item) => {
|
|
1122
|
+
if (!isNode(item)) return item;
|
|
1123
|
+
const next = transform(item, childOptions);
|
|
1124
|
+
if (next !== item) changed = true;
|
|
1125
|
+
return next;
|
|
1126
|
+
});
|
|
1127
|
+
if (changed) (updates ??= {})[key] = mapped;
|
|
1128
|
+
} else if (isNode(value)) {
|
|
1129
|
+
const next = transform(value, childOptions);
|
|
1130
|
+
if (next !== value) (updates ??= {})[key] = next;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return updates ? {
|
|
1134
|
+
...node,
|
|
1135
|
+
...updates
|
|
1136
|
+
} : node;
|
|
1335
1137
|
}
|
|
1336
1138
|
/**
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
* @example Grouped param (TypeScript declaration)
|
|
1340
|
-
* ```ts
|
|
1341
|
-
* createParameterGroup({
|
|
1342
|
-
* properties: [
|
|
1343
|
-
* createFunctionParameter({ name: 'id', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
|
|
1344
|
-
* createFunctionParameter({ name: 'name', type: createParamsType({ variant: 'reference', name: 'string' }), optional: true }),
|
|
1345
|
-
* ],
|
|
1346
|
-
* default: '{}',
|
|
1347
|
-
* })
|
|
1348
|
-
* // declaration → { id, name? }: { id: string; name?: string } = {}
|
|
1349
|
-
* // call → { id, name }
|
|
1350
|
-
* ```
|
|
1139
|
+
* Lazy depth-first collection pass. Yields every non-null value returned by
|
|
1140
|
+
* the visitor callbacks. Use `collect` for the eager array form.
|
|
1351
1141
|
*
|
|
1352
|
-
* @example
|
|
1142
|
+
* @example Collect every operationId
|
|
1353
1143
|
* ```ts
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1144
|
+
* const ids: string[] = []
|
|
1145
|
+
* for (const id of collectLazy<string>(root, {
|
|
1146
|
+
* operation(node) {
|
|
1147
|
+
* return node.operationId
|
|
1148
|
+
* },
|
|
1149
|
+
* })) {
|
|
1150
|
+
* ids.push(id)
|
|
1151
|
+
* }
|
|
1360
1152
|
* ```
|
|
1361
1153
|
*/
|
|
1362
|
-
function
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1154
|
+
function* collectLazy(node, options) {
|
|
1155
|
+
const { depth, parent, ...visitor } = options;
|
|
1156
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
1157
|
+
const v = applyVisitor(node, visitor, parent);
|
|
1158
|
+
if (v != null) yield v;
|
|
1159
|
+
for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
|
|
1160
|
+
...options,
|
|
1161
|
+
parent: node
|
|
1162
|
+
});
|
|
1367
1163
|
}
|
|
1368
1164
|
/**
|
|
1369
|
-
*
|
|
1165
|
+
* Eager depth-first collection pass. Returns an array of every non-null value
|
|
1166
|
+
* the visitor callbacks return.
|
|
1370
1167
|
*
|
|
1371
|
-
* @example
|
|
1168
|
+
* @example Collect every operationId
|
|
1372
1169
|
* ```ts
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
* ],
|
|
1170
|
+
* const ids = collect<string>(root, {
|
|
1171
|
+
* operation(node) {
|
|
1172
|
+
* return node.operationId
|
|
1173
|
+
* },
|
|
1378
1174
|
* })
|
|
1379
1175
|
* ```
|
|
1380
|
-
*
|
|
1381
|
-
* @example
|
|
1382
|
-
* ```ts
|
|
1383
|
-
* const empty = createFunctionParameters()
|
|
1384
|
-
* // { kind: 'FunctionParameters', params: [] }
|
|
1385
|
-
* ```
|
|
1386
1176
|
*/
|
|
1387
|
-
function
|
|
1388
|
-
return
|
|
1389
|
-
params: [],
|
|
1390
|
-
...props,
|
|
1391
|
-
kind: "FunctionParameters"
|
|
1392
|
-
};
|
|
1177
|
+
function collect(node, options) {
|
|
1178
|
+
return Array.from(collectLazy(node, options));
|
|
1393
1179
|
}
|
|
1180
|
+
//#endregion
|
|
1181
|
+
//#region src/dedupe.ts
|
|
1394
1182
|
/**
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
* @example Named import
|
|
1398
|
-
* ```ts
|
|
1399
|
-
* createImport({ name: ['useState'], path: 'react' })
|
|
1400
|
-
* // import { useState } from 'react'
|
|
1401
|
-
* ```
|
|
1402
|
-
*
|
|
1403
|
-
* @example Type-only import
|
|
1404
|
-
* ```ts
|
|
1405
|
-
* createImport({ name: ['FC'], path: 'react', isTypeOnly: true })
|
|
1406
|
-
* // import type { FC } from 'react'
|
|
1407
|
-
* ```
|
|
1183
|
+
* Builds the shared `ref` replacement for a duplicate occurrence, carrying the
|
|
1184
|
+
* usage-slot and documentation fields that are not part of the canonical type.
|
|
1408
1185
|
*/
|
|
1409
|
-
function
|
|
1410
|
-
return {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1186
|
+
function createRefNode(node, canonical) {
|
|
1187
|
+
return createSchema({
|
|
1188
|
+
type: "ref",
|
|
1189
|
+
name: canonical.name,
|
|
1190
|
+
ref: canonical.ref,
|
|
1191
|
+
optional: node.optional,
|
|
1192
|
+
nullish: node.nullish,
|
|
1193
|
+
readOnly: node.readOnly,
|
|
1194
|
+
writeOnly: node.writeOnly,
|
|
1195
|
+
deprecated: node.deprecated,
|
|
1196
|
+
description: node.description,
|
|
1197
|
+
default: node.default,
|
|
1198
|
+
example: node.example
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
function applyDedupe(node, plan, skipRootMatch = false) {
|
|
1202
|
+
const { canonicalBySignature, aliasNames } = plan;
|
|
1203
|
+
if (canonicalBySignature.size === 0 && aliasNames.size === 0) return node;
|
|
1204
|
+
const root = node;
|
|
1205
|
+
return transform(node, { schema(schemaNode) {
|
|
1206
|
+
if (schemaNode.type === "ref") {
|
|
1207
|
+
const target = schemaNode.ref ? extractRefName(schemaNode.ref) : schemaNode.name;
|
|
1208
|
+
const canonical = target ? aliasNames.get(target) : void 0;
|
|
1209
|
+
return canonical ? {
|
|
1210
|
+
...schemaNode,
|
|
1211
|
+
name: canonical.name,
|
|
1212
|
+
ref: canonical.ref
|
|
1213
|
+
} : void 0;
|
|
1214
|
+
}
|
|
1215
|
+
const signature = signatureOf(schemaNode);
|
|
1216
|
+
if (skipRootMatch && schemaNode === root) return void 0;
|
|
1217
|
+
const canonical = canonicalBySignature.get(signature);
|
|
1218
|
+
if (!canonical) return void 0;
|
|
1219
|
+
return createRefNode(schemaNode, canonical);
|
|
1220
|
+
} });
|
|
1221
|
+
}
|
|
1415
1222
|
/**
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
* @example Named export
|
|
1419
|
-
* ```ts
|
|
1420
|
-
* createExport({ name: ['Pet'], path: './Pet' })
|
|
1421
|
-
* // export { Pet } from './Pet'
|
|
1422
|
-
* ```
|
|
1423
|
-
*
|
|
1424
|
-
* @example Wildcard export
|
|
1425
|
-
* ```ts
|
|
1426
|
-
* createExport({ path: './utils' })
|
|
1427
|
-
* // export * from './utils'
|
|
1428
|
-
* ```
|
|
1223
|
+
* Strips usage-slot flags from a hoisted definition and applies its canonical name.
|
|
1224
|
+
* A standalone definition is never optional, so `optional`/`nullish` are cleared.
|
|
1429
1225
|
*/
|
|
1430
|
-
function
|
|
1226
|
+
function cleanDefinition(node, name) {
|
|
1431
1227
|
return {
|
|
1432
|
-
...
|
|
1433
|
-
|
|
1228
|
+
...node,
|
|
1229
|
+
name,
|
|
1230
|
+
optional: void 0,
|
|
1231
|
+
nullish: void 0
|
|
1434
1232
|
};
|
|
1435
1233
|
}
|
|
1436
1234
|
/**
|
|
1437
|
-
*
|
|
1235
|
+
* Scans a forest of schema and operation nodes and produces a {@link DedupePlan}.
|
|
1236
|
+
*
|
|
1237
|
+
* A shape that occurs at least `minOccurrences` times is deduplicated: if any occurrence
|
|
1238
|
+
* is a named top-level schema, the first one becomes the canonical (so other top-level
|
|
1239
|
+
* duplicates and inline copies turn into references to it). Every other top-level name with
|
|
1240
|
+
* the same content is recorded in `aliasNames`, so refs to it can be repointed at the
|
|
1241
|
+
* canonical. Otherwise a new definition is hoisted using `nameFor`. The plan is then applied
|
|
1242
|
+
* per node with {@link applyDedupe}.
|
|
1438
1243
|
*
|
|
1439
1244
|
* @example
|
|
1440
1245
|
* ```ts
|
|
1441
|
-
*
|
|
1246
|
+
* const plan = buildDedupePlan([...schemaNodes, ...operationNodes], {
|
|
1247
|
+
* isCandidate: (node) => node.type === 'enum' || node.type === 'object',
|
|
1248
|
+
* nameFor: (node) => node.name ?? null,
|
|
1249
|
+
* refFor: (name) => `#/components/schemas/${name}`,
|
|
1250
|
+
* })
|
|
1442
1251
|
* ```
|
|
1443
1252
|
*/
|
|
1444
|
-
function
|
|
1253
|
+
function buildDedupePlan(roots, options) {
|
|
1254
|
+
const { isCandidate, nameFor, refFor, minOccurrences = 2 } = options;
|
|
1255
|
+
const topLevelNodes = /* @__PURE__ */ new Set();
|
|
1256
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1257
|
+
function record(schemaNode) {
|
|
1258
|
+
const signature = signatureOf(schemaNode);
|
|
1259
|
+
if (!isCandidate(schemaNode)) return;
|
|
1260
|
+
const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
|
|
1261
|
+
const group = groups.get(signature);
|
|
1262
|
+
if (group) {
|
|
1263
|
+
group.count++;
|
|
1264
|
+
if (isTopLevel) group.topLevelNames.push(schemaNode.name);
|
|
1265
|
+
} else groups.set(signature, {
|
|
1266
|
+
count: 1,
|
|
1267
|
+
representative: schemaNode,
|
|
1268
|
+
topLevelNames: isTopLevel ? [schemaNode.name] : []
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
for (const root of roots) {
|
|
1272
|
+
if (root.kind === "Schema") topLevelNodes.add(root);
|
|
1273
|
+
for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
|
|
1274
|
+
}
|
|
1275
|
+
const canonicalBySignature = /* @__PURE__ */ new Map();
|
|
1276
|
+
const aliasNames = /* @__PURE__ */ new Map();
|
|
1277
|
+
const pendingHoists = [];
|
|
1278
|
+
for (const [signature, group] of groups) {
|
|
1279
|
+
if (group.count < minOccurrences) continue;
|
|
1280
|
+
const [firstName, ...duplicateNames] = group.topLevelNames;
|
|
1281
|
+
if (firstName) {
|
|
1282
|
+
const canonical = {
|
|
1283
|
+
name: firstName,
|
|
1284
|
+
ref: refFor(firstName)
|
|
1285
|
+
};
|
|
1286
|
+
canonicalBySignature.set(signature, canonical);
|
|
1287
|
+
for (const duplicate of duplicateNames) aliasNames.set(duplicate, canonical);
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const name = nameFor(group.representative, signature);
|
|
1291
|
+
if (!name) continue;
|
|
1292
|
+
canonicalBySignature.set(signature, {
|
|
1293
|
+
name,
|
|
1294
|
+
ref: refFor(name)
|
|
1295
|
+
});
|
|
1296
|
+
pendingHoists.push({
|
|
1297
|
+
name,
|
|
1298
|
+
representative: group.representative
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1445
1301
|
return {
|
|
1446
|
-
|
|
1447
|
-
|
|
1302
|
+
canonicalBySignature,
|
|
1303
|
+
aliasNames,
|
|
1304
|
+
hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, {
|
|
1305
|
+
canonicalBySignature,
|
|
1306
|
+
aliasNames
|
|
1307
|
+
}, true), name))
|
|
1448
1308
|
};
|
|
1449
1309
|
}
|
|
1310
|
+
//#endregion
|
|
1311
|
+
//#region src/dialect.ts
|
|
1450
1312
|
/**
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
* Computes:
|
|
1454
|
-
* - `id` SHA256 hash of the file path
|
|
1455
|
-
* - `name` `baseName` without extension
|
|
1456
|
-
* - `extname` extension extracted from `baseName`
|
|
1457
|
-
*
|
|
1458
|
-
* Deduplicates:
|
|
1459
|
-
* - `sources` via `combineSources`
|
|
1460
|
-
* - `exports` via `combineExports`
|
|
1461
|
-
* - `imports` via `combineImports` (also filters unused imports)
|
|
1462
|
-
*
|
|
1463
|
-
* @throws {Error} when `baseName` has no extension.
|
|
1313
|
+
* Types a {@link SchemaDialect} for an adapter. Adds no runtime behavior and only pins the
|
|
1314
|
+
* dialect's type for inference.
|
|
1464
1315
|
*
|
|
1465
1316
|
* @example
|
|
1466
1317
|
* ```ts
|
|
1467
|
-
* const
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1318
|
+
* export const oasDialect = defineSchemaDialect({
|
|
1319
|
+
* name: 'oas',
|
|
1320
|
+
* isNullable,
|
|
1321
|
+
* isReference,
|
|
1322
|
+
* isDiscriminator,
|
|
1323
|
+
* isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
|
|
1324
|
+
* resolveRef,
|
|
1473
1325
|
* })
|
|
1474
|
-
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
1475
|
-
* // file.name = 'petStore'
|
|
1476
|
-
* // file.extname = '.ts'
|
|
1477
1326
|
* ```
|
|
1478
1327
|
*/
|
|
1479
|
-
function
|
|
1480
|
-
|
|
1481
|
-
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
1482
|
-
const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
|
|
1483
|
-
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
1484
|
-
const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
|
|
1485
|
-
const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name) => Boolean(name)));
|
|
1486
|
-
const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
1487
|
-
const resolvedImports = combinedImports.filter((imp) => imp.path !== input.path).flatMap((imp) => {
|
|
1488
|
-
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
1489
|
-
const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
|
|
1490
|
-
if (!kept.length) return [];
|
|
1491
|
-
return [kept.length === imp.name.length ? imp : {
|
|
1492
|
-
...imp,
|
|
1493
|
-
name: kept
|
|
1494
|
-
}];
|
|
1495
|
-
});
|
|
1496
|
-
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
1497
|
-
return {
|
|
1498
|
-
kind: "File",
|
|
1499
|
-
...input,
|
|
1500
|
-
id: hash("sha256", input.path, "hex"),
|
|
1501
|
-
name: trimExtName(input.baseName),
|
|
1502
|
-
extname,
|
|
1503
|
-
imports: resolvedImports,
|
|
1504
|
-
exports: resolvedExports,
|
|
1505
|
-
sources: resolvedSources,
|
|
1506
|
-
meta: input.meta ?? {}
|
|
1507
|
-
};
|
|
1328
|
+
function defineSchemaDialect(dialect) {
|
|
1329
|
+
return dialect;
|
|
1508
1330
|
}
|
|
1331
|
+
//#endregion
|
|
1332
|
+
//#region src/guards.ts
|
|
1509
1333
|
/**
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
* Mirrors the `Const` component from `@kubb/renderer-jsx`.
|
|
1513
|
-
* The component's `children` are represented as `nodes`.
|
|
1514
|
-
*
|
|
1515
|
-
* @example Simple constant
|
|
1516
|
-
* ```ts
|
|
1517
|
-
* createConst({ name: 'pet' })
|
|
1518
|
-
* // const pet = ...
|
|
1519
|
-
* ```
|
|
1520
|
-
*
|
|
1521
|
-
* @example Exported constant with type and `as const`
|
|
1522
|
-
* ```ts
|
|
1523
|
-
* createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
|
|
1524
|
-
* // export const pets: Pet[] = ... as const
|
|
1525
|
-
* ```
|
|
1334
|
+
* Narrows a `SchemaNode` to the variant that matches `type`.
|
|
1526
1335
|
*
|
|
1527
|
-
* @example
|
|
1336
|
+
* @example
|
|
1528
1337
|
* ```ts
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1531
|
-
* export: true,
|
|
1532
|
-
* JSDoc: { comments: ['@description App configuration'] },
|
|
1533
|
-
* nodes: [],
|
|
1534
|
-
* })
|
|
1338
|
+
* const schema = createSchema({ type: 'string' })
|
|
1339
|
+
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
|
|
1535
1340
|
* ```
|
|
1536
1341
|
*/
|
|
1537
|
-
function
|
|
1538
|
-
return
|
|
1539
|
-
...props,
|
|
1540
|
-
kind: "Const"
|
|
1541
|
-
};
|
|
1342
|
+
function narrowSchema(node, type) {
|
|
1343
|
+
return node?.type === type ? node : null;
|
|
1542
1344
|
}
|
|
1543
1345
|
/**
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
* Mirrors the `Type` component from `@kubb/renderer-jsx`.
|
|
1547
|
-
* The component's `children` are represented as `nodes`.
|
|
1548
|
-
*
|
|
1549
|
-
* @example Simple type alias
|
|
1550
|
-
* ```ts
|
|
1551
|
-
* createType({ name: 'Pet' })
|
|
1552
|
-
* // type Pet = ...
|
|
1553
|
-
* ```
|
|
1346
|
+
* Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
|
|
1554
1347
|
*
|
|
1555
|
-
* @example
|
|
1348
|
+
* @example
|
|
1556
1349
|
* ```ts
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1560
|
-
* JSDoc: { comments: ['@description Status of a pet'] },
|
|
1561
|
-
* })
|
|
1562
|
-
* // export type PetStatus = ...
|
|
1350
|
+
* if (isHttpOperationNode(node)) {
|
|
1351
|
+
* console.log(node.method, node.path)
|
|
1352
|
+
* }
|
|
1563
1353
|
* ```
|
|
1564
1354
|
*/
|
|
1565
|
-
function
|
|
1566
|
-
return
|
|
1567
|
-
...props,
|
|
1568
|
-
kind: "Type"
|
|
1569
|
-
};
|
|
1355
|
+
function isHttpOperationNode(node) {
|
|
1356
|
+
return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
|
|
1570
1357
|
}
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/utils/ast.ts
|
|
1360
|
+
const plainStringTypes = new Set([
|
|
1361
|
+
"string",
|
|
1362
|
+
"uuid",
|
|
1363
|
+
"email",
|
|
1364
|
+
"url",
|
|
1365
|
+
"datetime"
|
|
1366
|
+
]);
|
|
1571
1367
|
/**
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
* Mirrors the `Function` component from `@kubb/renderer-jsx`.
|
|
1575
|
-
* The component's `children` are represented as `nodes`.
|
|
1576
|
-
*
|
|
1577
|
-
* @example Simple function
|
|
1578
|
-
* ```ts
|
|
1579
|
-
* createFunction({ name: 'getPet' })
|
|
1580
|
-
* // function getPet() { ... }
|
|
1581
|
-
* ```
|
|
1368
|
+
* Merges a ref node with its resolved schema, giving usage-site fields precedence.
|
|
1582
1369
|
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1585
|
-
* createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
|
|
1586
|
-
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
1587
|
-
* ```
|
|
1370
|
+
* Usage-site fields (`description`, `readOnly`, `nullable`, `deprecated`) on the ref node
|
|
1371
|
+
* override the same fields in the resolved `node.schema`. Non-ref nodes are returned unchanged.
|
|
1588
1372
|
*
|
|
1589
|
-
* @example
|
|
1373
|
+
* @example
|
|
1590
1374
|
* ```ts
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
* generics: ['T'],
|
|
1595
|
-
* params: 'value: T',
|
|
1596
|
-
* returnType: 'T',
|
|
1597
|
-
* })
|
|
1598
|
-
* // export function identity<T>(value: T): T { ... }
|
|
1375
|
+
* // Ref with description override
|
|
1376
|
+
* const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A cute pet' })
|
|
1377
|
+
* const merged = syncSchemaRef(ref) // merges with resolved Pet schema
|
|
1599
1378
|
* ```
|
|
1600
1379
|
*/
|
|
1601
|
-
function
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
};
|
|
1380
|
+
function syncSchemaRef(node) {
|
|
1381
|
+
const ref = narrowSchema(node, "ref");
|
|
1382
|
+
if (!ref) return node;
|
|
1383
|
+
if (!ref.schema) return node;
|
|
1384
|
+
const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref;
|
|
1385
|
+
const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
|
|
1386
|
+
return createSchema({
|
|
1387
|
+
...ref.schema,
|
|
1388
|
+
...definedOverrides
|
|
1389
|
+
});
|
|
1606
1390
|
}
|
|
1607
1391
|
/**
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1610
|
-
* Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
|
|
1611
|
-
* The component's `children` are represented as `nodes`.
|
|
1612
|
-
*
|
|
1613
|
-
* @example Simple arrow function
|
|
1614
|
-
* ```ts
|
|
1615
|
-
* createArrowFunction({ name: 'getPet' })
|
|
1616
|
-
* // const getPet = () => { ... }
|
|
1617
|
-
* ```
|
|
1392
|
+
* Type guard that returns `true` when a schema emits as a plain `string` type.
|
|
1618
1393
|
*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1394
|
+
* Covers `string`, `uuid`, `email`, `url`, and `datetime` types. For `date` and `time`
|
|
1395
|
+
* types, returns `true` only when `representation` is `'string'` rather than `'date'`.
|
|
1396
|
+
*/
|
|
1397
|
+
function isStringType(node) {
|
|
1398
|
+
if (plainStringTypes.has(node.type)) return true;
|
|
1399
|
+
const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
|
|
1400
|
+
if (temporal) return temporal.representation !== "date";
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Applies casing rules to parameter names and returns a new parameter array.
|
|
1624
1405
|
*
|
|
1625
|
-
*
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1628
|
-
* name: 'fetchPet',
|
|
1629
|
-
* export: true,
|
|
1630
|
-
* async: true,
|
|
1631
|
-
* generics: ['T'],
|
|
1632
|
-
* params: 'id: string',
|
|
1633
|
-
* returnType: 'T',
|
|
1634
|
-
* })
|
|
1635
|
-
* // export const fetchPet = async <T>(id: string): Promise<T> => { ... }
|
|
1636
|
-
* ```
|
|
1406
|
+
* Use this before passing parameters to schema builders so output property keys match
|
|
1407
|
+
* the desired casing while preserving `OperationNode.parameters` for other consumers.
|
|
1408
|
+
* The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
|
|
1637
1409
|
*/
|
|
1638
|
-
|
|
1410
|
+
const caseParamsMemo = memoize(/* @__PURE__ */ new WeakMap(), (params) => memoize(/* @__PURE__ */ new Map(), (casing) => params.map((param) => {
|
|
1411
|
+
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
1639
1412
|
return {
|
|
1640
|
-
...
|
|
1641
|
-
|
|
1413
|
+
...param,
|
|
1414
|
+
name: transformed
|
|
1642
1415
|
};
|
|
1416
|
+
})));
|
|
1417
|
+
function caseParams(params, casing) {
|
|
1418
|
+
if (!casing) return params;
|
|
1419
|
+
return caseParamsMemo(params)(casing);
|
|
1643
1420
|
}
|
|
1644
1421
|
/**
|
|
1645
|
-
* Creates a
|
|
1646
|
-
*
|
|
1647
|
-
* Use this instead of bare strings when building `nodes` arrays so that every
|
|
1648
|
-
* entry in the array is a typed {@link CodeNode}.
|
|
1422
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
1649
1423
|
*
|
|
1650
1424
|
* @example
|
|
1651
1425
|
* ```ts
|
|
1652
|
-
*
|
|
1653
|
-
* // {
|
|
1426
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
1427
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
1654
1428
|
* ```
|
|
1655
1429
|
*/
|
|
1656
|
-
function
|
|
1657
|
-
return {
|
|
1658
|
-
|
|
1659
|
-
|
|
1430
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
1431
|
+
return createSchema({
|
|
1432
|
+
type: "object",
|
|
1433
|
+
primitive: "object",
|
|
1434
|
+
properties: [createProperty({
|
|
1435
|
+
name: propertyName,
|
|
1436
|
+
schema: createSchema({
|
|
1437
|
+
type: "enum",
|
|
1438
|
+
primitive: "string",
|
|
1439
|
+
enumValues: [value]
|
|
1440
|
+
}),
|
|
1441
|
+
required: true
|
|
1442
|
+
})]
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Resolves the {@link TypeExpression} for an individual parameter.
|
|
1447
|
+
*
|
|
1448
|
+
* Without a resolver, falls back to the schema primitive (a plain type-name string).
|
|
1449
|
+
* When the parameter belongs to a named group, emits an {@link IndexedAccessTypeNode}
|
|
1450
|
+
* (`GroupParams['petId']`); otherwise the resolved individual name as a plain string.
|
|
1451
|
+
*/
|
|
1452
|
+
function resolveParamType({ node, param, resolver }) {
|
|
1453
|
+
if (!resolver) return param.schema.primitive ?? "unknown";
|
|
1454
|
+
const individualName = resolver.resolveParamName(node, param);
|
|
1455
|
+
const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
|
|
1456
|
+
const groupResolvers = {
|
|
1457
|
+
path: resolver.resolvePathParamsName,
|
|
1458
|
+
query: resolver.resolveQueryParamsName,
|
|
1459
|
+
header: resolver.resolveHeaderParamsName
|
|
1660
1460
|
};
|
|
1461
|
+
const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
|
|
1462
|
+
if (groupName && groupName !== individualName) return createIndexedAccessType({
|
|
1463
|
+
objectType: groupName,
|
|
1464
|
+
indexType: param.name
|
|
1465
|
+
});
|
|
1466
|
+
return individualName;
|
|
1661
1467
|
}
|
|
1662
1468
|
/**
|
|
1663
|
-
*
|
|
1469
|
+
* Converts an `OperationNode` into function parameters for code generation.
|
|
1664
1470
|
*
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1471
|
+
* Centralizes parameter grouping logic for all plugins. `paramsType` chooses between one
|
|
1472
|
+
* destructured object parameter (`object`) and separate top-level parameters (`inline`), while
|
|
1473
|
+
* `pathParamsType` controls how path params render in inline mode. Provide a `resolver` for type
|
|
1474
|
+
* name resolution and `extraParams` for plugin-specific trailing parameters such as an `options` object.
|
|
1475
|
+
*/
|
|
1476
|
+
function createOperationParams(node, options) {
|
|
1477
|
+
const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
|
|
1478
|
+
const dataName = paramNames?.data ?? "data";
|
|
1479
|
+
const paramsName = paramNames?.params ?? "params";
|
|
1480
|
+
const headersName = paramNames?.headers ?? "headers";
|
|
1481
|
+
const pathName = paramNames?.path ?? "pathParams";
|
|
1482
|
+
const wrapType = (type) => typeWrapper ? typeWrapper(type) : type;
|
|
1483
|
+
const wrapTypeExpression = (type) => typeof type === "string" ? wrapType(type) : type;
|
|
1484
|
+
const casedParams = caseParams(node.parameters, paramsCasing);
|
|
1485
|
+
const pathParams = casedParams.filter((p) => p.in === "path");
|
|
1486
|
+
const queryParams = casedParams.filter((p) => p.in === "query");
|
|
1487
|
+
const headerParams = casedParams.filter((p) => p.in === "header");
|
|
1488
|
+
const toProperty = (param) => ({
|
|
1489
|
+
name: param.name,
|
|
1490
|
+
type: wrapTypeExpression(resolveParamType({
|
|
1491
|
+
node,
|
|
1492
|
+
param,
|
|
1493
|
+
resolver
|
|
1494
|
+
})),
|
|
1495
|
+
optional: !param.required
|
|
1496
|
+
});
|
|
1497
|
+
const emptyObjectDefault = (props) => props.every((p) => p.optional) ? "{}" : void 0;
|
|
1498
|
+
const bodyType = node.requestBody?.content?.[0]?.schema ? wrapType(resolver?.resolveDataName(node) ?? "unknown") : void 0;
|
|
1499
|
+
const bodyProperty = bodyType ? [{
|
|
1500
|
+
name: dataName,
|
|
1501
|
+
type: bodyType,
|
|
1502
|
+
optional: !(node.requestBody?.required ?? false)
|
|
1503
|
+
}] : [];
|
|
1504
|
+
const trailingGroups = [{
|
|
1505
|
+
name: paramsName,
|
|
1506
|
+
node,
|
|
1507
|
+
params: queryParams,
|
|
1508
|
+
groupType: resolveGroupType({
|
|
1509
|
+
node,
|
|
1510
|
+
params: queryParams,
|
|
1511
|
+
group: "query",
|
|
1512
|
+
resolver
|
|
1513
|
+
}),
|
|
1514
|
+
resolver,
|
|
1515
|
+
wrapType
|
|
1516
|
+
}, {
|
|
1517
|
+
name: headersName,
|
|
1518
|
+
node,
|
|
1519
|
+
params: headerParams,
|
|
1520
|
+
groupType: resolveGroupType({
|
|
1521
|
+
node,
|
|
1522
|
+
params: headerParams,
|
|
1523
|
+
group: "header",
|
|
1524
|
+
resolver
|
|
1525
|
+
}),
|
|
1526
|
+
resolver,
|
|
1527
|
+
wrapType
|
|
1528
|
+
}];
|
|
1529
|
+
const params = [];
|
|
1530
|
+
if (paramsType === "object") {
|
|
1531
|
+
const children = [
|
|
1532
|
+
...pathParams.map(toProperty),
|
|
1533
|
+
...bodyProperty,
|
|
1534
|
+
...trailingGroups.flatMap(buildGroupProperty)
|
|
1535
|
+
];
|
|
1536
|
+
if (children.length) params.push(createFunctionParameter({
|
|
1537
|
+
properties: children,
|
|
1538
|
+
default: emptyObjectDefault(children)
|
|
1539
|
+
}));
|
|
1540
|
+
} else {
|
|
1541
|
+
if (pathParamsType === "inlineSpread" && pathParams.length) {
|
|
1542
|
+
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]);
|
|
1543
|
+
params.push(createFunctionParameter({
|
|
1544
|
+
name: pathName,
|
|
1545
|
+
type: spreadType ? wrapType(spreadType) : void 0,
|
|
1546
|
+
rest: true
|
|
1547
|
+
}));
|
|
1548
|
+
} else if (pathParamsType === "inline") params.push(...pathParams.map((p) => createFunctionParameter(toProperty(p))));
|
|
1549
|
+
else if (pathParams.length) {
|
|
1550
|
+
const pathChildren = pathParams.map(toProperty);
|
|
1551
|
+
params.push(createFunctionParameter({
|
|
1552
|
+
properties: pathChildren,
|
|
1553
|
+
default: pathParamsDefault ?? emptyObjectDefault(pathChildren)
|
|
1554
|
+
}));
|
|
1555
|
+
}
|
|
1556
|
+
params.push(...bodyProperty.map((p) => createFunctionParameter(p)));
|
|
1557
|
+
params.push(...trailingGroups.flatMap(buildGroupParam));
|
|
1558
|
+
}
|
|
1559
|
+
params.push(...extraParams);
|
|
1560
|
+
return createFunctionParameters({ params });
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Builds the property descriptor for a query or header group.
|
|
1564
|
+
* Returns an empty array when there are no params to emit.
|
|
1667
1565
|
*
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1670
|
-
* createBreak()
|
|
1671
|
-
* // { kind: 'Break' }
|
|
1672
|
-
* ```
|
|
1566
|
+
* A pre-resolved `groupType` emits `name: GroupType`. Otherwise it builds an inline
|
|
1567
|
+
* {@link TypeLiteralNode} from the individual params.
|
|
1673
1568
|
*/
|
|
1674
|
-
function
|
|
1675
|
-
return {
|
|
1569
|
+
function buildGroupProperty({ name, node, params, groupType, resolver, wrapType }) {
|
|
1570
|
+
if (groupType) return [{
|
|
1571
|
+
name,
|
|
1572
|
+
type: typeof groupType.type === "string" ? wrapType(groupType.type) : groupType.type,
|
|
1573
|
+
optional: groupType.optional
|
|
1574
|
+
}];
|
|
1575
|
+
if (params.length) return [{
|
|
1576
|
+
name,
|
|
1577
|
+
type: buildTypeLiteral({
|
|
1578
|
+
node,
|
|
1579
|
+
params,
|
|
1580
|
+
resolver
|
|
1581
|
+
}),
|
|
1582
|
+
optional: params.every((p) => !p.required)
|
|
1583
|
+
}];
|
|
1584
|
+
return [];
|
|
1676
1585
|
}
|
|
1677
1586
|
/**
|
|
1678
|
-
*
|
|
1587
|
+
* Builds a single {@link FunctionParameterNode} for a query or header group.
|
|
1588
|
+
* Returns an empty array when there are no params to emit.
|
|
1679
1589
|
*
|
|
1680
|
-
*
|
|
1590
|
+
* A pre-resolved `groupType` emits `name: GroupType`. Otherwise it builds an inline
|
|
1591
|
+
* {@link TypeLiteralNode} from the individual params.
|
|
1592
|
+
*/
|
|
1593
|
+
function buildGroupParam(args) {
|
|
1594
|
+
return buildGroupProperty(args).map((p) => createFunctionParameter(p));
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Derives a {@link ParamGroupType} for a query or header group from the resolver.
|
|
1681
1598
|
*
|
|
1682
|
-
*
|
|
1683
|
-
*
|
|
1684
|
-
* createJsx('<>\n <a href={href}>Open</a>\n</>')
|
|
1685
|
-
* // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
|
|
1686
|
-
* ```
|
|
1599
|
+
* Returns `null` when there is no resolver, no params, or the group name equals the
|
|
1600
|
+
* individual param name (so there is no real group to emit).
|
|
1687
1601
|
*/
|
|
1688
|
-
function
|
|
1602
|
+
function resolveGroupType({ node, params, group, resolver }) {
|
|
1603
|
+
if (!resolver || !params.length) return null;
|
|
1604
|
+
const firstParam = params[0];
|
|
1605
|
+
const groupName = (group === "query" ? resolver.resolveQueryParamsName : resolver.resolveHeaderParamsName).call(resolver, node, firstParam);
|
|
1606
|
+
if (groupName === resolver.resolveParamName(node, firstParam)) return null;
|
|
1689
1607
|
return {
|
|
1690
|
-
|
|
1691
|
-
|
|
1608
|
+
type: groupName,
|
|
1609
|
+
optional: params.every((p) => !p.required)
|
|
1692
1610
|
};
|
|
1693
1611
|
}
|
|
1694
|
-
//#endregion
|
|
1695
|
-
//#region src/signature.ts
|
|
1696
1612
|
/**
|
|
1697
|
-
*
|
|
1613
|
+
* Builds a {@link TypeLiteralNode} for an inline anonymous type grouping named fields.
|
|
1614
|
+
*
|
|
1615
|
+
* Used when query or header parameters have no dedicated group type name.
|
|
1616
|
+
* Each language printer renders this appropriately (TypeScript: `{ petId: string; name?: string }`).
|
|
1698
1617
|
*/
|
|
1699
|
-
function
|
|
1700
|
-
return
|
|
1618
|
+
function buildTypeLiteral({ node, params, resolver }) {
|
|
1619
|
+
return createTypeLiteral({ members: params.map((p) => ({
|
|
1620
|
+
name: p.name,
|
|
1621
|
+
type: resolveParamType({
|
|
1622
|
+
node,
|
|
1623
|
+
param: p,
|
|
1624
|
+
resolver
|
|
1625
|
+
}),
|
|
1626
|
+
optional: !p.required
|
|
1627
|
+
})) });
|
|
1701
1628
|
}
|
|
1702
|
-
function
|
|
1703
|
-
|
|
1704
|
-
|
|
1629
|
+
function sourceKey(source) {
|
|
1630
|
+
return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
|
|
1631
|
+
}
|
|
1632
|
+
function pathTypeKey(path, isTypeOnly) {
|
|
1633
|
+
return `${path}:${isTypeOnly ?? false}`;
|
|
1634
|
+
}
|
|
1635
|
+
function exportKey(path, name, isTypeOnly, asAlias) {
|
|
1636
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
1637
|
+
}
|
|
1638
|
+
function importKey(path, name, isTypeOnly) {
|
|
1639
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
|
|
1705
1640
|
}
|
|
1706
|
-
const arrayTupleFields = [
|
|
1707
|
-
{
|
|
1708
|
-
kind: "children",
|
|
1709
|
-
key: "items",
|
|
1710
|
-
prefix: "i"
|
|
1711
|
-
},
|
|
1712
|
-
{
|
|
1713
|
-
kind: "child",
|
|
1714
|
-
key: "rest",
|
|
1715
|
-
prefix: "r"
|
|
1716
|
-
},
|
|
1717
|
-
{
|
|
1718
|
-
kind: "scalar",
|
|
1719
|
-
key: "min",
|
|
1720
|
-
prefix: "mn"
|
|
1721
|
-
},
|
|
1722
|
-
{
|
|
1723
|
-
kind: "scalar",
|
|
1724
|
-
key: "max",
|
|
1725
|
-
prefix: "mx"
|
|
1726
|
-
},
|
|
1727
|
-
{
|
|
1728
|
-
kind: "bool",
|
|
1729
|
-
key: "unique",
|
|
1730
|
-
prefix: "u"
|
|
1731
|
-
}
|
|
1732
|
-
];
|
|
1733
|
-
const numericFields = [
|
|
1734
|
-
{
|
|
1735
|
-
kind: "scalar",
|
|
1736
|
-
key: "min",
|
|
1737
|
-
prefix: "mn"
|
|
1738
|
-
},
|
|
1739
|
-
{
|
|
1740
|
-
kind: "scalar",
|
|
1741
|
-
key: "max",
|
|
1742
|
-
prefix: "mx"
|
|
1743
|
-
},
|
|
1744
|
-
{
|
|
1745
|
-
kind: "scalar",
|
|
1746
|
-
key: "exclusiveMinimum",
|
|
1747
|
-
prefix: "emn"
|
|
1748
|
-
},
|
|
1749
|
-
{
|
|
1750
|
-
kind: "scalar",
|
|
1751
|
-
key: "exclusiveMaximum",
|
|
1752
|
-
prefix: "emx"
|
|
1753
|
-
},
|
|
1754
|
-
{
|
|
1755
|
-
kind: "scalar",
|
|
1756
|
-
key: "multipleOf",
|
|
1757
|
-
prefix: "mo"
|
|
1758
|
-
}
|
|
1759
|
-
];
|
|
1760
|
-
const rangeFields = [{
|
|
1761
|
-
kind: "scalar",
|
|
1762
|
-
key: "min",
|
|
1763
|
-
prefix: "mn"
|
|
1764
|
-
}, {
|
|
1765
|
-
kind: "scalar",
|
|
1766
|
-
key: "max",
|
|
1767
|
-
prefix: "mx"
|
|
1768
|
-
}];
|
|
1769
1641
|
/**
|
|
1770
|
-
*
|
|
1771
|
-
* (
|
|
1642
|
+
* Computes a multi-level sort key for exports and imports:
|
|
1643
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
1772
1644
|
*/
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
key
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
prefix: "path"
|
|
1840
|
-
},
|
|
1841
|
-
{
|
|
1842
|
-
kind: "scalar",
|
|
1843
|
-
key: "min",
|
|
1844
|
-
prefix: "mn"
|
|
1845
|
-
},
|
|
1846
|
-
{
|
|
1847
|
-
kind: "scalar",
|
|
1848
|
-
key: "max",
|
|
1849
|
-
prefix: "mx"
|
|
1850
|
-
}
|
|
1851
|
-
],
|
|
1852
|
-
uuid: rangeFields,
|
|
1853
|
-
email: rangeFields,
|
|
1854
|
-
datetime: [{
|
|
1855
|
-
kind: "bool",
|
|
1856
|
-
key: "offset",
|
|
1857
|
-
prefix: "o"
|
|
1858
|
-
}, {
|
|
1859
|
-
kind: "bool",
|
|
1860
|
-
key: "local",
|
|
1861
|
-
prefix: "l"
|
|
1862
|
-
}],
|
|
1863
|
-
date: [{
|
|
1864
|
-
kind: "scalar",
|
|
1865
|
-
key: "representation",
|
|
1866
|
-
prefix: "rep"
|
|
1867
|
-
}],
|
|
1868
|
-
time: [{
|
|
1869
|
-
kind: "scalar",
|
|
1870
|
-
key: "representation",
|
|
1871
|
-
prefix: "rep"
|
|
1872
|
-
}]
|
|
1873
|
-
};
|
|
1874
|
-
function serializeShapeField(field, node, record) {
|
|
1875
|
-
switch (field.kind) {
|
|
1876
|
-
case "scalar": return `${field.prefix}:${record[field.key] ?? ""}`;
|
|
1877
|
-
case "bool": return `${field.prefix}:${record[field.key] ? 1 : 0}`;
|
|
1878
|
-
case "child": {
|
|
1879
|
-
const child = record[field.key];
|
|
1880
|
-
return `${field.prefix}:${child ? signatureOf(child) : ""}`;
|
|
1881
|
-
}
|
|
1882
|
-
case "children": {
|
|
1883
|
-
const children = record[field.key] ?? [];
|
|
1884
|
-
return `${field.prefix}[${children.map((c) => signatureOf(c)).join(",")}]`;
|
|
1645
|
+
function sortKey(node) {
|
|
1646
|
+
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
1647
|
+
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
1648
|
+
const hasName = node.name != null ? "1" : "0";
|
|
1649
|
+
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
1650
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Deduplicates and merges `SourceNode` objects by `name + isExportable + isTypeOnly`.
|
|
1654
|
+
*
|
|
1655
|
+
* Unnamed sources are deduplicated by object reference. Returns a deduplicated array in original order.
|
|
1656
|
+
*/
|
|
1657
|
+
function combineSources(sources) {
|
|
1658
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1659
|
+
for (const source of sources) {
|
|
1660
|
+
const key = sourceKey(source);
|
|
1661
|
+
if (!seen.has(key)) seen.set(key, source);
|
|
1662
|
+
}
|
|
1663
|
+
return [...seen.values()];
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Merges `incoming` names into `existing`, preserving order and dropping duplicates.
|
|
1667
|
+
*
|
|
1668
|
+
* Shared by `combineExports` and `combineImports` for the same-path name-merge case.
|
|
1669
|
+
*/
|
|
1670
|
+
function mergeNameArrays(existing, incoming) {
|
|
1671
|
+
const merged = new Set(existing);
|
|
1672
|
+
for (const name of incoming) merged.add(name);
|
|
1673
|
+
return [...merged];
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
1677
|
+
*
|
|
1678
|
+
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
1679
|
+
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
1680
|
+
*/
|
|
1681
|
+
function combineExports(exports) {
|
|
1682
|
+
const result = [];
|
|
1683
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
1684
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1685
|
+
const keyed = exports.map((node) => ({
|
|
1686
|
+
node,
|
|
1687
|
+
key: sortKey(node)
|
|
1688
|
+
}));
|
|
1689
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
1690
|
+
for (const { node: curr } of keyed) {
|
|
1691
|
+
const { name, path, isTypeOnly, asAlias } = curr;
|
|
1692
|
+
if (Array.isArray(name)) {
|
|
1693
|
+
if (!name.length) continue;
|
|
1694
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
1695
|
+
const existing = namedByPath.get(key);
|
|
1696
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
1697
|
+
else {
|
|
1698
|
+
const newItem = {
|
|
1699
|
+
...curr,
|
|
1700
|
+
name: [...new Set(name)]
|
|
1701
|
+
};
|
|
1702
|
+
result.push(newItem);
|
|
1703
|
+
namedByPath.set(key, newItem);
|
|
1704
|
+
}
|
|
1705
|
+
} else {
|
|
1706
|
+
const key = exportKey(path, name, isTypeOnly, asAlias);
|
|
1707
|
+
if (!seen.has(key)) {
|
|
1708
|
+
result.push(curr);
|
|
1709
|
+
seen.add(key);
|
|
1710
|
+
}
|
|
1885
1711
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1712
|
+
}
|
|
1713
|
+
return result;
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Deduplicates and merges `ImportNode` objects, filtering out unused imports.
|
|
1717
|
+
*
|
|
1718
|
+
* Retains imports that are referenced in `source` or re-exported. Imports with the same path and
|
|
1719
|
+
* `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
|
|
1720
|
+
*
|
|
1721
|
+
* @note Use this when combining imports from multiple files to avoid duplicate declarations.
|
|
1722
|
+
*/
|
|
1723
|
+
function combineImports(imports, exports, source) {
|
|
1724
|
+
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
1725
|
+
const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
|
|
1726
|
+
const importNameMemo = /* @__PURE__ */ new Map();
|
|
1727
|
+
const canonicalizeName = (n) => {
|
|
1728
|
+
if (typeof n === "string") return n;
|
|
1729
|
+
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
1730
|
+
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
1731
|
+
return importNameMemo.get(key);
|
|
1732
|
+
};
|
|
1733
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
1734
|
+
for (const node of imports) {
|
|
1735
|
+
if (!Array.isArray(node.name)) continue;
|
|
1736
|
+
if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
|
|
1737
|
+
}
|
|
1738
|
+
const result = [];
|
|
1739
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
1740
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1741
|
+
const keyed = imports.map((node) => ({
|
|
1742
|
+
node,
|
|
1743
|
+
key: sortKey(node)
|
|
1744
|
+
}));
|
|
1745
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
1746
|
+
for (const { node: curr } of keyed) {
|
|
1747
|
+
if (curr.path === curr.root) continue;
|
|
1748
|
+
const { path, isTypeOnly } = curr;
|
|
1749
|
+
let { name } = curr;
|
|
1750
|
+
if (Array.isArray(name)) {
|
|
1751
|
+
name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
|
|
1752
|
+
if (!name.length) continue;
|
|
1753
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
1754
|
+
const existing = namedByPath.get(key);
|
|
1755
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
1756
|
+
else {
|
|
1757
|
+
const newItem = {
|
|
1758
|
+
...curr,
|
|
1759
|
+
name
|
|
1760
|
+
};
|
|
1761
|
+
result.push(newItem);
|
|
1762
|
+
namedByPath.set(key, newItem);
|
|
1763
|
+
}
|
|
1764
|
+
} else {
|
|
1765
|
+
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
1766
|
+
const key = importKey(path, name, isTypeOnly);
|
|
1767
|
+
if (!seen.has(key)) {
|
|
1768
|
+
result.push(curr);
|
|
1769
|
+
seen.add(key);
|
|
1770
|
+
}
|
|
1892
1771
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1772
|
+
}
|
|
1773
|
+
return result;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Extracts all string content from a `CodeNode` tree recursively.
|
|
1777
|
+
*
|
|
1778
|
+
* Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
|
|
1779
|
+
* and nested node content. Used internally to build the full source string for import filtering.
|
|
1780
|
+
*/
|
|
1781
|
+
function extractStringsFromNodes(nodes) {
|
|
1782
|
+
if (!nodes?.length) return "";
|
|
1783
|
+
return nodes.map((node) => {
|
|
1784
|
+
if (typeof node === "string") return node;
|
|
1785
|
+
if (node.kind === "Text") return node.value;
|
|
1786
|
+
if (node.kind === "Break") return "";
|
|
1787
|
+
if (node.kind === "Jsx") return node.value;
|
|
1788
|
+
const parts = [];
|
|
1789
|
+
if ("params" in node && node.params) parts.push(node.params);
|
|
1790
|
+
if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
|
|
1791
|
+
if ("returnType" in node && node.returnType) parts.push(node.returnType);
|
|
1792
|
+
if ("type" in node && typeof node.type === "string") parts.push(node.type);
|
|
1793
|
+
const nested = extractStringsFromNodes(node.nodes);
|
|
1794
|
+
if (nested) parts.push(nested);
|
|
1795
|
+
return parts.join("\n");
|
|
1796
|
+
}).filter(Boolean).join("\n");
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
|
|
1800
|
+
*
|
|
1801
|
+
* Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
|
|
1802
|
+
* identifier for type definitions or error messages.
|
|
1803
|
+
*
|
|
1804
|
+
* @example
|
|
1805
|
+
* ```ts
|
|
1806
|
+
* resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' })
|
|
1807
|
+
* // => 'Pet'
|
|
1808
|
+
* ```
|
|
1809
|
+
*/
|
|
1810
|
+
function resolveRefName(node) {
|
|
1811
|
+
if (!node || node.type !== "ref") return null;
|
|
1812
|
+
if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null;
|
|
1813
|
+
return node.name ?? node.schema?.name ?? null;
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Collects every named schema referenced (transitively) from a node via ref edges.
|
|
1817
|
+
*
|
|
1818
|
+
* Refs are followed by name only, the resolved `node.schema` is not traversed inline.
|
|
1819
|
+
* Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
|
|
1820
|
+
*
|
|
1821
|
+
* @example Collect refs from a single schema
|
|
1822
|
+
* ```ts
|
|
1823
|
+
* const names = collectReferencedSchemaNames(petSchema)
|
|
1824
|
+
* // → Set { 'Category', 'Tag' }
|
|
1825
|
+
* ```
|
|
1826
|
+
*
|
|
1827
|
+
* @example Accumulate refs from multiple schemas into one set
|
|
1828
|
+
* ```ts
|
|
1829
|
+
* const out = new Set<string>()
|
|
1830
|
+
* for (const schema of schemas) {
|
|
1831
|
+
* collectReferencedSchemaNames(schema, out)
|
|
1832
|
+
* }
|
|
1833
|
+
* ```
|
|
1834
|
+
*/
|
|
1835
|
+
const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
|
|
1836
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1837
|
+
collect(node, { schema(child) {
|
|
1838
|
+
if (child.type === "ref") {
|
|
1839
|
+
const name = resolveRefName(child);
|
|
1840
|
+
if (name) refs.add(name);
|
|
1896
1841
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1842
|
+
} });
|
|
1843
|
+
return refs;
|
|
1844
|
+
});
|
|
1845
|
+
function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
|
|
1846
|
+
if (!node) return out;
|
|
1847
|
+
for (const name of collectSchemaRefs(node)) out.add(name);
|
|
1848
|
+
return out;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Collects the names of all top-level schemas transitively used by a set of operations.
|
|
1852
|
+
*
|
|
1853
|
+
* An operation uses a schema when any of its parameters, request body content, or responses
|
|
1854
|
+
* reference it, directly or indirectly through other named schemas.
|
|
1855
|
+
* The walk is iterative and safe against reference cycles.
|
|
1856
|
+
*
|
|
1857
|
+
* Use this together with `include` filters to determine which schemas from `components/schemas`
|
|
1858
|
+
* are reachable from the allowed operations, so that schemas used only by excluded operations
|
|
1859
|
+
* are not generated.
|
|
1860
|
+
*
|
|
1861
|
+
* @example Only generate schemas referenced by included operations
|
|
1862
|
+
* ```ts
|
|
1863
|
+
* const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
|
|
1864
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1865
|
+
*
|
|
1866
|
+
* for (const schema of schemas) {
|
|
1867
|
+
* if (schema.name && !allowed.has(schema.name)) continue
|
|
1868
|
+
* // … generate schema
|
|
1869
|
+
* }
|
|
1870
|
+
* ```
|
|
1871
|
+
*
|
|
1872
|
+
* @example Check whether a specific schema is needed
|
|
1873
|
+
* ```ts
|
|
1874
|
+
* const allowed = collectUsedSchemaNames(includedOps, schemas)
|
|
1875
|
+
* allowed.has('OrderStatus') // false when no included operation references OrderStatus
|
|
1876
|
+
* ```
|
|
1877
|
+
*/
|
|
1878
|
+
const collectUsedSchemaNamesMemo = memoize(/* @__PURE__ */ new WeakMap(), (ops) => memoize(/* @__PURE__ */ new WeakMap(), (schemas) => computeUsedSchemaNames(ops, schemas)));
|
|
1879
|
+
function computeUsedSchemaNames(operations, schemas) {
|
|
1880
|
+
const schemaMap = /* @__PURE__ */ new Map();
|
|
1881
|
+
for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
|
|
1882
|
+
const result = /* @__PURE__ */ new Set();
|
|
1883
|
+
function visitSchema(schema) {
|
|
1884
|
+
const directRefs = collectReferencedSchemaNames(schema);
|
|
1885
|
+
for (const name of directRefs) if (!result.has(name)) {
|
|
1886
|
+
result.add(name);
|
|
1887
|
+
const namedSchema = schemaMap.get(name);
|
|
1888
|
+
if (namedSchema) visitSchema(namedSchema);
|
|
1903
1889
|
}
|
|
1904
|
-
case "refTarget": return `->${refTargetName(node)}`;
|
|
1905
1890
|
}
|
|
1891
|
+
for (const op of operations) for (const schema of collectLazy(op, {
|
|
1892
|
+
depth: "shallow",
|
|
1893
|
+
schema: (node) => node
|
|
1894
|
+
})) visitSchema(schema);
|
|
1895
|
+
return result;
|
|
1906
1896
|
}
|
|
1897
|
+
function collectUsedSchemaNames(operations, schemas) {
|
|
1898
|
+
return collectUsedSchemaNamesMemo(operations)(schemas);
|
|
1899
|
+
}
|
|
1900
|
+
const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
|
|
1901
|
+
const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
|
|
1902
|
+
const graph = /* @__PURE__ */ new Map();
|
|
1903
|
+
for (const schema of schemas) {
|
|
1904
|
+
if (!schema.name) continue;
|
|
1905
|
+
graph.set(schema.name, collectReferencedSchemaNames(schema));
|
|
1906
|
+
}
|
|
1907
|
+
const circular = /* @__PURE__ */ new Set();
|
|
1908
|
+
for (const start of graph.keys()) {
|
|
1909
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1910
|
+
const stack = [...graph.get(start) ?? []];
|
|
1911
|
+
while (stack.length > 0) {
|
|
1912
|
+
const node = stack.pop();
|
|
1913
|
+
if (node === start) {
|
|
1914
|
+
circular.add(start);
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
if (visited.has(node)) continue;
|
|
1918
|
+
visited.add(node);
|
|
1919
|
+
const next = graph.get(node);
|
|
1920
|
+
if (next) for (const r of next) stack.push(r);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return circular;
|
|
1924
|
+
});
|
|
1907
1925
|
/**
|
|
1908
|
-
*
|
|
1909
|
-
*
|
|
1926
|
+
* Identifies all schemas that participate in circular dependency chains, including direct self-loops.
|
|
1927
|
+
*
|
|
1928
|
+
* Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
|
|
1929
|
+
* in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
|
|
1930
|
+
* Refs are followed by name only, keeping the algorithm linear in the schema graph size.
|
|
1931
|
+
*
|
|
1932
|
+
* @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
|
|
1910
1933
|
*/
|
|
1911
|
-
function
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
if (!fields) return `${node.type}|${flags}`;
|
|
1915
|
-
const record = node;
|
|
1916
|
-
const parts = [`${node.type}|${flags}`];
|
|
1917
|
-
for (const field of fields) parts.push(serializeShapeField(field, node, record));
|
|
1918
|
-
return parts.join("|");
|
|
1934
|
+
function findCircularSchemas(schemas) {
|
|
1935
|
+
if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
|
|
1936
|
+
return findCircularSchemasMemo(schemas);
|
|
1919
1937
|
}
|
|
1920
1938
|
/**
|
|
1921
|
-
*
|
|
1922
|
-
* hashed during dedupe planning is not walked again when it is rewritten during streaming. Reuse
|
|
1923
|
-
* is safe because a signature depends only on content, and nodes are immutable once created.
|
|
1924
|
-
*/
|
|
1925
|
-
const signatureCache = /* @__PURE__ */ new WeakMap();
|
|
1926
|
-
/**
|
|
1927
|
-
* Computes a deterministic, shape-only signature (a content hash) for a schema node. Two schemas
|
|
1928
|
-
* share a signature when they are structurally identical, ignoring documentation (`name`, `title`,
|
|
1929
|
-
* `description`, `example`, `default`, `deprecated`) and usage-slot flags (`optional`, `nullish`,
|
|
1930
|
-
* `readOnly`, `writeOnly`). `nullable` is kept because it changes the produced type, and `ref`
|
|
1931
|
-
* nodes compare by target name, which also terminates on circular shapes.
|
|
1939
|
+
* Type guard returning `true` when a schema or anything nested within it contains a ref to a circular schema.
|
|
1932
1940
|
*
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
* ```
|
|
1941
|
+
* Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
|
|
1942
|
+
* Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
|
|
1943
|
+
*
|
|
1944
|
+
* @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
|
|
1938
1945
|
*/
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1946
|
+
function containsCircularRef(node, { circularSchemas, excludeName }) {
|
|
1947
|
+
if (!node || circularSchemas.size === 0) return false;
|
|
1948
|
+
for (const _ of collectLazy(node, { schema(child) {
|
|
1949
|
+
if (child.type !== "ref") return null;
|
|
1950
|
+
const name = resolveRefName(child);
|
|
1951
|
+
return name && name !== excludeName && circularSchemas.has(name) ? true : null;
|
|
1952
|
+
} })) return true;
|
|
1953
|
+
return false;
|
|
1945
1954
|
}
|
|
1946
1955
|
//#endregion
|
|
1947
|
-
//#region src/
|
|
1948
|
-
/**
|
|
1949
|
-
* Builds the shared `ref` replacement for a duplicate occurrence, carrying the
|
|
1950
|
-
* usage-slot and documentation fields that are not part of the canonical type.
|
|
1951
|
-
*/
|
|
1952
|
-
function createRefNode(node, canonical) {
|
|
1953
|
-
return createSchema({
|
|
1954
|
-
type: "ref",
|
|
1955
|
-
name: canonical.name,
|
|
1956
|
-
ref: canonical.ref,
|
|
1957
|
-
optional: node.optional,
|
|
1958
|
-
nullish: node.nullish,
|
|
1959
|
-
readOnly: node.readOnly,
|
|
1960
|
-
writeOnly: node.writeOnly,
|
|
1961
|
-
deprecated: node.deprecated,
|
|
1962
|
-
description: node.description,
|
|
1963
|
-
default: node.default,
|
|
1964
|
-
example: node.example
|
|
1965
|
-
});
|
|
1966
|
-
}
|
|
1967
|
-
function applyDedupe(node, plan, skipRootMatch = false) {
|
|
1968
|
-
const { canonicalBySignature, aliasNames } = plan;
|
|
1969
|
-
if (canonicalBySignature.size === 0 && aliasNames.size === 0) return node;
|
|
1970
|
-
const root = node;
|
|
1971
|
-
return transform(node, { schema(schemaNode) {
|
|
1972
|
-
if (schemaNode.type === "ref") {
|
|
1973
|
-
const target = schemaNode.ref ? extractRefName(schemaNode.ref) : schemaNode.name;
|
|
1974
|
-
const canonical = target ? aliasNames.get(target) : void 0;
|
|
1975
|
-
return canonical ? {
|
|
1976
|
-
...schemaNode,
|
|
1977
|
-
name: canonical.name,
|
|
1978
|
-
ref: canonical.ref
|
|
1979
|
-
} : void 0;
|
|
1980
|
-
}
|
|
1981
|
-
const signature = signatureOf(schemaNode);
|
|
1982
|
-
if (skipRootMatch && schemaNode === root) return void 0;
|
|
1983
|
-
const canonical = canonicalBySignature.get(signature);
|
|
1984
|
-
if (!canonical) return void 0;
|
|
1985
|
-
return createRefNode(schemaNode, canonical);
|
|
1986
|
-
} });
|
|
1987
|
-
}
|
|
1988
|
-
/**
|
|
1989
|
-
* Strips usage-slot flags from a hoisted definition and applies its canonical name.
|
|
1990
|
-
* A standalone definition is never optional, so `optional`/`nullish` are cleared.
|
|
1991
|
-
*/
|
|
1992
|
-
function cleanDefinition(node, name) {
|
|
1993
|
-
return {
|
|
1994
|
-
...node,
|
|
1995
|
-
name,
|
|
1996
|
-
optional: void 0,
|
|
1997
|
-
nullish: void 0
|
|
1998
|
-
};
|
|
1999
|
-
}
|
|
1956
|
+
//#region src/factory.ts
|
|
2000
1957
|
/**
|
|
2001
|
-
*
|
|
1958
|
+
* Identity-preserving node update: returns `node` unchanged when every field in
|
|
1959
|
+
* `changes` already equals (by reference) the current value, otherwise a new node
|
|
1960
|
+
* with the changes applied.
|
|
2002
1961
|
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
* canonical. Otherwise a new definition is hoisted using `nameFor`. The plan is then applied
|
|
2008
|
-
* per node with {@link applyDedupe}.
|
|
1962
|
+
* Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
|
|
1963
|
+
* structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
|
|
1964
|
+
* downstream passes can detect "nothing changed" by identity. Comparison is
|
|
1965
|
+
* shallow: a structurally-equal but newly-allocated array/object counts as a change.
|
|
2009
1966
|
*
|
|
2010
1967
|
* @example
|
|
2011
1968
|
* ```ts
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
* nameFor: (node) => node.name ?? null,
|
|
2015
|
-
* refFor: (name) => `#/components/schemas/${name}`,
|
|
2016
|
-
* })
|
|
1969
|
+
* update(node, { name: node.name }) // -> same `node` reference
|
|
1970
|
+
* update(node, { name: 'renamed' }) // -> new node, `name` replaced
|
|
2017
1971
|
* ```
|
|
2018
1972
|
*/
|
|
2019
|
-
function
|
|
2020
|
-
const
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
function record(schemaNode) {
|
|
2024
|
-
const signature = signatureOf(schemaNode);
|
|
2025
|
-
if (!isCandidate(schemaNode)) return;
|
|
2026
|
-
const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
|
|
2027
|
-
const group = groups.get(signature);
|
|
2028
|
-
if (group) {
|
|
2029
|
-
group.count++;
|
|
2030
|
-
if (isTopLevel) group.topLevelNames.push(schemaNode.name);
|
|
2031
|
-
} else groups.set(signature, {
|
|
2032
|
-
count: 1,
|
|
2033
|
-
representative: schemaNode,
|
|
2034
|
-
topLevelNames: isTopLevel ? [schemaNode.name] : []
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
for (const root of roots) {
|
|
2038
|
-
if (root.kind === "Schema") topLevelNodes.add(root);
|
|
2039
|
-
for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
|
|
2040
|
-
}
|
|
2041
|
-
const canonicalBySignature = /* @__PURE__ */ new Map();
|
|
2042
|
-
const aliasNames = /* @__PURE__ */ new Map();
|
|
2043
|
-
const pendingHoists = [];
|
|
2044
|
-
for (const [signature, group] of groups) {
|
|
2045
|
-
if (group.count < minOccurrences) continue;
|
|
2046
|
-
const [firstName, ...duplicateNames] = group.topLevelNames;
|
|
2047
|
-
if (firstName) {
|
|
2048
|
-
const canonical = {
|
|
2049
|
-
name: firstName,
|
|
2050
|
-
ref: refFor(firstName)
|
|
2051
|
-
};
|
|
2052
|
-
canonicalBySignature.set(signature, canonical);
|
|
2053
|
-
for (const duplicate of duplicateNames) aliasNames.set(duplicate, canonical);
|
|
2054
|
-
continue;
|
|
2055
|
-
}
|
|
2056
|
-
const name = nameFor(group.representative, signature);
|
|
2057
|
-
if (!name) continue;
|
|
2058
|
-
canonicalBySignature.set(signature, {
|
|
2059
|
-
name,
|
|
2060
|
-
ref: refFor(name)
|
|
2061
|
-
});
|
|
2062
|
-
pendingHoists.push({
|
|
2063
|
-
name,
|
|
2064
|
-
representative: group.representative
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
return {
|
|
2068
|
-
canonicalBySignature,
|
|
2069
|
-
aliasNames,
|
|
2070
|
-
hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, {
|
|
2071
|
-
canonicalBySignature,
|
|
2072
|
-
aliasNames
|
|
2073
|
-
}, true), name))
|
|
1973
|
+
function update(node, changes) {
|
|
1974
|
+
for (const key in changes) if (changes[key] !== node[key]) return {
|
|
1975
|
+
...node,
|
|
1976
|
+
...changes
|
|
2074
1977
|
};
|
|
1978
|
+
return node;
|
|
2075
1979
|
}
|
|
2076
|
-
//#endregion
|
|
2077
|
-
//#region src/dialect.ts
|
|
2078
1980
|
/**
|
|
2079
|
-
*
|
|
2080
|
-
*
|
|
1981
|
+
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1982
|
+
*
|
|
1983
|
+
* Computes:
|
|
1984
|
+
* - `id` SHA256 hash of the file path
|
|
1985
|
+
* - `name` `baseName` without extension
|
|
1986
|
+
* - `extname` extension extracted from `baseName`
|
|
1987
|
+
*
|
|
1988
|
+
* Deduplicates:
|
|
1989
|
+
* - `sources` via `combineSources`
|
|
1990
|
+
* - `exports` via `combineExports`
|
|
1991
|
+
* - `imports` via `combineImports` (also filters unused imports)
|
|
1992
|
+
*
|
|
1993
|
+
* @throws {Error} when `baseName` has no extension.
|
|
2081
1994
|
*
|
|
2082
1995
|
* @example
|
|
2083
1996
|
* ```ts
|
|
2084
|
-
*
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
* resolveRef,
|
|
1997
|
+
* const file = createFile({
|
|
1998
|
+
* baseName: 'petStore.ts',
|
|
1999
|
+
* path: 'src/models/petStore.ts',
|
|
2000
|
+
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
2001
|
+
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
2002
|
+
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
2091
2003
|
* })
|
|
2004
|
+
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
2005
|
+
* // file.name = 'petStore'
|
|
2006
|
+
* // file.extname = '.ts'
|
|
2092
2007
|
* ```
|
|
2093
2008
|
*/
|
|
2094
|
-
function
|
|
2095
|
-
|
|
2009
|
+
function createFile(input) {
|
|
2010
|
+
const extname = path.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
|
|
2011
|
+
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
2012
|
+
const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
|
|
2013
|
+
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
2014
|
+
const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
|
|
2015
|
+
const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name) => Boolean(name)));
|
|
2016
|
+
const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
2017
|
+
const resolvedImports = combinedImports.filter((imp) => imp.path !== input.path).flatMap((imp) => {
|
|
2018
|
+
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
2019
|
+
const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
|
|
2020
|
+
if (!kept.length) return [];
|
|
2021
|
+
return [kept.length === imp.name.length ? imp : {
|
|
2022
|
+
...imp,
|
|
2023
|
+
name: kept
|
|
2024
|
+
}];
|
|
2025
|
+
});
|
|
2026
|
+
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
2027
|
+
return {
|
|
2028
|
+
kind: "File",
|
|
2029
|
+
...input,
|
|
2030
|
+
id: hash("sha256", input.path, "hex"),
|
|
2031
|
+
name: trimExtName(input.baseName),
|
|
2032
|
+
extname,
|
|
2033
|
+
imports: resolvedImports,
|
|
2034
|
+
exports: resolvedExports,
|
|
2035
|
+
sources: resolvedSources,
|
|
2036
|
+
meta: input.meta ?? {}
|
|
2037
|
+
};
|
|
2096
2038
|
}
|
|
2097
2039
|
//#endregion
|
|
2098
2040
|
//#region src/printer.ts
|
|
@@ -2279,6 +2221,6 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
|
2279
2221
|
return propNode;
|
|
2280
2222
|
}
|
|
2281
2223
|
//#endregion
|
|
2282
|
-
export { applyDedupe, buildDedupePlan, caseParams, collect, collectUsedSchemaNames, containsCircularRef, createArrowFunction, createBreak, createConst, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter,
|
|
2224
|
+
export { applyDedupe, arrowFunctionDef, breakDef, buildDedupePlan, caseParams, collect, collectUsedSchemaNames, constDef, containsCircularRef, contentDef, createArrowFunction, createBreak, createConst, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createIndexedAccessType, createInput, createJsx, createObjectBindingPattern, createOperation, createOperationParams, createOutput, createParameter, createPrinterFactory, createProperty, createResponse, createSchema, createSource, createStreamInput, createText, createType, createTypeLiteral, defineNode, definePrinter, defineSchemaDialect, exportDef, extractStringsFromNodes, fileDef, findCircularSchemas, functionDef, functionParameterDef, functionParametersDef, httpMethods, importDef, indexedAccessTypeDef, inputDef, isHttpOperationNode, isStringType, jsxDef, mergeAdjacentObjectsLazy, narrowSchema, objectBindingPatternDef, operationDef, outputDef, parameterDef, propertyDef, requestBodyDef, responseDef, schemaDef, schemaTypes, setDiscriminatorEnum, setEnumName, signatureOf, simplifyUnion, sourceDef, syncOptionality, syncSchemaRef, textDef, transform, typeDef, typeLiteralDef, update, walk };
|
|
2283
2225
|
|
|
2284
2226
|
//# sourceMappingURL=index.js.map
|