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