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

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