@kubb/ast 5.0.0-alpha.4 → 5.0.0-alpha.6

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,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  Object.defineProperty;
3
3
  //#endregion
4
+ let node_util = require("node:util");
4
5
  //#region src/constants.ts
5
6
  const visitorDepths = {
6
7
  shallow: "shallow",
@@ -37,7 +38,8 @@ const schemaTypes = {
37
38
  uuid: "uuid",
38
39
  email: "email",
39
40
  url: "url",
40
- blob: "blob"
41
+ blob: "blob",
42
+ never: "never"
41
43
  };
42
44
  const httpMethods = {
43
45
  get: "GET",
@@ -177,50 +179,63 @@ const isResponseNode = isKind("Response");
177
179
  * from `@kubb/core` — wraps a builder to make options optional and separates raw options
178
180
  * from resolved options.
179
181
  *
180
- * @example
182
+ * The builder receives resolved options and returns:
183
+ * - `name` — a unique identifier for the printer
184
+ * - `options` — options stored on the returned printer instance
185
+ * - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
186
+ * - `print` _(optional)_ — a root-level override that becomes the public `printer.print`.
187
+ * Inside it, `this.print(node)` still dispatches to the `nodes` map — safe recursion, no infinite loop.
188
+ *
189
+ * When no `print` override is provided, `printer.print` is the node-level dispatcher directly.
190
+ *
191
+ * @example Basic usage — Zod schema printer
181
192
  * ```ts
182
- * type ZodPrinter = PrinterFactoryOptions<'zod', { strict?: boolean }, { strict: boolean }, string>
193
+ * type ZodPrinter = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
183
194
  *
184
- * export const zodPrinter = definePrinter<ZodPrinter>((options) => {
185
- * const { strict = true } = options
186
- * return {
187
- * name: 'zod',
188
- * options: { strict },
189
- * nodes: {
190
- * string(node) {
191
- * return `z.string()`
192
- * },
193
- * object(node) {
194
- * const props = node.properties
195
- * ?.map(p => `${p.name}: ${this.print(p)}`)
196
- * .join(', ') ?? ''
197
- * return `z.object({ ${props} })`
198
- * },
195
+ * export const zodPrinter = definePrinter<ZodPrinter>((options) => ({
196
+ * name: 'zod',
197
+ * options: { strict: options.strict ?? true },
198
+ * nodes: {
199
+ * string: () => 'z.string()',
200
+ * object(node) {
201
+ * const props = node.properties.map(p => `${p.name}: ${this.print(p.schema)}`).join(', ')
202
+ * return `z.object({ ${props} })`
199
203
  * },
200
- * }
201
- * })
204
+ * },
205
+ * }))
206
+ * ```
207
+ *
208
+ * @example With a root-level `print` override to wrap output in a full declaration
209
+ * ```ts
210
+ * type TsPrinter = PrinterFactoryOptions<'ts', { typeName?: string }, ts.TypeNode, ts.Node>
202
211
  *
203
- * const printer = zodPrinter({ strict: false })
204
- * printer.name // 'zod'
205
- * printer.options // { strict: false }
206
- * printer.print(node) // 'z.string()'
212
+ * export const printerTs = definePrinter<TsPrinter>((options) => ({
213
+ * name: 'ts',
214
+ * options,
215
+ * nodes: { string: () => factory.keywordTypeNodes.string },
216
+ * print(node) {
217
+ * const type = this.print(node) // calls the node-level dispatcher
218
+ * if (!type || !this.options.typeName) return type
219
+ * return factory.createTypeAliasDeclaration(this.options.typeName, type)
220
+ * },
221
+ * }))
207
222
  * ```
208
223
  */
209
224
  function definePrinter(build) {
210
225
  return (options) => {
211
- const { name, options: resolvedOptions, nodes } = build(options ?? {});
226
+ const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? {});
212
227
  const context = {
213
228
  options: resolvedOptions,
214
229
  print: (node) => {
215
230
  const handler = nodes[node.type];
216
- return handler ? handler.call(context, node) : void 0;
231
+ if (!handler) return void 0;
232
+ return handler.call(context, node);
217
233
  }
218
234
  };
219
235
  return {
220
236
  name,
221
237
  options: resolvedOptions,
222
- print: context.print,
223
- for: (nodes) => nodes.map(context.print)
238
+ print: printOverride ? printOverride.bind(context) : context.print
224
239
  };
225
240
  };
226
241
  }
@@ -247,6 +262,263 @@ function refMapToObject(refMap) {
247
262
  return Object.fromEntries(refMap);
248
263
  }
249
264
  //#endregion
265
+ //#region ../../internals/utils/dist/index.js
266
+ /**
267
+ * Shared implementation for camelCase and PascalCase conversion.
268
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
269
+ * and capitalizes each word according to `pascal`.
270
+ *
271
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
272
+ */
273
+ function toCamelOrPascal(text, pascal) {
274
+ 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) => {
275
+ if (word.length > 1 && word === word.toUpperCase()) return word;
276
+ if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
277
+ return word.charAt(0).toUpperCase() + word.slice(1);
278
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
279
+ }
280
+ /**
281
+ * Splits `text` on `.` and applies `transformPart` to each segment.
282
+ * The last segment receives `isLast = true`, all earlier segments receive `false`.
283
+ * Segments are joined with `/` to form a file path.
284
+ */
285
+ function applyToFileParts(text, transformPart) {
286
+ const parts = text.split(".");
287
+ return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
288
+ }
289
+ /**
290
+ * Converts `text` to camelCase.
291
+ * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
292
+ *
293
+ * @example
294
+ * camelCase('hello-world') // 'helloWorld'
295
+ * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
296
+ */
297
+ function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
298
+ if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
299
+ prefix,
300
+ suffix
301
+ } : {}));
302
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
303
+ }
304
+ /** Returns a `CLIAdapter` with type inference. Pass a different adapter to `createCLI` to swap the CLI engine. */
305
+ function defineCLIAdapter(adapter) {
306
+ return adapter;
307
+ }
308
+ /**
309
+ * Serializes `CommandDefinition[]` to a plain, JSON-serializable structure.
310
+ * Use to expose CLI capabilities to AI agents or MCP tools.
311
+ */
312
+ function getCommandSchema(defs) {
313
+ return defs.map(serializeCommand);
314
+ }
315
+ function serializeCommand(def) {
316
+ return {
317
+ name: def.name,
318
+ description: def.description,
319
+ arguments: def.arguments,
320
+ options: serializeOptions(def.options ?? {}),
321
+ subCommands: def.subCommands ? def.subCommands.map(serializeCommand) : []
322
+ };
323
+ }
324
+ function serializeOptions(options) {
325
+ return Object.entries(options).map(([name, opt]) => {
326
+ return {
327
+ name,
328
+ flags: `${opt.short ? `-${opt.short}, ` : ""}--${name}${opt.type === "string" ? ` <${opt.hint ?? name}>` : ""}`,
329
+ type: opt.type,
330
+ description: opt.description,
331
+ ...opt.default !== void 0 ? { default: opt.default } : {},
332
+ ...opt.hint ? { hint: opt.hint } : {},
333
+ ...opt.enum ? { enum: opt.enum } : {},
334
+ ...opt.required ? { required: opt.required } : {}
335
+ };
336
+ });
337
+ }
338
+ /** Prints formatted help output for a command using its `CommandDefinition`. */
339
+ function renderHelp(def, parentName) {
340
+ const schema = getCommandSchema([def])[0];
341
+ const programName = parentName ? `${parentName} ${schema.name}` : schema.name;
342
+ const argsPart = schema.arguments?.length ? ` ${schema.arguments.join(" ")}` : "";
343
+ const subCmdPart = schema.subCommands.length ? " <command>" : "";
344
+ console.log(`\n${(0, node_util.styleText)("bold", "Usage:")} ${programName}${argsPart}${subCmdPart} [options]\n`);
345
+ if (schema.description) console.log(` ${schema.description}\n`);
346
+ if (schema.subCommands.length) {
347
+ console.log((0, node_util.styleText)("bold", "Commands:"));
348
+ for (const sub of schema.subCommands) console.log(` ${(0, node_util.styleText)("cyan", sub.name.padEnd(16))}${sub.description}`);
349
+ console.log();
350
+ }
351
+ const options = [...schema.options, {
352
+ name: "help",
353
+ flags: "-h, --help",
354
+ type: "boolean",
355
+ description: "Show help"
356
+ }];
357
+ console.log((0, node_util.styleText)("bold", "Options:"));
358
+ for (const opt of options) {
359
+ const flags = (0, node_util.styleText)("cyan", opt.flags.padEnd(30));
360
+ const defaultPart = opt.default !== void 0 ? (0, node_util.styleText)("dim", ` (default: ${opt.default})`) : "";
361
+ console.log(` ${flags}${opt.description}${defaultPart}`);
362
+ }
363
+ console.log();
364
+ }
365
+ function buildParseOptions(def) {
366
+ const result = { help: {
367
+ type: "boolean",
368
+ short: "h"
369
+ } };
370
+ for (const [name, opt] of Object.entries(def.options ?? {})) result[name] = {
371
+ type: opt.type,
372
+ ...opt.short ? { short: opt.short } : {},
373
+ ...opt.default !== void 0 ? { default: opt.default } : {}
374
+ };
375
+ return result;
376
+ }
377
+ async function runCommand(def, argv, parentName) {
378
+ const parseOptions = buildParseOptions(def);
379
+ let parsed;
380
+ try {
381
+ const result = (0, node_util.parseArgs)({
382
+ args: argv,
383
+ options: parseOptions,
384
+ allowPositionals: true,
385
+ strict: false
386
+ });
387
+ parsed = {
388
+ values: result.values,
389
+ positionals: result.positionals
390
+ };
391
+ } catch {
392
+ renderHelp(def, parentName);
393
+ process.exit(1);
394
+ }
395
+ if (parsed.values["help"]) {
396
+ renderHelp(def, parentName);
397
+ process.exit(0);
398
+ }
399
+ for (const [name, opt] of Object.entries(def.options ?? {})) if (opt.required && parsed.values[name] === void 0) {
400
+ console.error((0, node_util.styleText)("red", `Error: --${name} is required`));
401
+ renderHelp(def, parentName);
402
+ process.exit(1);
403
+ }
404
+ if (!def.run) {
405
+ renderHelp(def, parentName);
406
+ process.exit(0);
407
+ }
408
+ try {
409
+ await def.run(parsed);
410
+ } catch (err) {
411
+ console.error((0, node_util.styleText)("red", `Error: ${err instanceof Error ? err.message : String(err)}`));
412
+ renderHelp(def, parentName);
413
+ process.exit(1);
414
+ }
415
+ }
416
+ function printRootHelp(programName, version, defs) {
417
+ console.log(`\n${(0, node_util.styleText)("bold", "Usage:")} ${programName} <command> [options]\n`);
418
+ console.log(` Kubb generation — v${version}\n`);
419
+ console.log((0, node_util.styleText)("bold", "Commands:"));
420
+ for (const def of defs) console.log(` ${(0, node_util.styleText)("cyan", def.name.padEnd(16))}${def.description}`);
421
+ console.log();
422
+ console.log((0, node_util.styleText)("bold", "Options:"));
423
+ console.log(` ${(0, node_util.styleText)("cyan", "-v, --version".padEnd(30))}Show version number`);
424
+ console.log(` ${(0, node_util.styleText)("cyan", "-h, --help".padEnd(30))}Show help`);
425
+ console.log();
426
+ console.log(`Run ${(0, node_util.styleText)("cyan", `${programName} <command> --help`)} for command-specific help.\n`);
427
+ }
428
+ defineCLIAdapter({
429
+ renderHelp(def, parentName) {
430
+ renderHelp(def, parentName);
431
+ },
432
+ async run(defs, argv, opts) {
433
+ const { programName, defaultCommandName, version } = opts;
434
+ const args = argv.length >= 2 && argv[0]?.includes("node") ? argv.slice(2) : argv;
435
+ if (args[0] === "--version" || args[0] === "-v") {
436
+ console.log(version);
437
+ process.exit(0);
438
+ }
439
+ if (args[0] === "--help" || args[0] === "-h") {
440
+ printRootHelp(programName, version, defs);
441
+ process.exit(0);
442
+ }
443
+ if (args.length === 0) {
444
+ const defaultDef = defs.find((d) => d.name === defaultCommandName);
445
+ if (defaultDef?.run) await runCommand(defaultDef, [], programName);
446
+ else printRootHelp(programName, version, defs);
447
+ return;
448
+ }
449
+ const [first, ...rest] = args;
450
+ const isKnownSubcommand = defs.some((d) => d.name === first);
451
+ let def;
452
+ let commandArgv;
453
+ let parentName;
454
+ if (isKnownSubcommand) {
455
+ def = defs.find((d) => d.name === first);
456
+ commandArgv = rest;
457
+ parentName = programName;
458
+ } else {
459
+ def = defs.find((d) => d.name === defaultCommandName);
460
+ commandArgv = args;
461
+ parentName = programName;
462
+ }
463
+ if (!def) {
464
+ console.error(`Unknown command: ${first}`);
465
+ printRootHelp(programName, version, defs);
466
+ process.exit(1);
467
+ }
468
+ if (def.subCommands?.length) {
469
+ const [subName, ...subRest] = commandArgv;
470
+ const subDef = def.subCommands.find((s) => s.name === subName);
471
+ if (subName === "--help" || subName === "-h") {
472
+ renderHelp(def, parentName);
473
+ process.exit(0);
474
+ }
475
+ if (!subDef) {
476
+ renderHelp(def, parentName);
477
+ process.exit(subName ? 1 : 0);
478
+ }
479
+ await runCommand(subDef, subRest, `${parentName} ${def.name}`);
480
+ return;
481
+ }
482
+ await runCommand(def, commandArgv, parentName);
483
+ }
484
+ });
485
+ /**
486
+ * Parses a CSS hex color string (`#RGB`) into its RGB channels.
487
+ * Falls back to `255` for any channel that cannot be parsed.
488
+ */
489
+ function parseHex(color) {
490
+ const int = Number.parseInt(color.replace("#", ""), 16);
491
+ return Number.isNaN(int) ? {
492
+ r: 255,
493
+ g: 255,
494
+ b: 255
495
+ } : {
496
+ r: int >> 16 & 255,
497
+ g: int >> 8 & 255,
498
+ b: int & 255
499
+ };
500
+ }
501
+ /**
502
+ * Returns a function that wraps a string in a 24-bit ANSI true-color escape sequence
503
+ * for the given hex color.
504
+ */
505
+ function hex(color) {
506
+ const { r, g, b } = parseHex(color);
507
+ return (text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
508
+ }
509
+ hex("#F55A17"), hex("#F5A217"), hex("#F58517"), hex("#B45309"), hex("#FFFFFF"), hex("#adadc6"), hex("#FDA4AF");
510
+ /**
511
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
512
+ */
513
+ function isValidVarName(name) {
514
+ try {
515
+ new Function(`var ${name}`);
516
+ } catch {
517
+ return false;
518
+ }
519
+ return true;
520
+ }
521
+ //#endregion
250
522
  //#region src/utils.ts
251
523
  const plainStringTypes = new Set([
252
524
  "string",
@@ -267,8 +539,32 @@ function isPlainStringType(node) {
267
539
  if (temporal) return temporal.representation !== "date";
268
540
  return false;
269
541
  }
542
+ /**
543
+ * Transforms the `name` field of each parameter node according to the given casing strategy.
544
+ *
545
+ * The original `params` array is never mutated — a new array of cloned nodes is returned.
546
+ * When no `casing` is provided the original array is returned as-is.
547
+ *
548
+ * Use this before passing parameters to schema builders so that property keys
549
+ * in the generated output match the desired casing while the original
550
+ * `OperationNode.parameters` array remains untouched for other consumers.
551
+ */
552
+ function applyParamsCasing(params, casing) {
553
+ if (!casing) return params;
554
+ return params.map((param) => {
555
+ const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
556
+ return {
557
+ ...param,
558
+ name: transformed
559
+ };
560
+ });
561
+ }
270
562
  //#endregion
271
563
  //#region src/visitor.ts
564
+ /**
565
+ * Creates a concurrency-limiting wrapper. At most `concurrency` promises may be
566
+ * in-flight simultaneously; additional calls are queued and dispatched as slots free.
567
+ */
272
568
  function createLimit(concurrency) {
273
569
  let active = 0;
274
570
  const queue = [];
@@ -291,7 +587,10 @@ function createLimit(concurrency) {
291
587
  };
292
588
  }
293
589
  /**
294
- * Traversable children of `node`, respecting `recurse` for schema nodes.
590
+ * Returns the immediate traversable children of `node`.
591
+ *
592
+ * For `Schema` nodes, children (properties, items, members) are only included
593
+ * when `recurse` is `true`; shallow traversal omits them entirely.
295
594
  */
296
595
  function getChildren(node, recurse) {
297
596
  switch (node.kind) {
@@ -321,6 +620,9 @@ function getChildren(node, recurse) {
321
620
  async function walk(node, visitor, options = {}) {
322
621
  return _walk(node, visitor, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30));
323
622
  }
623
+ /**
624
+ * Internal recursive walk implementation — calls visitor then recurses into children.
625
+ */
324
626
  async function _walk(node, visitor, recurse, limit) {
325
627
  switch (node.kind) {
326
628
  case "Root":
@@ -441,6 +743,7 @@ function collect(node, visitor, options = {}) {
441
743
  return results;
442
744
  }
443
745
  //#endregion
746
+ exports.applyParamsCasing = applyParamsCasing;
444
747
  exports.buildRefMap = buildRefMap;
445
748
  exports.collect = collect;
446
749
  exports.createOperation = createOperation;