|
8 | 8 | */ |
9 | 9 |
|
10 | 10 | import type { |
| 11 | + CamelCaseKeys, |
11 | 12 | CliBuilder, |
12 | 13 | Command, |
13 | 14 | CreateOptions, |
@@ -155,30 +156,59 @@ export function map< |
155 | 156 | parserOrFn: Parser<V1, P1> | TransformFn<V1, P1, V2, P2>, |
156 | 157 | maybeFn?: TransformFn<V1, P1, V2, P2>, |
157 | 158 | ): ((parser: Parser<V1, P1>) => Parser<V2, P2>) | Parser<V2, P2> { |
| 159 | + // Helper to compose transforms (chains existing + new) |
| 160 | + const composeTransform = ( |
| 161 | + parser: Parser<V1, P1>, |
| 162 | + fn: TransformFn<V1, P1, V2, P2>, |
| 163 | + ): TransformFn<unknown, readonly unknown[], V2, P2> => { |
| 164 | + const existing = ( |
| 165 | + parser as { |
| 166 | + __transform?: ( |
| 167 | + r: ParseResult<unknown, readonly unknown[]>, |
| 168 | + ) => ParseResult<V1, P1> | Promise<ParseResult<V1, P1>>; |
| 169 | + } |
| 170 | + ).__transform; |
| 171 | + |
| 172 | + if (!existing) { |
| 173 | + return fn as TransformFn<unknown, readonly unknown[], V2, P2>; |
| 174 | + } |
| 175 | + |
| 176 | + // Chain: existing transform first, then new transform |
| 177 | + return (r: ParseResult<unknown, readonly unknown[]>) => { |
| 178 | + const r1 = existing(r); |
| 179 | + if (r1 instanceof Promise) { |
| 180 | + return r1.then(fn); |
| 181 | + } |
| 182 | + return fn(r1); |
| 183 | + }; |
| 184 | + }; |
| 185 | + |
158 | 186 | // Direct form: map(parser, fn) returns Parser |
159 | 187 | // Check for Parser first since CallableParser is also a function |
160 | 188 | if (isParser(parserOrFn)) { |
161 | 189 | const parser = parserOrFn; |
162 | 190 | const fn = maybeFn!; |
| 191 | + const composedTransform = composeTransform(parser, fn); |
163 | 192 | return { |
164 | 193 | ...parser, |
165 | 194 | __brand: 'Parser', |
166 | 195 | __positionals: [] as unknown as P2, |
167 | | - __transform: fn, |
| 196 | + __transform: composedTransform, |
168 | 197 | __values: {} as V2, |
169 | | - } as Parser<V2, P2> & { __transform: typeof fn }; |
| 198 | + } as Parser<V2, P2> & { __transform: typeof composedTransform }; |
170 | 199 | } |
171 | 200 |
|
172 | 201 | // Curried form: map(fn) returns (parser) => Parser |
173 | 202 | const fn = parserOrFn; |
174 | 203 | return (parser: Parser<V1, P1>): Parser<V2, P2> => { |
| 204 | + const composedTransform = composeTransform(parser, fn); |
175 | 205 | return { |
176 | 206 | ...parser, |
177 | 207 | __brand: 'Parser', |
178 | 208 | __positionals: [] as unknown as P2, |
179 | | - __transform: fn, |
| 209 | + __transform: composedTransform, |
180 | 210 | __values: {} as V2, |
181 | | - } as Parser<V2, P2> & { __transform: typeof fn }; |
| 211 | + } as Parser<V2, P2> & { __transform: typeof composedTransform }; |
182 | 212 | }; |
183 | 213 | } |
184 | 214 | /** |
@@ -307,6 +337,48 @@ export function merge( |
307 | 337 |
|
308 | 338 | return result; |
309 | 339 | } |
| 340 | + |
| 341 | +// ═══════════════════════════════════════════════════════════════════════════════ |
| 342 | +// CAMEL CASE HELPER |
| 343 | +// ═══════════════════════════════════════════════════════════════════════════════ |
| 344 | + |
| 345 | +/** |
| 346 | + * Convert kebab-case string to camelCase. |
| 347 | + */ |
| 348 | +const kebabToCamel = (s: string): string => |
| 349 | + s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); |
| 350 | + |
| 351 | +/** |
| 352 | + * Transform for use with `map()` that converts kebab-case option keys to |
| 353 | + * camelCase. |
| 354 | + * |
| 355 | + * @example |
| 356 | + * |
| 357 | + * ```typescript |
| 358 | + * import { bargs, opt, map, camelCaseValues } from '@boneskull/bargs'; |
| 359 | + * |
| 360 | + * const { values } = await bargs |
| 361 | + * .create('my-cli') |
| 362 | + * .globals( |
| 363 | + * map(opt.options({ 'output-dir': opt.string() }), camelCaseValues), |
| 364 | + * ) |
| 365 | + * .parseAsync(); |
| 366 | + * |
| 367 | + * console.log(values.outputDir); // camelCased! |
| 368 | + * ``` |
| 369 | + */ |
| 370 | +export const camelCaseValues = <V, P extends readonly unknown[]>( |
| 371 | + result: ParseResult<V, P>, |
| 372 | +): ParseResult<CamelCaseKeys<V>, P> => ({ |
| 373 | + ...result, |
| 374 | + values: Object.fromEntries( |
| 375 | + Object.entries(result.values as Record<string, unknown>).map(([k, v]) => [ |
| 376 | + kebabToCamel(k), |
| 377 | + v, |
| 378 | + ]), |
| 379 | + ) as CamelCaseKeys<V>, |
| 380 | +}); |
| 381 | + |
310 | 382 | // ═══════════════════════════════════════════════════════════════════════════════ |
311 | 383 | // CLI BUILDER |
312 | 384 | // ═══════════════════════════════════════════════════════════════════════════════ |
|
0 commit comments