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

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