@kubb/ast 5.0.0-alpha.5 → 5.0.0-alpha.51

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