@kubb/ast 5.0.0-alpha.16 → 5.0.0-alpha.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,6 +16,17 @@ const nodeKinds = {
16
16
  objectBindingParameter: "ObjectBindingParameter",
17
17
  functionParameters: "FunctionParameters"
18
18
  };
19
+ /**
20
+ * Canonical schema type strings used by AST schema nodes.
21
+ *
22
+ * These values are used across the AST as stable discriminators
23
+ * (for example `schema.type === schemaTypes.object`).
24
+ *
25
+ * The map is grouped by intent:
26
+ * - primitives (`string`, `number`, `boolean`, ...)
27
+ * - structural/composite (`object`, `array`, `union`, ...)
28
+ * - special OpenAPI-oriented types (`ref`, `datetime`, `uuid`, ...)
29
+ */
19
30
  const schemaTypes = {
20
31
  string: "string",
21
32
  number: "number",
@@ -43,7 +54,7 @@ const schemaTypes = {
43
54
  never: "never"
44
55
  };
45
56
  /**
46
- * Scalar primitive schema types used for union member simplification.
57
+ * Primitive scalar schema types used when simplifying union members.
47
58
  */
48
59
  const SCALAR_PRIMITIVE_TYPES = new Set([
49
60
  "string",
@@ -84,818 +95,945 @@ const mediaTypes = {
84
95
  videoMp4: "video/mp4"
85
96
  };
86
97
  //#endregion
87
- //#region src/factory.ts
98
+ //#region ../../internals/utils/dist/index.js
88
99
  /**
89
- * Creates a `RootNode`.
100
+ * Shared implementation for camelCase and PascalCase conversion.
101
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
102
+ * and capitalizes each word according to `pascal`.
103
+ *
104
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
90
105
  */
91
- function createRoot(overrides = {}) {
92
- return {
93
- schemas: [],
94
- operations: [],
95
- ...overrides,
96
- kind: "Root"
97
- };
106
+ function toCamelOrPascal(text, pascal) {
107
+ 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) => {
108
+ if (word.length > 1 && word === word.toUpperCase()) return word;
109
+ if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
110
+ return word.charAt(0).toUpperCase() + word.slice(1);
111
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
98
112
  }
99
113
  /**
100
- * Creates an `OperationNode`.
114
+ * Splits `text` on `.` and applies `transformPart` to each segment.
115
+ * The last segment receives `isLast = true`, all earlier segments receive `false`.
116
+ * Segments are joined with `/` to form a file path.
117
+ *
118
+ * Only splits on dots followed by a letter so that version numbers
119
+ * embedded in operationIds (e.g. `v2025.0`) are kept intact.
101
120
  */
102
- function createOperation(props) {
103
- return {
104
- tags: [],
105
- parameters: [],
106
- responses: [],
107
- ...props,
108
- kind: "Operation"
109
- };
110
- }
111
- function createSchema(props) {
112
- if (props["type"] === "object") return {
113
- properties: [],
114
- ...props,
115
- kind: "Schema"
116
- };
117
- return {
118
- ...props,
119
- kind: "Schema"
120
- };
121
+ function applyToFileParts(text, transformPart) {
122
+ const parts = text.split(/\.(?=[a-zA-Z])/);
123
+ return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
121
124
  }
122
125
  /**
123
- * Derives `schema.optional` and `schema.nullish` from `required` and `schema.nullable`.
124
- * This keeps `PropertyNode.required` as the single source of truth for optionality.
126
+ * Converts `text` to camelCase.
127
+ * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
128
+ *
129
+ * @example
130
+ * camelCase('hello-world') // 'helloWorld'
131
+ * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
125
132
  */
126
- function syncPropertySchema(required, schema) {
127
- const nullable = schema.nullable ?? false;
128
- return {
129
- ...schema,
130
- optional: !required && !nullable ? true : void 0,
131
- nullish: !required && nullable ? true : void 0
132
- };
133
+ function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
134
+ if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
135
+ prefix,
136
+ suffix
137
+ } : {}));
138
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
133
139
  }
134
140
  /**
135
- * Creates a `PropertyNode`. `required` defaults to `false`.
136
- * `schema.optional` and `schema.nullish` are auto-derived from `required` and `schema.nullable`.
141
+ * Converts `text` to PascalCase.
142
+ * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
143
+ *
144
+ * @example
145
+ * pascalCase('hello-world') // 'HelloWorld'
146
+ * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
137
147
  */
138
- function createProperty(props) {
139
- const required = props.required ?? false;
140
- return {
141
- ...props,
142
- kind: "Property",
143
- required,
144
- schema: syncPropertySchema(required, props.schema)
145
- };
148
+ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
149
+ if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
150
+ prefix,
151
+ suffix
152
+ }) : camelCase(part));
153
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
146
154
  }
147
- /**
148
- * Creates a `ParameterNode`. `required` defaults to `false`.
149
- * `schema.optional` is auto-derived from `required` and `schema.nullable`.
150
- */
151
- function createParameter(props) {
152
- const required = props.required ?? false;
153
- return {
154
- ...props,
155
- kind: "Parameter",
156
- required,
157
- schema: syncPropertySchema(required, props.schema)
158
- };
155
+ /** Returns a `CLIAdapter` with type inference. Pass a different adapter to `createCLI` to swap the CLI engine. */
156
+ function defineCLIAdapter(adapter) {
157
+ return adapter;
159
158
  }
160
159
  /**
161
- * Creates a `ResponseNode`.
160
+ * Serializes `CommandDefinition[]` to a plain, JSON-serializable structure.
161
+ * Use to expose CLI capabilities to AI agents or MCP tools.
162
162
  */
163
- function createResponse(props) {
164
- return {
165
- ...props,
166
- kind: "Response"
167
- };
163
+ function getCommandSchema(defs) {
164
+ return defs.map(serializeCommand);
168
165
  }
169
- /**
170
- * Creates a `FunctionParameterNode`. `optional` defaults to `false`.
171
- *
172
- * @example Required typed param
173
- * ```ts
174
- * createFunctionParameter({ name: 'petId', type: 'string' })
175
- * // → petId: string
176
- * ```
177
- *
178
- * @example Optional param
179
- * ```ts
180
- * createFunctionParameter({ name: 'params', type: 'QueryParams', optional: true })
181
- * // → params?: QueryParams
182
- * ```
183
- *
184
- * @example Param with default (implicitly optional — cannot combine with `optional: true`)
185
- * ```ts
186
- * createFunctionParameter({ name: 'config', type: 'RequestConfig', default: '{}' })
187
- * // → config: RequestConfig = {}
188
- * ```
189
- */
190
- function createFunctionParameter(props) {
166
+ function serializeCommand(def) {
191
167
  return {
192
- optional: false,
193
- ...props,
194
- kind: "FunctionParameter"
168
+ name: def.name,
169
+ description: def.description,
170
+ arguments: def.arguments,
171
+ options: serializeOptions(def.options ?? {}),
172
+ subCommands: def.subCommands ? def.subCommands.map(serializeCommand) : []
195
173
  };
196
174
  }
197
- /**
198
- * Creates an `ObjectBindingParameterNode` — an object-destructured parameter group.
199
- *
200
- * @example Destructured object param
201
- * ```ts
202
- * createObjectBindingParameter({
203
- * properties: [
204
- * createFunctionParameter({ name: 'id', type: 'string', optional: false }),
205
- * createFunctionParameter({ name: 'name', type: 'string', optional: true }),
206
- * ],
207
- * default: '{}',
208
- * })
209
- * // declaration → { id, name? }: { id: string; name?: string } = {}
210
- * // call → { id, name }
211
- * ```
212
- *
213
- * @example Inline — children emitted as individual top-level params
214
- * ```ts
215
- * createObjectBindingParameter({
216
- * properties: [createFunctionParameter({ name: 'petId', type: 'string', optional: false })],
217
- * inline: true,
218
- * })
219
- * // declaration → petId: string
220
- * // call → petId
221
- * ```
222
- */
223
- function createObjectBindingParameter(props) {
224
- return {
225
- ...props,
226
- kind: "ObjectBindingParameter"
227
- };
175
+ function serializeOptions(options) {
176
+ return Object.entries(options).map(([name, opt]) => {
177
+ return {
178
+ name,
179
+ flags: `${opt.short ? `-${opt.short}, ` : ""}--${name}${opt.type === "string" ? ` <${opt.hint ?? name}>` : ""}`,
180
+ type: opt.type,
181
+ description: opt.description,
182
+ ...opt.default !== void 0 ? { default: opt.default } : {},
183
+ ...opt.hint ? { hint: opt.hint } : {},
184
+ ...opt.enum ? { enum: opt.enum } : {},
185
+ ...opt.required ? { required: opt.required } : {}
186
+ };
187
+ });
228
188
  }
229
- /**
230
- * Creates a `FunctionParametersNode` from an ordered list of params.
231
- *
232
- * @example
233
- * ```ts
234
- * createFunctionParameters({
235
- * params: [
236
- * createFunctionParameter({ name: 'petId', type: 'string', optional: false }),
237
- * createFunctionParameter({ name: 'config', type: 'RequestConfig', optional: false, default: '{}' }),
238
- * ],
239
- * })
240
- * ```
241
- */
242
- function createFunctionParameters(props = {}) {
243
- return {
244
- params: [],
245
- ...props,
246
- kind: "FunctionParameters"
189
+ /** Prints formatted help output for a command using its `CommandDefinition`. */
190
+ function renderHelp(def, parentName) {
191
+ const schema = getCommandSchema([def])[0];
192
+ const programName = parentName ? `${parentName} ${schema.name}` : schema.name;
193
+ const argsPart = schema.arguments?.length ? ` ${schema.arguments.join(" ")}` : "";
194
+ const subCmdPart = schema.subCommands.length ? " <command>" : "";
195
+ console.log(`\n${styleText("bold", "Usage:")} ${programName}${argsPart}${subCmdPart} [options]\n`);
196
+ if (schema.description) console.log(` ${schema.description}\n`);
197
+ if (schema.subCommands.length) {
198
+ console.log(styleText("bold", "Commands:"));
199
+ for (const sub of schema.subCommands) console.log(` ${styleText("cyan", sub.name.padEnd(16))}${sub.description}`);
200
+ console.log();
201
+ }
202
+ const options = [...schema.options, {
203
+ name: "help",
204
+ flags: "-h, --help",
205
+ type: "boolean",
206
+ description: "Show help"
207
+ }];
208
+ console.log(styleText("bold", "Options:"));
209
+ for (const opt of options) {
210
+ const flags = styleText("cyan", opt.flags.padEnd(30));
211
+ const defaultPart = opt.default !== void 0 ? styleText("dim", ` (default: ${opt.default})`) : "";
212
+ console.log(` ${flags}${opt.description}${defaultPart}`);
213
+ }
214
+ console.log();
215
+ }
216
+ function buildParseOptions(def) {
217
+ const result = { help: {
218
+ type: "boolean",
219
+ short: "h"
220
+ } };
221
+ for (const [name, opt] of Object.entries(def.options ?? {})) result[name] = {
222
+ type: opt.type,
223
+ ...opt.short ? { short: opt.short } : {},
224
+ ...opt.default !== void 0 ? { default: opt.default } : {}
247
225
  };
226
+ return result;
248
227
  }
249
- //#endregion
250
- //#region src/printer.ts
251
- /**
252
- * Creates a named printer factory. Mirrors the `createPlugin` / `createAdapter` pattern
253
- * from `@kubb/core` — wraps a builder to make options optional and separates raw options
254
- * from resolved options.
255
- *
256
- * The builder receives resolved options and returns:
257
- * - `name` — a unique identifier for the printer
258
- * - `options` — options stored on the returned printer instance
259
- * - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
260
- * - `print` _(optional)_ — a root-level override that becomes the public `printer.print`.
261
- * Inside it, `this.print(node)` still dispatches to the `nodes` map — safe recursion, no infinite loop.
262
- *
263
- * When no `print` override is provided, `printer.print` is the node-level dispatcher directly.
264
- *
265
- * @example Basic usage — Zod schema printer
266
- * ```ts
267
- * type ZodPrinter = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
268
- *
269
- * export const zodPrinter = definePrinter<ZodPrinter>((options) => ({
270
- * name: 'zod',
271
- * options: { strict: options.strict ?? true },
272
- * nodes: {
273
- * string: () => 'z.string()',
274
- * object(node) {
275
- * const props = node.properties.map(p => `${p.name}: ${this.print(p.schema)}`).join(', ')
276
- * return `z.object({ ${props} })`
277
- * },
278
- * },
279
- * }))
280
- * ```
281
- *
282
- * @example With a root-level `print` override to wrap output in a full declaration
283
- * ```ts
284
- * type TsPrinter = PrinterFactoryOptions<'ts', { typeName?: string }, ts.TypeNode, ts.Node>
285
- *
286
- * export const printerTs = definePrinter<TsPrinter>((options) => ({
287
- * name: 'ts',
288
- * options,
289
- * nodes: { string: () => factory.keywordTypeNodes.string },
290
- * print(node) {
291
- * const type = this.print(node) // calls the node-level dispatcher
292
- * if (!type || !this.options.typeName) return type
293
- * return factory.createTypeAliasDeclaration(this.options.typeName, type)
294
- * },
295
- * }))
296
- * ```
297
- */
298
- function definePrinter(build) {
299
- return createPrinterFactory((node) => node.type)(build);
228
+ async function runCommand(def, argv, parentName) {
229
+ const parseOptions = buildParseOptions(def);
230
+ let parsed;
231
+ try {
232
+ const result = parseArgs({
233
+ args: argv,
234
+ options: parseOptions,
235
+ allowPositionals: true,
236
+ strict: false
237
+ });
238
+ parsed = {
239
+ values: result.values,
240
+ positionals: result.positionals
241
+ };
242
+ } catch {
243
+ renderHelp(def, parentName);
244
+ process.exit(1);
245
+ }
246
+ if (parsed.values["help"]) {
247
+ renderHelp(def, parentName);
248
+ process.exit(0);
249
+ }
250
+ for (const [name, opt] of Object.entries(def.options ?? {})) if (opt.required && parsed.values[name] === void 0) {
251
+ console.error(styleText("red", `Error: --${name} is required`));
252
+ renderHelp(def, parentName);
253
+ process.exit(1);
254
+ }
255
+ if (!def.run) {
256
+ renderHelp(def, parentName);
257
+ process.exit(0);
258
+ }
259
+ try {
260
+ await def.run(parsed);
261
+ } catch (err) {
262
+ console.error(styleText("red", `Error: ${err instanceof Error ? err.message : String(err)}`));
263
+ renderHelp(def, parentName);
264
+ process.exit(1);
265
+ }
266
+ }
267
+ function printRootHelp(programName, version, defs) {
268
+ console.log(`\n${styleText("bold", "Usage:")} ${programName} <command> [options]\n`);
269
+ console.log(` Kubb generation — v${version}\n`);
270
+ console.log(styleText("bold", "Commands:"));
271
+ for (const def of defs) console.log(` ${styleText("cyan", def.name.padEnd(16))}${def.description}`);
272
+ console.log();
273
+ console.log(styleText("bold", "Options:"));
274
+ console.log(` ${styleText("cyan", "-v, --version".padEnd(30))}Show version number`);
275
+ console.log(` ${styleText("cyan", "-h, --help".padEnd(30))}Show help`);
276
+ console.log();
277
+ console.log(`Run ${styleText("cyan", `${programName} <command> --help`)} for command-specific help.\n`);
300
278
  }
279
+ defineCLIAdapter({
280
+ renderHelp(def, parentName) {
281
+ renderHelp(def, parentName);
282
+ },
283
+ async run(defs, argv, opts) {
284
+ const { programName, defaultCommandName, version } = opts;
285
+ const args = argv.length >= 2 && argv[0]?.includes("node") ? argv.slice(2) : argv;
286
+ if (args[0] === "--version" || args[0] === "-v") {
287
+ console.log(version);
288
+ process.exit(0);
289
+ }
290
+ if (args[0] === "--help" || args[0] === "-h") {
291
+ printRootHelp(programName, version, defs);
292
+ process.exit(0);
293
+ }
294
+ if (args.length === 0) {
295
+ const defaultDef = defs.find((d) => d.name === defaultCommandName);
296
+ if (defaultDef?.run) await runCommand(defaultDef, [], programName);
297
+ else printRootHelp(programName, version, defs);
298
+ return;
299
+ }
300
+ const [first, ...rest] = args;
301
+ const isKnownSubcommand = defs.some((d) => d.name === first);
302
+ let def;
303
+ let commandArgv;
304
+ let parentName;
305
+ if (isKnownSubcommand) {
306
+ def = defs.find((d) => d.name === first);
307
+ commandArgv = rest;
308
+ parentName = programName;
309
+ } else {
310
+ def = defs.find((d) => d.name === defaultCommandName);
311
+ commandArgv = args;
312
+ parentName = programName;
313
+ }
314
+ if (!def) {
315
+ console.error(`Unknown command: ${first}`);
316
+ printRootHelp(programName, version, defs);
317
+ process.exit(1);
318
+ }
319
+ if (def.subCommands?.length) {
320
+ const [subName, ...subRest] = commandArgv;
321
+ const subDef = def.subCommands.find((s) => s.name === subName);
322
+ if (subName === "--help" || subName === "-h") {
323
+ renderHelp(def, parentName);
324
+ process.exit(0);
325
+ }
326
+ if (!subDef) {
327
+ renderHelp(def, parentName);
328
+ process.exit(subName ? 1 : 0);
329
+ }
330
+ await runCommand(subDef, subRest, `${parentName} ${def.name}`);
331
+ return;
332
+ }
333
+ await runCommand(def, commandArgv, parentName);
334
+ }
335
+ });
301
336
  /**
302
- * Generic printer factory. Extracts the core dispatch + context logic so it can be reused
303
- * for any node type — not just `SchemaNode`. `definePrinter` is built on top of this.
304
- *
305
- * @param getKey — derives the handler-map key from a node. Return `undefined` to skip.
306
- *
307
- * @example
308
- * ```ts
309
- * export const defineFunctionPrinter = createPrinterFactory<FunctionNode, FunctionNodeType, FunctionNodeByType>(
310
- * (node) => kindToHandlerKey[node.kind],
311
- * )
312
- * ```
337
+ * Parses a CSS hex color string (`#RGB`) into its RGB channels.
338
+ * Falls back to `255` for any channel that cannot be parsed.
313
339
  */
314
- function createPrinterFactory(getKey) {
315
- return function(build) {
316
- return (options) => {
317
- const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? {});
318
- const context = {
319
- options: resolvedOptions,
320
- print: (node) => {
321
- const key = getKey(node);
322
- if (key === void 0) return void 0;
323
- const handler = nodes[key];
324
- if (!handler) return void 0;
325
- return handler.call(context, node);
326
- }
327
- };
328
- return {
329
- name,
330
- options: resolvedOptions,
331
- print: printOverride ? printOverride.bind(context) : context.print
332
- };
333
- };
340
+ function parseHex(color) {
341
+ const int = Number.parseInt(color.replace("#", ""), 16);
342
+ return Number.isNaN(int) ? {
343
+ r: 255,
344
+ g: 255,
345
+ b: 255
346
+ } : {
347
+ r: int >> 16 & 255,
348
+ g: int >> 8 & 255,
349
+ b: int & 255
334
350
  };
335
351
  }
352
+ /**
353
+ * Returns a function that wraps a string in a 24-bit ANSI true-color escape sequence
354
+ * for the given hex color.
355
+ */
356
+ function hex(color) {
357
+ const { r, g, b } = parseHex(color);
358
+ return (text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
359
+ }
360
+ hex("#F55A17"), hex("#F5A217"), hex("#F58517"), hex("#B45309"), hex("#FFFFFF"), hex("#adadc6"), hex("#FDA4AF");
361
+ /**
362
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
363
+ */
364
+ function isValidVarName(name) {
365
+ try {
366
+ new Function(`var ${name}`);
367
+ } catch {
368
+ return false;
369
+ }
370
+ return true;
371
+ }
336
372
  //#endregion
337
- //#region src/functionPrinter.ts
338
- const kindToHandlerKey = {
339
- FunctionParameter: "functionParameter",
340
- ObjectBindingParameter: "objectBindingParameter",
341
- FunctionParameters: "functionParameters"
342
- };
373
+ //#region src/guards.ts
343
374
  /**
344
- * Creates a named function-signature printer factory.
345
- * Built on `createPrinterFactory` — dispatches on `node.kind` instead of `node.type`.
375
+ * Narrows a `SchemaNode` to the variant that matches `type`.
346
376
  *
347
377
  * @example
348
378
  * ```ts
349
- * type MyPrinter = PrinterFactoryOptions<'my', { mode: 'declaration' | 'call' }, string>
350
- *
351
- * export const myPrinter = defineFunctionPrinter<MyPrinter>((options) => ({
352
- * name: 'my',
353
- * options,
354
- * nodes: {
355
- * functionParameter(node) {
356
- * return options.mode === 'declaration' && node.type ? `${node.name}: ${node.type}` : node.name
357
- * },
358
- * objectBindingParameter(node) {
359
- * const inner = node.properties.map(p => this.print(p)).filter(Boolean).join(', ')
360
- * return `{ ${inner} }`
361
- * },
362
- * functionParameters(node) {
363
- * return node.params.map(p => this.print(p)).filter(Boolean).join(', ')
364
- * },
365
- * },
366
- * }))
379
+ * const schema = createSchema({ type: 'string' })
380
+ * const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | undefined
367
381
  * ```
368
382
  */
369
- const defineFunctionPrinter = createPrinterFactory((node) => kindToHandlerKey[node.kind]);
370
- function rank(param) {
371
- if (param.kind === "ObjectBindingParameter") {
372
- if (param.default) return 2;
373
- return param.optional ?? param.properties.every((p) => p.optional || p.default !== void 0) ? 1 : 0;
374
- }
375
- if (param.rest) return 3;
376
- if (param.default) return 2;
377
- return param.optional ? 1 : 0;
378
- }
379
- function sortParams(params) {
380
- return [...params].sort((a, b) => rank(a) - rank(b));
383
+ function narrowSchema(node, type) {
384
+ return node?.type === type ? node : void 0;
381
385
  }
382
- function sortChildParams(params) {
383
- return [...params].sort((a, b) => rank(a) - rank(b));
386
+ function isKind(kind) {
387
+ return (node) => node.kind === kind;
384
388
  }
385
389
  /**
386
- * Default function-signature printer. Covers the four standard output modes
387
- * used throughout Kubb plugins.
390
+ * Returns `true` when the input is a `RootNode`.
388
391
  *
389
392
  * @example
390
393
  * ```ts
391
- * const printer = functionSignaturePrinter({ mode: 'declaration' })
394
+ * if (isRootNode(node)) {
395
+ * console.log(node.schemas.length)
396
+ * }
397
+ * ```
398
+ */
399
+ const isRootNode = isKind("Root");
400
+ /**
401
+ * Returns `true` when the input is an `OperationNode`.
392
402
  *
393
- * const sig = createFunctionParameters({
394
- * params: [
395
- * createFunctionParameter({ name: 'petId', type: 'string', optional: false }),
396
- * createFunctionParameter({ name: 'config', type: 'Config', optional: false, default: '{}' }),
397
- * ],
398
- * })
403
+ * @example
404
+ * ```ts
405
+ * if (isOperationNode(node)) {
406
+ * console.log(node.operationId)
407
+ * }
408
+ * ```
409
+ */
410
+ const isOperationNode = isKind("Operation");
411
+ /**
412
+ * Returns `true` when the input is a `SchemaNode`.
399
413
  *
400
- * printer.print(sig) // → "petId: string, config: Config = {}"
414
+ * @example
415
+ * ```ts
416
+ * if (isSchemaNode(node)) {
417
+ * console.log(node.type)
418
+ * }
401
419
  * ```
402
420
  */
403
- const functionPrinter = defineFunctionPrinter((options) => ({
404
- name: "functionParameters",
405
- options,
406
- nodes: {
407
- functionParameter(node) {
408
- const { mode, transformName, transformType } = this.options;
409
- const name = transformName ? transformName(node.name) : node.name;
410
- const type = node.type && transformType ? transformType(node.type) : node.type;
411
- if (mode === "keys" || mode === "values") return node.rest ? `...${name}` : name;
412
- if (mode === "call") return node.rest ? `...${name}` : name;
413
- if (node.rest) return type ? `...${name}: ${type}` : `...${name}`;
414
- if (type) {
415
- if (node.optional) return `${name}?: ${type}`;
416
- return node.default ? `${name}: ${type} = ${node.default}` : `${name}: ${type}`;
417
- }
418
- return node.default ? `${name} = ${node.default}` : name;
419
- },
420
- objectBindingParameter(node) {
421
- const { mode, transformName, transformType } = this.options;
422
- const sorted = sortChildParams(node.properties);
423
- const isOptional = node.optional ?? sorted.every((p) => p.optional || p.default !== void 0);
424
- if (node.inline) return sorted.map((p) => this.print(p)).filter(Boolean).join(", ");
425
- if (mode === "keys" || mode === "values") return `{ ${sorted.map((p) => p.name).join(", ")} }`;
426
- if (mode === "call") return `{ ${sorted.map((p) => p.name).join(", ")} }`;
427
- const names = sorted.map((p) => {
428
- return transformName ? transformName(p.name) : p.name;
429
- });
430
- const nameStr = names.length ? `{ ${names.join(", ")} }` : void 0;
431
- if (!nameStr) return null;
432
- let typeAnnotation = node.type;
433
- if (!typeAnnotation) {
434
- const typeParts = sorted.filter((p) => p.type).map((p) => {
435
- const t = transformType && p.type ? transformType(p.type) : p.type;
436
- return p.optional || p.default !== void 0 ? `${p.name}?: ${t}` : `${p.name}: ${t}`;
437
- });
438
- typeAnnotation = typeParts.length ? `{ ${typeParts.join("; ")} }` : void 0;
439
- }
440
- if (typeAnnotation) {
441
- if (isOptional) return `${nameStr}: ${typeAnnotation} = ${node.default ?? "{}"}`;
442
- return node.default ? `${nameStr}: ${typeAnnotation} = ${node.default}` : `${nameStr}: ${typeAnnotation}`;
443
- }
444
- return node.default ? `${nameStr} = ${node.default}` : nameStr;
445
- },
446
- functionParameters(node) {
447
- return sortParams(node.params).map((p) => this.print(p)).filter(Boolean).join(", ");
448
- }
449
- }
450
- }));
451
- //#endregion
452
- //#region src/guards.ts
453
- /**
454
- * Narrows a `SchemaNode` to the specific variant matching `type`.
455
- */
456
- function narrowSchema(node, type) {
457
- return node?.type === type ? node : void 0;
458
- }
459
- function isKind(kind) {
460
- return (node) => node.kind === kind;
461
- }
462
- /**
463
- * Type guard for `RootNode`.
464
- */
465
- const isRootNode = isKind("Root");
466
- /**
467
- * Type guard for `OperationNode`.
468
- */
469
- const isOperationNode = isKind("Operation");
470
- /**
471
- * Type guard for `SchemaNode`.
472
- */
473
421
  const isSchemaNode = isKind("Schema");
474
422
  /**
475
- * Type guard for `PropertyNode`.
423
+ * Returns `true` when the input is a `PropertyNode`.
476
424
  */
477
425
  const isPropertyNode = isKind("Property");
478
426
  /**
479
- * Type guard for `ParameterNode`.
427
+ * Returns `true` when the input is a `ParameterNode`.
480
428
  */
481
429
  const isParameterNode = isKind("Parameter");
482
430
  /**
483
- * Type guard for `ResponseNode`.
431
+ * Returns `true` when the input is a `ResponseNode`.
484
432
  */
485
433
  const isResponseNode = isKind("Response");
486
434
  /**
487
- * Type guard for `FunctionParameterNode`.
435
+ * Returns `true` when the input is a `FunctionParameterNode`.
488
436
  */
489
437
  const isFunctionParameterNode = isKind("FunctionParameter");
490
438
  /**
491
- * Type guard for `ObjectBindingParameterNode`.
439
+ * Returns `true` when the input is an `ObjectBindingParameterNode`.
492
440
  */
493
441
  const isObjectBindingParameterNode = isKind("ObjectBindingParameter");
494
442
  /**
495
- * Type guard for `FunctionParametersNode`.
443
+ * Returns `true` when the input is a `FunctionParametersNode`.
496
444
  */
497
445
  const isFunctionParametersNode = isKind("FunctionParameters");
498
446
  //#endregion
499
- //#region src/refs.ts
500
- /**
501
- * Extracts the final segment from a reference string.
502
- * Falls back to the original string when no slash exists.
503
- */
504
- function extractRefName(ref) {
505
- return ref.split("/").at(-1) ?? ref;
506
- }
447
+ //#region src/utils.ts
448
+ const plainStringTypes = new Set([
449
+ "string",
450
+ "uuid",
451
+ "email",
452
+ "url",
453
+ "datetime"
454
+ ]);
507
455
  /**
508
- * Indexes named schemas from `root.schemas` by name. Unnamed schemas are skipped.
456
+ * Returns `true` when a schema is emitted as a plain TypeScript `string`.
457
+ *
458
+ * - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings.
459
+ * - `date` and `time` are plain strings when their `representation` is `'string'` rather than `'date'`.
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * isStringType(createSchema({ type: 'uuid' })) // true
464
+ * isStringType(createSchema({ type: 'date', representation: 'date' })) // false
465
+ * ```
509
466
  */
510
- function buildRefMap(root) {
511
- const map = /* @__PURE__ */ new Map();
512
- for (const schema of root.schemas) if (schema.name) map.set(schema.name, schema);
513
- return map;
467
+ function isStringType(node) {
468
+ if (plainStringTypes.has(node.type)) return true;
469
+ const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
470
+ if (temporal) return temporal.representation !== "date";
471
+ return false;
514
472
  }
515
473
  /**
516
- * Looks up a schema by name. Prefer over `RefMap.get()` to keep the resolution strategy swappable.
474
+ * Applies casing rules to parameter names and returns a new parameter array.
475
+ *
476
+ * The input array is not mutated.
477
+ * If `casing` is not set, the original array is returned unchanged.
478
+ *
479
+ * Use this before passing parameters to schema builders so that property keys
480
+ * in generated output match the desired casing while preserving
481
+ * `OperationNode.parameters` for other consumers.
482
+ *
483
+ * @example
484
+ * ```ts
485
+ * const params = [createParameter({ name: 'pet_id', in: 'query', schema: createSchema({ type: 'string' }) })]
486
+ * const cased = caseParams(params, 'camelcase')
487
+ * // cased[0].name === 'petId'
488
+ * ```
517
489
  */
518
- function resolveRef(refMap, ref) {
519
- return refMap.get(ref);
490
+ function caseParams(params, casing) {
491
+ if (!casing) return params;
492
+ return params.map((param) => {
493
+ const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
494
+ return {
495
+ ...param,
496
+ name: transformed
497
+ };
498
+ });
520
499
  }
521
500
  /**
522
- * Converts a `RefMap` to a plain object.
501
+ * Syncs property/parameter schema optionality flags from `required` and `schema.nullable`.
502
+ *
503
+ * - `optional` is set for non-required, non-nullable schemas.
504
+ * - `nullish` is set for non-required, nullable schemas.
523
505
  */
524
- function refMapToObject(refMap) {
525
- return Object.fromEntries(refMap);
506
+ function syncOptionality(required, schema) {
507
+ const nullable = schema.nullable ?? false;
508
+ return {
509
+ ...schema,
510
+ optional: !required && !nullable ? true : void 0,
511
+ nullish: !required && nullable ? true : void 0
512
+ };
526
513
  }
527
514
  //#endregion
528
- //#region src/transforms.ts
515
+ //#region src/factory.ts
529
516
  /**
530
- * Replaces the discriminator property's schema inside an object node with
531
- * an enum of the provided values.
517
+ * Creates a `RootNode` with stable defaults for `schemas` and `operations`.
518
+ *
519
+ * @example
520
+ * ```ts
521
+ * const root = createRoot()
522
+ * // { kind: 'Root', schemas: [], operations: [] }
523
+ * ```
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * const root = createRoot({ schemas: [petSchema] })
528
+ * // keeps default operations: []
529
+ * ```
532
530
  */
533
- function applyDiscriminatorEnum({ node, propertyName, values, enumName }) {
534
- const objectNode = narrowSchema(node, "object");
535
- if (!objectNode?.properties?.length) return node;
536
- if (!objectNode.properties.some((prop) => prop.name === propertyName)) return node;
537
- return createSchema({
538
- ...objectNode,
539
- properties: objectNode.properties.map((prop) => {
540
- if (prop.name !== propertyName) return prop;
541
- return createProperty({
542
- ...prop,
543
- schema: createSchema({
544
- type: "enum",
545
- primitive: "string",
546
- enumValues: values,
547
- name: enumName,
548
- readOnly: prop.schema.readOnly,
549
- writeOnly: prop.schema.writeOnly
550
- })
551
- });
552
- })
553
- });
531
+ function createRoot(overrides = {}) {
532
+ return {
533
+ schemas: [],
534
+ operations: [],
535
+ ...overrides,
536
+ kind: "Root"
537
+ };
554
538
  }
555
539
  /**
556
- * Merges adjacent anonymous object members into a single anonymous object.
540
+ * Creates an `OperationNode` with default empty arrays for `tags`, `parameters`, and `responses`.
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * const operation = createOperation({
545
+ * operationId: 'getPetById',
546
+ * method: 'GET',
547
+ * path: '/pet/{petId}',
548
+ * })
549
+ * // tags, parameters, and responses are []
550
+ * ```
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * const operation = createOperation({
555
+ * operationId: 'findPets',
556
+ * method: 'GET',
557
+ * path: '/pet/findByStatus',
558
+ * tags: ['pet'],
559
+ * })
560
+ * ```
557
561
  */
558
- function mergeAdjacentAnonymousObjects(members) {
559
- return members.reduce((acc, member) => {
560
- const objectMember = narrowSchema(member, "object");
561
- if (objectMember && !objectMember.name) {
562
- const previous = acc.at(-1);
563
- const previousObject = previous ? narrowSchema(previous, "object") : void 0;
564
- if (previousObject && !previousObject.name) {
565
- acc[acc.length - 1] = createSchema({
566
- ...previousObject,
567
- properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
568
- });
569
- return acc;
570
- }
571
- }
572
- acc.push(member);
573
- return acc;
574
- }, []);
562
+ function createOperation(props) {
563
+ return {
564
+ tags: [],
565
+ parameters: [],
566
+ responses: [],
567
+ ...props,
568
+ kind: "Operation"
569
+ };
575
570
  }
576
- /**
577
- * Removes enum members subsumed by broader scalar members in the same union.
578
- */
579
- function simplifyUnionMembers(members) {
580
- const scalarPrimitives = new Set(members.filter((member) => SCALAR_PRIMITIVE_TYPES.has(member.type)).map((m) => m.type));
581
- if (!scalarPrimitives.size) return members;
582
- return members.filter((member) => {
583
- const enumNode = narrowSchema(member, "enum");
584
- if (!enumNode) return true;
585
- const primitive = enumNode.primitive;
586
- if (!primitive) return true;
587
- if (!enumNode.enumType) return true;
588
- if (scalarPrimitives.has(primitive)) return false;
589
- if ((primitive === "integer" || primitive === "number") && (scalarPrimitives.has("integer") || scalarPrimitives.has("number"))) return false;
590
- return true;
591
- });
571
+ function createSchema(props) {
572
+ if (props["type"] === "object") return {
573
+ properties: [],
574
+ ...props,
575
+ kind: "Schema"
576
+ };
577
+ return {
578
+ ...props,
579
+ kind: "Schema"
580
+ };
592
581
  }
593
- //#endregion
594
- //#region ../../internals/utils/dist/index.js
595
582
  /**
596
- * Shared implementation for camelCase and PascalCase conversion.
597
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
598
- * and capitalizes each word according to `pascal`.
583
+ * Creates a `PropertyNode`.
599
584
  *
600
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
585
+ * `required` defaults to `false`.
586
+ * `schema.optional` and `schema.nullish` are derived from `required` and `schema.nullable`.
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * const property = createProperty({
591
+ * name: 'status',
592
+ * schema: createSchema({ type: 'string' }),
593
+ * })
594
+ * // required=false, schema.optional=true
595
+ * ```
596
+ *
597
+ * @example
598
+ * ```ts
599
+ * const property = createProperty({
600
+ * name: 'status',
601
+ * required: true,
602
+ * schema: createSchema({ type: 'string', nullable: true }),
603
+ * })
604
+ * // required=true, no optional/nullish
605
+ * ```
601
606
  */
602
- function toCamelOrPascal(text, pascal) {
603
- 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) => {
604
- if (word.length > 1 && word === word.toUpperCase()) return word;
605
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
606
- return word.charAt(0).toUpperCase() + word.slice(1);
607
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
607
+ function createProperty(props) {
608
+ const required = props.required ?? false;
609
+ return {
610
+ ...props,
611
+ kind: "Property",
612
+ required,
613
+ schema: syncOptionality(required, props.schema)
614
+ };
608
615
  }
609
616
  /**
610
- * Splits `text` on `.` and applies `transformPart` to each segment.
611
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
612
- * Segments are joined with `/` to form a file path.
617
+ * Creates a `ParameterNode`.
613
618
  *
614
- * Only splits on dots followed by a letter so that version numbers
615
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
619
+ * `required` defaults to `false`.
620
+ * Nested schema flags are set from `required` and `schema.nullable`.
621
+ *
622
+ * @example
623
+ * ```ts
624
+ * const param = createParameter({
625
+ * name: 'petId',
626
+ * in: 'path',
627
+ * required: true,
628
+ * schema: createSchema({ type: 'string' }),
629
+ * })
630
+ * ```
631
+ *
632
+ * @example
633
+ * ```ts
634
+ * const param = createParameter({
635
+ * name: 'status',
636
+ * in: 'query',
637
+ * schema: createSchema({ type: 'string', nullable: true }),
638
+ * })
639
+ * // required=false, schema.nullish=true
640
+ * ```
616
641
  */
617
- function applyToFileParts(text, transformPart) {
618
- const parts = text.split(/\.(?=[a-zA-Z])/);
619
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
642
+ function createParameter(props) {
643
+ const required = props.required ?? false;
644
+ return {
645
+ ...props,
646
+ kind: "Parameter",
647
+ required,
648
+ schema: syncOptionality(required, props.schema)
649
+ };
620
650
  }
621
651
  /**
622
- * Converts `text` to camelCase.
623
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
652
+ * Creates a `ResponseNode`.
624
653
  *
625
654
  * @example
626
- * camelCase('hello-world') // 'helloWorld'
627
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
655
+ * ```ts
656
+ * const response = createResponse({
657
+ * statusCode: '200',
658
+ * description: 'Success',
659
+ * schema: createSchema({ type: 'object', properties: [] }),
660
+ * })
661
+ * ```
628
662
  */
629
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
630
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
631
- prefix,
632
- suffix
633
- } : {}));
634
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
663
+ function createResponse(props) {
664
+ return {
665
+ ...props,
666
+ kind: "Response"
667
+ };
635
668
  }
636
- /** Returns a `CLIAdapter` with type inference. Pass a different adapter to `createCLI` to swap the CLI engine. */
637
- function defineCLIAdapter(adapter) {
638
- return adapter;
669
+ /**
670
+ * Creates a single-property object schema used as a discriminator literal.
671
+ *
672
+ * @example
673
+ * ```ts
674
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
675
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
676
+ * ```
677
+ */
678
+ function createDiscriminantNode({ propertyName, value }) {
679
+ return createSchema({
680
+ type: "object",
681
+ primitive: "object",
682
+ properties: [createProperty({
683
+ name: propertyName,
684
+ schema: createSchema({
685
+ type: "enum",
686
+ primitive: "string",
687
+ enumValues: [value]
688
+ }),
689
+ required: true
690
+ })]
691
+ });
639
692
  }
640
693
  /**
641
- * Serializes `CommandDefinition[]` to a plain, JSON-serializable structure.
642
- * Use to expose CLI capabilities to AI agents or MCP tools.
694
+ * Creates a `FunctionParameterNode`.
695
+ *
696
+ * `optional` defaults to `false`.
697
+ *
698
+ * @example Required typed param
699
+ * ```ts
700
+ * createFunctionParameter({ name: 'petId', type: 'string' })
701
+ * // → petId: string
702
+ * ```
703
+ *
704
+ * @example Optional param
705
+ * ```ts
706
+ * createFunctionParameter({ name: 'params', type: 'QueryParams', optional: true })
707
+ * // → params?: QueryParams
708
+ * ```
709
+ *
710
+ * @example Param with default (implicitly optional; cannot combine with `optional: true`)
711
+ * ```ts
712
+ * createFunctionParameter({ name: 'config', type: 'RequestConfig', default: '{}' })
713
+ * // → config: RequestConfig = {}
714
+ * ```
643
715
  */
644
- function getCommandSchema(defs) {
645
- return defs.map(serializeCommand);
716
+ function createFunctionParameter(props) {
717
+ return {
718
+ optional: false,
719
+ ...props,
720
+ kind: "FunctionParameter"
721
+ };
646
722
  }
647
- function serializeCommand(def) {
723
+ /**
724
+ * Creates an `ObjectBindingParameterNode` for object-destructured parameter groups.
725
+ *
726
+ * @example Destructured object param
727
+ * ```ts
728
+ * createObjectBindingParameter({
729
+ * properties: [
730
+ * createFunctionParameter({ name: 'id', type: 'string', optional: false }),
731
+ * createFunctionParameter({ name: 'name', type: 'string', optional: true }),
732
+ * ],
733
+ * default: '{}',
734
+ * })
735
+ * // declaration → { id, name? }: { id: string; name?: string } = {}
736
+ * // call → { id, name }
737
+ * ```
738
+ *
739
+ * @example Inline mode — children emitted as individual top-level parameters
740
+ * ```ts
741
+ * createObjectBindingParameter({
742
+ * properties: [createFunctionParameter({ name: 'petId', type: 'string', optional: false })],
743
+ * inline: true,
744
+ * })
745
+ * // declaration → petId: string
746
+ * // call → petId
747
+ * ```
748
+ */
749
+ function createObjectBindingParameter(props) {
648
750
  return {
649
- name: def.name,
650
- description: def.description,
651
- arguments: def.arguments,
652
- options: serializeOptions(def.options ?? {}),
653
- subCommands: def.subCommands ? def.subCommands.map(serializeCommand) : []
751
+ ...props,
752
+ kind: "ObjectBindingParameter"
654
753
  };
655
754
  }
656
- function serializeOptions(options) {
657
- return Object.entries(options).map(([name, opt]) => {
658
- return {
659
- name,
660
- flags: `${opt.short ? `-${opt.short}, ` : ""}--${name}${opt.type === "string" ? ` <${opt.hint ?? name}>` : ""}`,
661
- type: opt.type,
662
- description: opt.description,
663
- ...opt.default !== void 0 ? { default: opt.default } : {},
664
- ...opt.hint ? { hint: opt.hint } : {},
665
- ...opt.enum ? { enum: opt.enum } : {},
666
- ...opt.required ? { required: opt.required } : {}
667
- };
668
- });
755
+ /**
756
+ * Creates a `FunctionParametersNode` from an ordered list of parameters.
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * createFunctionParameters({
761
+ * params: [
762
+ * createFunctionParameter({ name: 'petId', type: 'string', optional: false }),
763
+ * createFunctionParameter({ name: 'config', type: 'RequestConfig', optional: false, default: '{}' }),
764
+ * ],
765
+ * })
766
+ * ```
767
+ *
768
+ * @example
769
+ * ```ts
770
+ * const empty = createFunctionParameters()
771
+ * // { kind: 'FunctionParameters', params: [] }
772
+ * ```
773
+ */
774
+ function createFunctionParameters(props = {}) {
775
+ return {
776
+ params: [],
777
+ ...props,
778
+ kind: "FunctionParameters"
779
+ };
669
780
  }
670
- /** Prints formatted help output for a command using its `CommandDefinition`. */
671
- function renderHelp(def, parentName) {
672
- const schema = getCommandSchema([def])[0];
673
- const programName = parentName ? `${parentName} ${schema.name}` : schema.name;
674
- const argsPart = schema.arguments?.length ? ` ${schema.arguments.join(" ")}` : "";
675
- const subCmdPart = schema.subCommands.length ? " <command>" : "";
676
- console.log(`\n${styleText("bold", "Usage:")} ${programName}${argsPart}${subCmdPart} [options]\n`);
677
- if (schema.description) console.log(` ${schema.description}\n`);
678
- if (schema.subCommands.length) {
679
- console.log(styleText("bold", "Commands:"));
680
- for (const sub of schema.subCommands) console.log(` ${styleText("cyan", sub.name.padEnd(16))}${sub.description}`);
681
- console.log();
682
- }
683
- const options = [...schema.options, {
684
- name: "help",
685
- flags: "-h, --help",
686
- type: "boolean",
687
- description: "Show help"
688
- }];
689
- console.log(styleText("bold", "Options:"));
690
- for (const opt of options) {
691
- const flags = styleText("cyan", opt.flags.padEnd(30));
692
- const defaultPart = opt.default !== void 0 ? styleText("dim", ` (default: ${opt.default})`) : "";
693
- console.log(` ${flags}${opt.description}${defaultPart}`);
694
- }
695
- console.log();
781
+ //#endregion
782
+ //#region src/printers/printer.ts
783
+ /**
784
+ * Creates a schema printer factory.
785
+ *
786
+ * This function wraps a builder and makes options optional at call sites.
787
+ *
788
+ * The builder receives resolved options and returns:
789
+ * - `name` — a unique identifier for the printer
790
+ * - `options` — options stored on the returned printer instance
791
+ * - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
792
+ * - `print` _(optional)_ — top-level override exposed as `printer.print`
793
+ * - Inside this function, `this.print(node)` still dispatches to the `nodes` map
794
+ * - This keeps recursion safe and avoids self-calls
795
+ *
796
+ * When no `print` override is provided, `printer.print` is the node-level dispatcher directly.
797
+ *
798
+ * @example Basic usage — Zod schema printer
799
+ * ```ts
800
+ * type ZodPrinter = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
801
+ *
802
+ * export const zodPrinter = definePrinter<ZodPrinter>((options) => ({
803
+ * name: 'zod',
804
+ * options: { strict: options.strict ?? true },
805
+ * nodes: {
806
+ * string: () => 'z.string()',
807
+ * object(node) {
808
+ * const props = node.properties.map(p => `${p.name}: ${this.print(p.schema)}`).join(', ')
809
+ * return `z.object({ ${props} })`
810
+ * },
811
+ * },
812
+ * }))
813
+ * ```
814
+ */
815
+ function definePrinter(build) {
816
+ return createPrinterFactory((node) => node.type)(build);
696
817
  }
697
- function buildParseOptions(def) {
698
- const result = { help: {
699
- type: "boolean",
700
- short: "h"
701
- } };
702
- for (const [name, opt] of Object.entries(def.options ?? {})) result[name] = {
703
- type: opt.type,
704
- ...opt.short ? { short: opt.short } : {},
705
- ...opt.default !== void 0 ? { default: opt.default } : {}
818
+ /**
819
+ * Generic printer-factory function used by `definePrinter` and `defineFunctionPrinter`.
820
+ **
821
+ * @example
822
+ * ```ts
823
+ * export const defineFunctionPrinter = createPrinterFactory<FunctionNode, FunctionNodeType, FunctionNodeByType>(
824
+ * (node) => kindToHandlerKey[node.kind],
825
+ * )
826
+ * ```
827
+ */
828
+ function createPrinterFactory(getKey) {
829
+ return function(build) {
830
+ return (options) => {
831
+ const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? {});
832
+ const context = {
833
+ options: resolvedOptions,
834
+ print: (node) => {
835
+ const key = getKey(node);
836
+ if (key === void 0) return void 0;
837
+ const handler = nodes[key];
838
+ if (!handler) return void 0;
839
+ return handler.call(context, node);
840
+ }
841
+ };
842
+ return {
843
+ name,
844
+ options: resolvedOptions,
845
+ print: printOverride ? printOverride.bind(context) : context.print
846
+ };
847
+ };
706
848
  };
707
- return result;
708
849
  }
709
- async function runCommand(def, argv, parentName) {
710
- const parseOptions = buildParseOptions(def);
711
- let parsed;
712
- try {
713
- const result = parseArgs({
714
- args: argv,
715
- options: parseOptions,
716
- allowPositionals: true,
717
- strict: false
718
- });
719
- parsed = {
720
- values: result.values,
721
- positionals: result.positionals
722
- };
723
- } catch {
724
- renderHelp(def, parentName);
725
- process.exit(1);
726
- }
727
- if (parsed.values["help"]) {
728
- renderHelp(def, parentName);
729
- process.exit(0);
730
- }
731
- for (const [name, opt] of Object.entries(def.options ?? {})) if (opt.required && parsed.values[name] === void 0) {
732
- console.error(styleText("red", `Error: --${name} is required`));
733
- renderHelp(def, parentName);
734
- process.exit(1);
735
- }
736
- if (!def.run) {
737
- renderHelp(def, parentName);
738
- process.exit(0);
739
- }
740
- try {
741
- await def.run(parsed);
742
- } catch (err) {
743
- console.error(styleText("red", `Error: ${err instanceof Error ? err.message : String(err)}`));
744
- renderHelp(def, parentName);
745
- process.exit(1);
850
+ //#endregion
851
+ //#region src/printers/functionPrinter.ts
852
+ const kindToHandlerKey = {
853
+ FunctionParameter: "functionParameter",
854
+ ObjectBindingParameter: "objectBindingParameter",
855
+ FunctionParameters: "functionParameters"
856
+ };
857
+ /**
858
+ * Creates a function-parameter printer factory.
859
+ *
860
+ * This wrapper uses `createPrinterFactory` and dispatches handlers by `node.kind`
861
+ * (for function nodes) rather than by `node.type` (for schema nodes).
862
+ *
863
+ * @example
864
+ * ```ts
865
+ * type MyPrinter = PrinterFactoryOptions<'my', { mode: 'declaration' | 'call' }, string>
866
+ *
867
+ * export const myPrinter = defineFunctionPrinter<MyPrinter>((options) => ({
868
+ * name: 'my',
869
+ * options,
870
+ * nodes: {
871
+ * functionParameter(node) {
872
+ * return options.mode === 'declaration' && node.type ? `${node.name}: ${node.type}` : node.name
873
+ * },
874
+ * objectBindingParameter(node) {
875
+ * const inner = node.properties.map(p => this.print(p)).filter(Boolean).join(', ')
876
+ * return `{ ${inner} }`
877
+ * },
878
+ * functionParameters(node) {
879
+ * return node.params.map(p => this.print(p)).filter(Boolean).join(', ')
880
+ * },
881
+ * },
882
+ * }))
883
+ * ```
884
+ */
885
+ const defineFunctionPrinter = createPrinterFactory((node) => kindToHandlerKey[node.kind]);
886
+ function rank(param) {
887
+ if (param.kind === "ObjectBindingParameter") {
888
+ if (param.default) return 2;
889
+ return param.optional ?? param.properties.every((p) => p.optional || p.default !== void 0) ? 1 : 0;
746
890
  }
891
+ if (param.rest) return 3;
892
+ if (param.default) return 2;
893
+ return param.optional ? 1 : 0;
747
894
  }
748
- function printRootHelp(programName, version, defs) {
749
- console.log(`\n${styleText("bold", "Usage:")} ${programName} <command> [options]\n`);
750
- console.log(` Kubb generation — v${version}\n`);
751
- console.log(styleText("bold", "Commands:"));
752
- for (const def of defs) console.log(` ${styleText("cyan", def.name.padEnd(16))}${def.description}`);
753
- console.log();
754
- console.log(styleText("bold", "Options:"));
755
- console.log(` ${styleText("cyan", "-v, --version".padEnd(30))}Show version number`);
756
- console.log(` ${styleText("cyan", "-h, --help".padEnd(30))}Show help`);
757
- console.log();
758
- console.log(`Run ${styleText("cyan", `${programName} <command> --help`)} for command-specific help.\n`);
895
+ function sortParams(params) {
896
+ return [...params].sort((a, b) => rank(a) - rank(b));
759
897
  }
760
- defineCLIAdapter({
761
- renderHelp(def, parentName) {
762
- renderHelp(def, parentName);
763
- },
764
- async run(defs, argv, opts) {
765
- const { programName, defaultCommandName, version } = opts;
766
- const args = argv.length >= 2 && argv[0]?.includes("node") ? argv.slice(2) : argv;
767
- if (args[0] === "--version" || args[0] === "-v") {
768
- console.log(version);
769
- process.exit(0);
770
- }
771
- if (args[0] === "--help" || args[0] === "-h") {
772
- printRootHelp(programName, version, defs);
773
- process.exit(0);
774
- }
775
- if (args.length === 0) {
776
- const defaultDef = defs.find((d) => d.name === defaultCommandName);
777
- if (defaultDef?.run) await runCommand(defaultDef, [], programName);
778
- else printRootHelp(programName, version, defs);
779
- return;
780
- }
781
- const [first, ...rest] = args;
782
- const isKnownSubcommand = defs.some((d) => d.name === first);
783
- let def;
784
- let commandArgv;
785
- let parentName;
786
- if (isKnownSubcommand) {
787
- def = defs.find((d) => d.name === first);
788
- commandArgv = rest;
789
- parentName = programName;
790
- } else {
791
- def = defs.find((d) => d.name === defaultCommandName);
792
- commandArgv = args;
793
- parentName = programName;
794
- }
795
- if (!def) {
796
- console.error(`Unknown command: ${first}`);
797
- printRootHelp(programName, version, defs);
798
- process.exit(1);
799
- }
800
- if (def.subCommands?.length) {
801
- const [subName, ...subRest] = commandArgv;
802
- const subDef = def.subCommands.find((s) => s.name === subName);
803
- if (subName === "--help" || subName === "-h") {
804
- renderHelp(def, parentName);
805
- process.exit(0);
898
+ function sortChildParams(params) {
899
+ return [...params].sort((a, b) => rank(a) - rank(b));
900
+ }
901
+ /**
902
+ * Default function-signature printer.
903
+ * Covers the four standard output modes used across Kubb plugins.
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * const printer = functionPrinter({ mode: 'declaration' })
908
+ *
909
+ * const sig = createFunctionParameters({
910
+ * params: [
911
+ * createFunctionParameter({ name: 'petId', type: 'string', optional: false }),
912
+ * createFunctionParameter({ name: 'config', type: 'Config', optional: false, default: '{}' }),
913
+ * ],
914
+ * })
915
+ *
916
+ * printer.print(sig) // → "petId: string, config: Config = {}"
917
+ * ```
918
+ */
919
+ const functionPrinter = defineFunctionPrinter((options) => ({
920
+ name: "functionParameters",
921
+ options,
922
+ nodes: {
923
+ functionParameter(node) {
924
+ const { mode, transformName, transformType } = this.options;
925
+ const name = transformName ? transformName(node.name) : node.name;
926
+ const type = node.type && transformType ? transformType(node.type) : node.type;
927
+ if (mode === "keys" || mode === "values") return node.rest ? `...${name}` : name;
928
+ if (mode === "call") return node.rest ? `...${name}` : name;
929
+ if (node.rest) return type ? `...${name}: ${type}` : `...${name}`;
930
+ if (type) {
931
+ if (node.optional) return `${name}?: ${type}`;
932
+ return node.default ? `${name}: ${type} = ${node.default}` : `${name}: ${type}`;
806
933
  }
807
- if (!subDef) {
808
- renderHelp(def, parentName);
809
- process.exit(subName ? 1 : 0);
934
+ return node.default ? `${name} = ${node.default}` : name;
935
+ },
936
+ objectBindingParameter(node) {
937
+ const { mode, transformName, transformType } = this.options;
938
+ const sorted = sortChildParams(node.properties);
939
+ const isOptional = node.optional ?? sorted.every((p) => p.optional || p.default !== void 0);
940
+ if (node.inline) return sorted.map((p) => this.print(p)).filter(Boolean).join(", ");
941
+ if (mode === "keys" || mode === "values") return `{ ${sorted.map((p) => p.name).join(", ")} }`;
942
+ if (mode === "call") return `{ ${sorted.map((p) => p.name).join(", ")} }`;
943
+ const names = sorted.map((p) => {
944
+ return transformName ? transformName(p.name) : p.name;
945
+ });
946
+ const nameStr = names.length ? `{ ${names.join(", ")} }` : void 0;
947
+ if (!nameStr) return null;
948
+ let typeAnnotation = node.type;
949
+ if (!typeAnnotation) {
950
+ const typeParts = sorted.filter((p) => p.type).map((p) => {
951
+ const t = transformType && p.type ? transformType(p.type) : p.type;
952
+ return p.optional || p.default !== void 0 ? `${p.name}?: ${t}` : `${p.name}: ${t}`;
953
+ });
954
+ typeAnnotation = typeParts.length ? `{ ${typeParts.join("; ")} }` : void 0;
810
955
  }
811
- await runCommand(subDef, subRest, `${parentName} ${def.name}`);
812
- return;
956
+ if (typeAnnotation) {
957
+ if (isOptional) return `${nameStr}: ${typeAnnotation} = ${node.default ?? "{}"}`;
958
+ return node.default ? `${nameStr}: ${typeAnnotation} = ${node.default}` : `${nameStr}: ${typeAnnotation}`;
959
+ }
960
+ return node.default ? `${nameStr} = ${node.default}` : nameStr;
961
+ },
962
+ functionParameters(node) {
963
+ return sortParams(node.params).map((p) => this.print(p)).filter(Boolean).join(", ");
813
964
  }
814
- await runCommand(def, commandArgv, parentName);
815
965
  }
816
- });
817
- /**
818
- * Parses a CSS hex color string (`#RGB`) into its RGB channels.
819
- * Falls back to `255` for any channel that cannot be parsed.
820
- */
821
- function parseHex(color) {
822
- const int = Number.parseInt(color.replace("#", ""), 16);
823
- return Number.isNaN(int) ? {
824
- r: 255,
825
- g: 255,
826
- b: 255
827
- } : {
828
- r: int >> 16 & 255,
829
- g: int >> 8 & 255,
830
- b: int & 255
831
- };
832
- }
966
+ }));
967
+ //#endregion
968
+ //#region src/refs.ts
833
969
  /**
834
- * Returns a function that wraps a string in a 24-bit ANSI true-color escape sequence
835
- * for the given hex color.
970
+ * Returns the last path segment of a reference string.
971
+ *
972
+ * Example: `#/components/schemas/Pet` becomes `Pet`.
973
+ *
974
+ * @example
975
+ * ```ts
976
+ * extractRefName('#/components/schemas/Pet') // 'Pet'
977
+ * ```
836
978
  */
837
- function hex(color) {
838
- const { r, g, b } = parseHex(color);
839
- return (text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
979
+ function extractRefName(ref) {
980
+ return ref.split("/").at(-1) ?? ref;
840
981
  }
841
- hex("#F55A17"), hex("#F5A217"), hex("#F58517"), hex("#B45309"), hex("#FFFFFF"), hex("#adadc6"), hex("#FDA4AF");
842
982
  /**
843
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
983
+ * Builds a `RefMap` from `root.schemas` using each schema's `name`.
984
+ *
985
+ * Unnamed schemas are skipped.
986
+ *
987
+ * @example
988
+ * ```ts
989
+ * const refMap = buildRefMap(root)
990
+ * const pet = refMap.get('Pet')
991
+ * ```
844
992
  */
845
- function isValidVarName(name) {
846
- try {
847
- new Function(`var ${name}`);
848
- } catch {
849
- return false;
850
- }
851
- return true;
993
+ function buildRefMap(root) {
994
+ const map = /* @__PURE__ */ new Map();
995
+ for (const schema of root.schemas) if (schema.name) map.set(schema.name, schema);
996
+ return map;
852
997
  }
853
- //#endregion
854
- //#region src/utils.ts
855
- const plainStringTypes = new Set([
856
- "string",
857
- "uuid",
858
- "email",
859
- "url",
860
- "datetime"
861
- ]);
862
998
  /**
863
- * Returns `true` when a schema node will be represented as a plain string in generated code.
999
+ * Resolves a schema by name from a `RefMap`.
864
1000
  *
865
- * - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings.
866
- * - `date` and `time` are plain strings when their `representation` is `'string'` rather than `'date'`.
1001
+ * @example
1002
+ * ```ts
1003
+ * const petSchema = resolveRef(refMap, 'Pet')
1004
+ * ```
867
1005
  */
868
- function isPlainStringType(node) {
869
- if (plainStringTypes.has(node.type)) return true;
870
- const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
871
- if (temporal) return temporal.representation !== "date";
872
- return false;
1006
+ function resolveRef(refMap, ref) {
1007
+ return refMap.get(ref);
873
1008
  }
874
1009
  /**
875
- * Transforms the `name` field of each parameter node according to the given casing strategy.
876
- *
877
- * The original `params` array is never mutated — a new array of cloned nodes is returned.
878
- * When no `casing` is provided the original array is returned as-is.
1010
+ * Converts a `RefMap` into a plain object.
879
1011
  *
880
- * Use this before passing parameters to schema builders so that property keys
881
- * in the generated output match the desired casing while the original
882
- * `OperationNode.parameters` array remains untouched for other consumers.
1012
+ * @example
1013
+ * ```ts
1014
+ * const refsObject = refMapToObject(refMap)
1015
+ * ```
883
1016
  */
884
- function applyParamsCasing(params, casing) {
885
- if (!casing) return params;
886
- return params.map((param) => {
887
- const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
888
- return {
889
- ...param,
890
- name: transformed
891
- };
892
- });
1017
+ function refMapToObject(refMap) {
1018
+ return Object.fromEntries(refMap);
893
1019
  }
894
1020
  //#endregion
895
1021
  //#region src/visitor.ts
896
1022
  /**
897
- * Creates a concurrency-limiting wrapper. At most `concurrency` promises may be
898
- * in-flight simultaneously; additional calls are queued and dispatched as slots free.
1023
+ * Creates a small async concurrency limiter.
1024
+ *
1025
+ * At most `concurrency` tasks are in flight at once. Extra tasks are queued.
1026
+ *
1027
+ * @example
1028
+ * ```ts
1029
+ * const limit = createLimit(2)
1030
+ * await Promise.all([
1031
+ * limit(() => taskA()),
1032
+ * limit(() => taskB()),
1033
+ * limit(() => taskC()),
1034
+ * ])
1035
+ * // only 2 tasks run at the same time
1036
+ * ```
899
1037
  */
900
1038
  function createLimit(concurrency) {
901
1039
  let active = 0;
@@ -921,8 +1059,15 @@ function createLimit(concurrency) {
921
1059
  /**
922
1060
  * Returns the immediate traversable children of `node`.
923
1061
  *
924
- * For `Schema` nodes, children (properties, items, members) are only included
925
- * when `recurse` is `true`; shallow traversal omits them entirely.
1062
+ * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
1063
+ * `additionalProperties`) are only included
1064
+ * when `recurse` is `true`; shallow mode skips them.
1065
+ *
1066
+ * @example
1067
+ * ```ts
1068
+ * const children = getChildren(operationNode, true)
1069
+ * // returns parameters, requestBody schema (if present), and responses
1070
+ * ```
926
1071
  */
927
1072
  function getChildren(node, recurse) {
928
1073
  switch (node.kind) {
@@ -951,7 +1096,23 @@ function getChildren(node, recurse) {
951
1096
  }
952
1097
  /**
953
1098
  * Depth-first traversal for side effects. Visitor return values are ignored.
954
- * Sibling nodes at each level are visited concurrently up to `options.concurrency` (default: 30).
1099
+ * Sibling nodes at each level are visited concurrently up to `options.concurrency`
1100
+ * (default: `WALK_CONCURRENCY`).
1101
+ *
1102
+ * @example
1103
+ * ```ts
1104
+ * await walk(root, {
1105
+ * operation(node) {
1106
+ * console.log(node.operationId)
1107
+ * },
1108
+ * })
1109
+ * ```
1110
+ *
1111
+ * @example
1112
+ * ```ts
1113
+ * // Visit only the current node
1114
+ * await walk(root, { depth: 'shallow', root: () => {} })
1115
+ * ```
955
1116
  */
956
1117
  async function walk(node, options) {
957
1118
  return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
@@ -1084,8 +1245,18 @@ function transform(node, options) {
1084
1245
  }
1085
1246
  }
1086
1247
  /**
1087
- * Combines multiple visitors into a single visitor that applies them sequentially (left to right).
1088
- * For each node kind, the output of one visitor becomes the input of the next.
1248
+ * Composes multiple visitors into one visitor, applied left to right.
1249
+ *
1250
+ * For each node kind, output from one visitor is input to the next.
1251
+ * If a visitor returns `undefined`, the previous node value is kept.
1252
+ *
1253
+ * @example
1254
+ * ```ts
1255
+ * const visitor = composeTransformers(
1256
+ * { operation: (node) => ({ ...node, operationId: `a_${node.operationId}` }) },
1257
+ * { operation: (node) => ({ ...node, operationId: `b_${node.operationId}` }) },
1258
+ * )
1259
+ * ```
1089
1260
  */
1090
1261
  function composeTransformers(...visitors) {
1091
1262
  return {
@@ -1110,7 +1281,24 @@ function composeTransformers(...visitors) {
1110
1281
  };
1111
1282
  }
1112
1283
  /**
1113
- * Depth-first synchronous reduction. Collects non-`undefined` visitor return values into an array.
1284
+ * Runs a depth-first synchronous collection pass.
1285
+ *
1286
+ * Non-`undefined` values returned by visitor callbacks are appended to the result.
1287
+ *
1288
+ * @example
1289
+ * ```ts
1290
+ * const ids = collect(root, {
1291
+ * operation(node) {
1292
+ * return node.operationId
1293
+ * },
1294
+ * })
1295
+ * ```
1296
+ *
1297
+ * @example
1298
+ * ```ts
1299
+ * // Collect from only the current node
1300
+ * const values = collect(root, { depth: 'shallow', root: () => 'root' })
1301
+ * ```
1114
1302
  */
1115
1303
  function collect(node, options) {
1116
1304
  const { depth, parent, ...visitor } = options;
@@ -1148,6 +1336,165 @@ function collect(node, options) {
1148
1336
  return results;
1149
1337
  }
1150
1338
  //#endregion
1151
- export { SCALAR_PRIMITIVE_TYPES, applyDiscriminatorEnum, applyParamsCasing, buildRefMap, collect, composeTransformers, createFunctionParameter, createFunctionParameters, createObjectBindingParameter, createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, definePrinter, extractRefName, functionPrinter, httpMethods, isFunctionParameterNode, isFunctionParametersNode, isObjectBindingParameterNode, isOperationNode, isParameterNode, isPlainStringType, isPropertyNode, isResponseNode, isRootNode, isSchemaNode, mediaTypes, mergeAdjacentAnonymousObjects, narrowSchema, nodeKinds, refMapToObject, resolveRef, schemaTypes, simplifyUnionMembers, syncPropertySchema, transform, walk };
1339
+ //#region src/resolvers.ts
1340
+ function findDiscriminator(mapping, ref) {
1341
+ if (!mapping || !ref) return void 0;
1342
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0];
1343
+ }
1344
+ function childName(parentName, propName) {
1345
+ return parentName ? pascalCase([parentName, propName].join(" ")) : void 0;
1346
+ }
1347
+ function enumPropName(parentName, propName, enumSuffix) {
1348
+ return pascalCase([
1349
+ parentName,
1350
+ propName,
1351
+ enumSuffix
1352
+ ].filter(Boolean).join(" "));
1353
+ }
1354
+ /**
1355
+ * Collects import entries for all `ref` schema nodes in `node`.
1356
+ */
1357
+ function collectImports({ node, nameMapping, resolve }) {
1358
+ return collect(node, { schema(schemaNode) {
1359
+ const schemaRef = narrowSchema(schemaNode, "ref");
1360
+ if (!schemaRef?.ref) return;
1361
+ const rawName = extractRefName(schemaRef.ref);
1362
+ const result = resolve(nameMapping.get(rawName) ?? rawName);
1363
+ if (!result) return;
1364
+ return result;
1365
+ } });
1366
+ }
1367
+ //#endregion
1368
+ //#region src/transformers.ts
1369
+ /**
1370
+ * Replaces a discriminator property's schema with a string enum of allowed values.
1371
+ *
1372
+ * If `node` is not an object schema, or if the property does not exist, the input
1373
+ * node is returned as-is.
1374
+ *
1375
+ * @example
1376
+ * ```ts
1377
+ * const schema = createSchema({
1378
+ * type: 'object',
1379
+ * properties: [createProperty({ name: 'type', required: true, schema: createSchema({ type: 'string' }) })],
1380
+ * })
1381
+ * const result = setDiscriminatorEnum({ node: schema, propertyName: 'type', values: ['dog', 'cat'] })
1382
+ * ```
1383
+ */
1384
+ function setDiscriminatorEnum({ node, propertyName, values, enumName }) {
1385
+ const objectNode = narrowSchema(node, "object");
1386
+ if (!objectNode?.properties?.length) return node;
1387
+ if (!objectNode.properties.some((prop) => prop.name === propertyName)) return node;
1388
+ return createSchema({
1389
+ ...objectNode,
1390
+ properties: objectNode.properties.map((prop) => {
1391
+ if (prop.name !== propertyName) return prop;
1392
+ return createProperty({
1393
+ ...prop,
1394
+ schema: createSchema({
1395
+ type: "enum",
1396
+ primitive: "string",
1397
+ enumValues: values,
1398
+ name: enumName,
1399
+ readOnly: prop.schema.readOnly,
1400
+ writeOnly: prop.schema.writeOnly
1401
+ })
1402
+ });
1403
+ })
1404
+ });
1405
+ }
1406
+ /**
1407
+ * Merges adjacent anonymous object members into a single anonymous object member.
1408
+ *
1409
+ * @example
1410
+ * ```ts
1411
+ * const merged = mergeAdjacentObjects([
1412
+ * createSchema({ type: 'object', properties: [createProperty({ name: 'a', schema: createSchema({ type: 'string' }) })] }),
1413
+ * createSchema({ type: 'object', properties: [createProperty({ name: 'b', schema: createSchema({ type: 'number' }) })] }),
1414
+ * ])
1415
+ * ```
1416
+ */
1417
+ function mergeAdjacentObjects(members) {
1418
+ return members.reduce((acc, member) => {
1419
+ const objectMember = narrowSchema(member, "object");
1420
+ if (objectMember && !objectMember.name) {
1421
+ const previous = acc.at(-1);
1422
+ const previousObject = previous ? narrowSchema(previous, "object") : void 0;
1423
+ if (previousObject && !previousObject.name) {
1424
+ acc[acc.length - 1] = createSchema({
1425
+ ...previousObject,
1426
+ properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
1427
+ });
1428
+ return acc;
1429
+ }
1430
+ }
1431
+ acc.push(member);
1432
+ return acc;
1433
+ }, []);
1434
+ }
1435
+ /**
1436
+ * Removes enum members that are covered by broader scalar primitives in the same union.
1437
+ *
1438
+ * @example
1439
+ * ```ts
1440
+ * const simplified = simplifyUnion([
1441
+ * createSchema({ type: 'enum', primitive: 'string', enumValues: ['active'] }),
1442
+ * createSchema({ type: 'string' }),
1443
+ * ])
1444
+ * // keeps only string member
1445
+ * ```
1446
+ */
1447
+ function simplifyUnion(members) {
1448
+ const scalarPrimitives = new Set(members.filter((member) => SCALAR_PRIMITIVE_TYPES.has(member.type)).map((m) => m.type));
1449
+ if (!scalarPrimitives.size) return members;
1450
+ return members.filter((member) => {
1451
+ const enumNode = narrowSchema(member, "enum");
1452
+ if (!enumNode) return true;
1453
+ const primitive = enumNode.primitive;
1454
+ if (!primitive) return true;
1455
+ if ((enumNode.namedEnumValues?.length ?? enumNode.enumValues?.length ?? 0) <= 1) return true;
1456
+ if (scalarPrimitives.has(primitive)) return false;
1457
+ if ((primitive === "integer" || primitive === "number") && (scalarPrimitives.has("integer") || scalarPrimitives.has("number"))) return false;
1458
+ return true;
1459
+ });
1460
+ }
1461
+ function setEnumName(propNode, parentName, propName, enumSuffix) {
1462
+ const enumNode = narrowSchema(propNode, "enum");
1463
+ if (enumNode?.primitive === "boolean") return {
1464
+ ...propNode,
1465
+ name: void 0
1466
+ };
1467
+ if (enumNode) return {
1468
+ ...propNode,
1469
+ name: enumPropName(parentName, propName, enumSuffix)
1470
+ };
1471
+ return propNode;
1472
+ }
1473
+ /**
1474
+ * Walks a schema tree and resolves `ref`/`enum` names through callbacks.
1475
+ */
1476
+ function resolveNames({ node, nameMapping, resolveName, resolveEnumName }) {
1477
+ return transform(node, { schema(schemaNode) {
1478
+ const schemaRef = narrowSchema(schemaNode, "ref");
1479
+ if (schemaRef && (schemaRef.ref || schemaRef.name)) {
1480
+ const rawRef = schemaRef.ref ?? schemaRef.name;
1481
+ const resolved = resolveName(nameMapping.get(rawRef) ?? rawRef);
1482
+ if (resolved) return {
1483
+ ...schemaNode,
1484
+ name: resolved
1485
+ };
1486
+ }
1487
+ const schemaEnum = narrowSchema(schemaNode, "enum");
1488
+ if (schemaEnum?.name) {
1489
+ const resolved = (resolveEnumName ?? resolveName)(schemaEnum.name);
1490
+ if (resolved) return {
1491
+ ...schemaNode,
1492
+ name: resolved
1493
+ };
1494
+ }
1495
+ } });
1496
+ }
1497
+ //#endregion
1498
+ export { SCALAR_PRIMITIVE_TYPES, buildRefMap, caseParams, childName, collect, collectImports, composeTransformers, createDiscriminantNode, createFunctionParameter, createFunctionParameters, createObjectBindingParameter, createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, defineFunctionPrinter, definePrinter, enumPropName, extractRefName, findDiscriminator, functionPrinter, httpMethods, isFunctionParameterNode, isFunctionParametersNode, isObjectBindingParameterNode, isOperationNode, isParameterNode, isPropertyNode, isResponseNode, isRootNode, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, narrowSchema, nodeKinds, refMapToObject, resolveNames, resolveRef, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, transform, walk };
1152
1499
 
1153
1500
  //# sourceMappingURL=index.js.map