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

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