@kubb/ast 5.0.0-alpha.5 → 5.0.0-alpha.51
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 +24 -10
- package/dist/index.cjs +1643 -433
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3318 -21
- package/dist/index.js +1585 -423
- package/dist/index.js.map +1 -1
- package/package.json +23 -34
- package/src/constants.ts +126 -5
- package/src/factory.ts +680 -22
- package/src/guards.ts +77 -9
- package/src/index.ts +37 -6
- package/src/infer.ts +130 -0
- package/src/mocks.ts +101 -25
- package/src/nodes/base.ts +44 -4
- package/src/nodes/code.ts +304 -0
- package/src/nodes/file.ts +230 -0
- package/src/nodes/function.ts +223 -0
- package/src/nodes/http.ts +17 -5
- package/src/nodes/index.ts +47 -7
- package/src/nodes/operation.ts +69 -6
- package/src/nodes/output.ts +26 -0
- package/src/nodes/parameter.ts +27 -1
- package/src/nodes/property.ts +23 -1
- package/src/nodes/response.ts +29 -3
- package/src/nodes/root.ts +38 -12
- package/src/nodes/schema.ts +414 -42
- package/src/printer.ts +181 -60
- package/src/refs.ts +39 -7
- package/src/resolvers.ts +45 -0
- package/src/transformers.ts +159 -0
- package/src/types.ts +29 -4
- package/src/utils.ts +703 -10
- package/src/visitor.ts +408 -96
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +0 -2
- package/dist/types.js +0 -1
- package/dist/visitor-CE4-xBHW.d.ts +0 -674
package/dist/index.js
CHANGED
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
import "./chunk--u3MIqq1.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
//#region src/constants.ts
|
|
4
5
|
const visitorDepths = {
|
|
5
6
|
shallow: "shallow",
|
|
6
7
|
deep: "deep"
|
|
7
8
|
};
|
|
8
9
|
const nodeKinds = {
|
|
9
|
-
|
|
10
|
+
input: "Input",
|
|
11
|
+
output: "Output",
|
|
10
12
|
operation: "Operation",
|
|
11
13
|
schema: "Schema",
|
|
12
14
|
property: "Property",
|
|
13
15
|
parameter: "Parameter",
|
|
14
|
-
response: "Response"
|
|
16
|
+
response: "Response",
|
|
17
|
+
functionParameter: "FunctionParameter",
|
|
18
|
+
parameterGroup: "ParameterGroup",
|
|
19
|
+
functionParameters: "FunctionParameters",
|
|
20
|
+
type: "Type",
|
|
21
|
+
file: "File",
|
|
22
|
+
import: "Import",
|
|
23
|
+
export: "Export",
|
|
24
|
+
source: "Source",
|
|
25
|
+
text: "Text",
|
|
26
|
+
break: "Break"
|
|
15
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Canonical schema type strings used by AST schema nodes.
|
|
30
|
+
*
|
|
31
|
+
* These values are used across the AST as stable discriminators
|
|
32
|
+
* (for example `schema.type === schemaTypes.object`).
|
|
33
|
+
*
|
|
34
|
+
* The map is grouped by intent:
|
|
35
|
+
* - primitives (`string`, `number`, `boolean`, ...)
|
|
36
|
+
* - structural/composite (`object`, `array`, `union`, ...)
|
|
37
|
+
* - special OpenAPI-oriented types (`ref`, `datetime`, `uuid`, ...)
|
|
38
|
+
*/
|
|
16
39
|
const schemaTypes = {
|
|
17
40
|
string: "string",
|
|
18
41
|
number: "number",
|
|
@@ -36,9 +59,27 @@ const schemaTypes = {
|
|
|
36
59
|
uuid: "uuid",
|
|
37
60
|
email: "email",
|
|
38
61
|
url: "url",
|
|
62
|
+
ipv4: "ipv4",
|
|
63
|
+
ipv6: "ipv6",
|
|
39
64
|
blob: "blob",
|
|
40
65
|
never: "never"
|
|
41
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Primitive scalar schema types used when simplifying union members.
|
|
69
|
+
*/
|
|
70
|
+
const SCALAR_PRIMITIVE_TYPES = new Set([
|
|
71
|
+
"string",
|
|
72
|
+
"number",
|
|
73
|
+
"integer",
|
|
74
|
+
"bigint",
|
|
75
|
+
"boolean"
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* Returns `true` when `type` is a scalar primitive schema type.
|
|
79
|
+
*/
|
|
80
|
+
function isScalarPrimitive(type) {
|
|
81
|
+
return SCALAR_PRIMITIVE_TYPES.has(type);
|
|
82
|
+
}
|
|
42
83
|
const httpMethods = {
|
|
43
84
|
get: "GET",
|
|
44
85
|
post: "POST",
|
|
@@ -71,20 +112,725 @@ const mediaTypes = {
|
|
|
71
112
|
videoMp4: "video/mp4"
|
|
72
113
|
};
|
|
73
114
|
//#endregion
|
|
115
|
+
//#region ../../internals/utils/src/casing.ts
|
|
116
|
+
/**
|
|
117
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
118
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
119
|
+
* and capitalizes each word according to `pascal`.
|
|
120
|
+
*
|
|
121
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
122
|
+
*/
|
|
123
|
+
function toCamelOrPascal(text, pascal) {
|
|
124
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
125
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
126
|
+
if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
|
|
127
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
128
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Splits `text` on `.` and applies `transformPart` to each segment.
|
|
132
|
+
* The last segment receives `isLast = true`, all earlier segments receive `false`.
|
|
133
|
+
* Segments are joined with `/` to form a file path.
|
|
134
|
+
*
|
|
135
|
+
* Only splits on dots followed by a letter so that version numbers
|
|
136
|
+
* embedded in operationIds (e.g. `v2025.0`) are kept intact.
|
|
137
|
+
*
|
|
138
|
+
* Empty segments are filtered before joining. They arise when the text starts with
|
|
139
|
+
* a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
|
|
140
|
+
* and `'..'` transforms to an empty string). Without this filter the join would produce
|
|
141
|
+
* a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
|
|
142
|
+
* generated files to escape the configured output directory.
|
|
143
|
+
*/
|
|
144
|
+
function applyToFileParts(text, transformPart) {
|
|
145
|
+
const parts = text.split(/\.(?=[a-zA-Z])/);
|
|
146
|
+
return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Converts `text` to camelCase.
|
|
150
|
+
* When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* camelCase('hello-world') // 'helloWorld'
|
|
154
|
+
* camelCase('pet.petId', { isFile: true }) // 'pet/petId'
|
|
155
|
+
*/
|
|
156
|
+
function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
157
|
+
if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
|
|
158
|
+
prefix,
|
|
159
|
+
suffix
|
|
160
|
+
} : {}));
|
|
161
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Converts `text` to PascalCase.
|
|
165
|
+
* When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* pascalCase('hello-world') // 'HelloWorld'
|
|
169
|
+
* pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
|
|
170
|
+
*/
|
|
171
|
+
function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
172
|
+
if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
|
|
173
|
+
prefix,
|
|
174
|
+
suffix
|
|
175
|
+
}) : camelCase(part));
|
|
176
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region ../../internals/utils/src/reserved.ts
|
|
180
|
+
/**
|
|
181
|
+
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* isValidVarName('status') // true
|
|
186
|
+
* isValidVarName('class') // false (reserved word)
|
|
187
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
function isValidVarName(name) {
|
|
191
|
+
try {
|
|
192
|
+
new Function(`var ${name}`);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region ../../internals/utils/src/string.ts
|
|
200
|
+
/**
|
|
201
|
+
* Strips the file extension from a path or file name.
|
|
202
|
+
* Only removes the last `.ext` segment when the dot is not part of a directory name.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* trimExtName('petStore.ts') // 'petStore'
|
|
206
|
+
* trimExtName('/src/models/pet.ts') // '/src/models/pet'
|
|
207
|
+
* trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'
|
|
208
|
+
* trimExtName('noExtension') // 'noExtension'
|
|
209
|
+
*/
|
|
210
|
+
function trimExtName(text) {
|
|
211
|
+
const dotIndex = text.lastIndexOf(".");
|
|
212
|
+
if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/guards.ts
|
|
217
|
+
/**
|
|
218
|
+
* Narrows a `SchemaNode` to the variant that matches `type`.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* const schema = createSchema({ type: 'string' })
|
|
223
|
+
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | undefined
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
function narrowSchema(node, type) {
|
|
227
|
+
return node?.type === type ? node : void 0;
|
|
228
|
+
}
|
|
229
|
+
function isKind(kind) {
|
|
230
|
+
return (node) => node.kind === kind;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Returns `true` when the input is an `InputNode`.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* if (isInputNode(node)) {
|
|
238
|
+
* console.log(node.schemas.length)
|
|
239
|
+
* }
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
const isInputNode = isKind("Input");
|
|
243
|
+
/**
|
|
244
|
+
* Returns `true` when the input is an `OutputNode`.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* if (isOutputNode(node)) {
|
|
249
|
+
* console.log(node.files.length)
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
const isOutputNode = isKind("Output");
|
|
254
|
+
/**
|
|
255
|
+
* Returns `true` when the input is an `OperationNode`.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* if (isOperationNode(node)) {
|
|
260
|
+
* console.log(node.operationId)
|
|
261
|
+
* }
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
const isOperationNode = isKind("Operation");
|
|
265
|
+
/**
|
|
266
|
+
* Returns `true` when the input is a `SchemaNode`.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* if (isSchemaNode(node)) {
|
|
271
|
+
* console.log(node.type)
|
|
272
|
+
* }
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
const isSchemaNode = isKind("Schema");
|
|
276
|
+
isKind("Property");
|
|
277
|
+
isKind("Parameter");
|
|
278
|
+
isKind("Response");
|
|
279
|
+
isKind("FunctionParameter");
|
|
280
|
+
isKind("ParameterGroup");
|
|
281
|
+
isKind("FunctionParameters");
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/utils.ts
|
|
284
|
+
const plainStringTypes = new Set([
|
|
285
|
+
"string",
|
|
286
|
+
"uuid",
|
|
287
|
+
"email",
|
|
288
|
+
"url",
|
|
289
|
+
"datetime"
|
|
290
|
+
]);
|
|
291
|
+
/**
|
|
292
|
+
* Returns a merged schema view for a ref node, combining the resolved `node.schema`
|
|
293
|
+
* (base from the referenced definition) with any usage-site sibling fields set directly
|
|
294
|
+
* on the ref node (description, readOnly, nullable, deprecated, etc.).
|
|
295
|
+
*
|
|
296
|
+
* Usage-site fields take precedence over the resolved schema's own fields when both are defined.
|
|
297
|
+
*
|
|
298
|
+
* For non-ref nodes the node itself is returned unchanged.
|
|
299
|
+
*/
|
|
300
|
+
function syncSchemaRef(node) {
|
|
301
|
+
const ref = narrowSchema(node, "ref");
|
|
302
|
+
if (!ref) return node;
|
|
303
|
+
if (!ref.schema) return node;
|
|
304
|
+
const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref;
|
|
305
|
+
const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
|
|
306
|
+
return createSchema({
|
|
307
|
+
...ref.schema,
|
|
308
|
+
...definedOverrides
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Returns `true` when a schema is emitted as a plain `string` type.
|
|
313
|
+
*
|
|
314
|
+
* - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings.
|
|
315
|
+
* - `date` and `time` are plain strings when their `representation` is `'string'` rather than `'date'`.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```ts
|
|
319
|
+
* isStringType(createSchema({ type: 'uuid' })) // true
|
|
320
|
+
* isStringType(createSchema({ type: 'date', representation: 'date' })) // false
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
function isStringType(node) {
|
|
324
|
+
if (plainStringTypes.has(node.type)) return true;
|
|
325
|
+
const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
|
|
326
|
+
if (temporal) return temporal.representation !== "date";
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Applies casing rules to parameter names and returns a new parameter array.
|
|
331
|
+
*
|
|
332
|
+
* The input array is not mutated.
|
|
333
|
+
* If `casing` is not set, the original array is returned unchanged.
|
|
334
|
+
*
|
|
335
|
+
* Use this before passing parameters to schema builders so that property keys
|
|
336
|
+
* in generated output match the desired casing while preserving
|
|
337
|
+
* `OperationNode.parameters` for other consumers.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* const params = [createParameter({ name: 'pet_id', in: 'query', schema: createSchema({ type: 'string' }) })]
|
|
342
|
+
* const cased = caseParams(params, 'camelcase')
|
|
343
|
+
* // cased[0].name === 'petId'
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
function caseParams(params, casing) {
|
|
347
|
+
if (!casing) return params;
|
|
348
|
+
return params.map((param) => {
|
|
349
|
+
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
350
|
+
return {
|
|
351
|
+
...param,
|
|
352
|
+
name: transformed
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
362
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
366
|
+
return createSchema({
|
|
367
|
+
type: "object",
|
|
368
|
+
primitive: "object",
|
|
369
|
+
properties: [createProperty({
|
|
370
|
+
name: propertyName,
|
|
371
|
+
schema: createSchema({
|
|
372
|
+
type: "enum",
|
|
373
|
+
primitive: "string",
|
|
374
|
+
enumValues: [value]
|
|
375
|
+
}),
|
|
376
|
+
required: true
|
|
377
|
+
})]
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function resolveParamsType({ node, param, resolver }) {
|
|
381
|
+
if (!resolver) return createParamsType({
|
|
382
|
+
variant: "reference",
|
|
383
|
+
name: param.schema.primitive ?? "unknown"
|
|
384
|
+
});
|
|
385
|
+
const individualName = resolver.resolveParamName(node, param);
|
|
386
|
+
const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
|
|
387
|
+
const groupResolvers = {
|
|
388
|
+
path: resolver.resolvePathParamsName,
|
|
389
|
+
query: resolver.resolveQueryParamsName,
|
|
390
|
+
header: resolver.resolveHeaderParamsName
|
|
391
|
+
};
|
|
392
|
+
const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
|
|
393
|
+
if (groupName && groupName !== individualName) return createParamsType({
|
|
394
|
+
variant: "member",
|
|
395
|
+
base: groupName,
|
|
396
|
+
key: param.name
|
|
397
|
+
});
|
|
398
|
+
return createParamsType({
|
|
399
|
+
variant: "reference",
|
|
400
|
+
name: individualName
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Converts an {@link OperationNode} into a {@link FunctionParametersNode}.
|
|
405
|
+
*
|
|
406
|
+
* Centralizes the per-plugin `getParams()` pattern. Provide a `resolver` for
|
|
407
|
+
* type resolution and `extraParams` for plugin-specific trailing parameters.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* const params = createOperationParams(node, {
|
|
412
|
+
* paramsType: 'inline',
|
|
413
|
+
* pathParamsType: 'inline',
|
|
414
|
+
* resolver: tsResolver,
|
|
415
|
+
* extraParams: [createFunctionParameter({ name: 'options', type: createParamsType({ variant: 'reference', name: 'Partial<RequestOptions>' }), default: '{}' })],
|
|
416
|
+
* })
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
function createOperationParams(node, options) {
|
|
420
|
+
const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
|
|
421
|
+
const dataName = paramNames?.data ?? "data";
|
|
422
|
+
const paramsName = paramNames?.params ?? "params";
|
|
423
|
+
const headersName = paramNames?.headers ?? "headers";
|
|
424
|
+
const pathName = paramNames?.path ?? "pathParams";
|
|
425
|
+
const wrapType = (type) => createParamsType({
|
|
426
|
+
variant: "reference",
|
|
427
|
+
name: typeWrapper ? typeWrapper(type) : type
|
|
428
|
+
});
|
|
429
|
+
const wrapTypeNode = (type) => type.kind === "ParamsType" && type.variant === "reference" ? wrapType(type.name) : type;
|
|
430
|
+
const casedParams = caseParams(node.parameters, paramsCasing);
|
|
431
|
+
const pathParams = casedParams.filter((p) => p.in === "path");
|
|
432
|
+
const queryParams = casedParams.filter((p) => p.in === "query");
|
|
433
|
+
const headerParams = casedParams.filter((p) => p.in === "header");
|
|
434
|
+
const bodyType = node.requestBody?.schema ? wrapType(resolver?.resolveDataName(node) ?? "unknown") : void 0;
|
|
435
|
+
const bodyRequired = node.requestBody?.required ?? false;
|
|
436
|
+
const queryGroupType = resolver ? resolveGroupType({
|
|
437
|
+
node,
|
|
438
|
+
params: queryParams,
|
|
439
|
+
groupMethod: resolver.resolveQueryParamsName,
|
|
440
|
+
resolver
|
|
441
|
+
}) : void 0;
|
|
442
|
+
const headerGroupType = resolver ? resolveGroupType({
|
|
443
|
+
node,
|
|
444
|
+
params: headerParams,
|
|
445
|
+
groupMethod: resolver.resolveHeaderParamsName,
|
|
446
|
+
resolver
|
|
447
|
+
}) : void 0;
|
|
448
|
+
const params = [];
|
|
449
|
+
if (paramsType === "object") {
|
|
450
|
+
const children = [
|
|
451
|
+
...pathParams.map((p) => {
|
|
452
|
+
const type = resolveParamsType({
|
|
453
|
+
node,
|
|
454
|
+
param: p,
|
|
455
|
+
resolver
|
|
456
|
+
});
|
|
457
|
+
return createFunctionParameter({
|
|
458
|
+
name: p.name,
|
|
459
|
+
type: wrapTypeNode(type),
|
|
460
|
+
optional: !p.required
|
|
461
|
+
});
|
|
462
|
+
}),
|
|
463
|
+
...bodyType ? [createFunctionParameter({
|
|
464
|
+
name: dataName,
|
|
465
|
+
type: bodyType,
|
|
466
|
+
optional: !bodyRequired
|
|
467
|
+
})] : [],
|
|
468
|
+
...buildGroupParam({
|
|
469
|
+
name: paramsName,
|
|
470
|
+
node,
|
|
471
|
+
params: queryParams,
|
|
472
|
+
groupType: queryGroupType,
|
|
473
|
+
resolver,
|
|
474
|
+
wrapType
|
|
475
|
+
}),
|
|
476
|
+
...buildGroupParam({
|
|
477
|
+
name: headersName,
|
|
478
|
+
node,
|
|
479
|
+
params: headerParams,
|
|
480
|
+
groupType: headerGroupType,
|
|
481
|
+
resolver,
|
|
482
|
+
wrapType
|
|
483
|
+
})
|
|
484
|
+
];
|
|
485
|
+
if (children.length) params.push(createParameterGroup({
|
|
486
|
+
properties: children,
|
|
487
|
+
default: children.every((c) => c.optional) ? "{}" : void 0
|
|
488
|
+
}));
|
|
489
|
+
} else {
|
|
490
|
+
if (pathParams.length) if (pathParamsType === "inlineSpread") {
|
|
491
|
+
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]) ?? void 0;
|
|
492
|
+
params.push(createFunctionParameter({
|
|
493
|
+
name: pathName,
|
|
494
|
+
type: spreadType ? wrapType(spreadType) : void 0,
|
|
495
|
+
rest: true
|
|
496
|
+
}));
|
|
497
|
+
} else {
|
|
498
|
+
const pathChildren = pathParams.map((p) => {
|
|
499
|
+
const type = resolveParamsType({
|
|
500
|
+
node,
|
|
501
|
+
param: p,
|
|
502
|
+
resolver
|
|
503
|
+
});
|
|
504
|
+
return createFunctionParameter({
|
|
505
|
+
name: p.name,
|
|
506
|
+
type: wrapTypeNode(type),
|
|
507
|
+
optional: !p.required
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
params.push(createParameterGroup({
|
|
511
|
+
properties: pathChildren,
|
|
512
|
+
inline: pathParamsType === "inline",
|
|
513
|
+
default: pathParamsDefault ?? (pathChildren.every((c) => c.optional) ? "{}" : void 0)
|
|
514
|
+
}));
|
|
515
|
+
}
|
|
516
|
+
if (bodyType) params.push(createFunctionParameter({
|
|
517
|
+
name: dataName,
|
|
518
|
+
type: bodyType,
|
|
519
|
+
optional: !bodyRequired
|
|
520
|
+
}));
|
|
521
|
+
params.push(...buildGroupParam({
|
|
522
|
+
name: paramsName,
|
|
523
|
+
node,
|
|
524
|
+
params: queryParams,
|
|
525
|
+
groupType: queryGroupType,
|
|
526
|
+
resolver,
|
|
527
|
+
wrapType
|
|
528
|
+
}));
|
|
529
|
+
params.push(...buildGroupParam({
|
|
530
|
+
name: headersName,
|
|
531
|
+
node,
|
|
532
|
+
params: headerParams,
|
|
533
|
+
groupType: headerGroupType,
|
|
534
|
+
resolver,
|
|
535
|
+
wrapType
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
params.push(...extraParams);
|
|
539
|
+
return createFunctionParameters({ params });
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Builds a single {@link FunctionParameterNode} for a query or header group.
|
|
543
|
+
* Returns an empty array when there are no params to emit.
|
|
544
|
+
*
|
|
545
|
+
* If a pre-resolved `groupType` is provided it emits `name: GroupType`.
|
|
546
|
+
* Otherwise, it builds an inline struct from the individual params.
|
|
547
|
+
*/
|
|
548
|
+
function buildGroupParam({ name, node, params, groupType, resolver, wrapType }) {
|
|
549
|
+
if (groupType) return [createFunctionParameter({
|
|
550
|
+
name,
|
|
551
|
+
type: groupType.type.kind === "ParamsType" && groupType.type.variant === "reference" ? wrapType(groupType.type.name) : groupType.type,
|
|
552
|
+
optional: groupType.optional
|
|
553
|
+
})];
|
|
554
|
+
if (params.length) return [createFunctionParameter({
|
|
555
|
+
name,
|
|
556
|
+
type: toStructType({
|
|
557
|
+
node,
|
|
558
|
+
params,
|
|
559
|
+
resolver
|
|
560
|
+
}),
|
|
561
|
+
optional: params.every((p) => !p.required)
|
|
562
|
+
})];
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Derives a {@link ParamGroupType} from the resolver's group method.
|
|
567
|
+
* Returns `undefined` when the group name equals the individual param name (no real group).
|
|
568
|
+
*/
|
|
569
|
+
function resolveGroupType({ node, params, groupMethod, resolver }) {
|
|
570
|
+
if (!params.length) return;
|
|
571
|
+
const firstParam = params[0];
|
|
572
|
+
const groupName = groupMethod.call(resolver, node, firstParam);
|
|
573
|
+
if (groupName === resolver.resolveParamName(node, firstParam)) return;
|
|
574
|
+
const allOptional = params.every((p) => !p.required);
|
|
575
|
+
return {
|
|
576
|
+
type: createParamsType({
|
|
577
|
+
variant: "reference",
|
|
578
|
+
name: groupName
|
|
579
|
+
}),
|
|
580
|
+
optional: allOptional
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Builds a {@link TypeNode} with `variant: 'struct'` for an inline anonymous type grouping named fields.
|
|
585
|
+
*
|
|
586
|
+
* Used when query or header parameters have no dedicated group type name.
|
|
587
|
+
* Each language printer renders this appropriately (TypeScript: `{ petId: string; name?: string }`).
|
|
588
|
+
*/
|
|
589
|
+
function toStructType({ node, params, resolver }) {
|
|
590
|
+
return createParamsType({
|
|
591
|
+
variant: "struct",
|
|
592
|
+
properties: params.map((p) => ({
|
|
593
|
+
name: p.name,
|
|
594
|
+
optional: !p.required,
|
|
595
|
+
type: resolveParamsType({
|
|
596
|
+
node,
|
|
597
|
+
param: p,
|
|
598
|
+
resolver
|
|
599
|
+
})
|
|
600
|
+
}))
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
function sourceKey(source) {
|
|
604
|
+
return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
|
|
605
|
+
}
|
|
606
|
+
function pathTypeKey(path, isTypeOnly) {
|
|
607
|
+
return `${path}:${isTypeOnly ?? false}`;
|
|
608
|
+
}
|
|
609
|
+
function exportKey(path, name, isTypeOnly, asAlias) {
|
|
610
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
611
|
+
}
|
|
612
|
+
function importKey(path, name, isTypeOnly) {
|
|
613
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Computes a multi-level sort key for exports and imports:
|
|
617
|
+
* non-array names first (wildcards/namespace aliases); type-only before value; alphabetical path; unnamed before named.
|
|
618
|
+
*/
|
|
619
|
+
function sortKey(node) {
|
|
620
|
+
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
621
|
+
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
622
|
+
const hasName = node.name != null ? "1" : "0";
|
|
623
|
+
const name = Array.isArray(node.name) ? [...node.name].sort().join("\0") : node.name ?? "";
|
|
624
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Deduplicates an array of `SourceNode` objects.
|
|
628
|
+
* Named sources are deduplicated by `name + isExportable + isTypeOnly`.
|
|
629
|
+
* Unnamed sources are deduplicated by object reference.
|
|
630
|
+
*/
|
|
631
|
+
function combineSources(sources) {
|
|
632
|
+
const seen = /* @__PURE__ */ new Map();
|
|
633
|
+
for (const source of sources) {
|
|
634
|
+
const key = sourceKey(source);
|
|
635
|
+
if (!seen.has(key)) seen.set(key, source);
|
|
636
|
+
}
|
|
637
|
+
return [...seen.values()];
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Deduplicates and merges an array of `ExportNode` objects.
|
|
641
|
+
* Exports with the same path and `isTypeOnly` flag have their names merged.
|
|
642
|
+
*/
|
|
643
|
+
function combineExports(exports) {
|
|
644
|
+
const result = [];
|
|
645
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
646
|
+
const seen = /* @__PURE__ */ new Set();
|
|
647
|
+
const keyed = exports.map((node) => ({
|
|
648
|
+
node,
|
|
649
|
+
key: sortKey(node)
|
|
650
|
+
}));
|
|
651
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
652
|
+
for (const { node: curr } of keyed) {
|
|
653
|
+
const { name, path, isTypeOnly, asAlias } = curr;
|
|
654
|
+
if (Array.isArray(name)) {
|
|
655
|
+
if (!name.length) continue;
|
|
656
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
657
|
+
const existing = namedByPath.get(key);
|
|
658
|
+
if (existing && Array.isArray(existing.name)) {
|
|
659
|
+
const merged = new Set(existing.name);
|
|
660
|
+
for (const n of name) merged.add(n);
|
|
661
|
+
existing.name = [...merged];
|
|
662
|
+
} else {
|
|
663
|
+
const newItem = {
|
|
664
|
+
...curr,
|
|
665
|
+
name: [...new Set(name)]
|
|
666
|
+
};
|
|
667
|
+
result.push(newItem);
|
|
668
|
+
namedByPath.set(key, newItem);
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
const key = exportKey(path, name, isTypeOnly, asAlias);
|
|
672
|
+
if (!seen.has(key)) {
|
|
673
|
+
result.push(curr);
|
|
674
|
+
seen.add(key);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Deduplicates and merges an array of `ImportNode` objects.
|
|
682
|
+
* Filters out unused imports (names not referenced in `source` or re-exported).
|
|
683
|
+
* Imports with the same path and `isTypeOnly` flag have their names merged.
|
|
684
|
+
*/
|
|
685
|
+
function combineImports(imports, exports, source) {
|
|
686
|
+
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
687
|
+
const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
|
|
688
|
+
const result = [];
|
|
689
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
690
|
+
const seen = /* @__PURE__ */ new Set();
|
|
691
|
+
const keyed = imports.map((node) => ({
|
|
692
|
+
node,
|
|
693
|
+
key: sortKey(node)
|
|
694
|
+
}));
|
|
695
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
696
|
+
for (const { node: curr } of keyed) {
|
|
697
|
+
if (curr.path === curr.root) continue;
|
|
698
|
+
const { path, isTypeOnly } = curr;
|
|
699
|
+
let { name } = curr;
|
|
700
|
+
if (Array.isArray(name)) {
|
|
701
|
+
name = [...new Set(name)].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.propertyName));
|
|
702
|
+
if (!name.length) continue;
|
|
703
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
704
|
+
const existing = namedByPath.get(key);
|
|
705
|
+
if (existing && Array.isArray(existing.name)) {
|
|
706
|
+
const merged = new Set(existing.name);
|
|
707
|
+
for (const n of name) merged.add(n);
|
|
708
|
+
existing.name = [...merged];
|
|
709
|
+
} else {
|
|
710
|
+
const newItem = {
|
|
711
|
+
...curr,
|
|
712
|
+
name
|
|
713
|
+
};
|
|
714
|
+
result.push(newItem);
|
|
715
|
+
namedByPath.set(key, newItem);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
if (name && !isUsed(name)) continue;
|
|
719
|
+
const key = importKey(path, name, isTypeOnly);
|
|
720
|
+
if (!seen.has(key)) {
|
|
721
|
+
result.push(curr);
|
|
722
|
+
seen.add(key);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Recursively extracts all string content embedded in a {@link CodeNode} tree.
|
|
730
|
+
*
|
|
731
|
+
* Includes text node values, and string attribute fields (`params`, `generics`,
|
|
732
|
+
* `returnType`, `type`) that may reference identifiers needing imports.
|
|
733
|
+
* Used by `createFile` to build the full source string for import filtering.
|
|
734
|
+
*/
|
|
735
|
+
function extractStringsFromNodes(nodes) {
|
|
736
|
+
if (!nodes?.length) return "";
|
|
737
|
+
return nodes.map((node) => {
|
|
738
|
+
if (typeof node === "string") return node;
|
|
739
|
+
if (node.kind === "Text") return node.value;
|
|
740
|
+
if (node.kind === "Break") return "";
|
|
741
|
+
if (node.kind === "Jsx") return node.value;
|
|
742
|
+
const parts = [];
|
|
743
|
+
if ("params" in node && node.params) parts.push(node.params);
|
|
744
|
+
if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
|
|
745
|
+
if ("returnType" in node && node.returnType) parts.push(node.returnType);
|
|
746
|
+
if ("type" in node && typeof node.type === "string") parts.push(node.type);
|
|
747
|
+
const nested = extractStringsFromNodes(node.nodes);
|
|
748
|
+
if (nested) parts.push(nested);
|
|
749
|
+
return parts.join("\n");
|
|
750
|
+
}).filter(Boolean).join("\n");
|
|
751
|
+
}
|
|
752
|
+
//#endregion
|
|
74
753
|
//#region src/factory.ts
|
|
75
754
|
/**
|
|
76
|
-
*
|
|
755
|
+
* Syncs property/parameter schema optionality flags from `required` and `schema.nullable`.
|
|
756
|
+
*
|
|
757
|
+
* - `optional` is set for non-required, non-nullable schemas.
|
|
758
|
+
* - `nullish` is set for non-required, nullable schemas.
|
|
77
759
|
*/
|
|
78
|
-
function
|
|
760
|
+
function syncOptionality(schema, required) {
|
|
761
|
+
const nullable = schema.nullable ?? false;
|
|
762
|
+
return {
|
|
763
|
+
...schema,
|
|
764
|
+
optional: !required && !nullable ? true : void 0,
|
|
765
|
+
nullish: !required && nullable ? true : void 0
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Creates an `InputNode` with stable defaults for `schemas` and `operations`.
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```ts
|
|
773
|
+
* const input = createInput()
|
|
774
|
+
* // { kind: 'Input', schemas: [], operations: [] }
|
|
775
|
+
* ```
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```ts
|
|
779
|
+
* const input = createInput({ schemas: [petSchema] })
|
|
780
|
+
* // keeps default operations: []
|
|
781
|
+
* ```
|
|
782
|
+
*/
|
|
783
|
+
function createInput(overrides = {}) {
|
|
79
784
|
return {
|
|
80
785
|
schemas: [],
|
|
81
786
|
operations: [],
|
|
82
787
|
...overrides,
|
|
83
|
-
kind: "
|
|
788
|
+
kind: "Input"
|
|
84
789
|
};
|
|
85
790
|
}
|
|
86
791
|
/**
|
|
87
|
-
* Creates an `
|
|
792
|
+
* Creates an `OutputNode` with a stable default for `files`.
|
|
793
|
+
*
|
|
794
|
+
* @example
|
|
795
|
+
* ```ts
|
|
796
|
+
* const output = createOutput()
|
|
797
|
+
* // { kind: 'Output', files: [] }
|
|
798
|
+
* ```
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```ts
|
|
802
|
+
* const output = createOutput({ files: [petFile] })
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
function createOutput(overrides = {}) {
|
|
806
|
+
return {
|
|
807
|
+
files: [],
|
|
808
|
+
...overrides,
|
|
809
|
+
kind: "Output"
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Creates an `OperationNode` with default empty arrays for `tags`, `parameters`, and `responses`.
|
|
814
|
+
*
|
|
815
|
+
* @example
|
|
816
|
+
* ```ts
|
|
817
|
+
* const operation = createOperation({
|
|
818
|
+
* operationId: 'getPetById',
|
|
819
|
+
* method: 'GET',
|
|
820
|
+
* path: '/pet/{petId}',
|
|
821
|
+
* })
|
|
822
|
+
* // tags, parameters, and responses are []
|
|
823
|
+
* ```
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```ts
|
|
827
|
+
* const operation = createOperation({
|
|
828
|
+
* operationId: 'findPets',
|
|
829
|
+
* method: 'GET',
|
|
830
|
+
* path: '/pet/findByStatus',
|
|
831
|
+
* tags: ['pet'],
|
|
832
|
+
* })
|
|
833
|
+
* ```
|
|
88
834
|
*/
|
|
89
835
|
function createOperation(props) {
|
|
90
836
|
return {
|
|
@@ -95,39 +841,125 @@ function createOperation(props) {
|
|
|
95
841
|
kind: "Operation"
|
|
96
842
|
};
|
|
97
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Maps schema `type` to its underlying `primitive`.
|
|
846
|
+
* Primitive types map to themselves; special string formats map to `'string'`.
|
|
847
|
+
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
848
|
+
*/
|
|
849
|
+
const TYPE_TO_PRIMITIVE = {
|
|
850
|
+
string: "string",
|
|
851
|
+
number: "number",
|
|
852
|
+
integer: "integer",
|
|
853
|
+
bigint: "bigint",
|
|
854
|
+
boolean: "boolean",
|
|
855
|
+
null: "null",
|
|
856
|
+
any: "any",
|
|
857
|
+
unknown: "unknown",
|
|
858
|
+
void: "void",
|
|
859
|
+
never: "never",
|
|
860
|
+
object: "object",
|
|
861
|
+
array: "array",
|
|
862
|
+
date: "date",
|
|
863
|
+
uuid: "string",
|
|
864
|
+
email: "string",
|
|
865
|
+
url: "string",
|
|
866
|
+
datetime: "string",
|
|
867
|
+
time: "string"
|
|
868
|
+
};
|
|
98
869
|
function createSchema(props) {
|
|
870
|
+
const inferredPrimitive = TYPE_TO_PRIMITIVE[props.type];
|
|
99
871
|
if (props["type"] === "object") return {
|
|
100
872
|
properties: [],
|
|
873
|
+
primitive: "object",
|
|
101
874
|
...props,
|
|
102
875
|
kind: "Schema"
|
|
103
876
|
};
|
|
104
877
|
return {
|
|
878
|
+
primitive: inferredPrimitive,
|
|
105
879
|
...props,
|
|
106
880
|
kind: "Schema"
|
|
107
881
|
};
|
|
108
882
|
}
|
|
109
883
|
/**
|
|
110
|
-
* Creates a `PropertyNode`.
|
|
884
|
+
* Creates a `PropertyNode`.
|
|
885
|
+
*
|
|
886
|
+
* `required` defaults to `false`.
|
|
887
|
+
* `schema.optional` and `schema.nullish` are derived from `required` and `schema.nullable`.
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* ```ts
|
|
891
|
+
* const property = createProperty({
|
|
892
|
+
* name: 'status',
|
|
893
|
+
* schema: createSchema({ type: 'string' }),
|
|
894
|
+
* })
|
|
895
|
+
* // required=false, schema.optional=true
|
|
896
|
+
* ```
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* ```ts
|
|
900
|
+
* const property = createProperty({
|
|
901
|
+
* name: 'status',
|
|
902
|
+
* required: true,
|
|
903
|
+
* schema: createSchema({ type: 'string', nullable: true }),
|
|
904
|
+
* })
|
|
905
|
+
* // required=true, no optional/nullish
|
|
906
|
+
* ```
|
|
111
907
|
*/
|
|
112
908
|
function createProperty(props) {
|
|
909
|
+
const required = props.required ?? false;
|
|
113
910
|
return {
|
|
114
|
-
required: false,
|
|
115
911
|
...props,
|
|
116
|
-
kind: "Property"
|
|
912
|
+
kind: "Property",
|
|
913
|
+
required,
|
|
914
|
+
schema: syncOptionality(props.schema, required)
|
|
117
915
|
};
|
|
118
916
|
}
|
|
119
917
|
/**
|
|
120
|
-
* Creates a `ParameterNode`.
|
|
918
|
+
* Creates a `ParameterNode`.
|
|
919
|
+
*
|
|
920
|
+
* `required` defaults to `false`.
|
|
921
|
+
* Nested schema flags are set from `required` and `schema.nullable`.
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* ```ts
|
|
925
|
+
* const param = createParameter({
|
|
926
|
+
* name: 'petId',
|
|
927
|
+
* in: 'path',
|
|
928
|
+
* required: true,
|
|
929
|
+
* schema: createSchema({ type: 'string' }),
|
|
930
|
+
* })
|
|
931
|
+
* ```
|
|
932
|
+
*
|
|
933
|
+
* @example
|
|
934
|
+
* ```ts
|
|
935
|
+
* const param = createParameter({
|
|
936
|
+
* name: 'status',
|
|
937
|
+
* in: 'query',
|
|
938
|
+
* schema: createSchema({ type: 'string', nullable: true }),
|
|
939
|
+
* })
|
|
940
|
+
* // required=false, schema.nullish=true
|
|
941
|
+
* ```
|
|
121
942
|
*/
|
|
122
943
|
function createParameter(props) {
|
|
944
|
+
const required = props.required ?? false;
|
|
123
945
|
return {
|
|
124
|
-
required: false,
|
|
125
946
|
...props,
|
|
126
|
-
kind: "Parameter"
|
|
947
|
+
kind: "Parameter",
|
|
948
|
+
required,
|
|
949
|
+
schema: syncOptionality(props.schema, required)
|
|
127
950
|
};
|
|
128
951
|
}
|
|
129
952
|
/**
|
|
130
953
|
* Creates a `ResponseNode`.
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* ```ts
|
|
957
|
+
* const response = createResponse({
|
|
958
|
+
* statusCode: '200',
|
|
959
|
+
* description: 'Success',
|
|
960
|
+
* schema: createSchema({ type: 'object', properties: [] }),
|
|
961
|
+
* })
|
|
962
|
+
* ```
|
|
131
963
|
*/
|
|
132
964
|
function createResponse(props) {
|
|
133
965
|
return {
|
|
@@ -135,420 +967,511 @@ function createResponse(props) {
|
|
|
135
967
|
kind: "Response"
|
|
136
968
|
};
|
|
137
969
|
}
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/guards.ts
|
|
140
970
|
/**
|
|
141
|
-
*
|
|
971
|
+
* Creates a `FunctionParameterNode`.
|
|
972
|
+
*
|
|
973
|
+
* `optional` defaults to `false`.
|
|
974
|
+
*
|
|
975
|
+
* @example Required typed param
|
|
976
|
+
* ```ts
|
|
977
|
+
* createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }) })
|
|
978
|
+
* // → petId: string
|
|
979
|
+
* ```
|
|
980
|
+
*
|
|
981
|
+
* @example Optional param
|
|
982
|
+
* ```ts
|
|
983
|
+
* createFunctionParameter({ name: 'params', type: createParamsType({ variant: 'reference', name: 'QueryParams' }), optional: true })
|
|
984
|
+
* // → params?: QueryParams
|
|
985
|
+
* ```
|
|
986
|
+
*
|
|
987
|
+
* @example Param with default (implicitly optional; cannot combine with `optional: true`)
|
|
988
|
+
* ```ts
|
|
989
|
+
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
|
|
990
|
+
* // → config: RequestConfig = {}
|
|
991
|
+
* ```
|
|
142
992
|
*/
|
|
143
|
-
function
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
993
|
+
function createFunctionParameter(props) {
|
|
994
|
+
return {
|
|
995
|
+
optional: false,
|
|
996
|
+
...props,
|
|
997
|
+
kind: "FunctionParameter"
|
|
998
|
+
};
|
|
148
999
|
}
|
|
149
1000
|
/**
|
|
150
|
-
*
|
|
1001
|
+
* Creates a {@link TypeNode} representing a language-agnostic structured type expression.
|
|
1002
|
+
*
|
|
1003
|
+
* Use `variant: 'struct'` for inline anonymous types and `variant: 'member'` for a single
|
|
1004
|
+
* named field accessed from a group type. Each language's printer renders the variant
|
|
1005
|
+
* into its own syntax (TypeScript, Python, C#, Kotlin, …).
|
|
1006
|
+
*
|
|
1007
|
+
* @example Reference type (TypeScript: `QueryParams`)
|
|
1008
|
+
* ```ts
|
|
1009
|
+
* createParamsType({ variant: 'reference', name: 'QueryParams' })
|
|
1010
|
+
* ```
|
|
1011
|
+
*
|
|
1012
|
+
* @example Struct type (TypeScript: `{ petId: string }`)
|
|
1013
|
+
* ```ts
|
|
1014
|
+
* createParamsType({ variant: 'struct', properties: [{ name: 'petId', optional: false, type: createParamsType({ variant: 'reference', name: 'string' }) }] })
|
|
1015
|
+
* ```
|
|
1016
|
+
*
|
|
1017
|
+
* @example Member type (TypeScript: `DeletePetPathParams['petId']`)
|
|
1018
|
+
* ```ts
|
|
1019
|
+
* createParamsType({ variant: 'member', base: 'DeletePetPathParams', key: 'petId' })
|
|
1020
|
+
* ```
|
|
151
1021
|
*/
|
|
152
|
-
|
|
1022
|
+
function createParamsType(props) {
|
|
1023
|
+
return {
|
|
1024
|
+
...props,
|
|
1025
|
+
kind: "ParamsType"
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
153
1028
|
/**
|
|
154
|
-
*
|
|
1029
|
+
* Creates a `ParameterGroupNode` representing a group of related parameters treated as a unit.
|
|
1030
|
+
*
|
|
1031
|
+
* @example Grouped param (TypeScript declaration)
|
|
1032
|
+
* ```ts
|
|
1033
|
+
* createParameterGroup({
|
|
1034
|
+
* properties: [
|
|
1035
|
+
* createFunctionParameter({ name: 'id', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
|
|
1036
|
+
* createFunctionParameter({ name: 'name', type: createParamsType({ variant: 'reference', name: 'string' }), optional: true }),
|
|
1037
|
+
* ],
|
|
1038
|
+
* default: '{}',
|
|
1039
|
+
* })
|
|
1040
|
+
* // declaration → { id, name? }: { id: string; name?: string } = {}
|
|
1041
|
+
* // call → { id, name }
|
|
1042
|
+
* ```
|
|
1043
|
+
*
|
|
1044
|
+
* @example Inline (spread) — children emitted as individual top-level parameters
|
|
1045
|
+
* ```ts
|
|
1046
|
+
* createParameterGroup({
|
|
1047
|
+
* properties: [createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false })],
|
|
1048
|
+
* inline: true,
|
|
1049
|
+
* })
|
|
1050
|
+
* // declaration → petId: string
|
|
1051
|
+
* // call → petId
|
|
1052
|
+
* ```
|
|
155
1053
|
*/
|
|
156
|
-
|
|
1054
|
+
function createParameterGroup(props) {
|
|
1055
|
+
return {
|
|
1056
|
+
...props,
|
|
1057
|
+
kind: "ParameterGroup"
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
157
1060
|
/**
|
|
158
|
-
*
|
|
1061
|
+
* Creates a `FunctionParametersNode` from an ordered list of parameters.
|
|
1062
|
+
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```ts
|
|
1065
|
+
* createFunctionParameters({
|
|
1066
|
+
* params: [
|
|
1067
|
+
* createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
|
|
1068
|
+
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), optional: false, default: '{}' }),
|
|
1069
|
+
* ],
|
|
1070
|
+
* })
|
|
1071
|
+
* ```
|
|
1072
|
+
*
|
|
1073
|
+
* @example
|
|
1074
|
+
* ```ts
|
|
1075
|
+
* const empty = createFunctionParameters()
|
|
1076
|
+
* // { kind: 'FunctionParameters', params: [] }
|
|
1077
|
+
* ```
|
|
159
1078
|
*/
|
|
160
|
-
|
|
1079
|
+
function createFunctionParameters(props = {}) {
|
|
1080
|
+
return {
|
|
1081
|
+
params: [],
|
|
1082
|
+
...props,
|
|
1083
|
+
kind: "FunctionParameters"
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
161
1086
|
/**
|
|
162
|
-
*
|
|
1087
|
+
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
1088
|
+
*
|
|
1089
|
+
* @example Named import
|
|
1090
|
+
* ```ts
|
|
1091
|
+
* createImport({ name: ['useState'], path: 'react' })
|
|
1092
|
+
* // import { useState } from 'react'
|
|
1093
|
+
* ```
|
|
1094
|
+
*
|
|
1095
|
+
* @example Type-only import
|
|
1096
|
+
* ```ts
|
|
1097
|
+
* createImport({ name: ['FC'], path: 'react', isTypeOnly: true })
|
|
1098
|
+
* // import type { FC } from 'react'
|
|
1099
|
+
* ```
|
|
163
1100
|
*/
|
|
164
|
-
|
|
1101
|
+
function createImport(props) {
|
|
1102
|
+
return {
|
|
1103
|
+
...props,
|
|
1104
|
+
kind: "Import"
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
165
1107
|
/**
|
|
166
|
-
*
|
|
1108
|
+
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
1109
|
+
*
|
|
1110
|
+
* @example Named export
|
|
1111
|
+
* ```ts
|
|
1112
|
+
* createExport({ name: ['Pet'], path: './Pet' })
|
|
1113
|
+
* // export { Pet } from './Pet'
|
|
1114
|
+
* ```
|
|
1115
|
+
*
|
|
1116
|
+
* @example Wildcard export
|
|
1117
|
+
* ```ts
|
|
1118
|
+
* createExport({ path: './utils' })
|
|
1119
|
+
* // export * from './utils'
|
|
1120
|
+
* ```
|
|
167
1121
|
*/
|
|
168
|
-
|
|
1122
|
+
function createExport(props) {
|
|
1123
|
+
return {
|
|
1124
|
+
...props,
|
|
1125
|
+
kind: "Export"
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
169
1128
|
/**
|
|
170
|
-
*
|
|
1129
|
+
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
1130
|
+
*
|
|
1131
|
+
* @example
|
|
1132
|
+
* ```ts
|
|
1133
|
+
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
1134
|
+
* ```
|
|
171
1135
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1136
|
+
function createSource(props) {
|
|
1137
|
+
return {
|
|
1138
|
+
...props,
|
|
1139
|
+
kind: "Source"
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
175
1142
|
/**
|
|
176
|
-
* Creates a
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* nodes: {
|
|
190
|
-
* string(node) {
|
|
191
|
-
* return `z.string()`
|
|
192
|
-
* },
|
|
193
|
-
* object(node) {
|
|
194
|
-
* const props = node.properties
|
|
195
|
-
* ?.map(p => `${p.name}: ${this.print(p)}`)
|
|
196
|
-
* .join(', ') ?? ''
|
|
197
|
-
* return `z.object({ ${props} })`
|
|
198
|
-
* },
|
|
199
|
-
* },
|
|
200
|
-
* }
|
|
201
|
-
* })
|
|
1143
|
+
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1144
|
+
*
|
|
1145
|
+
* Computes:
|
|
1146
|
+
* - `id` — SHA256 hash of the file path
|
|
1147
|
+
* - `name` — `baseName` without extension
|
|
1148
|
+
* - `extname` — extension extracted from `baseName`
|
|
1149
|
+
*
|
|
1150
|
+
* Deduplicates:
|
|
1151
|
+
* - `sources` via `combineSources`
|
|
1152
|
+
* - `exports` via `combineExports`
|
|
1153
|
+
* - `imports` via `combineImports` (also filters unused imports)
|
|
1154
|
+
*
|
|
1155
|
+
* @throws {Error} when `baseName` has no extension.
|
|
202
1156
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
1157
|
+
* @example
|
|
1158
|
+
* ```ts
|
|
1159
|
+
* const file = createFile({
|
|
1160
|
+
* baseName: 'petStore.ts',
|
|
1161
|
+
* path: 'src/models/petStore.ts',
|
|
1162
|
+
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
1163
|
+
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
1164
|
+
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
1165
|
+
* })
|
|
1166
|
+
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
1167
|
+
* // file.name = 'petStore'
|
|
1168
|
+
* // file.extname = '.ts'
|
|
207
1169
|
* ```
|
|
208
1170
|
*/
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1171
|
+
function createFile(input) {
|
|
1172
|
+
const extname = path.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
|
|
1173
|
+
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
1174
|
+
const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
|
|
1175
|
+
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
1176
|
+
const resolvedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
|
|
1177
|
+
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
1178
|
+
return {
|
|
1179
|
+
kind: "File",
|
|
1180
|
+
...input,
|
|
1181
|
+
id: createHash("sha256").update(input.path).digest("hex"),
|
|
1182
|
+
name: trimExtName(input.baseName),
|
|
1183
|
+
extname,
|
|
1184
|
+
imports: resolvedImports,
|
|
1185
|
+
exports: resolvedExports,
|
|
1186
|
+
sources: resolvedSources,
|
|
1187
|
+
meta: input.meta ?? {}
|
|
225
1188
|
};
|
|
226
1189
|
}
|
|
227
|
-
//#endregion
|
|
228
|
-
//#region src/refs.ts
|
|
229
1190
|
/**
|
|
230
|
-
*
|
|
1191
|
+
* Creates a `ConstNode` representing a TypeScript `const` declaration.
|
|
1192
|
+
*
|
|
1193
|
+
* Mirrors the `Const` component from `@kubb/renderer-jsx`.
|
|
1194
|
+
* The component's `children` are represented as `nodes`.
|
|
1195
|
+
*
|
|
1196
|
+
* @example Simple constant
|
|
1197
|
+
* ```ts
|
|
1198
|
+
* createConst({ name: 'pet' })
|
|
1199
|
+
* // const pet = ...
|
|
1200
|
+
* ```
|
|
1201
|
+
*
|
|
1202
|
+
* @example Exported constant with type and `as const`
|
|
1203
|
+
* ```ts
|
|
1204
|
+
* createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
|
|
1205
|
+
* // export const pets: Pet[] = ... as const
|
|
1206
|
+
* ```
|
|
1207
|
+
*
|
|
1208
|
+
* @example With JSDoc and child nodes
|
|
1209
|
+
* ```ts
|
|
1210
|
+
* createConst({
|
|
1211
|
+
* name: 'config',
|
|
1212
|
+
* export: true,
|
|
1213
|
+
* JSDoc: { comments: ['@description App configuration'] },
|
|
1214
|
+
* nodes: [],
|
|
1215
|
+
* })
|
|
1216
|
+
* ```
|
|
231
1217
|
*/
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
1218
|
+
function createConst(props) {
|
|
1219
|
+
return {
|
|
1220
|
+
...props,
|
|
1221
|
+
kind: "Const"
|
|
1222
|
+
};
|
|
236
1223
|
}
|
|
237
1224
|
/**
|
|
238
|
-
*
|
|
1225
|
+
* Creates a `TypeNode` representing a TypeScript `type` alias declaration.
|
|
1226
|
+
*
|
|
1227
|
+
* Mirrors the `Type` component from `@kubb/renderer-jsx`.
|
|
1228
|
+
* The component's `children` are represented as `nodes`.
|
|
1229
|
+
*
|
|
1230
|
+
* @example Simple type alias
|
|
1231
|
+
* ```ts
|
|
1232
|
+
* createType({ name: 'Pet' })
|
|
1233
|
+
* // type Pet = ...
|
|
1234
|
+
* ```
|
|
1235
|
+
*
|
|
1236
|
+
* @example Exported type with JSDoc
|
|
1237
|
+
* ```ts
|
|
1238
|
+
* createType({
|
|
1239
|
+
* name: 'PetStatus',
|
|
1240
|
+
* export: true,
|
|
1241
|
+
* JSDoc: { comments: ['@description Status of a pet'] },
|
|
1242
|
+
* })
|
|
1243
|
+
* // export type PetStatus = ...
|
|
1244
|
+
* ```
|
|
239
1245
|
*/
|
|
240
|
-
function
|
|
241
|
-
return
|
|
1246
|
+
function createType(props) {
|
|
1247
|
+
return {
|
|
1248
|
+
...props,
|
|
1249
|
+
kind: "Type"
|
|
1250
|
+
};
|
|
242
1251
|
}
|
|
243
1252
|
/**
|
|
244
|
-
*
|
|
1253
|
+
* Creates a `FunctionNode` representing a TypeScript `function` declaration.
|
|
1254
|
+
*
|
|
1255
|
+
* Mirrors the `Function` component from `@kubb/renderer-jsx`.
|
|
1256
|
+
* The component's `children` are represented as `nodes`.
|
|
1257
|
+
*
|
|
1258
|
+
* @example Simple function
|
|
1259
|
+
* ```ts
|
|
1260
|
+
* createFunction({ name: 'getPet' })
|
|
1261
|
+
* // function getPet() { ... }
|
|
1262
|
+
* ```
|
|
1263
|
+
*
|
|
1264
|
+
* @example Exported async function with return type
|
|
1265
|
+
* ```ts
|
|
1266
|
+
* createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
|
|
1267
|
+
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
1268
|
+
* ```
|
|
1269
|
+
*
|
|
1270
|
+
* @example Function with generics and params
|
|
1271
|
+
* ```ts
|
|
1272
|
+
* createFunction({
|
|
1273
|
+
* name: 'identity',
|
|
1274
|
+
* export: true,
|
|
1275
|
+
* generics: ['T'],
|
|
1276
|
+
* params: 'value: T',
|
|
1277
|
+
* returnType: 'T',
|
|
1278
|
+
* })
|
|
1279
|
+
* // export function identity<T>(value: T): T { ... }
|
|
1280
|
+
* ```
|
|
245
1281
|
*/
|
|
246
|
-
function
|
|
247
|
-
return
|
|
1282
|
+
function createFunction(props) {
|
|
1283
|
+
return {
|
|
1284
|
+
...props,
|
|
1285
|
+
kind: "Function"
|
|
1286
|
+
};
|
|
248
1287
|
}
|
|
249
|
-
//#endregion
|
|
250
|
-
//#region ../../internals/utils/dist/index.js
|
|
251
1288
|
/**
|
|
252
|
-
*
|
|
253
|
-
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
254
|
-
* and capitalizes each word according to `pascal`.
|
|
1289
|
+
* Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
|
|
255
1290
|
*
|
|
256
|
-
*
|
|
1291
|
+
* Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
|
|
1292
|
+
* The component's `children` are represented as `nodes`.
|
|
1293
|
+
*
|
|
1294
|
+
* @example Simple arrow function
|
|
1295
|
+
* ```ts
|
|
1296
|
+
* createArrowFunction({ name: 'getPet' })
|
|
1297
|
+
* // const getPet = () => { ... }
|
|
1298
|
+
* ```
|
|
1299
|
+
*
|
|
1300
|
+
* @example Single-line exported arrow function
|
|
1301
|
+
* ```ts
|
|
1302
|
+
* createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
|
|
1303
|
+
* // export const double = (n: number) => ...
|
|
1304
|
+
* ```
|
|
1305
|
+
*
|
|
1306
|
+
* @example Async arrow function with generics
|
|
1307
|
+
* ```ts
|
|
1308
|
+
* createArrowFunction({
|
|
1309
|
+
* name: 'fetchPet',
|
|
1310
|
+
* export: true,
|
|
1311
|
+
* async: true,
|
|
1312
|
+
* generics: ['T'],
|
|
1313
|
+
* params: 'id: string',
|
|
1314
|
+
* returnType: 'T',
|
|
1315
|
+
* })
|
|
1316
|
+
* // export const fetchPet = async <T>(id: string): Promise<T> => { ... }
|
|
1317
|
+
* ```
|
|
257
1318
|
*/
|
|
258
|
-
function
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
1319
|
+
function createArrowFunction(props) {
|
|
1320
|
+
return {
|
|
1321
|
+
...props,
|
|
1322
|
+
kind: "ArrowFunction"
|
|
1323
|
+
};
|
|
264
1324
|
}
|
|
265
1325
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
1326
|
+
* Creates a {@link TextNode} representing a raw string fragment in the source output.
|
|
1327
|
+
*
|
|
1328
|
+
* Use this instead of bare strings when building `nodes` arrays so that every
|
|
1329
|
+
* entry in the array is a typed {@link CodeNode}.
|
|
1330
|
+
*
|
|
1331
|
+
* @example
|
|
1332
|
+
* ```ts
|
|
1333
|
+
* createText('return fetch(id)')
|
|
1334
|
+
* // { kind: 'Text', value: 'return fetch(id)' }
|
|
1335
|
+
* ```
|
|
269
1336
|
*/
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
1337
|
+
function createText(value) {
|
|
1338
|
+
return {
|
|
1339
|
+
value,
|
|
1340
|
+
kind: "Text"
|
|
1341
|
+
};
|
|
273
1342
|
}
|
|
274
1343
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
1344
|
+
* Creates a {@link BreakNode} representing a line break in the source output.
|
|
1345
|
+
*
|
|
1346
|
+
* Corresponds to `<br/>` in JSX components. Prints as an empty string which,
|
|
1347
|
+
* when joined with `\n` by `printNodes`, produces a blank line.
|
|
277
1348
|
*
|
|
278
1349
|
* @example
|
|
279
|
-
*
|
|
280
|
-
*
|
|
1350
|
+
* ```ts
|
|
1351
|
+
* createBreak()
|
|
1352
|
+
* // { kind: 'Break' }
|
|
1353
|
+
* ```
|
|
281
1354
|
*/
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
prefix,
|
|
285
|
-
suffix
|
|
286
|
-
} : {}));
|
|
287
|
-
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
288
|
-
}
|
|
289
|
-
/** Returns a `CLIAdapter` with type inference. Pass a different adapter to `createCLI` to swap the CLI engine. */
|
|
290
|
-
function defineCLIAdapter(adapter) {
|
|
291
|
-
return adapter;
|
|
1355
|
+
function createBreak() {
|
|
1356
|
+
return { kind: "Break" };
|
|
292
1357
|
}
|
|
293
1358
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
1359
|
+
* Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
|
|
1360
|
+
*
|
|
1361
|
+
* Use this to embed JSX markup (including fragments `<>…</>`) directly in generated code.
|
|
1362
|
+
*
|
|
1363
|
+
* @example
|
|
1364
|
+
* ```ts
|
|
1365
|
+
* createJsx('<>\n <a href={href}>Open</a>\n</>')
|
|
1366
|
+
* // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
|
|
1367
|
+
* ```
|
|
296
1368
|
*/
|
|
297
|
-
function
|
|
298
|
-
return defs.map(serializeCommand);
|
|
299
|
-
}
|
|
300
|
-
function serializeCommand(def) {
|
|
1369
|
+
function createJsx(value) {
|
|
301
1370
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
arguments: def.arguments,
|
|
305
|
-
options: serializeOptions(def.options ?? {}),
|
|
306
|
-
subCommands: def.subCommands ? def.subCommands.map(serializeCommand) : []
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
function serializeOptions(options) {
|
|
310
|
-
return Object.entries(options).map(([name, opt]) => {
|
|
311
|
-
return {
|
|
312
|
-
name,
|
|
313
|
-
flags: `${opt.short ? `-${opt.short}, ` : ""}--${name}${opt.type === "string" ? ` <${opt.hint ?? name}>` : ""}`,
|
|
314
|
-
type: opt.type,
|
|
315
|
-
description: opt.description,
|
|
316
|
-
...opt.default !== void 0 ? { default: opt.default } : {},
|
|
317
|
-
...opt.hint ? { hint: opt.hint } : {},
|
|
318
|
-
...opt.enum ? { enum: opt.enum } : {},
|
|
319
|
-
...opt.required ? { required: opt.required } : {}
|
|
320
|
-
};
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
/** Prints formatted help output for a command using its `CommandDefinition`. */
|
|
324
|
-
function renderHelp(def, parentName) {
|
|
325
|
-
const schema = getCommandSchema([def])[0];
|
|
326
|
-
const programName = parentName ? `${parentName} ${schema.name}` : schema.name;
|
|
327
|
-
const argsPart = schema.arguments?.length ? ` ${schema.arguments.join(" ")}` : "";
|
|
328
|
-
const subCmdPart = schema.subCommands.length ? " <command>" : "";
|
|
329
|
-
console.log(`\n${styleText("bold", "Usage:")} ${programName}${argsPart}${subCmdPart} [options]\n`);
|
|
330
|
-
if (schema.description) console.log(` ${schema.description}\n`);
|
|
331
|
-
if (schema.subCommands.length) {
|
|
332
|
-
console.log(styleText("bold", "Commands:"));
|
|
333
|
-
for (const sub of schema.subCommands) console.log(` ${styleText("cyan", sub.name.padEnd(16))}${sub.description}`);
|
|
334
|
-
console.log();
|
|
335
|
-
}
|
|
336
|
-
const options = [...schema.options, {
|
|
337
|
-
name: "help",
|
|
338
|
-
flags: "-h, --help",
|
|
339
|
-
type: "boolean",
|
|
340
|
-
description: "Show help"
|
|
341
|
-
}];
|
|
342
|
-
console.log(styleText("bold", "Options:"));
|
|
343
|
-
for (const opt of options) {
|
|
344
|
-
const flags = styleText("cyan", opt.flags.padEnd(30));
|
|
345
|
-
const defaultPart = opt.default !== void 0 ? styleText("dim", ` (default: ${opt.default})`) : "";
|
|
346
|
-
console.log(` ${flags}${opt.description}${defaultPart}`);
|
|
347
|
-
}
|
|
348
|
-
console.log();
|
|
349
|
-
}
|
|
350
|
-
function buildParseOptions(def) {
|
|
351
|
-
const result = { help: {
|
|
352
|
-
type: "boolean",
|
|
353
|
-
short: "h"
|
|
354
|
-
} };
|
|
355
|
-
for (const [name, opt] of Object.entries(def.options ?? {})) result[name] = {
|
|
356
|
-
type: opt.type,
|
|
357
|
-
...opt.short ? { short: opt.short } : {},
|
|
358
|
-
...opt.default !== void 0 ? { default: opt.default } : {}
|
|
359
|
-
};
|
|
360
|
-
return result;
|
|
361
|
-
}
|
|
362
|
-
async function runCommand(def, argv, parentName) {
|
|
363
|
-
const parseOptions = buildParseOptions(def);
|
|
364
|
-
let parsed;
|
|
365
|
-
try {
|
|
366
|
-
const result = parseArgs({
|
|
367
|
-
args: argv,
|
|
368
|
-
options: parseOptions,
|
|
369
|
-
allowPositionals: true,
|
|
370
|
-
strict: false
|
|
371
|
-
});
|
|
372
|
-
parsed = {
|
|
373
|
-
values: result.values,
|
|
374
|
-
positionals: result.positionals
|
|
375
|
-
};
|
|
376
|
-
} catch {
|
|
377
|
-
renderHelp(def, parentName);
|
|
378
|
-
process.exit(1);
|
|
379
|
-
}
|
|
380
|
-
if (parsed.values["help"]) {
|
|
381
|
-
renderHelp(def, parentName);
|
|
382
|
-
process.exit(0);
|
|
383
|
-
}
|
|
384
|
-
for (const [name, opt] of Object.entries(def.options ?? {})) if (opt.required && parsed.values[name] === void 0) {
|
|
385
|
-
console.error(styleText("red", `Error: --${name} is required`));
|
|
386
|
-
renderHelp(def, parentName);
|
|
387
|
-
process.exit(1);
|
|
388
|
-
}
|
|
389
|
-
if (!def.run) {
|
|
390
|
-
renderHelp(def, parentName);
|
|
391
|
-
process.exit(0);
|
|
392
|
-
}
|
|
393
|
-
try {
|
|
394
|
-
await def.run(parsed);
|
|
395
|
-
} catch (err) {
|
|
396
|
-
console.error(styleText("red", `Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
397
|
-
renderHelp(def, parentName);
|
|
398
|
-
process.exit(1);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
function printRootHelp(programName, version, defs) {
|
|
402
|
-
console.log(`\n${styleText("bold", "Usage:")} ${programName} <command> [options]\n`);
|
|
403
|
-
console.log(` Kubb generation — v${version}\n`);
|
|
404
|
-
console.log(styleText("bold", "Commands:"));
|
|
405
|
-
for (const def of defs) console.log(` ${styleText("cyan", def.name.padEnd(16))}${def.description}`);
|
|
406
|
-
console.log();
|
|
407
|
-
console.log(styleText("bold", "Options:"));
|
|
408
|
-
console.log(` ${styleText("cyan", "-v, --version".padEnd(30))}Show version number`);
|
|
409
|
-
console.log(` ${styleText("cyan", "-h, --help".padEnd(30))}Show help`);
|
|
410
|
-
console.log();
|
|
411
|
-
console.log(`Run ${styleText("cyan", `${programName} <command> --help`)} for command-specific help.\n`);
|
|
412
|
-
}
|
|
413
|
-
defineCLIAdapter({
|
|
414
|
-
renderHelp(def, parentName) {
|
|
415
|
-
renderHelp(def, parentName);
|
|
416
|
-
},
|
|
417
|
-
async run(defs, argv, opts) {
|
|
418
|
-
const { programName, defaultCommandName, version } = opts;
|
|
419
|
-
const args = argv.length >= 2 && argv[0]?.includes("node") ? argv.slice(2) : argv;
|
|
420
|
-
if (args[0] === "--version" || args[0] === "-v") {
|
|
421
|
-
console.log(version);
|
|
422
|
-
process.exit(0);
|
|
423
|
-
}
|
|
424
|
-
if (args[0] === "--help" || args[0] === "-h") {
|
|
425
|
-
printRootHelp(programName, version, defs);
|
|
426
|
-
process.exit(0);
|
|
427
|
-
}
|
|
428
|
-
if (args.length === 0) {
|
|
429
|
-
const defaultDef = defs.find((d) => d.name === defaultCommandName);
|
|
430
|
-
if (defaultDef?.run) await runCommand(defaultDef, [], programName);
|
|
431
|
-
else printRootHelp(programName, version, defs);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
const [first, ...rest] = args;
|
|
435
|
-
const isKnownSubcommand = defs.some((d) => d.name === first);
|
|
436
|
-
let def;
|
|
437
|
-
let commandArgv;
|
|
438
|
-
let parentName;
|
|
439
|
-
if (isKnownSubcommand) {
|
|
440
|
-
def = defs.find((d) => d.name === first);
|
|
441
|
-
commandArgv = rest;
|
|
442
|
-
parentName = programName;
|
|
443
|
-
} else {
|
|
444
|
-
def = defs.find((d) => d.name === defaultCommandName);
|
|
445
|
-
commandArgv = args;
|
|
446
|
-
parentName = programName;
|
|
447
|
-
}
|
|
448
|
-
if (!def) {
|
|
449
|
-
console.error(`Unknown command: ${first}`);
|
|
450
|
-
printRootHelp(programName, version, defs);
|
|
451
|
-
process.exit(1);
|
|
452
|
-
}
|
|
453
|
-
if (def.subCommands?.length) {
|
|
454
|
-
const [subName, ...subRest] = commandArgv;
|
|
455
|
-
const subDef = def.subCommands.find((s) => s.name === subName);
|
|
456
|
-
if (subName === "--help" || subName === "-h") {
|
|
457
|
-
renderHelp(def, parentName);
|
|
458
|
-
process.exit(0);
|
|
459
|
-
}
|
|
460
|
-
if (!subDef) {
|
|
461
|
-
renderHelp(def, parentName);
|
|
462
|
-
process.exit(subName ? 1 : 0);
|
|
463
|
-
}
|
|
464
|
-
await runCommand(subDef, subRest, `${parentName} ${def.name}`);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
await runCommand(def, commandArgv, parentName);
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
/**
|
|
471
|
-
* Parses a CSS hex color string (`#RGB`) into its RGB channels.
|
|
472
|
-
* Falls back to `255` for any channel that cannot be parsed.
|
|
473
|
-
*/
|
|
474
|
-
function parseHex(color) {
|
|
475
|
-
const int = Number.parseInt(color.replace("#", ""), 16);
|
|
476
|
-
return Number.isNaN(int) ? {
|
|
477
|
-
r: 255,
|
|
478
|
-
g: 255,
|
|
479
|
-
b: 255
|
|
480
|
-
} : {
|
|
481
|
-
r: int >> 16 & 255,
|
|
482
|
-
g: int >> 8 & 255,
|
|
483
|
-
b: int & 255
|
|
1371
|
+
value,
|
|
1372
|
+
kind: "Jsx"
|
|
484
1373
|
};
|
|
485
1374
|
}
|
|
1375
|
+
//#endregion
|
|
1376
|
+
//#region src/printer.ts
|
|
486
1377
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
1378
|
+
* Creates a schema printer factory.
|
|
1379
|
+
*
|
|
1380
|
+
* This function wraps a builder and makes options optional at call sites.
|
|
1381
|
+
*
|
|
1382
|
+
* The builder receives resolved options and returns:
|
|
1383
|
+
* - `name` — a unique identifier for the printer
|
|
1384
|
+
* - `options` — options stored on the returned printer instance
|
|
1385
|
+
* - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
|
|
1386
|
+
* - `print` _(optional)_ — top-level override exposed as `printer.print`
|
|
1387
|
+
* - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
|
|
1388
|
+
* - This keeps recursion safe and avoids self-calls
|
|
1389
|
+
*
|
|
1390
|
+
* When no `print` override is provided, `printer.print` falls back to `printer.transform` (the node-level dispatcher).
|
|
1391
|
+
*
|
|
1392
|
+
* @example Basic usage — Zod schema printer
|
|
1393
|
+
* ```ts
|
|
1394
|
+
* type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
|
|
1395
|
+
*
|
|
1396
|
+
* export const zodPrinter = definePrinter<PrinterZod>((options) => ({
|
|
1397
|
+
* name: 'zod',
|
|
1398
|
+
* options: { strict: options.strict ?? true },
|
|
1399
|
+
* nodes: {
|
|
1400
|
+
* string: () => 'z.string()',
|
|
1401
|
+
* object(node) {
|
|
1402
|
+
* const props = node.properties.map(p => `${p.name}: ${this.transform(p.schema)}`).join(', ')
|
|
1403
|
+
* return `z.object({ ${props} })`
|
|
1404
|
+
* },
|
|
1405
|
+
* },
|
|
1406
|
+
* }))
|
|
1407
|
+
* ```
|
|
489
1408
|
*/
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
return (text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
1409
|
+
function definePrinter(build) {
|
|
1410
|
+
return createPrinterFactory((node) => node.type)(build);
|
|
493
1411
|
}
|
|
494
|
-
hex("#F55A17"), hex("#F5A217"), hex("#F58517"), hex("#B45309"), hex("#FFFFFF"), hex("#adadc6"), hex("#FDA4AF");
|
|
495
1412
|
/**
|
|
496
|
-
*
|
|
1413
|
+
* Generic printer-factory function used by `definePrinter` and `defineFunctionPrinter`.
|
|
1414
|
+
**
|
|
1415
|
+
* @example
|
|
1416
|
+
* ```ts
|
|
1417
|
+
* export const defineFunctionPrinter = createPrinterFactory<FunctionNode, FunctionNodeType, FunctionNodeByType>(
|
|
1418
|
+
* (node) => kindToHandlerKey[node.kind],
|
|
1419
|
+
* )
|
|
1420
|
+
* ```
|
|
497
1421
|
*/
|
|
498
|
-
function
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1422
|
+
function createPrinterFactory(getKey) {
|
|
1423
|
+
return function(build) {
|
|
1424
|
+
return (options) => {
|
|
1425
|
+
const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? {});
|
|
1426
|
+
const context = {
|
|
1427
|
+
options: resolvedOptions,
|
|
1428
|
+
transform: (node) => {
|
|
1429
|
+
const key = getKey(node);
|
|
1430
|
+
if (key === void 0) return null;
|
|
1431
|
+
const handler = nodes[key];
|
|
1432
|
+
if (!handler) return null;
|
|
1433
|
+
return handler.call(context, node);
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
return {
|
|
1437
|
+
name,
|
|
1438
|
+
options: resolvedOptions,
|
|
1439
|
+
transform: context.transform,
|
|
1440
|
+
print: printOverride ? printOverride.bind(context) : context.transform
|
|
1441
|
+
};
|
|
1442
|
+
};
|
|
1443
|
+
};
|
|
505
1444
|
}
|
|
506
1445
|
//#endregion
|
|
507
|
-
//#region src/
|
|
508
|
-
const plainStringTypes = new Set([
|
|
509
|
-
"string",
|
|
510
|
-
"uuid",
|
|
511
|
-
"email",
|
|
512
|
-
"url",
|
|
513
|
-
"datetime"
|
|
514
|
-
]);
|
|
515
|
-
/**
|
|
516
|
-
* Returns `true` when a schema node will be represented as a plain string in generated code.
|
|
517
|
-
*
|
|
518
|
-
* - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings.
|
|
519
|
-
* - `date` and `time` are plain strings when their `representation` is `'string'` rather than `'date'`.
|
|
520
|
-
*/
|
|
521
|
-
function isPlainStringType(node) {
|
|
522
|
-
if (plainStringTypes.has(node.type)) return true;
|
|
523
|
-
const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
|
|
524
|
-
if (temporal) return temporal.representation !== "date";
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
1446
|
+
//#region src/refs.ts
|
|
527
1447
|
/**
|
|
528
|
-
*
|
|
1448
|
+
* Returns the last path segment of a reference string.
|
|
529
1449
|
*
|
|
530
|
-
*
|
|
531
|
-
* When no `casing` is provided the original array is returned as-is.
|
|
1450
|
+
* Example: `#/components/schemas/Pet` becomes `Pet`.
|
|
532
1451
|
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
1452
|
+
* @example
|
|
1453
|
+
* ```ts
|
|
1454
|
+
* extractRefName('#/components/schemas/Pet') // 'Pet'
|
|
1455
|
+
* ```
|
|
536
1456
|
*/
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
return params.map((param) => {
|
|
540
|
-
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
541
|
-
return {
|
|
542
|
-
...param,
|
|
543
|
-
name: transformed
|
|
544
|
-
};
|
|
545
|
-
});
|
|
1457
|
+
function extractRefName(ref) {
|
|
1458
|
+
return ref.split("/").at(-1) ?? ref;
|
|
546
1459
|
}
|
|
547
1460
|
//#endregion
|
|
548
1461
|
//#region src/visitor.ts
|
|
549
1462
|
/**
|
|
550
|
-
* Creates a
|
|
551
|
-
*
|
|
1463
|
+
* Creates a small async concurrency limiter.
|
|
1464
|
+
*
|
|
1465
|
+
* At most `concurrency` tasks are in flight at once. Extra tasks are queued.
|
|
1466
|
+
*
|
|
1467
|
+
* @example
|
|
1468
|
+
* ```ts
|
|
1469
|
+
* const limit = createLimit(2)
|
|
1470
|
+
* for (const task of [taskA, taskB, taskC]) {
|
|
1471
|
+
* await limit(() => task())
|
|
1472
|
+
* }
|
|
1473
|
+
* // only 2 tasks run at the same time
|
|
1474
|
+
* ```
|
|
552
1475
|
*/
|
|
553
1476
|
function createLimit(concurrency) {
|
|
554
1477
|
let active = 0;
|
|
@@ -574,15 +1497,23 @@ function createLimit(concurrency) {
|
|
|
574
1497
|
/**
|
|
575
1498
|
* Returns the immediate traversable children of `node`.
|
|
576
1499
|
*
|
|
577
|
-
* For `Schema` nodes, children (properties
|
|
578
|
-
*
|
|
1500
|
+
* For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
|
|
1501
|
+
* `additionalProperties`) are only included
|
|
1502
|
+
* when `recurse` is `true`; shallow mode skips them.
|
|
1503
|
+
*
|
|
1504
|
+
* @example
|
|
1505
|
+
* ```ts
|
|
1506
|
+
* const children = getChildren(operationNode, true)
|
|
1507
|
+
* // returns parameters, requestBody schema (if present), and responses
|
|
1508
|
+
* ```
|
|
579
1509
|
*/
|
|
580
1510
|
function getChildren(node, recurse) {
|
|
581
1511
|
switch (node.kind) {
|
|
582
|
-
case "
|
|
1512
|
+
case "Input": return [...node.schemas, ...node.operations];
|
|
1513
|
+
case "Output": return [];
|
|
583
1514
|
case "Operation": return [
|
|
584
1515
|
...node.parameters,
|
|
585
|
-
...node.requestBody ? [node.requestBody] : [],
|
|
1516
|
+
...node.requestBody?.schema ? [node.requestBody.schema] : [],
|
|
586
1517
|
...node.responses
|
|
587
1518
|
];
|
|
588
1519
|
case "Schema": {
|
|
@@ -591,143 +1522,374 @@ function getChildren(node, recurse) {
|
|
|
591
1522
|
if ("properties" in node && node.properties.length > 0) children.push(...node.properties);
|
|
592
1523
|
if ("items" in node && node.items) children.push(...node.items);
|
|
593
1524
|
if ("members" in node && node.members) children.push(...node.members);
|
|
1525
|
+
if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
|
|
594
1526
|
return children;
|
|
595
1527
|
}
|
|
596
1528
|
case "Property": return [node.schema];
|
|
597
1529
|
case "Parameter": return [node.schema];
|
|
598
1530
|
case "Response": return node.schema ? [node.schema] : [];
|
|
1531
|
+
case "FunctionParameter":
|
|
1532
|
+
case "ParameterGroup":
|
|
1533
|
+
case "FunctionParameters":
|
|
1534
|
+
case "Type": return [];
|
|
1535
|
+
default: return [];
|
|
599
1536
|
}
|
|
600
1537
|
}
|
|
601
1538
|
/**
|
|
602
1539
|
* Depth-first traversal for side effects. Visitor return values are ignored.
|
|
603
|
-
* Sibling nodes at each level are visited concurrently up to `options.concurrency`
|
|
1540
|
+
* Sibling nodes at each level are visited concurrently up to `options.concurrency`
|
|
1541
|
+
* (default: `WALK_CONCURRENCY`).
|
|
1542
|
+
*
|
|
1543
|
+
* @example
|
|
1544
|
+
* ```ts
|
|
1545
|
+
* await walk(root, {
|
|
1546
|
+
* operation(node) {
|
|
1547
|
+
* console.log(node.operationId)
|
|
1548
|
+
* },
|
|
1549
|
+
* })
|
|
1550
|
+
* ```
|
|
1551
|
+
*
|
|
1552
|
+
* @example
|
|
1553
|
+
* ```ts
|
|
1554
|
+
* // Visit only the current node
|
|
1555
|
+
* await walk(root, { depth: 'shallow', root: () => {} })
|
|
1556
|
+
* ```
|
|
604
1557
|
*/
|
|
605
|
-
async function walk(node,
|
|
606
|
-
return _walk(node,
|
|
1558
|
+
async function walk(node, options) {
|
|
1559
|
+
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
607
1560
|
}
|
|
608
|
-
|
|
609
|
-
* Internal recursive walk implementation — calls visitor then recurses into children.
|
|
610
|
-
*/
|
|
611
|
-
async function _walk(node, visitor, recurse, limit) {
|
|
1561
|
+
async function _walk(node, visitor, recurse, limit, parent) {
|
|
612
1562
|
switch (node.kind) {
|
|
613
|
-
case "
|
|
614
|
-
await limit(() => visitor.
|
|
1563
|
+
case "Input":
|
|
1564
|
+
await limit(() => visitor.input?.(node, { parent }));
|
|
1565
|
+
break;
|
|
1566
|
+
case "Output":
|
|
1567
|
+
await limit(() => visitor.output?.(node, { parent }));
|
|
615
1568
|
break;
|
|
616
1569
|
case "Operation":
|
|
617
|
-
await limit(() => visitor.operation?.(node));
|
|
1570
|
+
await limit(() => visitor.operation?.(node, { parent }));
|
|
618
1571
|
break;
|
|
619
1572
|
case "Schema":
|
|
620
|
-
await limit(() => visitor.schema?.(node));
|
|
1573
|
+
await limit(() => visitor.schema?.(node, { parent }));
|
|
621
1574
|
break;
|
|
622
1575
|
case "Property":
|
|
623
|
-
await limit(() => visitor.property?.(node));
|
|
1576
|
+
await limit(() => visitor.property?.(node, { parent }));
|
|
624
1577
|
break;
|
|
625
1578
|
case "Parameter":
|
|
626
|
-
await limit(() => visitor.parameter?.(node));
|
|
1579
|
+
await limit(() => visitor.parameter?.(node, { parent }));
|
|
627
1580
|
break;
|
|
628
1581
|
case "Response":
|
|
629
|
-
await limit(() => visitor.response?.(node));
|
|
1582
|
+
await limit(() => visitor.response?.(node, { parent }));
|
|
630
1583
|
break;
|
|
1584
|
+
case "FunctionParameter":
|
|
1585
|
+
case "ParameterGroup":
|
|
1586
|
+
case "FunctionParameters": break;
|
|
631
1587
|
}
|
|
632
1588
|
const children = getChildren(node, recurse);
|
|
633
|
-
|
|
1589
|
+
for (const child of children) await _walk(child, visitor, recurse, limit, node);
|
|
634
1590
|
}
|
|
635
|
-
function transform(node,
|
|
636
|
-
const
|
|
1591
|
+
function transform(node, options) {
|
|
1592
|
+
const { depth, parent, ...visitor } = options;
|
|
1593
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
637
1594
|
switch (node.kind) {
|
|
638
|
-
case "
|
|
639
|
-
let
|
|
640
|
-
const replaced = visitor.
|
|
641
|
-
if (replaced)
|
|
1595
|
+
case "Input": {
|
|
1596
|
+
let input = node;
|
|
1597
|
+
const replaced = visitor.input?.(input, { parent });
|
|
1598
|
+
if (replaced) input = replaced;
|
|
642
1599
|
return {
|
|
643
|
-
...
|
|
644
|
-
schemas:
|
|
645
|
-
|
|
1600
|
+
...input,
|
|
1601
|
+
schemas: input.schemas.map((s) => transform(s, {
|
|
1602
|
+
...options,
|
|
1603
|
+
parent: input
|
|
1604
|
+
})),
|
|
1605
|
+
operations: input.operations.map((op) => transform(op, {
|
|
1606
|
+
...options,
|
|
1607
|
+
parent: input
|
|
1608
|
+
}))
|
|
646
1609
|
};
|
|
647
1610
|
}
|
|
1611
|
+
case "Output": {
|
|
1612
|
+
let output = node;
|
|
1613
|
+
const replaced = visitor.output?.(output, { parent });
|
|
1614
|
+
if (replaced) output = replaced;
|
|
1615
|
+
return output;
|
|
1616
|
+
}
|
|
648
1617
|
case "Operation": {
|
|
649
1618
|
let op = node;
|
|
650
|
-
const replaced = visitor.operation?.(op);
|
|
1619
|
+
const replaced = visitor.operation?.(op, { parent });
|
|
651
1620
|
if (replaced) op = replaced;
|
|
652
1621
|
return {
|
|
653
1622
|
...op,
|
|
654
|
-
parameters: op.parameters.map((p) => transform(p,
|
|
655
|
-
|
|
656
|
-
|
|
1623
|
+
parameters: op.parameters.map((p) => transform(p, {
|
|
1624
|
+
...options,
|
|
1625
|
+
parent: op
|
|
1626
|
+
})),
|
|
1627
|
+
requestBody: op.requestBody ? {
|
|
1628
|
+
...op.requestBody,
|
|
1629
|
+
schema: op.requestBody.schema ? transform(op.requestBody.schema, {
|
|
1630
|
+
...options,
|
|
1631
|
+
parent: op
|
|
1632
|
+
}) : void 0
|
|
1633
|
+
} : void 0,
|
|
1634
|
+
responses: op.responses.map((r) => transform(r, {
|
|
1635
|
+
...options,
|
|
1636
|
+
parent: op
|
|
1637
|
+
}))
|
|
657
1638
|
};
|
|
658
1639
|
}
|
|
659
1640
|
case "Schema": {
|
|
660
1641
|
let schema = node;
|
|
661
|
-
const replaced = visitor.schema?.(schema);
|
|
1642
|
+
const replaced = visitor.schema?.(schema, { parent });
|
|
662
1643
|
if (replaced) schema = replaced;
|
|
1644
|
+
const childOptions = {
|
|
1645
|
+
...options,
|
|
1646
|
+
parent: schema
|
|
1647
|
+
};
|
|
663
1648
|
return {
|
|
664
1649
|
...schema,
|
|
665
|
-
..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p,
|
|
666
|
-
..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i,
|
|
667
|
-
..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m,
|
|
1650
|
+
..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
|
|
1651
|
+
..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
|
|
1652
|
+
..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
|
|
1653
|
+
..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
|
|
668
1654
|
};
|
|
669
1655
|
}
|
|
670
1656
|
case "Property": {
|
|
671
1657
|
let prop = node;
|
|
672
|
-
const replaced = visitor.property?.(prop);
|
|
1658
|
+
const replaced = visitor.property?.(prop, { parent });
|
|
673
1659
|
if (replaced) prop = replaced;
|
|
674
|
-
return {
|
|
1660
|
+
return createProperty({
|
|
675
1661
|
...prop,
|
|
676
|
-
schema: transform(prop.schema,
|
|
677
|
-
|
|
1662
|
+
schema: transform(prop.schema, {
|
|
1663
|
+
...options,
|
|
1664
|
+
parent: prop
|
|
1665
|
+
})
|
|
1666
|
+
});
|
|
678
1667
|
}
|
|
679
1668
|
case "Parameter": {
|
|
680
1669
|
let param = node;
|
|
681
|
-
const replaced = visitor.parameter?.(param);
|
|
1670
|
+
const replaced = visitor.parameter?.(param, { parent });
|
|
682
1671
|
if (replaced) param = replaced;
|
|
683
|
-
return {
|
|
1672
|
+
return createParameter({
|
|
684
1673
|
...param,
|
|
685
|
-
schema: transform(param.schema,
|
|
686
|
-
|
|
1674
|
+
schema: transform(param.schema, {
|
|
1675
|
+
...options,
|
|
1676
|
+
parent: param
|
|
1677
|
+
})
|
|
1678
|
+
});
|
|
687
1679
|
}
|
|
688
1680
|
case "Response": {
|
|
689
1681
|
let response = node;
|
|
690
|
-
const replaced = visitor.response?.(response);
|
|
1682
|
+
const replaced = visitor.response?.(response, { parent });
|
|
691
1683
|
if (replaced) response = replaced;
|
|
692
1684
|
return {
|
|
693
1685
|
...response,
|
|
694
|
-
schema:
|
|
1686
|
+
schema: transform(response.schema, {
|
|
1687
|
+
...options,
|
|
1688
|
+
parent: response
|
|
1689
|
+
})
|
|
695
1690
|
};
|
|
696
1691
|
}
|
|
1692
|
+
case "FunctionParameter":
|
|
1693
|
+
case "ParameterGroup":
|
|
1694
|
+
case "FunctionParameters":
|
|
1695
|
+
case "Type": return node;
|
|
1696
|
+
default: return node;
|
|
697
1697
|
}
|
|
698
1698
|
}
|
|
699
1699
|
/**
|
|
700
|
-
*
|
|
1700
|
+
* Runs a depth-first synchronous collection pass.
|
|
1701
|
+
*
|
|
1702
|
+
* Non-`undefined` values returned by visitor callbacks are appended to the result.
|
|
1703
|
+
*
|
|
1704
|
+
* @example
|
|
1705
|
+
* ```ts
|
|
1706
|
+
* const ids = collect(root, {
|
|
1707
|
+
* operation(node) {
|
|
1708
|
+
* return node.operationId
|
|
1709
|
+
* },
|
|
1710
|
+
* })
|
|
1711
|
+
* ```
|
|
1712
|
+
*
|
|
1713
|
+
* @example
|
|
1714
|
+
* ```ts
|
|
1715
|
+
* // Collect from only the current node
|
|
1716
|
+
* const values = collect(root, { depth: 'shallow', root: () => 'root' })
|
|
1717
|
+
* ```
|
|
701
1718
|
*/
|
|
702
|
-
function collect(node,
|
|
703
|
-
const
|
|
1719
|
+
function collect(node, options) {
|
|
1720
|
+
const { depth, parent, ...visitor } = options;
|
|
1721
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
704
1722
|
const results = [];
|
|
705
1723
|
let v;
|
|
706
1724
|
switch (node.kind) {
|
|
707
|
-
case "
|
|
708
|
-
v = visitor.
|
|
1725
|
+
case "Input":
|
|
1726
|
+
v = visitor.input?.(node, { parent });
|
|
1727
|
+
break;
|
|
1728
|
+
case "Output":
|
|
1729
|
+
v = visitor.output?.(node, { parent });
|
|
709
1730
|
break;
|
|
710
1731
|
case "Operation":
|
|
711
|
-
v = visitor.operation?.(node);
|
|
1732
|
+
v = visitor.operation?.(node, { parent });
|
|
712
1733
|
break;
|
|
713
1734
|
case "Schema":
|
|
714
|
-
v = visitor.schema?.(node);
|
|
1735
|
+
v = visitor.schema?.(node, { parent });
|
|
715
1736
|
break;
|
|
716
1737
|
case "Property":
|
|
717
|
-
v = visitor.property?.(node);
|
|
1738
|
+
v = visitor.property?.(node, { parent });
|
|
718
1739
|
break;
|
|
719
1740
|
case "Parameter":
|
|
720
|
-
v = visitor.parameter?.(node);
|
|
1741
|
+
v = visitor.parameter?.(node, { parent });
|
|
721
1742
|
break;
|
|
722
1743
|
case "Response":
|
|
723
|
-
v = visitor.response?.(node);
|
|
1744
|
+
v = visitor.response?.(node, { parent });
|
|
724
1745
|
break;
|
|
1746
|
+
case "FunctionParameter":
|
|
1747
|
+
case "ParameterGroup":
|
|
1748
|
+
case "FunctionParameters": break;
|
|
725
1749
|
}
|
|
726
1750
|
if (v !== void 0) results.push(v);
|
|
727
|
-
for (const child of getChildren(node, recurse)) for (const item of collect(child,
|
|
1751
|
+
for (const child of getChildren(node, recurse)) for (const item of collect(child, {
|
|
1752
|
+
...options,
|
|
1753
|
+
parent: node
|
|
1754
|
+
})) results.push(item);
|
|
728
1755
|
return results;
|
|
729
1756
|
}
|
|
730
1757
|
//#endregion
|
|
731
|
-
|
|
1758
|
+
//#region src/resolvers.ts
|
|
1759
|
+
function findDiscriminator(mapping, ref) {
|
|
1760
|
+
if (!mapping || !ref) return null;
|
|
1761
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
1762
|
+
}
|
|
1763
|
+
function childName(parentName, propName) {
|
|
1764
|
+
return parentName ? pascalCase([parentName, propName].join(" ")) : null;
|
|
1765
|
+
}
|
|
1766
|
+
function enumPropName(parentName, propName, enumSuffix) {
|
|
1767
|
+
return pascalCase([
|
|
1768
|
+
parentName,
|
|
1769
|
+
propName,
|
|
1770
|
+
enumSuffix
|
|
1771
|
+
].filter(Boolean).join(" "));
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Collects import entries for all `ref` schema nodes in `node`.
|
|
1775
|
+
*/
|
|
1776
|
+
function collectImports({ node, nameMapping, resolve }) {
|
|
1777
|
+
return collect(node, { schema(schemaNode) {
|
|
1778
|
+
const schemaRef = narrowSchema(schemaNode, "ref");
|
|
1779
|
+
if (!schemaRef?.ref) return;
|
|
1780
|
+
const rawName = extractRefName(schemaRef.ref);
|
|
1781
|
+
const result = resolve(nameMapping.get(rawName) ?? rawName);
|
|
1782
|
+
if (!result) return;
|
|
1783
|
+
return result;
|
|
1784
|
+
} });
|
|
1785
|
+
}
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/transformers.ts
|
|
1788
|
+
/**
|
|
1789
|
+
* Replaces a discriminator property's schema with a string enum of allowed values.
|
|
1790
|
+
*
|
|
1791
|
+
* If `node` is not an object schema, or if the property does not exist, the input
|
|
1792
|
+
* node is returned as-is.
|
|
1793
|
+
*
|
|
1794
|
+
* @example
|
|
1795
|
+
* ```ts
|
|
1796
|
+
* const schema = createSchema({
|
|
1797
|
+
* type: 'object',
|
|
1798
|
+
* properties: [createProperty({ name: 'type', required: true, schema: createSchema({ type: 'string' }) })],
|
|
1799
|
+
* })
|
|
1800
|
+
* const result = setDiscriminatorEnum({ node: schema, propertyName: 'type', values: ['dog', 'cat'] })
|
|
1801
|
+
* ```
|
|
1802
|
+
*/
|
|
1803
|
+
function setDiscriminatorEnum({ node, propertyName, values, enumName }) {
|
|
1804
|
+
const objectNode = narrowSchema(node, "object");
|
|
1805
|
+
if (!objectNode?.properties?.length) return node;
|
|
1806
|
+
if (!objectNode.properties.some((prop) => prop.name === propertyName)) return node;
|
|
1807
|
+
return createSchema({
|
|
1808
|
+
...objectNode,
|
|
1809
|
+
properties: objectNode.properties.map((prop) => {
|
|
1810
|
+
if (prop.name !== propertyName) return prop;
|
|
1811
|
+
return createProperty({
|
|
1812
|
+
...prop,
|
|
1813
|
+
schema: createSchema({
|
|
1814
|
+
type: "enum",
|
|
1815
|
+
primitive: "string",
|
|
1816
|
+
enumValues: values,
|
|
1817
|
+
name: enumName,
|
|
1818
|
+
readOnly: prop.schema.readOnly,
|
|
1819
|
+
writeOnly: prop.schema.writeOnly
|
|
1820
|
+
})
|
|
1821
|
+
});
|
|
1822
|
+
})
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Merges adjacent anonymous object members into a single anonymous object member.
|
|
1827
|
+
*
|
|
1828
|
+
* @example
|
|
1829
|
+
* ```ts
|
|
1830
|
+
* const merged = mergeAdjacentObjects([
|
|
1831
|
+
* createSchema({ type: 'object', properties: [createProperty({ name: 'a', schema: createSchema({ type: 'string' }) })] }),
|
|
1832
|
+
* createSchema({ type: 'object', properties: [createProperty({ name: 'b', schema: createSchema({ type: 'number' }) })] }),
|
|
1833
|
+
* ])
|
|
1834
|
+
* ```
|
|
1835
|
+
*/
|
|
1836
|
+
function mergeAdjacentObjects(members) {
|
|
1837
|
+
return members.reduce((acc, member) => {
|
|
1838
|
+
const objectMember = narrowSchema(member, "object");
|
|
1839
|
+
if (objectMember && !objectMember.name) {
|
|
1840
|
+
const previous = acc.at(-1);
|
|
1841
|
+
const previousObject = previous ? narrowSchema(previous, "object") : void 0;
|
|
1842
|
+
if (previousObject && !previousObject.name) {
|
|
1843
|
+
acc[acc.length - 1] = createSchema({
|
|
1844
|
+
...previousObject,
|
|
1845
|
+
properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
|
|
1846
|
+
});
|
|
1847
|
+
return acc;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
acc.push(member);
|
|
1851
|
+
return acc;
|
|
1852
|
+
}, []);
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Removes enum members that are covered by broader scalar primitives in the same union.
|
|
1856
|
+
*
|
|
1857
|
+
* @example
|
|
1858
|
+
* ```ts
|
|
1859
|
+
* const simplified = simplifyUnion([
|
|
1860
|
+
* createSchema({ type: 'enum', primitive: 'string', enumValues: ['active'] }),
|
|
1861
|
+
* createSchema({ type: 'string' }),
|
|
1862
|
+
* ])
|
|
1863
|
+
* // keeps only string member
|
|
1864
|
+
* ```
|
|
1865
|
+
*/
|
|
1866
|
+
function simplifyUnion(members) {
|
|
1867
|
+
const scalarPrimitives = new Set(members.filter((member) => isScalarPrimitive(member.type)).map((m) => m.type));
|
|
1868
|
+
if (!scalarPrimitives.size) return members;
|
|
1869
|
+
return members.filter((member) => {
|
|
1870
|
+
const enumNode = narrowSchema(member, "enum");
|
|
1871
|
+
if (!enumNode) return true;
|
|
1872
|
+
const primitive = enumNode.primitive;
|
|
1873
|
+
if (!primitive) return true;
|
|
1874
|
+
if ((enumNode.namedEnumValues?.length ?? enumNode.enumValues?.length ?? 0) <= 1) return true;
|
|
1875
|
+
if (scalarPrimitives.has(primitive)) return false;
|
|
1876
|
+
if ((primitive === "integer" || primitive === "number") && (scalarPrimitives.has("integer") || scalarPrimitives.has("number"))) return false;
|
|
1877
|
+
return true;
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
1881
|
+
const enumNode = narrowSchema(propNode, "enum");
|
|
1882
|
+
if (enumNode?.primitive === "boolean") return {
|
|
1883
|
+
...propNode,
|
|
1884
|
+
name: void 0
|
|
1885
|
+
};
|
|
1886
|
+
if (enumNode) return {
|
|
1887
|
+
...propNode,
|
|
1888
|
+
name: enumPropName(parentName, propName, enumSuffix)
|
|
1889
|
+
};
|
|
1890
|
+
return propNode;
|
|
1891
|
+
}
|
|
1892
|
+
//#endregion
|
|
1893
|
+
export { caseParams, childName, collect, collectImports, createArrowFunction, createBreak, createConst, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter, createParameterGroup, createParamsType, createPrinterFactory, createProperty, createResponse, createSchema, createSource, createText, createType, definePrinter, enumPropName, extractRefName, extractStringsFromNodes, findDiscriminator, httpMethods, isInputNode, isOperationNode, isOutputNode, isScalarPrimitive, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, narrowSchema, nodeKinds, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, walk };
|
|
732
1894
|
|
|
733
1895
|
//# sourceMappingURL=index.js.map
|