@kubb/ast 5.0.0-beta.1 → 5.0.0-beta.100

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,36 +1,15 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { createHash } from "node:crypto";
1
+ import { t as __exportAll } from "./rolldown-runtime-CNktS9qV.js";
2
+ import { hash } from "node:crypto";
3
3
  import path from "node:path";
4
4
  //#region src/constants.ts
5
5
  const visitorDepths = {
6
6
  shallow: "shallow",
7
7
  deep: "deep"
8
8
  };
9
- const nodeKinds = {
10
- input: "Input",
11
- output: "Output",
12
- operation: "Operation",
13
- schema: "Schema",
14
- property: "Property",
15
- parameter: "Parameter",
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"
27
- };
28
9
  /**
29
10
  * Schema type discriminators used by all AST schema nodes.
30
11
  *
31
- * These values serve as stable discriminators across the AST (e.g., `schema.type === schemaTypes.object`).
32
- * Grouped by category: primitives (`string`, `number`, `boolean`), structural types (`object`, `array`, `union`),
33
- * and format-specific types (`date`, `uuid`, `email`). Use `isScalarPrimitive()` to check for scalar types.
12
+ * Each value is a stable discriminator across the AST (for example `schema.type === schemaTypes.object`).
34
13
  */
35
14
  const schemaTypes = {
36
15
  /**
@@ -50,7 +29,7 @@ const schemaTypes = {
50
29
  */
51
30
  bigint: "bigint",
52
31
  /**
53
- * Boolean value
32
+ * Boolean value.
54
33
  */
55
34
  boolean: "boolean",
56
35
  /**
@@ -138,935 +117,314 @@ const schemaTypes = {
138
117
  */
139
118
  never: "never"
140
119
  };
120
+ //#endregion
121
+ //#region src/guards.ts
141
122
  /**
142
- * Scalar primitive schema types used for union simplification and type narrowing.
143
- *
144
- * Use `isScalarPrimitive()` to safely check whether a type is a scalar primitive.
145
- */
146
- const SCALAR_PRIMITIVE_TYPES = new Set([
147
- "string",
148
- "number",
149
- "integer",
150
- "bigint",
151
- "boolean"
152
- ]);
153
- /**
154
- * Type guard that returns `true` when `type` is a scalar primitive schema type.
123
+ * Narrows a `SchemaNode` to the variant that matches `type`.
155
124
  *
156
- * Use this to check if a schema type can be directly assigned without wrapping (e.g., `string | number | boolean`).
125
+ * @example
126
+ * ```ts
127
+ * const schema = createSchema({ type: 'string' })
128
+ * const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
129
+ * ```
157
130
  */
158
- function isScalarPrimitive(type) {
159
- return SCALAR_PRIMITIVE_TYPES.has(type);
131
+ function narrowSchema(node, type) {
132
+ return node?.type === type ? node : null;
160
133
  }
161
134
  /**
162
- * HTTP method identifiers used by operation nodes.
135
+ * Narrows an `OperationNode` to an `HttpOperationNode` so `method` and `path` are present.
163
136
  *
164
- * Includes all standard HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE).
137
+ * @example
138
+ * ```ts
139
+ * if (isHttpOperationNode(node)) {
140
+ * console.log(node.method, node.path)
141
+ * }
142
+ * ```
165
143
  */
166
- const httpMethods = {
167
- get: "GET",
168
- post: "POST",
169
- put: "PUT",
170
- patch: "PATCH",
171
- delete: "DELETE",
172
- head: "HEAD",
173
- options: "OPTIONS",
174
- trace: "TRACE"
175
- };
176
- /**
177
- * Common MIME types used in request/response content negotiation.
178
- *
179
- * Covers JSON, XML, form data, PDFs, images, audio, and video formats.
180
- * Use these as keys when serializing request/response bodies.
181
- */
182
- const mediaTypes = {
183
- applicationJson: "application/json",
184
- applicationXml: "application/xml",
185
- applicationFormUrlEncoded: "application/x-www-form-urlencoded",
186
- applicationOctetStream: "application/octet-stream",
187
- applicationPdf: "application/pdf",
188
- applicationZip: "application/zip",
189
- applicationGraphql: "application/graphql",
190
- multipartFormData: "multipart/form-data",
191
- textPlain: "text/plain",
192
- textHtml: "text/html",
193
- textCsv: "text/csv",
194
- textXml: "text/xml",
195
- imagePng: "image/png",
196
- imageJpeg: "image/jpeg",
197
- imageGif: "image/gif",
198
- imageWebp: "image/webp",
199
- imageSvgXml: "image/svg+xml",
200
- audioMpeg: "audio/mpeg",
201
- videoMp4: "video/mp4"
202
- };
144
+ function isHttpOperationNode(node) {
145
+ return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
146
+ }
203
147
  //#endregion
204
- //#region ../../internals/utils/src/casing.ts
148
+ //#region src/defineNode.ts
205
149
  /**
206
- * Shared implementation for camelCase and PascalCase conversion.
207
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
208
- * and capitalizes each word according to `pascal`.
209
- *
210
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
150
+ * Visitor callback names, one per traversable node kind, in traversal order.
151
+ * Kept in sync with the keys of `Visitor` in `visitor.ts`.
211
152
  */
212
- function toCamelOrPascal(text, pascal) {
213
- 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) => {
214
- if (word.length > 1 && word === word.toUpperCase()) return word;
215
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
216
- return word.charAt(0).toUpperCase() + word.slice(1);
217
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
218
- }
153
+ const visitorKeys = [
154
+ "input",
155
+ "output",
156
+ "operation",
157
+ "schema",
158
+ "property",
159
+ "parameter",
160
+ "response"
161
+ ];
219
162
  /**
220
- * Splits `text` on `.` and applies `transformPart` to each segment.
221
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
222
- * Segments are joined with `/` to form a file path.
223
- *
224
- * Only splits on dots followed by a letter so that version numbers
225
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
226
- *
227
- * Empty segments are filtered before joining. They arise when the text starts with
228
- * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
229
- * and `'..'` transforms to an empty string). Without this filter the join would produce
230
- * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
231
- * generated files to escape the configured output directory.
163
+ * Builds a type guard that matches nodes of the given `kind`.
232
164
  */
233
- function applyToFileParts(text, transformPart) {
234
- const parts = text.split(/\.(?=[a-zA-Z])/);
235
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
236
- }
237
- /**
238
- * Converts `text` to camelCase.
239
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
240
- *
241
- * @example
242
- * camelCase('hello-world') // 'helloWorld'
243
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
244
- */
245
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
246
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
247
- prefix,
248
- suffix
249
- } : {}));
250
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
165
+ function isKind(kind) {
166
+ return (node) => node?.kind === kind;
251
167
  }
252
168
  /**
253
- * Converts `text` to PascalCase.
254
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
169
+ * Defines a node once and derives its `create` builder, `is` guard, and traversal
170
+ * metadata. `create` merges `defaults`, the `build` hook (or the raw input), and the
171
+ * `kind`, so node construction lives in one place without scattered `as` casts.
255
172
  *
256
- * @example
257
- * pascalCase('hello-world') // 'HelloWorld'
258
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
259
- */
260
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
261
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
262
- prefix,
263
- suffix
264
- }) : camelCase(part));
265
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
266
- }
267
- //#endregion
268
- //#region ../../internals/utils/src/reserved.ts
269
- /**
270
- * JavaScript and Java reserved words.
271
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
272
- */
273
- const reservedWords = new Set([
274
- "abstract",
275
- "arguments",
276
- "boolean",
277
- "break",
278
- "byte",
279
- "case",
280
- "catch",
281
- "char",
282
- "class",
283
- "const",
284
- "continue",
285
- "debugger",
286
- "default",
287
- "delete",
288
- "do",
289
- "double",
290
- "else",
291
- "enum",
292
- "eval",
293
- "export",
294
- "extends",
295
- "false",
296
- "final",
297
- "finally",
298
- "float",
299
- "for",
300
- "function",
301
- "goto",
302
- "if",
303
- "implements",
304
- "import",
305
- "in",
306
- "instanceof",
307
- "int",
308
- "interface",
309
- "let",
310
- "long",
311
- "native",
312
- "new",
313
- "null",
314
- "package",
315
- "private",
316
- "protected",
317
- "public",
318
- "return",
319
- "short",
320
- "static",
321
- "super",
322
- "switch",
323
- "synchronized",
324
- "this",
325
- "throw",
326
- "throws",
327
- "transient",
328
- "true",
329
- "try",
330
- "typeof",
331
- "var",
332
- "void",
333
- "volatile",
334
- "while",
335
- "with",
336
- "yield",
337
- "Array",
338
- "Date",
339
- "hasOwnProperty",
340
- "Infinity",
341
- "isFinite",
342
- "isNaN",
343
- "isPrototypeOf",
344
- "length",
345
- "Math",
346
- "name",
347
- "NaN",
348
- "Number",
349
- "Object",
350
- "prototype",
351
- "String",
352
- "toString",
353
- "undefined",
354
- "valueOf"
355
- ]);
356
- /**
357
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
173
+ * @example Simple node
174
+ * ```ts
175
+ * const importDef = defineNode<ImportNode>({ kind: 'Import' })
176
+ * const createImport = importDef.create
177
+ * ```
358
178
  *
359
- * @example
179
+ * @example Node with a build hook
360
180
  * ```ts
361
- * isValidVarName('status') // true
362
- * isValidVarName('class') // false (reserved word)
363
- * isValidVarName('42foo') // false (starts with digit)
181
+ * const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
182
+ * kind: 'Property',
183
+ * build: (props) => ({ ...props, required: props.required ?? false }),
184
+ * children: ['schema'],
185
+ * visitorKey: 'property',
186
+ * })
364
187
  * ```
365
188
  */
366
- function isValidVarName(name) {
367
- if (!name || reservedWords.has(name)) return false;
368
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
189
+ function defineNode(config) {
190
+ const { kind, defaults, build, children, visitorKey } = config;
191
+ function create(input) {
192
+ const base = build ? build(input) : input;
193
+ const node = {
194
+ kind,
195
+ ...defaults,
196
+ ...base
197
+ };
198
+ node.kind = kind;
199
+ return node;
200
+ }
201
+ return {
202
+ kind,
203
+ create,
204
+ is: isKind(kind),
205
+ children,
206
+ visitorKey
207
+ };
369
208
  }
370
209
  //#endregion
371
- //#region ../../internals/utils/src/string.ts
210
+ //#region src/nodes/code.ts
372
211
  /**
373
- * Strips the file extension from a path or file name.
374
- * Only removes the last `.ext` segment when the dot is not part of a directory name.
375
- *
376
- * @example
377
- * trimExtName('petStore.ts') // 'petStore'
378
- * trimExtName('/src/models/pet.ts') // '/src/models/pet'
379
- * trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'
380
- * trimExtName('noExtension') // 'noExtension'
212
+ * Definition for the {@link ConstNode}.
381
213
  */
382
- function trimExtName(text) {
383
- const dotIndex = text.lastIndexOf(".");
384
- if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
385
- return text;
386
- }
387
- //#endregion
388
- //#region src/guards.ts
214
+ const constDef = defineNode({ kind: "Const" });
389
215
  /**
390
- * Narrows a `SchemaNode` to the variant that matches `type`.
391
- *
392
- * @example
393
- * ```ts
394
- * const schema = createSchema({ type: 'string' })
395
- * const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | undefined
396
- * ```
216
+ * Definition for the {@link TypeNode}.
397
217
  */
398
- function narrowSchema(node, type) {
399
- return node?.type === type ? node : void 0;
400
- }
401
- function isKind(kind) {
402
- return (node) => node.kind === kind;
403
- }
218
+ const typeDef = defineNode({ kind: "Type" });
404
219
  /**
405
- * Returns `true` when the input is an `InputNode`.
406
- *
407
- * @example
408
- * ```ts
409
- * if (isInputNode(node)) {
410
- * console.log(node.schemas.length)
411
- * }
412
- * ```
220
+ * Definition for the {@link FunctionNode}.
413
221
  */
414
- const isInputNode = isKind("Input");
222
+ const functionDef = defineNode({ kind: "Function" });
415
223
  /**
416
- * Returns `true` when the input is an `OutputNode`.
417
- *
418
- * @example
419
- * ```ts
420
- * if (isOutputNode(node)) {
421
- * console.log(node.files.length)
422
- * }
423
- * ```
224
+ * Definition for the {@link ArrowFunctionNode}.
424
225
  */
425
- const isOutputNode = isKind("Output");
226
+ const arrowFunctionDef = defineNode({ kind: "ArrowFunction" });
426
227
  /**
427
- * Returns `true` when the input is an `OperationNode`.
428
- *
429
- * @example
430
- * ```ts
431
- * if (isOperationNode(node)) {
432
- * console.log(node.operationId)
433
- * }
434
- * ```
228
+ * Definition for the {@link TextNode}.
435
229
  */
436
- const isOperationNode = isKind("Operation");
230
+ const textDef = defineNode({
231
+ kind: "Text",
232
+ build: (value) => ({ value })
233
+ });
437
234
  /**
438
- * Returns `true` when the input is a `SchemaNode`.
235
+ * Definition for the {@link BreakNode}.
236
+ */
237
+ const breakDef = defineNode({
238
+ kind: "Break",
239
+ build: () => ({})
240
+ });
241
+ /**
242
+ * Definition for the {@link JsxNode}.
243
+ */
244
+ const jsxDef = defineNode({
245
+ kind: "Jsx",
246
+ build: (value) => ({ value })
247
+ });
248
+ /**
249
+ * Creates a `ConstNode` representing a TypeScript `const` declaration.
439
250
  *
440
- * @example
251
+ * @example Exported constant with type and `as const`
441
252
  * ```ts
442
- * if (isSchemaNode(node)) {
443
- * console.log(node.type)
444
- * }
253
+ * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
254
+ * // export const pets: Pet[] = ... as const
445
255
  * ```
446
256
  */
447
- const isSchemaNode = isKind("Schema");
448
- isKind("Property");
449
- isKind("Parameter");
450
- isKind("Response");
451
- isKind("FunctionParameter");
452
- isKind("ParameterGroup");
453
- isKind("FunctionParameters");
454
- //#endregion
455
- //#region src/refs.ts
257
+ const createConst = constDef.create;
456
258
  /**
457
- * Returns the last path segment of a reference string.
458
- *
459
- * Example: `#/components/schemas/Pet` becomes `Pet`.
259
+ * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
460
260
  *
461
261
  * @example
462
262
  * ```ts
463
- * extractRefName('#/components/schemas/Pet') // 'Pet'
263
+ * createType({ name: 'Pet', export: true })
264
+ * // export type Pet = ...
464
265
  * ```
465
266
  */
466
- function extractRefName(ref) {
467
- return ref.split("/").at(-1) ?? ref;
468
- }
469
- //#endregion
470
- //#region src/visitor.ts
267
+ const createType = typeDef.create;
471
268
  /**
472
- * Creates a small async concurrency limiter.
473
- *
474
- * At most `concurrency` tasks are in flight at once. Extra tasks are queued.
269
+ * Creates a `FunctionNode` representing a TypeScript `function` declaration.
475
270
  *
476
271
  * @example
477
272
  * ```ts
478
- * const limit = createLimit(2)
479
- * for (const task of [taskA, taskB, taskC]) {
480
- * await limit(() => task())
481
- * }
482
- * // only 2 tasks run at the same time
273
+ * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
274
+ * // export async function fetchPet(): Promise<Pet> { ... }
483
275
  * ```
484
276
  */
485
- function createLimit(concurrency) {
486
- let active = 0;
487
- const queue = [];
488
- function next() {
489
- if (active < concurrency && queue.length > 0) {
490
- active++;
491
- queue.shift()();
492
- }
493
- }
494
- return function limit(fn) {
495
- return new Promise((resolve, reject) => {
496
- queue.push(() => {
497
- Promise.resolve(fn()).then(resolve, reject).finally(() => {
498
- active--;
499
- next();
500
- });
501
- });
502
- next();
503
- });
504
- };
505
- }
277
+ const createFunction = functionDef.create;
506
278
  /**
507
- * Returns the immediate traversable children of `node`.
508
- *
509
- * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
510
- * `additionalProperties`) are only included
511
- * when `recurse` is `true`; shallow mode skips them.
279
+ * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
512
280
  *
513
281
  * @example
514
282
  * ```ts
515
- * const children = getChildren(operationNode, true)
516
- * // returns parameters, requestBody schema (if present), and responses
283
+ * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
284
+ * // export const double = (n: number) => ...
517
285
  * ```
518
286
  */
519
- function getChildren(node, recurse) {
520
- switch (node.kind) {
521
- case "Input": return [...node.schemas, ...node.operations];
522
- case "Output": return [];
523
- case "Operation": return [
524
- ...node.parameters,
525
- ...node.requestBody?.content?.flatMap((c) => c.schema ? [c.schema] : []) ?? [],
526
- ...node.responses
527
- ];
528
- case "Schema": {
529
- const children = [];
530
- if (!recurse) return [];
531
- if ("properties" in node && node.properties.length > 0) children.push(...node.properties);
532
- if ("items" in node && node.items) children.push(...node.items);
533
- if ("members" in node && node.members) children.push(...node.members);
534
- if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
535
- return children;
536
- }
537
- case "Property": return [node.schema];
538
- case "Parameter": return [node.schema];
539
- case "Response": return node.schema ? [node.schema] : [];
540
- case "FunctionParameter":
541
- case "ParameterGroup":
542
- case "FunctionParameters":
543
- case "Type": return [];
544
- default: return [];
545
- }
546
- }
287
+ const createArrowFunction = arrowFunctionDef.create;
547
288
  /**
548
- * Depth-first traversal for side effects. Visitor return values are ignored.
549
- * Sibling nodes at each level are visited concurrently up to `options.concurrency`
550
- * (default: `WALK_CONCURRENCY`).
289
+ * Creates a {@link TextNode} representing a raw string fragment in the source output.
551
290
  *
552
291
  * @example
553
292
  * ```ts
554
- * await walk(root, {
555
- * operation(node) {
556
- * console.log(node.operationId)
557
- * },
558
- * })
293
+ * createText('return fetch(id)')
294
+ * // { kind: 'Text', value: 'return fetch(id)' }
559
295
  * ```
296
+ */
297
+ const createText = textDef.create;
298
+ /**
299
+ * Creates a {@link BreakNode} representing a line break in the source output.
560
300
  *
561
301
  * @example
562
302
  * ```ts
563
- * // Visit only the current node
564
- * await walk(root, { depth: 'shallow', root: () => {} })
303
+ * createBreak()
304
+ * // { kind: 'Break' }
565
305
  * ```
566
306
  */
567
- async function walk(node, options) {
568
- return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
569
- }
570
- async function _walk(node, visitor, recurse, limit, parent) {
571
- switch (node.kind) {
572
- case "Input":
573
- await limit(() => visitor.input?.(node, { parent }));
574
- break;
575
- case "Output":
576
- await limit(() => visitor.output?.(node, { parent }));
577
- break;
578
- case "Operation":
579
- await limit(() => visitor.operation?.(node, { parent }));
580
- break;
581
- case "Schema":
582
- await limit(() => visitor.schema?.(node, { parent }));
583
- break;
584
- case "Property":
585
- await limit(() => visitor.property?.(node, { parent }));
586
- break;
587
- case "Parameter":
588
- await limit(() => visitor.parameter?.(node, { parent }));
589
- break;
590
- case "Response":
591
- await limit(() => visitor.response?.(node, { parent }));
592
- break;
593
- case "FunctionParameter":
594
- case "ParameterGroup":
595
- case "FunctionParameters": break;
596
- }
597
- const children = getChildren(node, recurse);
598
- for (const child of children) await _walk(child, visitor, recurse, limit, node);
599
- }
600
- function transform(node, options) {
601
- const { depth, parent, ...visitor } = options;
602
- const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
603
- switch (node.kind) {
604
- case "Input": {
605
- let input = node;
606
- const replaced = visitor.input?.(input, { parent });
607
- if (replaced) input = replaced;
608
- return {
609
- ...input,
610
- schemas: input.schemas.map((s) => transform(s, {
611
- ...options,
612
- parent: input
613
- })),
614
- operations: input.operations.map((op) => transform(op, {
615
- ...options,
616
- parent: input
617
- }))
618
- };
619
- }
620
- case "Output": {
621
- let output = node;
622
- const replaced = visitor.output?.(output, { parent });
623
- if (replaced) output = replaced;
624
- return output;
625
- }
626
- case "Operation": {
627
- let op = node;
628
- const replaced = visitor.operation?.(op, { parent });
629
- if (replaced) op = replaced;
630
- return {
631
- ...op,
632
- parameters: op.parameters.map((p) => transform(p, {
633
- ...options,
634
- parent: op
635
- })),
636
- requestBody: op.requestBody ? {
637
- ...op.requestBody,
638
- content: op.requestBody.content?.map((c) => ({
639
- ...c,
640
- schema: c.schema ? transform(c.schema, {
641
- ...options,
642
- parent: op
643
- }) : void 0
644
- }))
645
- } : void 0,
646
- responses: op.responses.map((r) => transform(r, {
647
- ...options,
648
- parent: op
649
- }))
650
- };
651
- }
652
- case "Schema": {
653
- let schema = node;
654
- const replaced = visitor.schema?.(schema, { parent });
655
- if (replaced) schema = replaced;
656
- const childOptions = {
657
- ...options,
658
- parent: schema
659
- };
660
- return {
661
- ...schema,
662
- ..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
663
- ..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
664
- ..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
665
- ..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
666
- };
667
- }
668
- case "Property": {
669
- let prop = node;
670
- const replaced = visitor.property?.(prop, { parent });
671
- if (replaced) prop = replaced;
672
- return createProperty({
673
- ...prop,
674
- schema: transform(prop.schema, {
675
- ...options,
676
- parent: prop
677
- })
678
- });
679
- }
680
- case "Parameter": {
681
- let param = node;
682
- const replaced = visitor.parameter?.(param, { parent });
683
- if (replaced) param = replaced;
684
- return createParameter({
685
- ...param,
686
- schema: transform(param.schema, {
687
- ...options,
688
- parent: param
689
- })
690
- });
691
- }
692
- case "Response": {
693
- let response = node;
694
- const replaced = visitor.response?.(response, { parent });
695
- if (replaced) response = replaced;
696
- return {
697
- ...response,
698
- schema: transform(response.schema, {
699
- ...options,
700
- parent: response
701
- })
702
- };
703
- }
704
- case "FunctionParameter":
705
- case "ParameterGroup":
706
- case "FunctionParameters":
707
- case "Type": return node;
708
- default: return node;
709
- }
307
+ function createBreak() {
308
+ return breakDef.create();
710
309
  }
711
310
  /**
712
- * Runs a depth-first synchronous collection pass.
713
- *
714
- * Non-`undefined` values returned by visitor callbacks are appended to the result.
715
- *
716
- * @example
717
- * ```ts
718
- * const ids = collect(root, {
719
- * operation(node) {
720
- * return node.operationId
721
- * },
722
- * })
723
- * ```
311
+ * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
724
312
  *
725
313
  * @example
726
314
  * ```ts
727
- * // Collect from only the current node
728
- * const values = collect(root, { depth: 'shallow', root: () => 'root' })
315
+ * createJsx('<>\n <a href={href}>Open</a>\n</>')
316
+ * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
729
317
  * ```
730
318
  */
731
- function collect(node, options) {
732
- const { depth, parent, ...visitor } = options;
733
- const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
734
- const results = [];
735
- let v;
736
- switch (node.kind) {
737
- case "Input":
738
- v = visitor.input?.(node, { parent });
739
- break;
740
- case "Output":
741
- v = visitor.output?.(node, { parent });
742
- break;
743
- case "Operation":
744
- v = visitor.operation?.(node, { parent });
745
- break;
746
- case "Schema":
747
- v = visitor.schema?.(node, { parent });
748
- break;
749
- case "Property":
750
- v = visitor.property?.(node, { parent });
751
- break;
752
- case "Parameter":
753
- v = visitor.parameter?.(node, { parent });
754
- break;
755
- case "Response":
756
- v = visitor.response?.(node, { parent });
757
- break;
758
- case "FunctionParameter":
759
- case "ParameterGroup":
760
- case "FunctionParameters": break;
761
- }
762
- if (v !== void 0) results.push(v);
763
- for (const child of getChildren(node, recurse)) for (const item of collect(child, {
764
- ...options,
765
- parent: node
766
- })) results.push(item);
767
- return results;
768
- }
319
+ const createJsx = jsxDef.create;
769
320
  //#endregion
770
- //#region src/utils.ts
771
- const plainStringTypes = new Set([
772
- "string",
773
- "uuid",
774
- "email",
775
- "url",
776
- "datetime"
777
- ]);
321
+ //#region src/nodes/content.ts
778
322
  /**
779
- * Merges a ref node with its resolved schema, giving usage-site fields precedence.
780
- *
781
- * Usage-site fields (`description`, `readOnly`, `nullable`, `deprecated`) on the ref node
782
- * override the same fields in the resolved `node.schema`. Non-ref nodes are returned unchanged.
783
- *
784
- * @example
785
- * ```ts
786
- * // Ref with description override
787
- * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A cute pet' })
788
- * const merged = syncSchemaRef(ref) // merges with resolved Pet schema
789
- * ```
323
+ * Definition for the {@link ContentNode}.
790
324
  */
791
- function syncSchemaRef(node) {
792
- const ref = narrowSchema(node, "ref");
793
- if (!ref) return node;
794
- if (!ref.schema) return node;
795
- const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref;
796
- const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
797
- return createSchema({
798
- ...ref.schema,
799
- ...definedOverrides
800
- });
801
- }
325
+ const contentDef = defineNode({
326
+ kind: "Content",
327
+ children: ["schema"]
328
+ });
802
329
  /**
803
- * Type guard that returns `true` when a schema emits as a plain `string` type.
804
- *
805
- * Covers `string`, `uuid`, `email`, `url`, and `datetime` types. For `date` and `time`
806
- * types, returns `true` only when `representation` is `'string'` rather than `'date'`.
330
+ * Creates a `ContentNode` for a single request-body or response content type.
807
331
  */
808
- function isStringType(node) {
809
- if (plainStringTypes.has(node.type)) return true;
810
- const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
811
- if (temporal) return temporal.representation !== "date";
812
- return false;
813
- }
332
+ const createContent = contentDef.create;
333
+ //#endregion
334
+ //#region ../../internals/utils/src/fs.ts
814
335
  /**
815
- * Applies casing rules to parameter names and returns a new parameter array.
336
+ * Strips the file extension from a path or file name.
337
+ * Only removes the last `.ext` segment when the dot is not part of a directory name.
816
338
  *
817
- * Use this before passing parameters to schema builders so output property keys match
818
- * the desired casing while preserving `OperationNode.parameters` for other consumers.
819
- * The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
339
+ * @example
340
+ * trimExtName('petStore.ts') // 'petStore'
341
+ * trimExtName('/src/models/pet.ts') // '/src/models/pet'
342
+ * trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'
343
+ * trimExtName('noExtension') // 'noExtension'
820
344
  */
821
- function caseParams(params, casing) {
822
- if (!casing) return params;
823
- return params.map((param) => {
824
- const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
825
- return {
826
- ...param,
827
- name: transformed
828
- };
829
- });
345
+ function trimExtName(text) {
346
+ const dotIndex = text.lastIndexOf(".");
347
+ if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
348
+ return text;
830
349
  }
350
+ //#endregion
351
+ //#region ../../internals/utils/src/promise.ts
831
352
  /**
832
- * Creates a single-property object schema used as a discriminator literal.
353
+ * Wraps `factory` with a keyed cache backed by the provided store.
833
354
  *
834
- * @example
355
+ * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
356
+ * collected) or a `Map` for primitive keys. For multi-argument functions,
357
+ * nest two `memoize` calls — the outer keyed by the first argument, the
358
+ * inner (created once per outer miss) keyed by the second.
359
+ *
360
+ * Because the cache is owned by the caller, it can be shared, inspected, or
361
+ * cleared independently of the memoized function.
362
+ *
363
+ * @example Single WeakMap key
364
+ * ```ts
365
+ * const cache = new WeakMap<SchemaNode, Set<string>>()
366
+ * const getRefs = memoize(cache, (node) => collectRefs(node))
367
+ * ```
368
+ *
369
+ * @example Single Map key (primitive)
370
+ * ```ts
371
+ * const cache = new Map<string, Resolver>()
372
+ * const getResolver = memoize(cache, (name) => buildResolver(name))
373
+ * ```
374
+ *
375
+ * @example Two-level (object + primitive)
835
376
  * ```ts
836
- * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
837
- * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
377
+ * const outer = new WeakMap<Params[], Map<string, Params[]>>()
378
+ * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
379
+ * fn(params)('camelcase')
838
380
  * ```
839
381
  */
840
- function createDiscriminantNode({ propertyName, value }) {
841
- return createSchema({
842
- type: "object",
843
- primitive: "object",
844
- properties: [createProperty({
845
- name: propertyName,
846
- schema: createSchema({
847
- type: "enum",
848
- primitive: "string",
849
- enumValues: [value]
850
- }),
851
- required: true
852
- })]
853
- });
854
- }
855
- function resolveParamsType({ node, param, resolver }) {
856
- if (!resolver) return createParamsType({
857
- variant: "reference",
858
- name: param.schema.primitive ?? "unknown"
859
- });
860
- const individualName = resolver.resolveParamName(node, param);
861
- const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
862
- const groupResolvers = {
863
- path: resolver.resolvePathParamsName,
864
- query: resolver.resolveQueryParamsName,
865
- header: resolver.resolveHeaderParamsName
382
+ function memoize(store, factory) {
383
+ return (key) => {
384
+ if (store.has(key)) return store.get(key);
385
+ const value = factory(key);
386
+ store.set(key, value);
387
+ return value;
866
388
  };
867
- const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
868
- if (groupName && groupName !== individualName) return createParamsType({
869
- variant: "member",
870
- base: groupName,
871
- key: param.name
872
- });
873
- return createParamsType({
874
- variant: "reference",
875
- name: individualName
876
- });
877
389
  }
390
+ //#endregion
391
+ //#region src/utils/extractStringsFromNodes.ts
878
392
  /**
879
- * Converts an `OperationNode` into function parameters for code generation.
880
- *
881
- * Centralizes parameter grouping logic for all plugins. Provide a `resolver` for type name resolution
882
- * and `extraParams` for plugin-specific trailing parameters (e.g., `options` objects).
883
- * Supports three grouping modes: `object` (single destructured param), `inline` (separate params),
884
- * and `inlineSpread` (rest parameter). Use `CreateOperationParamsOptions` to fine-tune output.
885
- */
886
- function createOperationParams(node, options) {
887
- const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
888
- const dataName = paramNames?.data ?? "data";
889
- const paramsName = paramNames?.params ?? "params";
890
- const headersName = paramNames?.headers ?? "headers";
891
- const pathName = paramNames?.path ?? "pathParams";
892
- const wrapType = (type) => createParamsType({
893
- variant: "reference",
894
- name: typeWrapper ? typeWrapper(type) : type
895
- });
896
- const wrapTypeNode = (type) => type.kind === "ParamsType" && type.variant === "reference" ? wrapType(type.name) : type;
897
- const casedParams = caseParams(node.parameters, paramsCasing);
898
- const pathParams = casedParams.filter((p) => p.in === "path");
899
- const queryParams = casedParams.filter((p) => p.in === "query");
900
- const headerParams = casedParams.filter((p) => p.in === "header");
901
- const bodyType = node.requestBody?.content?.[0]?.schema ? wrapType(resolver?.resolveDataName(node) ?? "unknown") : void 0;
902
- const bodyRequired = node.requestBody?.required ?? false;
903
- const queryGroupType = resolver ? resolveGroupType({
904
- node,
905
- params: queryParams,
906
- groupMethod: resolver.resolveQueryParamsName,
907
- resolver
908
- }) : void 0;
909
- const headerGroupType = resolver ? resolveGroupType({
910
- node,
911
- params: headerParams,
912
- groupMethod: resolver.resolveHeaderParamsName,
913
- resolver
914
- }) : void 0;
915
- const params = [];
916
- if (paramsType === "object") {
917
- const children = [
918
- ...pathParams.map((p) => {
919
- const type = resolveParamsType({
920
- node,
921
- param: p,
922
- resolver
923
- });
924
- return createFunctionParameter({
925
- name: p.name,
926
- type: wrapTypeNode(type),
927
- optional: !p.required
928
- });
929
- }),
930
- ...bodyType ? [createFunctionParameter({
931
- name: dataName,
932
- type: bodyType,
933
- optional: !bodyRequired
934
- })] : [],
935
- ...buildGroupParam({
936
- name: paramsName,
937
- node,
938
- params: queryParams,
939
- groupType: queryGroupType,
940
- resolver,
941
- wrapType
942
- }),
943
- ...buildGroupParam({
944
- name: headersName,
945
- node,
946
- params: headerParams,
947
- groupType: headerGroupType,
948
- resolver,
949
- wrapType
950
- })
951
- ];
952
- if (children.length) params.push(createParameterGroup({
953
- properties: children,
954
- default: children.every((c) => c.optional) ? "{}" : void 0
955
- }));
956
- } else {
957
- if (pathParams.length) if (pathParamsType === "inlineSpread") {
958
- const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]) ?? void 0;
959
- params.push(createFunctionParameter({
960
- name: pathName,
961
- type: spreadType ? wrapType(spreadType) : void 0,
962
- rest: true
963
- }));
964
- } else {
965
- const pathChildren = pathParams.map((p) => {
966
- const type = resolveParamsType({
967
- node,
968
- param: p,
969
- resolver
970
- });
971
- return createFunctionParameter({
972
- name: p.name,
973
- type: wrapTypeNode(type),
974
- optional: !p.required
975
- });
976
- });
977
- params.push(createParameterGroup({
978
- properties: pathChildren,
979
- inline: pathParamsType === "inline",
980
- default: pathParamsDefault ?? (pathChildren.every((c) => c.optional) ? "{}" : void 0)
981
- }));
393
+ * Extracts all string content from a `CodeNode` tree recursively.
394
+ *
395
+ * Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
396
+ * and nested node content. Used to build the full source string for import filtering.
397
+ */
398
+ function extractStringsFromNodes(nodes) {
399
+ if (!nodes?.length) return "";
400
+ const collected = [];
401
+ for (const node of nodes) {
402
+ if (typeof node === "string") {
403
+ if (node) collected.push(node);
404
+ continue;
982
405
  }
983
- if (bodyType) params.push(createFunctionParameter({
984
- name: dataName,
985
- type: bodyType,
986
- optional: !bodyRequired
987
- }));
988
- params.push(...buildGroupParam({
989
- name: paramsName,
990
- node,
991
- params: queryParams,
992
- groupType: queryGroupType,
993
- resolver,
994
- wrapType
995
- }));
996
- params.push(...buildGroupParam({
997
- name: headersName,
998
- node,
999
- params: headerParams,
1000
- groupType: headerGroupType,
1001
- resolver,
1002
- wrapType
1003
- }));
406
+ if (node.kind === "Text") {
407
+ if (node.value) collected.push(node.value);
408
+ continue;
409
+ }
410
+ if (node.kind === "Break") continue;
411
+ if (node.kind === "Jsx") {
412
+ if (node.value) collected.push(node.value);
413
+ continue;
414
+ }
415
+ const parts = [];
416
+ if ("params" in node && node.params) parts.push(node.params);
417
+ if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
418
+ if ("returnType" in node && node.returnType) parts.push(node.returnType);
419
+ if ("type" in node && typeof node.type === "string") parts.push(node.type);
420
+ const nested = extractStringsFromNodes(node.nodes);
421
+ if (nested) parts.push(nested);
422
+ if (parts.length) collected.push(parts.join("\n"));
1004
423
  }
1005
- params.push(...extraParams);
1006
- return createFunctionParameters({ params });
1007
- }
1008
- /**
1009
- * Builds a single {@link FunctionParameterNode} for a query or header group.
1010
- * Returns an empty array when there are no params to emit.
1011
- *
1012
- * If a pre-resolved `groupType` is provided it emits `name: GroupType`.
1013
- * Otherwise, it builds an inline struct from the individual params.
1014
- */
1015
- function buildGroupParam({ name, node, params, groupType, resolver, wrapType }) {
1016
- if (groupType) return [createFunctionParameter({
1017
- name,
1018
- type: groupType.type.kind === "ParamsType" && groupType.type.variant === "reference" ? wrapType(groupType.type.name) : groupType.type,
1019
- optional: groupType.optional
1020
- })];
1021
- if (params.length) return [createFunctionParameter({
1022
- name,
1023
- type: toStructType({
1024
- node,
1025
- params,
1026
- resolver
1027
- }),
1028
- optional: params.every((p) => !p.required)
1029
- })];
1030
- return [];
1031
- }
1032
- /**
1033
- * Derives a {@link ParamGroupType} from the resolver's group method.
1034
- * Returns `undefined` when the group name equals the individual param name (no real group).
1035
- */
1036
- function resolveGroupType({ node, params, groupMethod, resolver }) {
1037
- if (!params.length) return;
1038
- const firstParam = params[0];
1039
- const groupName = groupMethod.call(resolver, node, firstParam);
1040
- if (groupName === resolver.resolveParamName(node, firstParam)) return;
1041
- const allOptional = params.every((p) => !p.required);
1042
- return {
1043
- type: createParamsType({
1044
- variant: "reference",
1045
- name: groupName
1046
- }),
1047
- optional: allOptional
1048
- };
1049
- }
1050
- /**
1051
- * Builds a {@link TypeNode} with `variant: 'struct'` for an inline anonymous type grouping named fields.
1052
- *
1053
- * Used when query or header parameters have no dedicated group type name.
1054
- * Each language printer renders this appropriately (TypeScript: `{ petId: string; name?: string }`).
1055
- */
1056
- function toStructType({ node, params, resolver }) {
1057
- return createParamsType({
1058
- variant: "struct",
1059
- properties: params.map((p) => ({
1060
- name: p.name,
1061
- optional: !p.required,
1062
- type: resolveParamsType({
1063
- node,
1064
- param: p,
1065
- resolver
1066
- })
1067
- }))
1068
- });
424
+ return collected.join("\n");
1069
425
  }
426
+ //#endregion
427
+ //#region src/utils/combineFileMembers.ts
1070
428
  function sourceKey(source) {
1071
429
  return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
1072
430
  }
@@ -1081,19 +439,19 @@ function importKey(path, name, isTypeOnly) {
1081
439
  }
1082
440
  /**
1083
441
  * Computes a multi-level sort key for exports and imports:
1084
- * non-array names first (wildcards/namespace aliases); type-only before value; alphabetical path; unnamed before named.
442
+ * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
1085
443
  */
1086
444
  function sortKey(node) {
1087
445
  const isArray = Array.isArray(node.name) ? "1" : "0";
1088
446
  const typeOnly = node.isTypeOnly ? "0" : "1";
1089
447
  const hasName = node.name != null ? "1" : "0";
1090
- const name = Array.isArray(node.name) ? [...node.name].sort().join("\0") : node.name ?? "";
448
+ const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
1091
449
  return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
1092
450
  }
1093
451
  /**
1094
- * Deduplicates and merges `SourceNode` objects by `name + isExportable + isTypeOnly`.
1095
- *
1096
- * Unnamed sources are deduplicated by object reference. Returns a deduplicated array in original order.
452
+ * Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
453
+ * key. Unnamed sources fall back to their extracted node strings as the name part of the key. Returns
454
+ * the deduplicated array in original order.
1097
455
  */
1098
456
  function combineSources(sources) {
1099
457
  const seen = /* @__PURE__ */ new Map();
@@ -1104,6 +462,16 @@ function combineSources(sources) {
1104
462
  return [...seen.values()];
1105
463
  }
1106
464
  /**
465
+ * Merges `incoming` names into `existing`, preserving order and dropping duplicates.
466
+ *
467
+ * Shared by `combineExports` and `combineImports` for the same-path name-merge case.
468
+ */
469
+ function mergeNameArrays(existing, incoming) {
470
+ const merged = new Set(existing);
471
+ for (const name of incoming) merged.add(name);
472
+ return [...merged];
473
+ }
474
+ /**
1107
475
  * Deduplicates and merges `ExportNode` objects by path and type.
1108
476
  *
1109
477
  * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
@@ -1124,11 +492,8 @@ function combineExports(exports) {
1124
492
  if (!name.length) continue;
1125
493
  const key = pathTypeKey(path, isTypeOnly);
1126
494
  const existing = namedByPath.get(key);
1127
- if (existing && Array.isArray(existing.name)) {
1128
- const merged = new Set(existing.name);
1129
- for (const n of name) merged.add(n);
1130
- existing.name = [...merged];
1131
- } else {
495
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
496
+ else {
1132
497
  const newItem = {
1133
498
  ...curr,
1134
499
  name: [...new Set(name)]
@@ -1151,12 +516,22 @@ function combineExports(exports) {
1151
516
  *
1152
517
  * Retains imports that are referenced in `source` or re-exported. Imports with the same path and
1153
518
  * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
1154
- *
1155
- * @note Use this when combining imports from multiple files to avoid duplicate declarations.
1156
519
  */
1157
520
  function combineImports(imports, exports, source) {
1158
521
  const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
1159
522
  const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
523
+ const importNameMemo = /* @__PURE__ */ new Map();
524
+ const canonicalizeName = (n) => {
525
+ if (typeof n === "string") return n;
526
+ const key = `${n.propertyName}:${n.name ?? ""}`;
527
+ if (!importNameMemo.has(key)) importNameMemo.set(key, n);
528
+ return importNameMemo.get(key);
529
+ };
530
+ const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
531
+ for (const node of imports) {
532
+ if (!Array.isArray(node.name)) continue;
533
+ if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
534
+ }
1160
535
  const result = [];
1161
536
  const namedByPath = /* @__PURE__ */ new Map();
1162
537
  const seen = /* @__PURE__ */ new Set();
@@ -1170,15 +545,12 @@ function combineImports(imports, exports, source) {
1170
545
  const { path, isTypeOnly } = curr;
1171
546
  let { name } = curr;
1172
547
  if (Array.isArray(name)) {
1173
- name = [...new Set(name)].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.propertyName));
548
+ name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
1174
549
  if (!name.length) continue;
1175
550
  const key = pathTypeKey(path, isTypeOnly);
1176
551
  const existing = namedByPath.get(key);
1177
- if (existing && Array.isArray(existing.name)) {
1178
- const merged = new Set(existing.name);
1179
- for (const n of name) merged.add(n);
1180
- existing.name = [...merged];
1181
- } else {
552
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
553
+ else {
1182
554
  const newItem = {
1183
555
  ...curr,
1184
556
  name
@@ -1187,7 +559,7 @@ function combineImports(imports, exports, source) {
1187
559
  namedByPath.set(key, newItem);
1188
560
  }
1189
561
  } else {
1190
- if (name && !isUsed(name)) continue;
562
+ if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
1191
563
  const key = importKey(path, name, isTypeOnly);
1192
564
  if (!seen.has(key)) {
1193
565
  result.push(curr);
@@ -1197,284 +569,266 @@ function combineImports(imports, exports, source) {
1197
569
  }
1198
570
  return result;
1199
571
  }
572
+ //#endregion
573
+ //#region src/nodes/file.ts
1200
574
  /**
1201
- * Extracts all string content from a `CodeNode` tree recursively.
1202
- *
1203
- * Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
1204
- * and nested node content. Used internally to build the full source string for import filtering.
575
+ * Definition for the {@link ImportNode}.
1205
576
  */
1206
- function extractStringsFromNodes(nodes) {
1207
- if (!nodes?.length) return "";
1208
- return nodes.map((node) => {
1209
- if (typeof node === "string") return node;
1210
- if (node.kind === "Text") return node.value;
1211
- if (node.kind === "Break") return "";
1212
- if (node.kind === "Jsx") return node.value;
1213
- const parts = [];
1214
- if ("params" in node && node.params) parts.push(node.params);
1215
- if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
1216
- if ("returnType" in node && node.returnType) parts.push(node.returnType);
1217
- if ("type" in node && typeof node.type === "string") parts.push(node.type);
1218
- const nested = extractStringsFromNodes(node.nodes);
1219
- if (nested) parts.push(nested);
1220
- return parts.join("\n");
1221
- }).filter(Boolean).join("\n");
1222
- }
577
+ const importDef = defineNode({ kind: "Import" });
1223
578
  /**
1224
- * Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
1225
- *
1226
- * Returns `undefined` for non-ref nodes or when no name can be resolved. Use this to get a schema's
1227
- * identifier for type definitions or error messages.
1228
- *
1229
- * @example
1230
- * ```ts
1231
- * resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' })
1232
- * // => 'Pet'
1233
- * ```
579
+ * Definition for the {@link ExportNode}.
1234
580
  */
1235
- function resolveRefName(node) {
1236
- if (!node || node.type !== "ref") return void 0;
1237
- if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? void 0;
1238
- return node.name ?? node.schema?.name ?? void 0;
1239
- }
581
+ const exportDef = defineNode({ kind: "Export" });
1240
582
  /**
1241
- * Collects every named schema referenced (transitively) from a node via ref edges.
1242
- *
1243
- * Refs are followed by name only — the resolved `node.schema` is not traversed inline.
1244
- * Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
1245
- *
1246
- * @note Returns a Set of schema names for efficient membership testing.
1247
- */
1248
- function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
1249
- if (!node) return out;
1250
- collect(node, { schema(child) {
1251
- if (child.type === "ref") {
1252
- const name = resolveRefName(child);
1253
- if (name) out.add(name);
1254
- }
1255
- } });
1256
- return out;
1257
- }
1258
- /**
1259
- * Identifies all schemas that participate in circular dependency chains, including direct self-loops.
1260
- *
1261
- * Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
1262
- * in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
1263
- * Refs are followed by name only, keeping the algorithm linear in the schema graph size.
1264
- *
1265
- * @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
1266
- */
1267
- function findCircularSchemas(schemas) {
1268
- const graph = /* @__PURE__ */ new Map();
1269
- for (const schema of schemas) {
1270
- if (!schema.name) continue;
1271
- graph.set(schema.name, collectReferencedSchemaNames(schema));
1272
- }
1273
- const circular = /* @__PURE__ */ new Set();
1274
- for (const start of graph.keys()) {
1275
- const visited = /* @__PURE__ */ new Set();
1276
- const stack = [...graph.get(start) ?? []];
1277
- while (stack.length > 0) {
1278
- const node = stack.pop();
1279
- if (node === start) {
1280
- circular.add(start);
1281
- break;
1282
- }
1283
- if (visited.has(node)) continue;
1284
- visited.add(node);
1285
- const next = graph.get(node);
1286
- if (next) for (const r of next) stack.push(r);
1287
- }
1288
- }
1289
- return circular;
1290
- }
1291
- /**
1292
- * Type guard returning `true` when a schema or anything nested within it contains a ref to a circular schema.
1293
- *
1294
- * Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
1295
- * Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
1296
- *
1297
- * @note Returns `true` for the first matching circular ref found; use for fast dependency checks.
583
+ * Definition for the {@link SourceNode}.
1298
584
  */
1299
- function containsCircularRef(node, { circularSchemas, excludeName }) {
1300
- if (!node || circularSchemas.size === 0) return false;
1301
- return collect(node, { schema(child) {
1302
- if (child.type !== "ref") return void 0;
1303
- const name = resolveRefName(child);
1304
- return name && name !== excludeName && circularSchemas.has(name) ? true : void 0;
1305
- } }).length > 0;
1306
- }
1307
- //#endregion
1308
- //#region src/factory.ts
585
+ const sourceDef = defineNode({ kind: "Source" });
1309
586
  /**
1310
- * Syncs property/parameter schema optionality flags from `required` and `schema.nullable`.
1311
- *
1312
- * - `optional` is set for non-required, non-nullable schemas.
1313
- * - `nullish` is set for non-required, nullable schemas.
587
+ * Definition for the {@link FileNode}. The fully resolved builder lives in
588
+ * `createFile`, so this definition only supplies the guard.
1314
589
  */
1315
- function syncOptionality(schema, required) {
1316
- const nullable = schema.nullable ?? false;
1317
- return {
1318
- ...schema,
1319
- optional: !required && !nullable ? true : void 0,
1320
- nullish: !required && nullable ? true : void 0
1321
- };
1322
- }
590
+ const fileDef = defineNode({ kind: "File" });
1323
591
  /**
1324
- * Creates an `InputNode` with stable defaults for `schemas` and `operations`.
1325
- *
1326
- * @example
1327
- * ```ts
1328
- * const input = createInput()
1329
- * // { kind: 'Input', schemas: [], operations: [] }
1330
- * ```
592
+ * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
1331
593
  *
1332
- * @example
594
+ * @example Named import
1333
595
  * ```ts
1334
- * const input = createInput({ schemas: [petSchema] })
1335
- * // keeps default operations: []
596
+ * createImport({ name: ['useState'], path: 'react' })
597
+ * // import { useState } from 'react'
1336
598
  * ```
1337
599
  */
1338
- function createInput(overrides = {}) {
1339
- return {
1340
- schemas: [],
1341
- operations: [],
1342
- ...overrides,
1343
- kind: "Input"
1344
- };
1345
- }
600
+ const createImport = importDef.create;
1346
601
  /**
1347
- * Creates an `OutputNode` with a stable default for `files`.
602
+ * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
1348
603
  *
1349
- * @example
604
+ * @example Named export
1350
605
  * ```ts
1351
- * const output = createOutput()
1352
- * // { kind: 'Output', files: [] }
606
+ * createExport({ name: ['Pet'], path: './Pet' })
607
+ * // export { Pet } from './Pet'
1353
608
  * ```
609
+ */
610
+ const createExport = exportDef.create;
611
+ /**
612
+ * Creates a `SourceNode` representing a fragment of source code within a file.
1354
613
  *
1355
614
  * @example
1356
615
  * ```ts
1357
- * const output = createOutput({ files: [petFile] })
616
+ * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
1358
617
  * ```
1359
618
  */
1360
- function createOutput(overrides = {}) {
1361
- return {
1362
- files: [],
1363
- ...overrides,
1364
- kind: "Output"
1365
- };
1366
- }
619
+ const createSource = sourceDef.create;
1367
620
  /**
1368
- * Creates an `OperationNode` with default empty arrays for `tags`, `parameters`, and `responses`.
621
+ * Creates a fully resolved `FileNode` from a file input descriptor.
622
+ *
623
+ * Computes:
624
+ * - `id` SHA256 hash of the file path
625
+ * - `name` `baseName` without extension
626
+ * - `extname` extension extracted from `baseName`
627
+ *
628
+ * Deduplicates:
629
+ * - `sources` via `combineSources`
630
+ * - `exports` via `combineExports`
631
+ * - `imports` via `combineImports` (also filters unused imports)
632
+ *
633
+ * @throws {Error} when `baseName` has no extension.
1369
634
  *
1370
635
  * @example
1371
636
  * ```ts
1372
- * const operation = createOperation({
1373
- * operationId: 'getPetById',
1374
- * method: 'GET',
1375
- * path: '/pet/{petId}',
637
+ * const file = createFile({
638
+ * baseName: 'petStore.ts',
639
+ * path: 'src/models/petStore.ts',
640
+ * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
641
+ * imports: [createImport({ name: ['z'], path: 'zod' })],
642
+ * exports: [createExport({ name: ['Pet'], path: './petStore' })],
1376
643
  * })
1377
- * // tags, parameters, and responses are []
644
+ * // file.id = SHA256 hash of 'src/models/petStore.ts'
645
+ * // file.name = 'petStore'
646
+ * // file.extname = '.ts'
1378
647
  * ```
1379
648
  *
1380
- * @example
649
+ * @example Copy a real file into the output verbatim
1381
650
  * ```ts
1382
- * const operation = createOperation({
1383
- * operationId: 'findPets',
1384
- * method: 'GET',
1385
- * path: '/pet/findByStatus',
1386
- * tags: ['pet'],
651
+ * const file = createFile({
652
+ * baseName: 'client.ts',
653
+ * path: 'src/gen/client.ts',
654
+ * copy: '/abs/path/to/templates/client.ts',
1387
655
  * })
1388
656
  * ```
1389
657
  */
1390
- function createOperation(props) {
658
+ function createFile(input) {
659
+ const extname = path.extname(input.baseName);
660
+ if (!extname) throw new Error(`No extname found for ${input.baseName}`);
661
+ const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
662
+ const resolvedImports = (() => {
663
+ if (!input.imports?.length) return [];
664
+ const sourceParts = [];
665
+ const localNames = /* @__PURE__ */ new Set();
666
+ for (const item of input.sources ?? []) {
667
+ const extracted = item.nodes && extractStringsFromNodes(item.nodes);
668
+ if (extracted) sourceParts.push(extracted);
669
+ if (item.name) localNames.add(item.name);
670
+ }
671
+ const source = sourceParts.join("\n") || void 0;
672
+ const combinedImports = combineImports(input.imports, resolvedExports, source);
673
+ const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
674
+ return combinedImports.flatMap((imp) => {
675
+ if (imp.path === input.path) return [];
676
+ if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
677
+ const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
678
+ if (!kept.length) return [];
679
+ return [kept.length === imp.name.length ? imp : {
680
+ ...imp,
681
+ name: kept
682
+ }];
683
+ });
684
+ })();
685
+ const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
1391
686
  return {
1392
- tags: [],
1393
- parameters: [],
1394
- responses: [],
1395
- ...props,
1396
- kind: "Operation"
687
+ kind: "File",
688
+ ...input,
689
+ id: hash("sha256", input.path, "hex"),
690
+ name: trimExtName(input.baseName),
691
+ extname,
692
+ imports: resolvedImports,
693
+ exports: resolvedExports,
694
+ sources: resolvedSources,
695
+ meta: input.meta ?? {}
1397
696
  };
1398
697
  }
698
+ //#endregion
699
+ //#region src/nodes/input.ts
1399
700
  /**
1400
- * Maps schema `type` to its underlying `primitive`.
1401
- * Primitive types map to themselves; special string formats map to `'string'`.
1402
- * Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
701
+ * Definition for the {@link InputNode}.
1403
702
  */
1404
- const TYPE_TO_PRIMITIVE = {
1405
- string: "string",
1406
- number: "number",
1407
- integer: "integer",
1408
- bigint: "bigint",
1409
- boolean: "boolean",
1410
- null: "null",
1411
- any: "any",
1412
- unknown: "unknown",
1413
- void: "void",
1414
- never: "never",
1415
- object: "object",
1416
- array: "array",
1417
- date: "date",
1418
- uuid: "string",
1419
- email: "string",
1420
- url: "string",
1421
- datetime: "string",
1422
- time: "string"
1423
- };
1424
- function createSchema(props) {
1425
- const inferredPrimitive = TYPE_TO_PRIMITIVE[props.type];
1426
- if (props["type"] === "object") return {
1427
- properties: [],
1428
- primitive: "object",
1429
- ...props,
1430
- kind: "Schema"
1431
- };
1432
- return {
1433
- primitive: inferredPrimitive,
1434
- ...props,
1435
- kind: "Schema"
1436
- };
1437
- }
703
+ const inputDef = defineNode({
704
+ kind: "Input",
705
+ defaults: {
706
+ schemas: [],
707
+ operations: [],
708
+ meta: {
709
+ circularNames: [],
710
+ enumNames: []
711
+ }
712
+ },
713
+ children: ["schemas", "operations"],
714
+ visitorKey: "input"
715
+ });
1438
716
  /**
1439
- * Creates a `PropertyNode`.
1440
- *
1441
- * `required` defaults to `false`.
1442
- * `schema.optional` and `schema.nullish` are derived from `required` and `schema.nullable`.
717
+ * Creates an `InputNode`, defaulting `schemas`/`operations` to empty arrays and `meta` per
718
+ * {@link inputDef}.
1443
719
  *
1444
720
  * @example
1445
721
  * ```ts
1446
- * const property = createProperty({
1447
- * name: 'status',
1448
- * schema: createSchema({ type: 'string' }),
1449
- * })
1450
- * // required=false, schema.optional=true
722
+ * const input = createInput()
723
+ * // { kind: 'Input', schemas: [], operations: [] }
1451
724
  * ```
725
+ */
726
+ function createInput(overrides = {}) {
727
+ return inputDef.create(overrides);
728
+ }
729
+ //#endregion
730
+ //#region src/nodes/requestBody.ts
731
+ /**
732
+ * Definition for the {@link RequestBodyNode}. Content entries are built upfront with
733
+ * {@link createContent}, mirroring how `parameters` and `responses` take prebuilt nodes.
734
+ */
735
+ const requestBodyDef = defineNode({
736
+ kind: "RequestBody",
737
+ children: ["content"]
738
+ });
739
+ /**
740
+ * Creates a `RequestBodyNode`.
741
+ */
742
+ const createRequestBody = requestBodyDef.create;
743
+ //#endregion
744
+ //#region src/nodes/operation.ts
745
+ /**
746
+ * Definition for the {@link OperationNode}. HTTP operations (those carrying both
747
+ * `method` and `path`) are tagged with `protocol: 'http'`, and the request body is
748
+ * normalized into a `RequestBodyNode`.
749
+ */
750
+ const operationDef = defineNode({
751
+ kind: "Operation",
752
+ build: (props) => {
753
+ const { requestBody, ...rest } = props;
754
+ const isHttp = rest.method !== void 0 && rest.path !== void 0;
755
+ return {
756
+ tags: [],
757
+ parameters: [],
758
+ responses: [],
759
+ ...rest,
760
+ ...isHttp ? { protocol: "http" } : {},
761
+ requestBody: requestBody ? createRequestBody(requestBody) : void 0
762
+ };
763
+ },
764
+ children: [
765
+ "parameters",
766
+ "requestBody",
767
+ "responses"
768
+ ],
769
+ visitorKey: "operation"
770
+ });
771
+ function createOperation(props) {
772
+ return operationDef.create(props);
773
+ }
774
+ //#endregion
775
+ //#region src/nodes/output.ts
776
+ /**
777
+ * Definition for the {@link OutputNode}.
778
+ */
779
+ const outputDef = defineNode({
780
+ kind: "Output",
781
+ defaults: { files: [] },
782
+ visitorKey: "output"
783
+ });
784
+ /**
785
+ * Creates an `OutputNode` with a stable default for `files`.
1452
786
  *
1453
787
  * @example
1454
788
  * ```ts
1455
- * const property = createProperty({
1456
- * name: 'status',
1457
- * required: true,
1458
- * schema: createSchema({ type: 'string', nullable: true }),
1459
- * })
1460
- * // required=true, no optional/nullish
789
+ * const output = createOutput()
790
+ * // { kind: 'Output', files: [] }
1461
791
  * ```
1462
792
  */
1463
- function createProperty(props) {
1464
- const required = props.required ?? false;
793
+ function createOutput(overrides = {}) {
794
+ return outputDef.create(overrides);
795
+ }
796
+ //#endregion
797
+ //#region src/optionality.ts
798
+ /**
799
+ * Generic JSON Schema optionality: a non-required field is optional, and a
800
+ * non-required nullable field is nullish.
801
+ */
802
+ function optionality(schema, required) {
803
+ const nullable = schema.nullable ?? false;
1465
804
  return {
1466
- ...props,
1467
- kind: "Property",
1468
- required,
1469
- schema: syncOptionality(props.schema, required)
805
+ ...schema,
806
+ optional: !required && !nullable ? true : void 0,
807
+ nullish: !required && nullable ? true : void 0
1470
808
  };
1471
809
  }
810
+ //#endregion
811
+ //#region src/nodes/parameter.ts
812
+ /**
813
+ * Definition for the {@link ParameterNode}. `required` defaults to `false`, and the schema's
814
+ * `optional`/`nullish` flags are derived from it through {@link optionality}.
815
+ */
816
+ const parameterDef = defineNode({
817
+ kind: "Parameter",
818
+ build: (props) => {
819
+ const required = props.required ?? false;
820
+ return {
821
+ ...props,
822
+ required,
823
+ schema: optionality(props.schema, required)
824
+ };
825
+ },
826
+ children: ["schema"],
827
+ visitorKey: "parameter"
828
+ });
1472
829
  /**
1473
830
  * Creates a `ParameterNode`.
1474
831
  *
1475
- * `required` defaults to `false`.
1476
- * Nested schema flags are set from `required` and `schema.nullable`.
1477
- *
1478
832
  * @example
1479
833
  * ```ts
1480
834
  * const param = createParameter({
@@ -1484,26 +838,64 @@ function createProperty(props) {
1484
838
  * schema: createSchema({ type: 'string' }),
1485
839
  * })
1486
840
  * ```
841
+ */
842
+ const createParameter = parameterDef.create;
843
+ //#endregion
844
+ //#region src/nodes/property.ts
845
+ /**
846
+ * Definition for the {@link PropertyNode}. `required` defaults to `false`, and the schema's
847
+ * `optional`/`nullish` flags are derived from it through {@link optionality}.
848
+ */
849
+ const propertyDef = defineNode({
850
+ kind: "Property",
851
+ build: (props) => {
852
+ const required = props.required ?? false;
853
+ return {
854
+ ...props,
855
+ required,
856
+ schema: optionality(props.schema, required)
857
+ };
858
+ },
859
+ children: ["schema"],
860
+ visitorKey: "property"
861
+ });
862
+ /**
863
+ * Creates a `PropertyNode`.
1487
864
  *
1488
865
  * @example
1489
866
  * ```ts
1490
- * const param = createParameter({
867
+ * const property = createProperty({
1491
868
  * name: 'status',
1492
- * in: 'query',
869
+ * required: true,
1493
870
  * schema: createSchema({ type: 'string', nullable: true }),
1494
871
  * })
1495
- * // required=false, schema.nullish=true
872
+ * // required=true, no optional/nullish
1496
873
  * ```
1497
874
  */
1498
- function createParameter(props) {
1499
- const required = props.required ?? false;
1500
- return {
1501
- ...props,
1502
- kind: "Parameter",
1503
- required,
1504
- schema: syncOptionality(props.schema, required)
1505
- };
1506
- }
875
+ const createProperty = propertyDef.create;
876
+ //#endregion
877
+ //#region src/nodes/response.ts
878
+ /**
879
+ * Definition for the {@link ResponseNode}. A single legacy `schema` (with optional
880
+ * `mediaType`/`keysToOmit`) is normalized into one `content` entry.
881
+ */
882
+ const responseDef = defineNode({
883
+ kind: "Response",
884
+ build: (props) => {
885
+ const { schema, mediaType, keysToOmit, content, ...rest } = props;
886
+ const entries = content ?? (schema ? [createContent({
887
+ contentType: mediaType ?? "application/json",
888
+ schema,
889
+ keysToOmit: keysToOmit ?? null
890
+ })] : void 0);
891
+ return {
892
+ ...rest,
893
+ content: entries
894
+ };
895
+ },
896
+ children: ["content"],
897
+ visitorKey: "response"
898
+ });
1507
899
  /**
1508
900
  * Creates a `ResponseNode`.
1509
901
  *
@@ -1511,628 +903,638 @@ function createParameter(props) {
1511
903
  * ```ts
1512
904
  * const response = createResponse({
1513
905
  * statusCode: '200',
1514
- * description: 'Success',
1515
- * schema: createSchema({ type: 'object', properties: [] }),
906
+ * content: [createContent({ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) })],
1516
907
  * })
1517
908
  * ```
1518
909
  */
1519
- function createResponse(props) {
1520
- return {
1521
- ...props,
1522
- kind: "Response"
1523
- };
1524
- }
1525
- /**
1526
- * Creates a `FunctionParameterNode`.
1527
- *
1528
- * `optional` defaults to `false`.
1529
- *
1530
- * @example Required typed param
1531
- * ```ts
1532
- * createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }) })
1533
- * // → petId: string
1534
- * ```
1535
- *
1536
- * @example Optional param
1537
- * ```ts
1538
- * createFunctionParameter({ name: 'params', type: createParamsType({ variant: 'reference', name: 'QueryParams' }), optional: true })
1539
- * // → params?: QueryParams
1540
- * ```
1541
- *
1542
- * @example Param with default (implicitly optional; cannot combine with `optional: true`)
1543
- * ```ts
1544
- * createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
1545
- * // → config: RequestConfig = {}
1546
- * ```
1547
- */
1548
- function createFunctionParameter(props) {
1549
- return {
1550
- optional: false,
1551
- ...props,
1552
- kind: "FunctionParameter"
1553
- };
1554
- }
910
+ const createResponse = responseDef.create;
911
+ //#endregion
912
+ //#region src/nodes/schema.ts
1555
913
  /**
1556
- * Creates a {@link TypeNode} representing a language-agnostic structured type expression.
1557
- *
1558
- * Use `variant: 'struct'` for inline anonymous types and `variant: 'member'` for a single
1559
- * named field accessed from a group type. Each language's printer renders the variant
1560
- * into its own syntax (TypeScript, Python, C#, Kotlin, …).
1561
- *
1562
- * @example Reference type (TypeScript: `QueryParams`)
1563
- * ```ts
1564
- * createParamsType({ variant: 'reference', name: 'QueryParams' })
1565
- * ```
1566
- *
1567
- * @example Struct type (TypeScript: `{ petId: string }`)
1568
- * ```ts
1569
- * createParamsType({ variant: 'struct', properties: [{ name: 'petId', optional: false, type: createParamsType({ variant: 'reference', name: 'string' }) }] })
1570
- * ```
1571
- *
1572
- * @example Member type (TypeScript: `DeletePetPathParams['petId']`)
1573
- * ```ts
1574
- * createParamsType({ variant: 'member', base: 'DeletePetPathParams', key: 'petId' })
1575
- * ```
914
+ * Maps schema `type` to its underlying `primitive`.
915
+ * Primitive types map to themselves and special string formats map to `'string'`.
916
+ * Any type not listed here (such as `ref`, `enum`, `union`, `intersection`, `tuple`, `ipv4`, `ipv6`, `blob`) has no `primitive`.
1576
917
  */
1577
- function createParamsType(props) {
1578
- return {
1579
- ...props,
1580
- kind: "ParamsType"
1581
- };
1582
- }
918
+ const TYPE_TO_PRIMITIVE = {
919
+ string: "string",
920
+ number: "number",
921
+ integer: "integer",
922
+ bigint: "bigint",
923
+ boolean: "boolean",
924
+ null: "null",
925
+ any: "any",
926
+ unknown: "unknown",
927
+ void: "void",
928
+ never: "never",
929
+ object: "object",
930
+ array: "array",
931
+ date: "date",
932
+ uuid: "string",
933
+ email: "string",
934
+ url: "string",
935
+ datetime: "string",
936
+ time: "string"
937
+ };
1583
938
  /**
1584
- * Creates a `ParameterGroupNode` representing a group of related parameters treated as a unit.
1585
- *
1586
- * @example Grouped param (TypeScript declaration)
1587
- * ```ts
1588
- * createParameterGroup({
1589
- * properties: [
1590
- * createFunctionParameter({ name: 'id', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
1591
- * createFunctionParameter({ name: 'name', type: createParamsType({ variant: 'reference', name: 'string' }), optional: true }),
1592
- * ],
1593
- * default: '{}',
1594
- * })
1595
- * // declaration → { id, name? }: { id: string; name?: string } = {}
1596
- * // call → { id, name }
1597
- * ```
1598
- *
1599
- * @example Inline (spread) — children emitted as individual top-level parameters
1600
- * ```ts
1601
- * createParameterGroup({
1602
- * properties: [createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false })],
1603
- * inline: true,
1604
- * })
1605
- * // declaration → petId: string
1606
- * // call → petId
1607
- * ```
939
+ * Definition for the {@link SchemaNode}. Object schemas default `properties` to an
940
+ * empty array, and `primitive` is inferred from `type` when not explicitly provided.
1608
941
  */
1609
- function createParameterGroup(props) {
1610
- return {
1611
- ...props,
1612
- kind: "ParameterGroup"
1613
- };
942
+ const schemaDef = defineNode({
943
+ kind: "Schema",
944
+ build: (props) => {
945
+ if (props.type === "object") return {
946
+ properties: [],
947
+ primitive: "object",
948
+ ...props
949
+ };
950
+ return {
951
+ primitive: TYPE_TO_PRIMITIVE[props.type],
952
+ ...props
953
+ };
954
+ },
955
+ children: [
956
+ "properties",
957
+ "items",
958
+ "members",
959
+ "additionalProperties"
960
+ ],
961
+ visitorKey: "schema"
962
+ });
963
+ function createSchema(props) {
964
+ return schemaDef.create(props);
1614
965
  }
966
+ //#endregion
967
+ //#region src/registry.ts
968
+ /**
969
+ * Every node definition. Adding a node means adding its `defineNode` to one
970
+ * `nodes/*.ts` file and listing it here. The visitor tables in `visitor.ts` derive from it.
971
+ */
972
+ const nodeDefs = [
973
+ inputDef,
974
+ outputDef,
975
+ operationDef,
976
+ requestBodyDef,
977
+ contentDef,
978
+ responseDef,
979
+ schemaDef,
980
+ propertyDef,
981
+ parameterDef,
982
+ constDef,
983
+ typeDef,
984
+ functionDef,
985
+ arrowFunctionDef,
986
+ textDef,
987
+ breakDef,
988
+ jsxDef,
989
+ importDef,
990
+ exportDef,
991
+ sourceDef,
992
+ fileDef
993
+ ];
994
+ //#endregion
995
+ //#region src/visitor.ts
1615
996
  /**
1616
- * Creates a `FunctionParametersNode` from an ordered list of parameters.
1617
- *
1618
- * @example
1619
- * ```ts
1620
- * createFunctionParameters({
1621
- * params: [
1622
- * createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
1623
- * createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), optional: false, default: '{}' }),
1624
- * ],
1625
- * })
1626
- * ```
1627
- *
1628
- * @example
1629
- * ```ts
1630
- * const empty = createFunctionParameters()
1631
- * // { kind: 'FunctionParameters', params: [] }
1632
- * ```
997
+ * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
998
+ * Derived from each definition's `children`.
1633
999
  */
1634
- function createFunctionParameters(props = {}) {
1635
- return {
1636
- params: [],
1637
- ...props,
1638
- kind: "FunctionParameters"
1639
- };
1640
- }
1000
+ const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => def.children ? [[def.kind, def.children]] : []));
1641
1001
  /**
1642
- * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
1643
- *
1644
- * @example Named import
1645
- * ```ts
1646
- * createImport({ name: ['useState'], path: 'react' })
1647
- * // import { useState } from 'react'
1648
- * ```
1649
- *
1650
- * @example Type-only import
1651
- * ```ts
1652
- * createImport({ name: ['FC'], path: 'react', isTypeOnly: true })
1653
- * // import type { FC } from 'react'
1654
- * ```
1002
+ * Maps a node kind to the matching visitor callback name. Derived from each
1003
+ * definition's `visitorKey`.
1655
1004
  */
1656
- function createImport(props) {
1657
- return {
1658
- ...props,
1659
- kind: "Import"
1660
- };
1005
+ const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => def.visitorKey ? [[def.kind, def.visitorKey]] : []));
1006
+ const visitorKeysByKind = VISITOR_KEYS;
1007
+ /**
1008
+ * Returns `true` when `value` is an AST node (an object carrying a `kind`).
1009
+ */
1010
+ function isNode(value) {
1011
+ return typeof value === "object" && value !== null && typeof value.kind === "string";
1661
1012
  }
1662
1013
  /**
1663
- * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
1014
+ * Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
1664
1015
  *
1665
- * @example Named export
1666
- * ```ts
1667
- * createExport({ name: ['Pet'], path: './Pet' })
1668
- * // export { Pet } from './Pet'
1669
- * ```
1016
+ * `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
1670
1017
  *
1671
- * @example Wildcard export
1018
+ * @example
1672
1019
  * ```ts
1673
- * createExport({ path: './utils' })
1674
- * // export * from './utils'
1020
+ * const children = getChildren(operationNode, true)
1021
+ * // returns parameters, the request body, and responses
1675
1022
  * ```
1676
1023
  */
1677
- function createExport(props) {
1678
- return {
1679
- ...props,
1680
- kind: "Export"
1681
- };
1024
+ function* getChildren(node, recurse) {
1025
+ if (node.kind === "Schema" && !recurse) return;
1026
+ const keys = visitorKeysByKind[node.kind];
1027
+ if (!keys) return;
1028
+ const record = node;
1029
+ for (const key of keys) {
1030
+ const value = record[key];
1031
+ if (Array.isArray(value)) {
1032
+ for (const item of value) if (isNode(item)) yield item;
1033
+ } else if (isNode(value)) yield value;
1034
+ }
1682
1035
  }
1683
1036
  /**
1684
- * Creates a `SourceNode` representing a fragment of source code within a file.
1037
+ * Runs the visitor callback that matches `node.kind` with the traversal
1038
+ * context. The result is a replacement node, a collected value, or `undefined`
1039
+ * when no callback is registered for the kind.
1685
1040
  *
1686
- * @example
1687
- * ```ts
1688
- * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
1689
- * ```
1041
+ * Shared by `transform` and `collect` so node-kind dispatch lives in one place.
1042
+ * `TResult` is the caller's expected return: the same node type for `transform`,
1043
+ * the collected value type for `collect`.
1690
1044
  */
1691
- function createSource(props) {
1045
+ function applyVisitor(node, visitor, parent) {
1046
+ const key = VISITOR_KEY_BY_KIND[node.kind];
1047
+ if (!key) return void 0;
1048
+ const fn = visitor[key];
1049
+ return fn?.(node, { parent });
1050
+ }
1051
+ function transform(node, options) {
1052
+ const { depth, parent, ...visitor } = options;
1053
+ return transformNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
1054
+ }
1055
+ /**
1056
+ * Visits a single node, then immutably rebuilds its children. Returns the original
1057
+ * reference when neither the visitor nor the child rebuild changed anything, so callers
1058
+ * can detect "nothing changed" by identity and ancestors avoid reallocating.
1059
+ */
1060
+ function transformNode(node, visitor, recurse, parent) {
1061
+ return transformChildren(applyVisitor(node, visitor, parent) ?? node, visitor, recurse);
1062
+ }
1063
+ /**
1064
+ * Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
1065
+ * each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
1066
+ * `Schema` children are skipped in shallow mode.
1067
+ */
1068
+ function transformChildren(node, visitor, recurse) {
1069
+ if (node.kind === "Schema" && !recurse) return node;
1070
+ const keys = visitorKeysByKind[node.kind];
1071
+ if (!keys) return node;
1072
+ const record = node;
1073
+ let updates;
1074
+ for (const key of keys) {
1075
+ if (!(key in record)) continue;
1076
+ const value = record[key];
1077
+ if (Array.isArray(value)) {
1078
+ let mapped;
1079
+ for (const [i, item] of value.entries()) {
1080
+ const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
1081
+ if (mapped) {
1082
+ mapped.push(next);
1083
+ continue;
1084
+ }
1085
+ if (next !== item) mapped = [...value.slice(0, i), next];
1086
+ }
1087
+ if (mapped) (updates ??= {})[key] = mapped;
1088
+ } else if (isNode(value)) {
1089
+ const next = transformNode(value, visitor, recurse, node);
1090
+ if (next !== value) (updates ??= {})[key] = next;
1091
+ }
1092
+ }
1093
+ if (!updates) return node;
1692
1094
  return {
1693
- ...props,
1694
- kind: "Source"
1095
+ ...node,
1096
+ ...updates
1695
1097
  };
1696
1098
  }
1697
1099
  /**
1698
- * Creates a fully resolved `FileNode` from a file input descriptor.
1699
- *
1700
- * Computes:
1701
- * - `id` — SHA256 hash of the file path
1702
- * - `name` — `baseName` without extension
1703
- * - `extname` — extension extracted from `baseName`
1704
- *
1705
- * Deduplicates:
1706
- * - `sources` via `combineSources`
1707
- * - `exports` via `combineExports`
1708
- * - `imports` via `combineImports` (also filters unused imports)
1100
+ * Lazy depth-first collection pass. Yields every non-null value returned by
1101
+ * the visitor callbacks. Use `collectSync` for the eager array form.
1709
1102
  *
1710
- * @throws {Error} when `baseName` has no extension.
1711
- *
1712
- * @example
1103
+ * @example Collect every operationId
1713
1104
  * ```ts
1714
- * const file = createFile({
1715
- * baseName: 'petStore.ts',
1716
- * path: 'src/models/petStore.ts',
1717
- * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
1718
- * imports: [createImport({ name: ['z'], path: 'zod' })],
1719
- * exports: [createExport({ name: ['Pet'], path: './petStore' })],
1720
- * })
1721
- * // file.id = SHA256 hash of 'src/models/petStore.ts'
1722
- * // file.name = 'petStore'
1723
- * // file.extname = '.ts'
1105
+ * const ids: string[] = []
1106
+ * for (const id of collect<string>(root, {
1107
+ * operation(node) {
1108
+ * return node.operationId
1109
+ * },
1110
+ * })) {
1111
+ * ids.push(id)
1112
+ * }
1724
1113
  * ```
1725
1114
  */
1726
- function createFile(input) {
1727
- const extname = path.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
1728
- if (!extname) throw new Error(`No extname found for ${input.baseName}`);
1729
- const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
1730
- const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
1731
- const resolvedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
1732
- const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
1733
- return {
1734
- kind: "File",
1735
- ...input,
1736
- id: createHash("sha256").update(input.path).digest("hex"),
1737
- name: trimExtName(input.baseName),
1738
- extname,
1739
- imports: resolvedImports,
1740
- exports: resolvedExports,
1741
- sources: resolvedSources,
1742
- meta: input.meta ?? {}
1743
- };
1115
+ function* collect(node, options) {
1116
+ const { depth, parent, ...visitor } = options;
1117
+ yield* collectNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
1118
+ }
1119
+ function* collectNode(node, visitor, recurse, parent) {
1120
+ const v = applyVisitor(node, visitor, parent);
1121
+ if (v != null) yield v;
1122
+ for (const child of getChildren(node, recurse)) yield* collectNode(child, visitor, recurse, node);
1744
1123
  }
1745
1124
  /**
1746
- * Creates a `ConstNode` representing a TypeScript `const` declaration.
1747
- *
1748
- * Mirrors the `Const` component from `@kubb/renderer-jsx`.
1749
- * The component's `children` are represented as `nodes`.
1750
- *
1751
- * @example Simple constant
1752
- * ```ts
1753
- * createConst({ name: 'pet' })
1754
- * // const pet = ...
1755
- * ```
1756
- *
1757
- * @example Exported constant with type and `as const`
1758
- * ```ts
1759
- * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
1760
- * // export const pets: Pet[] = ... as const
1761
- * ```
1125
+ * Eager depth-first collection pass. Gathers every non-null value the visitor
1126
+ * callbacks return into an array.
1762
1127
  *
1763
- * @example With JSDoc and child nodes
1128
+ * @example Collect every operationId
1764
1129
  * ```ts
1765
- * createConst({
1766
- * name: 'config',
1767
- * export: true,
1768
- * JSDoc: { comments: ['@description App configuration'] },
1769
- * nodes: [],
1130
+ * const ids = collectSync<string>(root, {
1131
+ * operation(node) {
1132
+ * return node.operationId
1133
+ * },
1770
1134
  * })
1771
1135
  * ```
1772
1136
  */
1773
- function createConst(props) {
1774
- return {
1775
- ...props,
1776
- kind: "Const"
1777
- };
1137
+ function collectSync(node, options) {
1138
+ return Array.from(collect(node, options));
1778
1139
  }
1140
+ //#endregion
1141
+ //#region src/defineMacro.ts
1779
1142
  /**
1780
- * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
1781
- *
1782
- * Mirrors the `Type` component from `@kubb/renderer-jsx`.
1783
- * The component's `children` are represented as `nodes`.
1784
- *
1785
- * @example Simple type alias
1786
- * ```ts
1787
- * createType({ name: 'Pet' })
1788
- * // type Pet = ...
1789
- * ```
1790
- *
1791
- * @example Exported type with JSDoc
1792
- * ```ts
1793
- * createType({
1794
- * name: 'PetStatus',
1795
- * export: true,
1796
- * JSDoc: { comments: ['@description Status of a pet'] },
1797
- * })
1798
- * // export type PetStatus = ...
1799
- * ```
1143
+ * Sort weight for an `enforce` hint. `pre` sorts before unmarked items and `post` after, so a plain
1144
+ * list keeps its authored order.
1800
1145
  */
1801
- function createType(props) {
1802
- return {
1803
- ...props,
1804
- kind: "Type"
1805
- };
1146
+ function enforceWeight(enforce) {
1147
+ if (enforce === "pre") return 0;
1148
+ if (enforce === "post") return 2;
1149
+ return 1;
1806
1150
  }
1807
1151
  /**
1808
- * Creates a `FunctionNode` representing a TypeScript `function` declaration.
1809
- *
1810
- * Mirrors the `Function` component from `@kubb/renderer-jsx`.
1811
- * The component's `children` are represented as `nodes`.
1812
- *
1813
- * @example Simple function
1814
- * ```ts
1815
- * createFunction({ name: 'getPet' })
1816
- * // function getPet() { ... }
1817
- * ```
1818
- *
1819
- * @example Exported async function with return type
1820
- * ```ts
1821
- * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
1822
- * // export async function fetchPet(): Promise<Pet> { ... }
1823
- * ```
1152
+ * Types a macro for inference and a single construction site, mirroring `definePlugin`.
1153
+ * Adds no runtime behavior.
1824
1154
  *
1825
- * @example Function with generics and params
1155
+ * @example
1826
1156
  * ```ts
1827
- * createFunction({
1828
- * name: 'identity',
1829
- * export: true,
1830
- * generics: ['T'],
1831
- * params: 'value: T',
1832
- * returnType: 'T',
1157
+ * const macroUntagged = defineMacro({
1158
+ * name: 'untagged',
1159
+ * operation(node) {
1160
+ * return node.tags?.length ? undefined : { ...node, tags: ['untagged'] }
1161
+ * },
1833
1162
  * })
1834
- * // export function identity<T>(value: T): T { ... }
1835
1163
  * ```
1836
1164
  */
1837
- function createFunction(props) {
1838
- return {
1839
- ...props,
1840
- kind: "Function"
1841
- };
1165
+ function defineMacro(macro) {
1166
+ return macro;
1842
1167
  }
1843
1168
  /**
1844
- * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
1845
- *
1846
- * Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
1847
- * The component's `children` are represented as `nodes`.
1848
- *
1849
- * @example Simple arrow function
1850
- * ```ts
1851
- * createArrowFunction({ name: 'getPet' })
1852
- * // const getPet = () => { ... }
1853
- * ```
1854
- *
1855
- * @example Single-line exported arrow function
1856
- * ```ts
1857
- * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
1858
- * // export const double = (n: number) => ...
1859
- * ```
1860
- *
1861
- * @example Async arrow function with generics
1862
- * ```ts
1863
- * createArrowFunction({
1864
- * name: 'fetchPet',
1865
- * export: true,
1866
- * async: true,
1867
- * generics: ['T'],
1868
- * params: 'id: string',
1869
- * returnType: 'T',
1870
- * })
1871
- * // export const fetchPet = async <T>(id: string): Promise<T> => { ... }
1872
- * ```
1169
+ * Runs every macro's callback for one node kind in order, chaining the result so each macro sees
1170
+ * the previous macro's output. Returns `undefined` when nothing changed, so `transform` keeps the
1171
+ * original reference (structural sharing).
1873
1172
  */
1874
- function createArrowFunction(props) {
1875
- return {
1876
- ...props,
1877
- kind: "ArrowFunction"
1878
- };
1173
+ function chain({ macros, key, node, context }) {
1174
+ let current = node;
1175
+ for (const macro of macros) {
1176
+ const callback = macro[key];
1177
+ if (!callback) continue;
1178
+ if (macro.when && !macro.when(current)) continue;
1179
+ const next = callback(current, context);
1180
+ if (next != null) current = next;
1181
+ }
1182
+ return current === node ? void 0 : current;
1879
1183
  }
1880
1184
  /**
1881
- * Creates a {@link TextNode} representing a raw string fragment in the source output.
1882
- *
1883
- * Use this instead of bare strings when building `nodes` arrays so that every
1884
- * entry in the array is a typed {@link CodeNode}.
1185
+ * Folds an ordered list of macros into a single {@link Visitor} that `transform` (and the per-plugin
1186
+ * transform layer in `@kubb/core`) can run. Macros are stable-sorted by `enforce`, then applied
1187
+ * sequentially per node so later macros see earlier output. This differs from a plain visitor, which
1188
+ * has no names, ordering, or composition.
1885
1189
  *
1886
1190
  * @example
1887
1191
  * ```ts
1888
- * createText('return fetch(id)')
1889
- * // { kind: 'Text', value: 'return fetch(id)' }
1192
+ * const visitor = composeMacros([macroSimplifyUnion, macroDiscriminatorEnum])
1193
+ * const next = transform(root, visitor)
1890
1194
  * ```
1891
1195
  */
1892
- function createText(value) {
1893
- return {
1894
- value,
1895
- kind: "Text"
1896
- };
1196
+ function composeMacros(macros) {
1197
+ const ordered = [...macros].sort((a, b) => enforceWeight(a.enforce) - enforceWeight(b.enforce));
1198
+ const visitor = {};
1199
+ for (const key of visitorKeys) {
1200
+ if (!ordered.some((macro) => typeof macro[key] === "function")) continue;
1201
+ const callback = (node, context) => chain({
1202
+ macros: ordered,
1203
+ key,
1204
+ node,
1205
+ context
1206
+ });
1207
+ visitor[key] = callback;
1208
+ }
1209
+ return visitor;
1897
1210
  }
1898
1211
  /**
1899
- * Creates a {@link BreakNode} representing a line break in the source output.
1900
- *
1901
- * Corresponds to `<br/>` in JSX components. Prints as an empty string which,
1902
- * when joined with `\n` by `printNodes`, produces a blank line.
1212
+ * Runs a list of macros over a node tree and returns the rewritten tree. Keeps `transform`'s
1213
+ * structural sharing, so an empty or no-op macro list returns the same reference. Pass
1214
+ * `depth: 'shallow'` to rewrite the root node only.
1903
1215
  *
1904
1216
  * @example
1905
1217
  * ```ts
1906
- * createBreak()
1907
- * // { kind: 'Break' }
1218
+ * const next = applyMacros(root, [macroIntegerToString])
1908
1219
  * ```
1909
- */
1910
- function createBreak() {
1911
- return { kind: "Break" };
1912
- }
1913
- /**
1914
- * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
1915
- *
1916
- * Use this to embed JSX markup (including fragments `<>…</>`) directly in generated code.
1917
1220
  *
1918
- * @example
1221
+ * @example Apply to the root node only
1919
1222
  * ```ts
1920
- * createJsx('<>\n <a href={href}>Open</a>\n</>')
1921
- * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
1223
+ * const named = applyMacros(node, [macroEnumName({ parentName, propName, enumSuffix })], { depth: 'shallow' })
1922
1224
  * ```
1923
1225
  */
1924
- function createJsx(value) {
1925
- return {
1926
- value,
1927
- kind: "Jsx"
1928
- };
1226
+ function applyMacros(root, macros, options) {
1227
+ if (macros.length === 0) return root;
1228
+ return transform(root, {
1229
+ ...composeMacros(macros),
1230
+ ...options
1231
+ });
1929
1232
  }
1930
1233
  //#endregion
1931
- //#region src/printer.ts
1234
+ //#region src/createPrinter.ts
1932
1235
  /**
1933
- * Creates a schema printer factory.
1934
- *
1935
- * This function wraps a builder and makes options optional at call sites.
1236
+ * Creates a schema printer: a function that takes a `SchemaNode` and emits
1237
+ * code in your target language. Each plugin that produces code from schemas
1238
+ * (TypeScript types, Zod schemas, Faker factories) ships a printer built
1239
+ * with this helper.
1936
1240
  *
1937
1241
  * The builder receives resolved options and returns:
1938
- * - `name` — a unique identifier for the printer
1939
- * - `options` — options stored on the returned printer instance
1940
- * - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
1941
- * - `print` _(optional)_ — top-level override exposed as `printer.print`
1942
- * - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
1943
- * - This keeps recursion safe and avoids self-calls
1944
1242
  *
1945
- * When no `print` override is provided, `printer.print` falls back to `printer.transform` (the node-level dispatcher).
1243
+ * - `name` unique identifier for the printer.
1244
+ * - `options` stored on the returned printer instance.
1245
+ * - `nodes` map of `SchemaType` → handler. Handlers return the rendered
1246
+ * output (a string, a TypeScript AST node, ...) for that schema type.
1247
+ * - `overrides` (optional), user-supplied handlers that win over `nodes`.
1248
+ * An override can call `this.base(node)` to reuse the handler it replaced.
1249
+ * - `print` (optional), top-level override exposed as `printer.print`.
1250
+ * Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
1251
+ *
1252
+ * Without a `print` override, `printer.print` falls back to `printer.transform`
1253
+ * (the node-level dispatcher).
1946
1254
  *
1947
- * @example Basic usage — Zod schema printer
1255
+ * @example Tiny Zod printer
1948
1256
  * ```ts
1257
+ * import { createPrinter, type PrinterFactoryOptions } from '@kubb/ast'
1258
+ *
1949
1259
  * type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
1950
1260
  *
1951
- * export const zodPrinter = definePrinter<PrinterZod>((options) => ({
1261
+ * export const zodPrinter = createPrinter<PrinterZod>((options) => ({
1952
1262
  * name: 'zod',
1953
1263
  * options: { strict: options.strict ?? true },
1954
1264
  * nodes: {
1955
1265
  * string: () => 'z.string()',
1956
1266
  * object(node) {
1957
- * const props = node.properties.map(p => `${p.name}: ${this.transform(p.schema)}`).join(', ')
1267
+ * const props = node.properties
1268
+ * .map((p) => `${p.name}: ${this.transform(p.schema)}`)
1269
+ * .join(', ')
1958
1270
  * return `z.object({ ${props} })`
1959
1271
  * },
1960
1272
  * },
1961
1273
  * }))
1962
1274
  * ```
1963
1275
  */
1964
- function definePrinter(build) {
1965
- return createPrinterFactory((node) => node.type)(build);
1276
+ function createPrinter(build) {
1277
+ return (options) => {
1278
+ const { name, options: resolvedOptions, nodes, overrides, print: printOverride } = build(options ?? {});
1279
+ const merged = overrides ? {
1280
+ ...nodes,
1281
+ ...overrides
1282
+ } : nodes;
1283
+ const context = {
1284
+ options: resolvedOptions,
1285
+ transform: (node) => {
1286
+ const handler = merged[node.type];
1287
+ if (!handler) return null;
1288
+ return handler.call(context, node);
1289
+ },
1290
+ base: (node) => {
1291
+ const handler = nodes[node.type];
1292
+ if (!handler) return null;
1293
+ return handler.call(context, node);
1294
+ }
1295
+ };
1296
+ return {
1297
+ name,
1298
+ options: resolvedOptions,
1299
+ transform: context.transform,
1300
+ print: printOverride ? printOverride.bind(context) : context.transform
1301
+ };
1302
+ };
1966
1303
  }
1304
+ //#endregion
1305
+ //#region src/utils/refs.ts
1967
1306
  /**
1968
- * Generic printer-factory function used by `definePrinter` and `defineFunctionPrinter`.
1969
- **
1307
+ * Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
1308
+ * the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
1309
+ * then `name`, then the nested `schema.name`.
1310
+ *
1311
+ * Returns `null` for non-ref nodes or when no name resolves.
1312
+ *
1970
1313
  * @example
1971
- * ```ts
1972
- * export const defineFunctionPrinter = createPrinterFactory<FunctionNode, FunctionNodeType, FunctionNodeByType>(
1973
- * (node) => kindToHandlerKey[node.kind],
1974
- * )
1975
- * ```
1314
+ * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
1315
+ *
1316
+ * @example Collision-renamed target
1317
+ * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
1976
1318
  */
1977
- function createPrinterFactory(getKey) {
1978
- return function(build) {
1979
- return (options) => {
1980
- const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? {});
1981
- const context = {
1982
- options: resolvedOptions,
1983
- transform: (node) => {
1984
- const key = getKey(node);
1985
- if (key === void 0) return null;
1986
- const handler = nodes[key];
1987
- if (!handler) return null;
1988
- return handler.call(context, node);
1989
- }
1990
- };
1991
- return {
1992
- name,
1993
- options: resolvedOptions,
1994
- transform: context.transform,
1995
- print: printOverride ? printOverride.bind(context) : context.transform
1996
- };
1997
- };
1998
- };
1319
+ function resolveRefName(node) {
1320
+ if (!node || node.type !== "ref") return null;
1321
+ if (node.targetName) return node.targetName;
1322
+ if (node.ref) return node.ref.split("/").at(-1) ?? node.ref;
1323
+ return node.name ?? node.schema?.name ?? null;
1999
1324
  }
2000
1325
  //#endregion
2001
- //#region src/resolvers.ts
2002
- function findDiscriminator(mapping, ref) {
2003
- if (!mapping || !ref) return null;
2004
- return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
2005
- }
2006
- function childName(parentName, propName) {
2007
- return parentName ? pascalCase([parentName, propName].join(" ")) : null;
2008
- }
2009
- function enumPropName(parentName, propName, enumSuffix) {
2010
- return pascalCase([
2011
- parentName,
2012
- propName,
2013
- enumSuffix
2014
- ].filter(Boolean).join(" "));
2015
- }
1326
+ //#region src/utils/schemaGraph.ts
2016
1327
  /**
2017
- * Collects import entries for all `ref` schema nodes in `node`.
1328
+ * Memoized inner pass that walks a single node and returns the names of every schema it references.
2018
1329
  */
2019
- function collectImports({ node, nameMapping, resolve }) {
2020
- return collect(node, { schema(schemaNode) {
2021
- const schemaRef = narrowSchema(schemaNode, "ref");
2022
- if (!schemaRef?.ref) return;
2023
- const rawName = extractRefName(schemaRef.ref);
2024
- const result = resolve(nameMapping.get(rawName) ?? rawName);
2025
- if (!result) return;
2026
- return result;
1330
+ const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1331
+ const refs = /* @__PURE__ */ new Set();
1332
+ collectSync(node, { schema(child) {
1333
+ if (child.type === "ref") {
1334
+ const name = resolveRefName(child);
1335
+ if (name) refs.add(name);
1336
+ }
2027
1337
  } });
2028
- }
2029
- //#endregion
2030
- //#region src/transformers.ts
1338
+ return refs;
1339
+ });
2031
1340
  /**
2032
- * Replaces a discriminator property's schema with a string enum of allowed values.
1341
+ * Collects the names of every ref found anywhere inside a node's own subtree.
2033
1342
  *
2034
- * If `node` is not an object schema, or if the property does not exist, the input
2035
- * node is returned as-is.
1343
+ * Each ref contributes its name only, so the schema it points to is never traversed here. Pass `out`
1344
+ * to accumulate names from several nodes into one set.
2036
1345
  *
2037
- * @example
1346
+ * @example Collect refs from a single schema
2038
1347
  * ```ts
2039
- * const schema = createSchema({
2040
- * type: 'object',
2041
- * properties: [createProperty({ name: 'type', required: true, schema: createSchema({ type: 'string' }) })],
2042
- * })
2043
- * const result = setDiscriminatorEnum({ node: schema, propertyName: 'type', values: ['dog', 'cat'] })
1348
+ * const names = collectReferencedSchemaNames(petSchema)
1349
+ * // Set { 'Category', 'Tag' }
1350
+ * ```
1351
+ *
1352
+ * @example Accumulate refs from multiple schemas into one set
1353
+ * ```ts
1354
+ * const out = new Set<string>()
1355
+ * for (const schema of schemas) {
1356
+ * collectReferencedSchemaNames(schema, out)
1357
+ * }
2044
1358
  * ```
2045
1359
  */
2046
- function setDiscriminatorEnum({ node, propertyName, values, enumName }) {
2047
- const objectNode = narrowSchema(node, "object");
2048
- if (!objectNode?.properties?.length) return node;
2049
- if (!objectNode.properties.some((prop) => prop.name === propertyName)) return node;
2050
- return createSchema({
2051
- ...objectNode,
2052
- properties: objectNode.properties.map((prop) => {
2053
- if (prop.name !== propertyName) return prop;
2054
- return createProperty({
2055
- ...prop,
2056
- schema: createSchema({
2057
- type: "enum",
2058
- primitive: "string",
2059
- enumValues: values,
2060
- name: enumName,
2061
- readOnly: prop.schema.readOnly,
2062
- writeOnly: prop.schema.writeOnly
2063
- })
2064
- });
2065
- })
2066
- });
1360
+ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
1361
+ if (!node) return out;
1362
+ for (const name of collectSchemaRefs(node)) out.add(name);
1363
+ return out;
1364
+ }
1365
+ function computeUsedSchemaNames(operations, schemas) {
1366
+ const schemaMap = /* @__PURE__ */ new Map();
1367
+ for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
1368
+ const result = /* @__PURE__ */ new Set();
1369
+ function visitSchema(schema) {
1370
+ const directRefs = collectReferencedSchemaNames(schema);
1371
+ for (const name of directRefs) if (!result.has(name)) {
1372
+ result.add(name);
1373
+ const namedSchema = schemaMap.get(name);
1374
+ if (namedSchema) visitSchema(namedSchema);
1375
+ }
1376
+ }
1377
+ for (const op of operations) for (const schema of collect(op, {
1378
+ depth: "shallow",
1379
+ schema: (node) => node
1380
+ })) visitSchema(schema);
1381
+ return result;
2067
1382
  }
2068
1383
  /**
2069
- * Merges adjacent anonymous object members into a single anonymous object member.
1384
+ * Collects the names of all top-level schemas transitively used by a set of operations.
2070
1385
  *
2071
- * @example
1386
+ * An operation uses a schema when its parameters, request body, or responses reference it, directly
1387
+ * or through other named schemas. Once a name is added to the result it is not revisited, so
1388
+ * reference cycles terminate.
1389
+ *
1390
+ * Pair it with `include` filters so schemas reachable only from excluded operations stay ungenerated.
1391
+ *
1392
+ * @example Only generate schemas referenced by included operations
2072
1393
  * ```ts
2073
- * const merged = mergeAdjacentObjects([
2074
- * createSchema({ type: 'object', properties: [createProperty({ name: 'a', schema: createSchema({ type: 'string' }) })] }),
2075
- * createSchema({ type: 'object', properties: [createProperty({ name: 'b', schema: createSchema({ type: 'number' }) })] }),
2076
- * ])
1394
+ * const includedOps = operations.filter((op) => resolver.default.options(op, { options, include }) !== null)
1395
+ * const allowed = collectUsedSchemaNames(includedOps, schemas)
1396
+ *
1397
+ * for (const schema of schemas) {
1398
+ * if (schema.name && !allowed.has(schema.name)) continue
1399
+ * // generate schema
1400
+ * }
2077
1401
  * ```
2078
1402
  */
2079
- function mergeAdjacentObjects(members) {
2080
- return members.reduce((acc, member) => {
2081
- const objectMember = narrowSchema(member, "object");
2082
- if (objectMember && !objectMember.name) {
2083
- const previous = acc.at(-1);
2084
- const previousObject = previous ? narrowSchema(previous, "object") : void 0;
2085
- if (previousObject && !previousObject.name) {
2086
- acc[acc.length - 1] = createSchema({
2087
- ...previousObject,
2088
- properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
2089
- });
2090
- return acc;
1403
+ function collectUsedSchemaNames(operations, schemas) {
1404
+ return computeUsedSchemaNames(operations, schemas);
1405
+ }
1406
+ const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
1407
+ const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
1408
+ const graph = /* @__PURE__ */ new Map();
1409
+ for (const schema of schemas) {
1410
+ if (!schema.name) continue;
1411
+ graph.set(schema.name, collectReferencedSchemaNames(schema));
1412
+ }
1413
+ const circular = /* @__PURE__ */ new Set();
1414
+ for (const start of graph.keys()) {
1415
+ const visited = /* @__PURE__ */ new Set();
1416
+ const stack = [...graph.get(start) ?? []];
1417
+ while (stack.length > 0) {
1418
+ const node = stack.pop();
1419
+ if (node === start) {
1420
+ circular.add(start);
1421
+ break;
2091
1422
  }
1423
+ if (visited.has(node)) continue;
1424
+ visited.add(node);
1425
+ const next = graph.get(node);
1426
+ if (next) for (const r of next) stack.push(r);
2092
1427
  }
2093
- acc.push(member);
2094
- return acc;
2095
- }, []);
2096
- }
1428
+ }
1429
+ return circular;
1430
+ });
2097
1431
  /**
2098
- * Removes enum members that are covered by broader scalar primitives in the same union.
1432
+ * Finds every schema that takes part in a circular dependency chain, including direct self-loops.
1433
+ *
1434
+ * Wrap the returned schema positions in a deferred construct (a lazy getter or `z.lazy(() => …)`) so
1435
+ * the generated code does not recurse forever. Refs are followed by name only, so the walk stays
1436
+ * linear in the size of the schema graph.
1437
+ *
1438
+ * @note Call this once on the full graph, then check individual schemas with `containsCircularRef()`.
1439
+ */
1440
+ function findCircularSchemas(schemas) {
1441
+ if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
1442
+ return findCircularSchemasMemo(schemas);
1443
+ }
1444
+ //#endregion
1445
+ //#region src/factory.ts
1446
+ var factory_exports = /* @__PURE__ */ __exportAll({
1447
+ createArrowFunction: () => createArrowFunction,
1448
+ createBreak: () => createBreak,
1449
+ createConst: () => createConst,
1450
+ createContent: () => createContent,
1451
+ createExport: () => createExport,
1452
+ createFile: () => createFile,
1453
+ createFunction: () => createFunction,
1454
+ createImport: () => createImport,
1455
+ createInput: () => createInput,
1456
+ createJsx: () => createJsx,
1457
+ createOperation: () => createOperation,
1458
+ createOutput: () => createOutput,
1459
+ createParameter: () => createParameter,
1460
+ createProperty: () => createProperty,
1461
+ createRequestBody: () => createRequestBody,
1462
+ createResponse: () => createResponse,
1463
+ createSchema: () => createSchema,
1464
+ createSource: () => createSource,
1465
+ createText: () => createText,
1466
+ createType: () => createType,
1467
+ update: () => update
1468
+ });
1469
+ /**
1470
+ * Identity-preserving node update: returns `node` unchanged when every field in
1471
+ * `changes` already equals (by reference) the current value, otherwise a new node
1472
+ * with the changes applied.
1473
+ *
1474
+ * Mirrors the TypeScript compiler's `factory.updateX` contract. Pair it with the
1475
+ * structural sharing in {@link transform} so a no-op rewrite does not allocate and
1476
+ * downstream passes can detect "nothing changed" by identity. Comparison is shallow,
1477
+ * so a structurally equal but newly allocated array or object counts as a change.
2099
1478
  *
2100
1479
  * @example
2101
1480
  * ```ts
2102
- * const simplified = simplifyUnion([
2103
- * createSchema({ type: 'enum', primitive: 'string', enumValues: ['active'] }),
2104
- * createSchema({ type: 'string' }),
2105
- * ])
2106
- * // keeps only string member
1481
+ * update(node, { name: node.name }) // -> same `node` reference
1482
+ * update(node, { name: 'renamed' }) // -> new node, `name` replaced
2107
1483
  * ```
2108
1484
  */
2109
- function simplifyUnion(members) {
2110
- const scalarPrimitives = new Set(members.filter((member) => isScalarPrimitive(member.type)).map((m) => m.type));
2111
- if (!scalarPrimitives.size) return members;
2112
- return members.filter((member) => {
2113
- const enumNode = narrowSchema(member, "enum");
2114
- if (!enumNode) return true;
2115
- const primitive = enumNode.primitive;
2116
- if (!primitive) return true;
2117
- if ((enumNode.namedEnumValues?.length ?? enumNode.enumValues?.length ?? 0) <= 1) return true;
2118
- if (scalarPrimitives.has(primitive)) return false;
2119
- if ((primitive === "integer" || primitive === "number") && (scalarPrimitives.has("integer") || scalarPrimitives.has("number"))) return false;
2120
- return true;
2121
- });
2122
- }
2123
- function setEnumName(propNode, parentName, propName, enumSuffix) {
2124
- const enumNode = narrowSchema(propNode, "enum");
2125
- if (enumNode?.primitive === "boolean") return {
2126
- ...propNode,
2127
- name: void 0
1485
+ function update(node, changes) {
1486
+ for (const key in changes) if (changes[key] !== node[key]) return {
1487
+ ...node,
1488
+ ...changes
2128
1489
  };
2129
- if (enumNode) return {
2130
- ...propNode,
2131
- name: enumPropName(parentName, propName, enumSuffix)
2132
- };
2133
- return propNode;
1490
+ return node;
2134
1491
  }
2135
1492
  //#endregion
2136
- export { caseParams, childName, collect, collectImports, collectReferencedSchemaNames, containsCircularRef, 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, findCircularSchemas, findDiscriminator, httpMethods, isInputNode, isOperationNode, isOutputNode, isScalarPrimitive, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, narrowSchema, nodeKinds, resolveRefName, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, walk };
1493
+ //#region src/exports.ts
1494
+ var exports_exports = /* @__PURE__ */ __exportAll({
1495
+ applyMacros: () => applyMacros,
1496
+ arrowFunctionDef: () => arrowFunctionDef,
1497
+ breakDef: () => breakDef,
1498
+ collect: () => collect,
1499
+ collectSync: () => collectSync,
1500
+ collectUsedSchemaNames: () => collectUsedSchemaNames,
1501
+ combineExports: () => combineExports,
1502
+ combineImports: () => combineImports,
1503
+ combineSources: () => combineSources,
1504
+ composeMacros: () => composeMacros,
1505
+ constDef: () => constDef,
1506
+ contentDef: () => contentDef,
1507
+ createPrinter: () => createPrinter,
1508
+ defineMacro: () => defineMacro,
1509
+ defineNode: () => defineNode,
1510
+ exportDef: () => exportDef,
1511
+ extractStringsFromNodes: () => extractStringsFromNodes,
1512
+ factory: () => factory_exports,
1513
+ fileDef: () => fileDef,
1514
+ findCircularSchemas: () => findCircularSchemas,
1515
+ functionDef: () => functionDef,
1516
+ importDef: () => importDef,
1517
+ inputDef: () => inputDef,
1518
+ isHttpOperationNode: () => isHttpOperationNode,
1519
+ jsxDef: () => jsxDef,
1520
+ narrowSchema: () => narrowSchema,
1521
+ nodeDefs: () => nodeDefs,
1522
+ operationDef: () => operationDef,
1523
+ optionality: () => optionality,
1524
+ outputDef: () => outputDef,
1525
+ parameterDef: () => parameterDef,
1526
+ propertyDef: () => propertyDef,
1527
+ requestBodyDef: () => requestBodyDef,
1528
+ resolveRefName: () => resolveRefName,
1529
+ responseDef: () => responseDef,
1530
+ schemaDef: () => schemaDef,
1531
+ schemaTypes: () => schemaTypes,
1532
+ sourceDef: () => sourceDef,
1533
+ textDef: () => textDef,
1534
+ transform: () => transform,
1535
+ typeDef: () => typeDef
1536
+ });
1537
+ //#endregion
1538
+ export { applyMacros, arrowFunctionDef, exports_exports as ast, breakDef, collect, collectSync, collectUsedSchemaNames, combineExports, combineImports, combineSources, composeMacros, constDef, contentDef, createPrinter, defineMacro, defineNode, exportDef, extractStringsFromNodes, factory_exports as factory, fileDef, findCircularSchemas, functionDef, importDef, inputDef, isHttpOperationNode, jsxDef, narrowSchema, nodeDefs, operationDef, optionality, outputDef, parameterDef, propertyDef, requestBodyDef, resolveRefName, responseDef, schemaDef, schemaTypes, sourceDef, textDef, transform, typeDef };
2137
1539
 
2138
1540
  //# sourceMappingURL=index.js.map