@kubb/ast 5.0.0-alpha.4 → 5.0.0-alpha.41

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