@optique/zod 1.1.0-dev.2096 → 1.1.0-dev.2148

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/README.md CHANGED
@@ -125,6 +125,45 @@ const startDate = argument(
125
125
  ~~~~
126
126
 
127
127
 
128
+ Async schemas
129
+ -------------
130
+
131
+ Use `zodAsync()` when a schema depends on async refinements or transforms, and
132
+ run the containing parser with `runAsync()` or `await run()`:
133
+
134
+ ~~~~ typescript
135
+ import { runAsync } from "@optique/run";
136
+ import { object } from "@optique/core/constructs";
137
+ import { option } from "@optique/core/primitives";
138
+ import { zodAsync } from "@optique/zod";
139
+ import { z } from "zod";
140
+
141
+ async function checkApiKey(value: string): Promise<boolean> {
142
+ return await Promise.resolve(value.startsWith("live_"));
143
+ }
144
+
145
+ const parser = object({
146
+ apiKey: option("--api-key",
147
+ zodAsync(z.string().refine(checkApiKey, "Unknown API key."), {
148
+ placeholder: "",
149
+ }),
150
+ ),
151
+ });
152
+
153
+ const cli = await runAsync(parser);
154
+ ~~~~
155
+
156
+ The `zod()` helper remains synchronous and rejects schemas that require Zod's
157
+ async parse path. `zodAsync()` preserves metavar inference, choices,
158
+ suggestions, Boolean literal conversion, formatting, and custom errors.
159
+ Fallback values from `bindEnv()` or `bindConfig()` are validated by the same
160
+ schema before they are accepted.
161
+
162
+ Async validation can run during fallback resolution and other repeated parser
163
+ paths, including shell completion requests. Keep remote checks bounded and
164
+ cached when possible.
165
+
166
+
128
167
  Custom error messages
129
168
  ---------------------
130
169
 
@@ -167,24 +206,28 @@ const port = option("-p", zod(z.coerce.number(), { placeholder: 0 }));
167
206
  // const port = option("-p", zod(z.number()));
168
207
  ~~~~
169
208
 
170
- ### Async refinements are not supported
209
+ ### `zod()` is synchronous
171
210
 
172
- Optique's `ValueParser.parse()` is synchronous, so async Zod features like
173
- async refinements cannot be supported:
211
+ The `zod()` helper returns a sync value parser, so async Zod features like
212
+ async refinements and transforms require `zodAsync()`:
174
213
 
175
214
  ~~~~ typescript
176
215
  import { option } from "@optique/core/primitives";
177
- import { zod } from "@optique/zod";
216
+ import { zod, zodAsync } from "@optique/zod";
178
217
  import { z } from "zod";
179
218
 
180
- // ❌ Not supported
181
- const email = option("--email",
219
+ // ❌ Not supported by zod()
220
+ const syncEmail = option("--email",
182
221
  zod(z.string().refine(async (val) => await checkDB(val)),
183
222
  { placeholder: "" }),
184
223
  );
185
- ~~~~
186
224
 
187
- If you need async validation, perform it after parsing the CLI arguments.
225
+ // Use zodAsync() and runAsync()
226
+ const asyncEmail = option("--email",
227
+ zodAsync(z.string().refine(async (val) => await checkDB(val)),
228
+ { placeholder: "" }),
229
+ );
230
+ ~~~~
188
231
 
189
232
 
190
233
  Zod version compatibility
package/dist/index.cjs CHANGED
@@ -288,6 +288,23 @@ function inferChoices(schema) {
288
288
  }
289
289
  return void 0;
290
290
  }
291
+ function validateOptions(functionName, options) {
292
+ if (options == null || typeof options !== "object") throw new TypeError(`${functionName}() requires an options object with a placeholder property.`);
293
+ if (!("placeholder" in options)) throw new TypeError(`${functionName}() options must include a placeholder property.`);
294
+ }
295
+ function formatValue(value, format) {
296
+ if (format) return format(value);
297
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
298
+ if (typeof value !== "object" || value === null) return String(value);
299
+ if (Array.isArray(value)) return String(value);
300
+ const str = String(value);
301
+ if (str !== "[object Object]") return str;
302
+ const proto = Object.getPrototypeOf(value);
303
+ if (proto === Object.prototype || proto === null) try {
304
+ return JSON.stringify(value) ?? str;
305
+ } catch {}
306
+ return str;
307
+ }
291
308
  /**
292
309
  * Creates a value parser from a Zod schema.
293
310
  *
@@ -370,8 +387,7 @@ function inferChoices(schema) {
370
387
  * @since 0.7.0
371
388
  */
372
389
  function zod$1(schema, options) {
373
- if (options == null || typeof options !== "object") throw new TypeError("zod() requires an options object with a placeholder property.");
374
- if (!("placeholder" in options)) throw new TypeError("zod() options must include a placeholder property.");
390
+ validateOptions("zod", options);
375
391
  const choices = inferChoices(schema);
376
392
  const boolInfo = analyzeBooleanSchema(schema);
377
393
  const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
@@ -469,21 +485,123 @@ function zod$1(schema, options) {
469
485
  return doSafeParse(input, input);
470
486
  },
471
487
  format(value) {
472
- if (options.format) return options.format(value);
473
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
474
- if (typeof value !== "object" || value === null) return String(value);
475
- if (Array.isArray(value)) return String(value);
476
- const str = String(value);
477
- if (str !== "[object Object]") return str;
478
- const proto = Object.getPrototypeOf(value);
479
- if (proto === Object.prototype || proto === null) try {
480
- return JSON.stringify(value) ?? str;
481
- } catch {}
482
- return str;
488
+ return formatValue(value, options.format);
489
+ }
490
+ };
491
+ return parser;
492
+ }
493
+ /**
494
+ * Creates an async value parser from a Zod schema.
495
+ *
496
+ * This parser validates CLI argument strings with Zod's async parse path, so
497
+ * schemas using async refinements or transforms can be reused directly in
498
+ * asynchronous Optique parsers.
499
+ *
500
+ * The metavar, choices, suggestions, Boolean input conversion, formatting,
501
+ * and error customization follow the same rules as {@link zod}. Because the
502
+ * returned parser runs asynchronously, use it with `parseAsync()`, `run()`, or
503
+ * `runAsync()`.
504
+ *
505
+ * @template T The output type of the Zod schema.
506
+ * @param schema A Zod schema to validate input against.
507
+ * @param options Configuration for the parser, including a required
508
+ * `placeholder` value used during deferred prompt resolution.
509
+ * @returns An async value parser that validates inputs using the provided
510
+ * schema.
511
+ *
512
+ * @throws {TypeError} If `options` is missing, not an object, or does not
513
+ * include `placeholder`.
514
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
515
+ * @since 1.1.0
516
+ */
517
+ function zodAsync(schema, options) {
518
+ validateOptions("zodAsync", options);
519
+ const choices = inferChoices(schema);
520
+ const boolInfo = analyzeBooleanSchema(schema);
521
+ const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
522
+ (0, __optique_core_nonempty.ensureNonEmptyString)(metavar);
523
+ async function doSafeParse(input, rawInput) {
524
+ const result = await schema.safeParseAsync(input);
525
+ if (result.success) return {
526
+ success: true,
527
+ value: result.data
528
+ };
529
+ if (options.errors?.zodError) return {
530
+ success: false,
531
+ error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, rawInput) : options.errors.zodError
532
+ };
533
+ const zodModule = schema;
534
+ if (typeof zodModule.constructor?.prettifyError === "function") try {
535
+ const pretty = zodModule.constructor.prettifyError(result.error);
536
+ return {
537
+ success: false,
538
+ error: __optique_core_message.message`${pretty}`
539
+ };
540
+ } catch {}
541
+ const firstError = result.error.issues[0];
542
+ return {
543
+ success: false,
544
+ error: __optique_core_message.message`${firstError?.message ?? "Validation failed"}`
545
+ };
546
+ }
547
+ async function handleBooleanLiteralError(boolResult, rawInput) {
548
+ if (!boolInfo.isCoerced) return await doSafeParse(rawInput, rawInput);
549
+ if (options.errors?.zodError) {
550
+ if (typeof options.errors.zodError !== "function") return {
551
+ success: false,
552
+ error: options.errors.zodError
553
+ };
554
+ const zodError = new zod.ZodError([{
555
+ code: "invalid_type",
556
+ expected: "boolean",
557
+ message: `Invalid Boolean value: ${rawInput}`,
558
+ path: []
559
+ }]);
560
+ return {
561
+ success: false,
562
+ error: options.errors.zodError(zodError, rawInput)
563
+ };
564
+ }
565
+ return boolResult;
566
+ }
567
+ const parser = {
568
+ mode: "async",
569
+ metavar,
570
+ placeholder: options.placeholder,
571
+ ...boolInfo.exposeChoices ? {
572
+ choices: Object.freeze([true, false]),
573
+ async *suggest(prefix) {
574
+ const allLiterals = [...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS];
575
+ const normalizedPrefix = prefix.toLowerCase();
576
+ for (const lit of allLiterals) if (lit.startsWith(normalizedPrefix)) yield {
577
+ kind: "literal",
578
+ text: lit
579
+ };
580
+ }
581
+ } : choices != null && choices.length > 0 ? {
582
+ choices: Object.freeze(choices),
583
+ async *suggest(prefix) {
584
+ for (const c of choices) if (c.startsWith(prefix)) yield {
585
+ kind: "literal",
586
+ text: c
587
+ };
588
+ }
589
+ } : {},
590
+ async parse(input) {
591
+ if (boolInfo.isBoolean) {
592
+ const boolResult = preConvertBoolean(input);
593
+ if (!boolResult.success) return await handleBooleanLiteralError(boolResult, input);
594
+ return await doSafeParse(boolResult.value, input);
595
+ }
596
+ return await doSafeParse(input, input);
597
+ },
598
+ format(value) {
599
+ return formatValue(value, options.format);
483
600
  }
484
601
  };
485
602
  return parser;
486
603
  }
487
604
 
488
605
  //#endregion
489
- exports.zod = zod$1;
606
+ exports.zod = zod$1;
607
+ exports.zodAsync = zodAsync;
package/dist/index.d.cts CHANGED
@@ -130,5 +130,30 @@ interface ZodParserOptions<T = unknown> {
130
130
  * @since 0.7.0
131
131
  */
132
132
  declare function zod<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"sync", T>;
133
+ /**
134
+ * Creates an async value parser from a Zod schema.
135
+ *
136
+ * This parser validates CLI argument strings with Zod's async parse path, so
137
+ * schemas using async refinements or transforms can be reused directly in
138
+ * asynchronous Optique parsers.
139
+ *
140
+ * The metavar, choices, suggestions, Boolean input conversion, formatting,
141
+ * and error customization follow the same rules as {@link zod}. Because the
142
+ * returned parser runs asynchronously, use it with `parseAsync()`, `run()`, or
143
+ * `runAsync()`.
144
+ *
145
+ * @template T The output type of the Zod schema.
146
+ * @param schema A Zod schema to validate input against.
147
+ * @param options Configuration for the parser, including a required
148
+ * `placeholder` value used during deferred prompt resolution.
149
+ * @returns An async value parser that validates inputs using the provided
150
+ * schema.
151
+ *
152
+ * @throws {TypeError} If `options` is missing, not an object, or does not
153
+ * include `placeholder`.
154
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
155
+ * @since 1.1.0
156
+ */
157
+ declare function zodAsync<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"async", T>;
133
158
  //#endregion
134
- export { ZodParserOptions, zod };
159
+ export { ZodParserOptions, zod, zodAsync };
package/dist/index.d.ts CHANGED
@@ -130,5 +130,30 @@ interface ZodParserOptions<T = unknown> {
130
130
  * @since 0.7.0
131
131
  */
132
132
  declare function zod<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"sync", T>;
133
+ /**
134
+ * Creates an async value parser from a Zod schema.
135
+ *
136
+ * This parser validates CLI argument strings with Zod's async parse path, so
137
+ * schemas using async refinements or transforms can be reused directly in
138
+ * asynchronous Optique parsers.
139
+ *
140
+ * The metavar, choices, suggestions, Boolean input conversion, formatting,
141
+ * and error customization follow the same rules as {@link zod}. Because the
142
+ * returned parser runs asynchronously, use it with `parseAsync()`, `run()`, or
143
+ * `runAsync()`.
144
+ *
145
+ * @template T The output type of the Zod schema.
146
+ * @param schema A Zod schema to validate input against.
147
+ * @param options Configuration for the parser, including a required
148
+ * `placeholder` value used during deferred prompt resolution.
149
+ * @returns An async value parser that validates inputs using the provided
150
+ * schema.
151
+ *
152
+ * @throws {TypeError} If `options` is missing, not an object, or does not
153
+ * include `placeholder`.
154
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
155
+ * @since 1.1.0
156
+ */
157
+ declare function zodAsync<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"async", T>;
133
158
  //#endregion
134
- export { ZodParserOptions, zod };
159
+ export { ZodParserOptions, zod, zodAsync };
package/dist/index.js CHANGED
@@ -265,6 +265,23 @@ function inferChoices(schema) {
265
265
  }
266
266
  return void 0;
267
267
  }
268
+ function validateOptions(functionName, options) {
269
+ if (options == null || typeof options !== "object") throw new TypeError(`${functionName}() requires an options object with a placeholder property.`);
270
+ if (!("placeholder" in options)) throw new TypeError(`${functionName}() options must include a placeholder property.`);
271
+ }
272
+ function formatValue(value, format) {
273
+ if (format) return format(value);
274
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
275
+ if (typeof value !== "object" || value === null) return String(value);
276
+ if (Array.isArray(value)) return String(value);
277
+ const str = String(value);
278
+ if (str !== "[object Object]") return str;
279
+ const proto = Object.getPrototypeOf(value);
280
+ if (proto === Object.prototype || proto === null) try {
281
+ return JSON.stringify(value) ?? str;
282
+ } catch {}
283
+ return str;
284
+ }
268
285
  /**
269
286
  * Creates a value parser from a Zod schema.
270
287
  *
@@ -347,8 +364,7 @@ function inferChoices(schema) {
347
364
  * @since 0.7.0
348
365
  */
349
366
  function zod(schema, options) {
350
- if (options == null || typeof options !== "object") throw new TypeError("zod() requires an options object with a placeholder property.");
351
- if (!("placeholder" in options)) throw new TypeError("zod() options must include a placeholder property.");
367
+ validateOptions("zod", options);
352
368
  const choices = inferChoices(schema);
353
369
  const boolInfo = analyzeBooleanSchema(schema);
354
370
  const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
@@ -446,21 +462,122 @@ function zod(schema, options) {
446
462
  return doSafeParse(input, input);
447
463
  },
448
464
  format(value) {
449
- if (options.format) return options.format(value);
450
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
451
- if (typeof value !== "object" || value === null) return String(value);
452
- if (Array.isArray(value)) return String(value);
453
- const str = String(value);
454
- if (str !== "[object Object]") return str;
455
- const proto = Object.getPrototypeOf(value);
456
- if (proto === Object.prototype || proto === null) try {
457
- return JSON.stringify(value) ?? str;
458
- } catch {}
459
- return str;
465
+ return formatValue(value, options.format);
466
+ }
467
+ };
468
+ return parser;
469
+ }
470
+ /**
471
+ * Creates an async value parser from a Zod schema.
472
+ *
473
+ * This parser validates CLI argument strings with Zod's async parse path, so
474
+ * schemas using async refinements or transforms can be reused directly in
475
+ * asynchronous Optique parsers.
476
+ *
477
+ * The metavar, choices, suggestions, Boolean input conversion, formatting,
478
+ * and error customization follow the same rules as {@link zod}. Because the
479
+ * returned parser runs asynchronously, use it with `parseAsync()`, `run()`, or
480
+ * `runAsync()`.
481
+ *
482
+ * @template T The output type of the Zod schema.
483
+ * @param schema A Zod schema to validate input against.
484
+ * @param options Configuration for the parser, including a required
485
+ * `placeholder` value used during deferred prompt resolution.
486
+ * @returns An async value parser that validates inputs using the provided
487
+ * schema.
488
+ *
489
+ * @throws {TypeError} If `options` is missing, not an object, or does not
490
+ * include `placeholder`.
491
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
492
+ * @since 1.1.0
493
+ */
494
+ function zodAsync(schema, options) {
495
+ validateOptions("zodAsync", options);
496
+ const choices = inferChoices(schema);
497
+ const boolInfo = analyzeBooleanSchema(schema);
498
+ const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
499
+ ensureNonEmptyString(metavar);
500
+ async function doSafeParse(input, rawInput) {
501
+ const result = await schema.safeParseAsync(input);
502
+ if (result.success) return {
503
+ success: true,
504
+ value: result.data
505
+ };
506
+ if (options.errors?.zodError) return {
507
+ success: false,
508
+ error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, rawInput) : options.errors.zodError
509
+ };
510
+ const zodModule = schema;
511
+ if (typeof zodModule.constructor?.prettifyError === "function") try {
512
+ const pretty = zodModule.constructor.prettifyError(result.error);
513
+ return {
514
+ success: false,
515
+ error: message`${pretty}`
516
+ };
517
+ } catch {}
518
+ const firstError = result.error.issues[0];
519
+ return {
520
+ success: false,
521
+ error: message`${firstError?.message ?? "Validation failed"}`
522
+ };
523
+ }
524
+ async function handleBooleanLiteralError(boolResult, rawInput) {
525
+ if (!boolInfo.isCoerced) return await doSafeParse(rawInput, rawInput);
526
+ if (options.errors?.zodError) {
527
+ if (typeof options.errors.zodError !== "function") return {
528
+ success: false,
529
+ error: options.errors.zodError
530
+ };
531
+ const zodError = new ZodError([{
532
+ code: "invalid_type",
533
+ expected: "boolean",
534
+ message: `Invalid Boolean value: ${rawInput}`,
535
+ path: []
536
+ }]);
537
+ return {
538
+ success: false,
539
+ error: options.errors.zodError(zodError, rawInput)
540
+ };
541
+ }
542
+ return boolResult;
543
+ }
544
+ const parser = {
545
+ mode: "async",
546
+ metavar,
547
+ placeholder: options.placeholder,
548
+ ...boolInfo.exposeChoices ? {
549
+ choices: Object.freeze([true, false]),
550
+ async *suggest(prefix) {
551
+ const allLiterals = [...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS];
552
+ const normalizedPrefix = prefix.toLowerCase();
553
+ for (const lit of allLiterals) if (lit.startsWith(normalizedPrefix)) yield {
554
+ kind: "literal",
555
+ text: lit
556
+ };
557
+ }
558
+ } : choices != null && choices.length > 0 ? {
559
+ choices: Object.freeze(choices),
560
+ async *suggest(prefix) {
561
+ for (const c of choices) if (c.startsWith(prefix)) yield {
562
+ kind: "literal",
563
+ text: c
564
+ };
565
+ }
566
+ } : {},
567
+ async parse(input) {
568
+ if (boolInfo.isBoolean) {
569
+ const boolResult = preConvertBoolean(input);
570
+ if (!boolResult.success) return await handleBooleanLiteralError(boolResult, input);
571
+ return await doSafeParse(boolResult.value, input);
572
+ }
573
+ return await doSafeParse(input, input);
574
+ },
575
+ format(value) {
576
+ return formatValue(value, options.format);
460
577
  }
461
578
  };
462
579
  return parser;
463
580
  }
464
581
 
465
582
  //#endregion
466
- export { zod };
583
+ export { zod, zodAsync };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/zod",
3
- "version": "1.1.0-dev.2096",
3
+ "version": "1.1.0-dev.2148",
4
4
  "description": "Zod value parsers for Optique",
5
5
  "keywords": [
6
6
  "CLI",
@@ -57,7 +57,7 @@
57
57
  "zod": "^3.25.0 || ^4.0.0"
58
58
  },
59
59
  "dependencies": {
60
- "@optique/core": "1.1.0-dev.2096+8eda4929"
60
+ "@optique/core": "1.1.0-dev.2148+ab56ac96"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^24.0.0",
@@ -68,12 +68,12 @@
68
68
  "scripts": {
69
69
  "build": "tsdown",
70
70
  "prepublish": "tsdown",
71
- "test": "node --experimental-transform-types --test",
71
+ "test": "node --test",
72
72
  "test:bun": "bun test",
73
73
  "test:deno": "deno test",
74
74
  "test:zod3": "cp package.json .package.json.bak && pnpm add -D zod@^3.25.0 && pnpm test && mv .package.json.bak package.json && pnpm install",
75
75
  "test:zod4": "cp package.json .package.json.bak && pnpm add -D zod@^4.0.0 && pnpm test && mv .package.json.bak package.json && pnpm install",
76
76
  "test:all-versions": "pnpm run test:zod3 && pnpm run test:zod4",
77
- "test-all": "tsdown && node --experimental-transform-types --test && bun test && deno test"
77
+ "test-all": "tsdown && node --test && bun test && deno test"
78
78
  }
79
79
  }