@kubb/ast 5.0.0-alpha.31 → 5.0.0-alpha.33

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