@kubb/ast 5.0.0-alpha.6 → 5.0.0-alpha.60

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