@optique/zod 1.0.0-dev.921 → 1.0.0
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 +16 -8
- package/dist/index.cjs +328 -37
- package/dist/index.d.cts +43 -13
- package/dist/index.d.ts +44 -14
- package/dist/index.js +328 -37
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ import { z } from "zod";
|
|
|
37
37
|
|
|
38
38
|
const cli = run(
|
|
39
39
|
object({
|
|
40
|
-
email: option("--email",
|
|
40
|
+
email: option("--email",
|
|
41
|
+
zod(z.string().email(), { placeholder: "" }),
|
|
42
|
+
),
|
|
41
43
|
}),
|
|
42
44
|
);
|
|
43
45
|
|
|
@@ -65,7 +67,9 @@ import { option } from "@optique/core/primitives";
|
|
|
65
67
|
import { zod } from "@optique/zod";
|
|
66
68
|
import { z } from "zod";
|
|
67
69
|
|
|
68
|
-
const email = option("--email",
|
|
70
|
+
const email = option("--email",
|
|
71
|
+
zod(z.string().email(), { placeholder: "" }),
|
|
72
|
+
);
|
|
69
73
|
~~~~
|
|
70
74
|
|
|
71
75
|
### URL validation
|
|
@@ -75,7 +79,9 @@ import { option } from "@optique/core/primitives";
|
|
|
75
79
|
import { zod } from "@optique/zod";
|
|
76
80
|
import { z } from "zod";
|
|
77
81
|
|
|
78
|
-
const url = option("--url",
|
|
82
|
+
const url = option("--url",
|
|
83
|
+
zod(z.string().url(), { placeholder: "" }),
|
|
84
|
+
);
|
|
79
85
|
~~~~
|
|
80
86
|
|
|
81
87
|
### Port numbers with range validation
|
|
@@ -90,7 +96,7 @@ import { zod } from "@optique/zod";
|
|
|
90
96
|
import { z } from "zod";
|
|
91
97
|
|
|
92
98
|
const port = option("-p", "--port",
|
|
93
|
-
zod(z.coerce.number().int().min(1024).max(65535))
|
|
99
|
+
zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
|
|
94
100
|
);
|
|
95
101
|
~~~~
|
|
96
102
|
|
|
@@ -102,7 +108,7 @@ import { zod } from "@optique/zod";
|
|
|
102
108
|
import { z } from "zod";
|
|
103
109
|
|
|
104
110
|
const logLevel = option("--log-level",
|
|
105
|
-
zod(z.enum(["debug", "info", "warn", "error"]))
|
|
111
|
+
zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
|
|
106
112
|
);
|
|
107
113
|
~~~~
|
|
108
114
|
|
|
@@ -114,7 +120,7 @@ import { zod } from "@optique/zod";
|
|
|
114
120
|
import { z } from "zod";
|
|
115
121
|
|
|
116
122
|
const startDate = argument(
|
|
117
|
-
zod(z.string().transform((s) => new Date(s)))
|
|
123
|
+
zod(z.string().transform((s) => new Date(s)), { placeholder: new Date(0) }),
|
|
118
124
|
);
|
|
119
125
|
~~~~
|
|
120
126
|
|
|
@@ -131,6 +137,7 @@ import { message } from "@optique/core/message";
|
|
|
131
137
|
import { z } from "zod";
|
|
132
138
|
|
|
133
139
|
const email = option("--email", zod(z.string().email(), {
|
|
140
|
+
placeholder: "",
|
|
134
141
|
metavar: "EMAIL",
|
|
135
142
|
errors: {
|
|
136
143
|
zodError: (error, input) =>
|
|
@@ -154,7 +161,7 @@ import { zod } from "@optique/zod";
|
|
|
154
161
|
import { z } from "zod";
|
|
155
162
|
|
|
156
163
|
// ✅ Correct
|
|
157
|
-
const port = option("-p", zod(z.coerce.number()));
|
|
164
|
+
const port = option("-p", zod(z.coerce.number(), { placeholder: 0 }));
|
|
158
165
|
|
|
159
166
|
// ❌ Won't work (CLI arguments are always strings)
|
|
160
167
|
// const port = option("-p", zod(z.number()));
|
|
@@ -172,7 +179,8 @@ import { z } from "zod";
|
|
|
172
179
|
|
|
173
180
|
// ❌ Not supported
|
|
174
181
|
const email = option("--email",
|
|
175
|
-
zod(z.string().refine(async (val) => await checkDB(val))
|
|
182
|
+
zod(z.string().refine(async (val) => await checkDB(val)),
|
|
183
|
+
{ placeholder: "" }),
|
|
176
184
|
);
|
|
177
185
|
~~~~
|
|
178
186
|
|
package/dist/index.cjs
CHANGED
|
@@ -22,9 +22,136 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
24
|
const __optique_core_message = __toESM(require("@optique/core/message"));
|
|
25
|
+
const __optique_core_nonempty = __toESM(require("@optique/core/nonempty"));
|
|
26
|
+
const zod = __toESM(require("zod"));
|
|
25
27
|
|
|
26
28
|
//#region src/index.ts
|
|
27
29
|
/**
|
|
30
|
+
* Checks whether the given error is a Zod async-parse error.
|
|
31
|
+
*
|
|
32
|
+
* - **Zod v4** throws a dedicated `$ZodAsyncError` class.
|
|
33
|
+
* - **Zod v3** (3.25+) throws a plain `Error` whose message starts with
|
|
34
|
+
* `"Async refinement encountered during synchronous parse operation"` for
|
|
35
|
+
* async refinements, or `"Asynchronous transform encountered during
|
|
36
|
+
* synchronous parse operation"` for async transforms.
|
|
37
|
+
*/
|
|
38
|
+
function isZodAsyncError(error) {
|
|
39
|
+
if (error.constructor.name === "$ZodAsyncError") return true;
|
|
40
|
+
if (error.message === "Async refinement encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Synchronous parse encountered promise.") return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const BOOL_TRUE_LITERALS = [
|
|
44
|
+
"true",
|
|
45
|
+
"1",
|
|
46
|
+
"yes",
|
|
47
|
+
"on"
|
|
48
|
+
];
|
|
49
|
+
const BOOL_FALSE_LITERALS = [
|
|
50
|
+
"false",
|
|
51
|
+
"0",
|
|
52
|
+
"no",
|
|
53
|
+
"off"
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Analyzes whether the given Zod schema represents a boolean type,
|
|
57
|
+
* unwrapping all known Zod wrappers. Also determines whether it is
|
|
58
|
+
* safe to expose `choices` and `suggest()` — wrappers that can narrow
|
|
59
|
+
* the accepted domain (effects, catch) suppress choice exposure.
|
|
60
|
+
*/
|
|
61
|
+
function analyzeBooleanSchema(schema) {
|
|
62
|
+
const result = analyzeBooleanInner(schema, true, /* @__PURE__ */ new WeakSet());
|
|
63
|
+
if (!result.isBoolean) return {
|
|
64
|
+
isBoolean: false,
|
|
65
|
+
exposeChoices: false,
|
|
66
|
+
isCoerced: false
|
|
67
|
+
};
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
function analyzeBooleanInner(schema, canExposeChoices, visited) {
|
|
71
|
+
if (visited.has(schema)) return {
|
|
72
|
+
isBoolean: false,
|
|
73
|
+
exposeChoices: false,
|
|
74
|
+
isCoerced: false
|
|
75
|
+
};
|
|
76
|
+
visited.add(schema);
|
|
77
|
+
const def = schema._def;
|
|
78
|
+
if (!def) return {
|
|
79
|
+
isBoolean: false,
|
|
80
|
+
exposeChoices: false,
|
|
81
|
+
isCoerced: false
|
|
82
|
+
};
|
|
83
|
+
const typeName = def.typeName ?? def.type;
|
|
84
|
+
if (typeName === "ZodBoolean" || typeName === "boolean") {
|
|
85
|
+
const hasCustomChecks = Array.isArray(def.checks) && def.checks.some((c) => c.kind === "custom" || c.type === "custom" || c._zod?.def?.check === "custom");
|
|
86
|
+
return {
|
|
87
|
+
isBoolean: true,
|
|
88
|
+
exposeChoices: canExposeChoices && !hasCustomChecks,
|
|
89
|
+
isCoerced: def.coerce === true
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default" || typeName === "ZodReadonly" || typeName === "readonly" || typeName === "prefault" || typeName === "nonoptional") {
|
|
93
|
+
const innerType = def.innerType;
|
|
94
|
+
if (innerType != null) return analyzeBooleanInner(innerType, canExposeChoices, visited);
|
|
95
|
+
}
|
|
96
|
+
if (typeName === "ZodLazy" || typeName === "lazy") {
|
|
97
|
+
if (typeof def.getter === "function") return analyzeBooleanInner(def.getter(), canExposeChoices, visited);
|
|
98
|
+
}
|
|
99
|
+
if (typeName === "ZodEffects" || typeName === "effects") {
|
|
100
|
+
if (def.effect?.type === "preprocess") return {
|
|
101
|
+
isBoolean: false,
|
|
102
|
+
exposeChoices: false,
|
|
103
|
+
isCoerced: false
|
|
104
|
+
};
|
|
105
|
+
const innerSchema = def.schema;
|
|
106
|
+
if (innerSchema != null) return analyzeBooleanInner(innerSchema, false, visited);
|
|
107
|
+
}
|
|
108
|
+
if (typeName === "ZodCatch" || typeName === "catch") {
|
|
109
|
+
const innerType = def.innerType;
|
|
110
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
111
|
+
}
|
|
112
|
+
if (typeName === "ZodBranded" || typeName === "branded") {
|
|
113
|
+
const innerType = def.innerType ?? (typeof def.type === "object" && def.type != null ? def.type : void 0);
|
|
114
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
115
|
+
}
|
|
116
|
+
if (typeName === "pipe" || typeName === "ZodPipeline") {
|
|
117
|
+
const inSchema = def.in;
|
|
118
|
+
if (inSchema != null) return analyzeBooleanInner(inSchema, false, visited);
|
|
119
|
+
const innerType = def.innerType;
|
|
120
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
121
|
+
}
|
|
122
|
+
if (typeName === "pipeline") {
|
|
123
|
+
const innerType = def.innerType;
|
|
124
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
isBoolean: false,
|
|
128
|
+
exposeChoices: false,
|
|
129
|
+
isCoerced: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Pre-converts a CLI string input to an actual boolean value using
|
|
134
|
+
* CLI-friendly literals (true/false, 1/0, yes/no, on/off).
|
|
135
|
+
*/
|
|
136
|
+
function preConvertBoolean(input) {
|
|
137
|
+
const normalized = input.trim().toLowerCase();
|
|
138
|
+
if (BOOL_TRUE_LITERALS.includes(normalized)) return {
|
|
139
|
+
success: true,
|
|
140
|
+
value: true
|
|
141
|
+
};
|
|
142
|
+
if (BOOL_FALSE_LITERALS.includes(normalized)) return {
|
|
143
|
+
success: true,
|
|
144
|
+
value: false
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: __optique_core_message.message`Invalid Boolean value: ${input}. Expected one of ${(0, __optique_core_message.valueSet)([...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS], {
|
|
149
|
+
fallback: "",
|
|
150
|
+
locale: "en-US"
|
|
151
|
+
})}.`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
28
155
|
* Infers an appropriate metavar string from a Zod schema.
|
|
29
156
|
*
|
|
30
157
|
* This function analyzes the Zod schema's internal structure to determine
|
|
@@ -47,7 +174,7 @@ const __optique_core_message = __toESM(require("@optique/core/message"));
|
|
|
47
174
|
function inferMetavar(schema) {
|
|
48
175
|
const def = schema._def;
|
|
49
176
|
if (!def) return "VALUE";
|
|
50
|
-
const typeName = def.typeName
|
|
177
|
+
const typeName = def.typeName ?? def.type;
|
|
51
178
|
if (typeName === "ZodString" || typeName === "string") {
|
|
52
179
|
if (Array.isArray(def.checks)) for (const check of def.checks) {
|
|
53
180
|
const kind = check.kind || check.format;
|
|
@@ -76,8 +203,15 @@ function inferMetavar(schema) {
|
|
|
76
203
|
}
|
|
77
204
|
if (typeName === "ZodBoolean" || typeName === "boolean") return "BOOLEAN";
|
|
78
205
|
if (typeName === "ZodDate" || typeName === "date") return "DATE";
|
|
79
|
-
if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return "CHOICE";
|
|
80
|
-
if (typeName === "
|
|
206
|
+
if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return inferChoices(schema) != null ? "CHOICE" : "VALUE";
|
|
207
|
+
if (typeName === "ZodLiteral" || typeName === "literal") {
|
|
208
|
+
if (inferChoices(schema) != null) return "CHOICE";
|
|
209
|
+
return "VALUE";
|
|
210
|
+
}
|
|
211
|
+
if (typeName === "ZodUnion" || typeName === "union") {
|
|
212
|
+
if (inferChoices(schema) != null) return "CHOICE";
|
|
213
|
+
return "VALUE";
|
|
214
|
+
}
|
|
81
215
|
if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable") {
|
|
82
216
|
const innerType = def.innerType;
|
|
83
217
|
if (innerType != null) return inferMetavar(innerType);
|
|
@@ -91,6 +225,70 @@ function inferMetavar(schema) {
|
|
|
91
225
|
return "VALUE";
|
|
92
226
|
}
|
|
93
227
|
/**
|
|
228
|
+
* Extracts valid choices from a Zod schema that represents a fixed set of
|
|
229
|
+
* values (enum, literal, or union of literals).
|
|
230
|
+
*
|
|
231
|
+
* @param schema A Zod schema to analyze.
|
|
232
|
+
* @returns An array of string representations of valid choices, or `undefined`
|
|
233
|
+
* if the schema does not represent a fixed set of values.
|
|
234
|
+
*/
|
|
235
|
+
function inferChoices(schema) {
|
|
236
|
+
const def = schema._def;
|
|
237
|
+
if (!def) return void 0;
|
|
238
|
+
const typeName = def.typeName ?? def.type;
|
|
239
|
+
if (typeName === "ZodEnum" || typeName === "enum") {
|
|
240
|
+
const values = def.values;
|
|
241
|
+
if (Array.isArray(values)) return values.map(String);
|
|
242
|
+
const entries = def.entries;
|
|
243
|
+
if (entries != null && typeof entries === "object") {
|
|
244
|
+
const result = /* @__PURE__ */ new Set();
|
|
245
|
+
for (const val of Object.values(entries)) if (typeof val === "string") result.add(val);
|
|
246
|
+
else return void 0;
|
|
247
|
+
return result.size > 0 ? [...result] : void 0;
|
|
248
|
+
}
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
if (typeName === "ZodNativeEnum" || typeName === "nativeEnum") {
|
|
252
|
+
const values = def.values;
|
|
253
|
+
if (values != null && typeof values === "object" && !Array.isArray(values)) {
|
|
254
|
+
const result = /* @__PURE__ */ new Set();
|
|
255
|
+
for (const val of Object.values(values)) if (typeof val === "string") result.add(val);
|
|
256
|
+
else return void 0;
|
|
257
|
+
return result.size > 0 ? [...result] : void 0;
|
|
258
|
+
}
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
if (typeName === "ZodLiteral" || typeName === "literal") {
|
|
262
|
+
const value = def.value;
|
|
263
|
+
if (typeof value === "string") return [value];
|
|
264
|
+
const values = def.values;
|
|
265
|
+
if (Array.isArray(values)) {
|
|
266
|
+
const result = [];
|
|
267
|
+
for (const v of values) if (typeof v === "string") result.push(v);
|
|
268
|
+
else return void 0;
|
|
269
|
+
return result.length > 0 ? result : void 0;
|
|
270
|
+
}
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
if (typeName === "ZodUnion" || typeName === "union") {
|
|
274
|
+
const options = def.options;
|
|
275
|
+
if (!Array.isArray(options)) return void 0;
|
|
276
|
+
const allChoices = /* @__PURE__ */ new Set();
|
|
277
|
+
for (const opt of options) {
|
|
278
|
+
const sub = inferChoices(opt);
|
|
279
|
+
if (sub == null) return void 0;
|
|
280
|
+
for (const choice of sub) allChoices.add(choice);
|
|
281
|
+
}
|
|
282
|
+
return allChoices.size > 0 ? [...allChoices] : void 0;
|
|
283
|
+
}
|
|
284
|
+
if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default") {
|
|
285
|
+
const innerType = def.innerType;
|
|
286
|
+
if (innerType != null) return inferChoices(innerType);
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
94
292
|
* Creates a value parser from a Zod schema.
|
|
95
293
|
*
|
|
96
294
|
* This parser validates CLI argument strings using Zod schemas, enabling
|
|
@@ -107,7 +305,8 @@ function inferMetavar(schema) {
|
|
|
107
305
|
*
|
|
108
306
|
* @template T The output type of the Zod schema.
|
|
109
307
|
* @param schema A Zod schema to validate input against.
|
|
110
|
-
* @param options
|
|
308
|
+
* @param options Configuration for the parser, including a required
|
|
309
|
+
* `placeholder` value used during deferred prompt resolution.
|
|
111
310
|
* @returns A value parser that validates inputs using the provided schema.
|
|
112
311
|
*
|
|
113
312
|
* @example Basic string validation
|
|
@@ -116,7 +315,9 @@ function inferMetavar(schema) {
|
|
|
116
315
|
* import { zod } from "@optique/zod";
|
|
117
316
|
* import { option } from "@optique/core/primitives";
|
|
118
317
|
*
|
|
119
|
-
* const email = option("--email",
|
|
318
|
+
* const email = option("--email",
|
|
319
|
+
* zod(z.string().email(), { placeholder: "" }),
|
|
320
|
+
* );
|
|
120
321
|
* ```
|
|
121
322
|
*
|
|
122
323
|
* @example Number validation with coercion
|
|
@@ -127,7 +328,7 @@ function inferMetavar(schema) {
|
|
|
127
328
|
*
|
|
128
329
|
* // Use z.coerce for non-string types since CLI args are always strings
|
|
129
330
|
* const port = option("-p", "--port",
|
|
130
|
-
* zod(z.coerce.number().int().min(1024).max(65535))
|
|
331
|
+
* zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
|
|
131
332
|
* );
|
|
132
333
|
* ```
|
|
133
334
|
*
|
|
@@ -138,7 +339,7 @@ function inferMetavar(schema) {
|
|
|
138
339
|
* import { option } from "@optique/core/primitives";
|
|
139
340
|
*
|
|
140
341
|
* const logLevel = option("--log-level",
|
|
141
|
-
* zod(z.enum(["debug", "info", "warn", "error"]))
|
|
342
|
+
* zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
|
|
142
343
|
* );
|
|
143
344
|
* ```
|
|
144
345
|
*
|
|
@@ -149,50 +350,140 @@ function inferMetavar(schema) {
|
|
|
149
350
|
* import { message } from "@optique/core/message";
|
|
150
351
|
* import { option } from "@optique/core/primitives";
|
|
151
352
|
*
|
|
152
|
-
* const email = option("--email",
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* }
|
|
353
|
+
* const email = option("--email",
|
|
354
|
+
* zod(z.string().email(), {
|
|
355
|
+
* placeholder: "",
|
|
356
|
+
* metavar: "EMAIL",
|
|
357
|
+
* errors: {
|
|
358
|
+
* zodError: (error, input) =>
|
|
359
|
+
* message`Please provide a valid email address, got ${input}.`
|
|
360
|
+
* },
|
|
361
|
+
* }),
|
|
362
|
+
* );
|
|
159
363
|
* ```
|
|
160
364
|
*
|
|
365
|
+
* @throws {TypeError} If `options` is missing, not an object, or does not
|
|
366
|
+
* include `placeholder`.
|
|
367
|
+
* @throws {TypeError} If the resolved `metavar` is an empty string.
|
|
368
|
+
* @throws {TypeError} If the schema contains async refinements or other async
|
|
369
|
+
* operations that cannot be executed synchronously.
|
|
161
370
|
* @since 0.7.0
|
|
162
371
|
*/
|
|
163
|
-
function zod(schema, options
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
372
|
+
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.");
|
|
375
|
+
const choices = inferChoices(schema);
|
|
376
|
+
const boolInfo = analyzeBooleanSchema(schema);
|
|
377
|
+
const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
|
|
378
|
+
(0, __optique_core_nonempty.ensureNonEmptyString)(metavar);
|
|
379
|
+
function doSafeParse(input, rawInput) {
|
|
380
|
+
let result;
|
|
381
|
+
try {
|
|
382
|
+
result = schema.safeParse(input);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (error instanceof Error && isZodAsyncError(error)) throw new TypeError("Async Zod schemas (e.g., async refinements) are not supported by zod(). Use synchronous schemas instead.");
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
if (result.success) return {
|
|
388
|
+
success: true,
|
|
389
|
+
value: result.data
|
|
390
|
+
};
|
|
391
|
+
if (options.errors?.zodError) return {
|
|
392
|
+
success: false,
|
|
393
|
+
error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, rawInput) : options.errors.zodError
|
|
394
|
+
};
|
|
395
|
+
const zodModule = schema;
|
|
396
|
+
if (typeof zodModule.constructor?.prettifyError === "function") try {
|
|
397
|
+
const pretty = zodModule.constructor.prettifyError(result.error);
|
|
398
|
+
return {
|
|
399
|
+
success: false,
|
|
400
|
+
error: __optique_core_message.message`${pretty}`
|
|
172
401
|
};
|
|
173
|
-
|
|
402
|
+
} catch {}
|
|
403
|
+
const firstError = result.error.issues[0];
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: __optique_core_message.message`${firstError?.message ?? "Validation failed"}`
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Handles a failed boolean literal pre-conversion.
|
|
411
|
+
*
|
|
412
|
+
* - *Non-coerced* (`z.boolean()`): falls through to `doSafeParse`
|
|
413
|
+
* so that catch/default, custom errors, and async detection all
|
|
414
|
+
* work. This is safe because `safeParse(string)` fails at the
|
|
415
|
+
* type level before any refinements execute.
|
|
416
|
+
* - *Coerced* (`z.coerce.boolean()`): runs the lazy async probe
|
|
417
|
+
* (if not yet completed), then returns the pre-conversion error
|
|
418
|
+
* or delegates to the custom `zodError` callback.
|
|
419
|
+
*/
|
|
420
|
+
function handleBooleanLiteralError(boolResult, rawInput) {
|
|
421
|
+
if (!boolInfo.isCoerced) return doSafeParse(rawInput, rawInput);
|
|
422
|
+
if (options.errors?.zodError) {
|
|
423
|
+
if (typeof options.errors.zodError !== "function") return {
|
|
174
424
|
success: false,
|
|
175
|
-
error:
|
|
425
|
+
error: options.errors.zodError
|
|
176
426
|
};
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
};
|
|
184
|
-
} catch {}
|
|
185
|
-
const firstError = result.error.issues[0];
|
|
427
|
+
const zodError = new zod.ZodError([{
|
|
428
|
+
code: "invalid_type",
|
|
429
|
+
expected: "boolean",
|
|
430
|
+
message: `Invalid Boolean value: ${rawInput}`,
|
|
431
|
+
path: []
|
|
432
|
+
}]);
|
|
186
433
|
return {
|
|
187
434
|
success: false,
|
|
188
|
-
error:
|
|
435
|
+
error: options.errors.zodError(zodError, rawInput)
|
|
189
436
|
};
|
|
437
|
+
}
|
|
438
|
+
return boolResult;
|
|
439
|
+
}
|
|
440
|
+
const parser = {
|
|
441
|
+
mode: "sync",
|
|
442
|
+
metavar,
|
|
443
|
+
placeholder: options.placeholder,
|
|
444
|
+
...boolInfo.exposeChoices ? {
|
|
445
|
+
choices: Object.freeze([true, false]),
|
|
446
|
+
suggest(prefix) {
|
|
447
|
+
const allLiterals = [...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS];
|
|
448
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
449
|
+
return allLiterals.filter((lit) => lit.startsWith(normalizedPrefix)).map((lit) => ({
|
|
450
|
+
kind: "literal",
|
|
451
|
+
text: lit
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
} : choices != null && choices.length > 0 ? {
|
|
455
|
+
choices: Object.freeze(choices),
|
|
456
|
+
*suggest(prefix) {
|
|
457
|
+
for (const c of choices) if (c.startsWith(prefix)) yield {
|
|
458
|
+
kind: "literal",
|
|
459
|
+
text: c
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
} : {},
|
|
463
|
+
parse(input) {
|
|
464
|
+
if (boolInfo.isBoolean) {
|
|
465
|
+
const boolResult = preConvertBoolean(input);
|
|
466
|
+
if (!boolResult.success) return handleBooleanLiteralError(boolResult, input);
|
|
467
|
+
return doSafeParse(boolResult.value, input);
|
|
468
|
+
}
|
|
469
|
+
return doSafeParse(input, input);
|
|
190
470
|
},
|
|
191
471
|
format(value) {
|
|
192
|
-
return
|
|
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;
|
|
193
483
|
}
|
|
194
484
|
};
|
|
485
|
+
return parser;
|
|
195
486
|
}
|
|
196
487
|
|
|
197
488
|
//#endregion
|
|
198
|
-
exports.zod = zod;
|
|
489
|
+
exports.zod = zod$1;
|
package/dist/index.d.cts
CHANGED
|
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
* Options for creating a Zod value parser.
|
|
9
9
|
* @since 0.7.0
|
|
10
10
|
*/
|
|
11
|
-
interface ZodParserOptions {
|
|
11
|
+
interface ZodParserOptions<T = unknown> {
|
|
12
12
|
/**
|
|
13
13
|
* The metavariable name for this parser. This is used in help messages to
|
|
14
14
|
* indicate what kind of value this parser expects. Usually a single
|
|
@@ -16,6 +16,25 @@ interface ZodParserOptions {
|
|
|
16
16
|
* @default `"VALUE"`
|
|
17
17
|
*/
|
|
18
18
|
readonly metavar?: NonEmptyString;
|
|
19
|
+
/**
|
|
20
|
+
* A phase-one stand-in value of type `T` used during deferred prompt
|
|
21
|
+
* resolution. Because the output type of a Zod schema cannot be
|
|
22
|
+
* inferred to a concrete default, callers must provide this explicitly.
|
|
23
|
+
* @since 1.0.0
|
|
24
|
+
*/
|
|
25
|
+
readonly placeholder: T;
|
|
26
|
+
/**
|
|
27
|
+
* Custom formatter for displaying parsed values in help messages.
|
|
28
|
+
* When not provided, the default formatter is used: primitives use
|
|
29
|
+
* `String()`, valid `Date` values use `.toISOString()`, and plain
|
|
30
|
+
* objects use `JSON.stringify()`. All other objects (arrays, class
|
|
31
|
+
* instances, etc.) use `String()`.
|
|
32
|
+
*
|
|
33
|
+
* @param value The parsed value to format.
|
|
34
|
+
* @returns A string representation of the value.
|
|
35
|
+
* @since 1.0.0
|
|
36
|
+
*/
|
|
37
|
+
readonly format?: (value: T) => string;
|
|
19
38
|
/**
|
|
20
39
|
* Custom error messages for Zod validation failures.
|
|
21
40
|
*/
|
|
@@ -46,7 +65,8 @@ interface ZodParserOptions {
|
|
|
46
65
|
*
|
|
47
66
|
* @template T The output type of the Zod schema.
|
|
48
67
|
* @param schema A Zod schema to validate input against.
|
|
49
|
-
* @param options
|
|
68
|
+
* @param options Configuration for the parser, including a required
|
|
69
|
+
* `placeholder` value used during deferred prompt resolution.
|
|
50
70
|
* @returns A value parser that validates inputs using the provided schema.
|
|
51
71
|
*
|
|
52
72
|
* @example Basic string validation
|
|
@@ -55,7 +75,9 @@ interface ZodParserOptions {
|
|
|
55
75
|
* import { zod } from "@optique/zod";
|
|
56
76
|
* import { option } from "@optique/core/primitives";
|
|
57
77
|
*
|
|
58
|
-
* const email = option("--email",
|
|
78
|
+
* const email = option("--email",
|
|
79
|
+
* zod(z.string().email(), { placeholder: "" }),
|
|
80
|
+
* );
|
|
59
81
|
* ```
|
|
60
82
|
*
|
|
61
83
|
* @example Number validation with coercion
|
|
@@ -66,7 +88,7 @@ interface ZodParserOptions {
|
|
|
66
88
|
*
|
|
67
89
|
* // Use z.coerce for non-string types since CLI args are always strings
|
|
68
90
|
* const port = option("-p", "--port",
|
|
69
|
-
* zod(z.coerce.number().int().min(1024).max(65535))
|
|
91
|
+
* zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
|
|
70
92
|
* );
|
|
71
93
|
* ```
|
|
72
94
|
*
|
|
@@ -77,7 +99,7 @@ interface ZodParserOptions {
|
|
|
77
99
|
* import { option } from "@optique/core/primitives";
|
|
78
100
|
*
|
|
79
101
|
* const logLevel = option("--log-level",
|
|
80
|
-
* zod(z.enum(["debug", "info", "warn", "error"]))
|
|
102
|
+
* zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
|
|
81
103
|
* );
|
|
82
104
|
* ```
|
|
83
105
|
*
|
|
@@ -88,17 +110,25 @@ interface ZodParserOptions {
|
|
|
88
110
|
* import { message } from "@optique/core/message";
|
|
89
111
|
* import { option } from "@optique/core/primitives";
|
|
90
112
|
*
|
|
91
|
-
* const email = option("--email",
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* }
|
|
113
|
+
* const email = option("--email",
|
|
114
|
+
* zod(z.string().email(), {
|
|
115
|
+
* placeholder: "",
|
|
116
|
+
* metavar: "EMAIL",
|
|
117
|
+
* errors: {
|
|
118
|
+
* zodError: (error, input) =>
|
|
119
|
+
* message`Please provide a valid email address, got ${input}.`
|
|
120
|
+
* },
|
|
121
|
+
* }),
|
|
122
|
+
* );
|
|
98
123
|
* ```
|
|
99
124
|
*
|
|
125
|
+
* @throws {TypeError} If `options` is missing, not an object, or does not
|
|
126
|
+
* include `placeholder`.
|
|
127
|
+
* @throws {TypeError} If the resolved `metavar` is an empty string.
|
|
128
|
+
* @throws {TypeError} If the schema contains async refinements or other async
|
|
129
|
+
* operations that cannot be executed synchronously.
|
|
100
130
|
* @since 0.7.0
|
|
101
131
|
*/
|
|
102
|
-
declare function zod<T>(schema: z.Schema<T>, options
|
|
132
|
+
declare function zod<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"sync", T>;
|
|
103
133
|
//#endregion
|
|
104
134
|
export { ZodParserOptions, zod };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Message } from "@optique/core/message";
|
|
2
|
-
import { NonEmptyString, ValueParser } from "@optique/core/valueparser";
|
|
3
2
|
import { z } from "zod";
|
|
3
|
+
import { NonEmptyString, ValueParser } from "@optique/core/valueparser";
|
|
4
4
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
* Options for creating a Zod value parser.
|
|
9
9
|
* @since 0.7.0
|
|
10
10
|
*/
|
|
11
|
-
interface ZodParserOptions {
|
|
11
|
+
interface ZodParserOptions<T = unknown> {
|
|
12
12
|
/**
|
|
13
13
|
* The metavariable name for this parser. This is used in help messages to
|
|
14
14
|
* indicate what kind of value this parser expects. Usually a single
|
|
@@ -16,6 +16,25 @@ interface ZodParserOptions {
|
|
|
16
16
|
* @default `"VALUE"`
|
|
17
17
|
*/
|
|
18
18
|
readonly metavar?: NonEmptyString;
|
|
19
|
+
/**
|
|
20
|
+
* A phase-one stand-in value of type `T` used during deferred prompt
|
|
21
|
+
* resolution. Because the output type of a Zod schema cannot be
|
|
22
|
+
* inferred to a concrete default, callers must provide this explicitly.
|
|
23
|
+
* @since 1.0.0
|
|
24
|
+
*/
|
|
25
|
+
readonly placeholder: T;
|
|
26
|
+
/**
|
|
27
|
+
* Custom formatter for displaying parsed values in help messages.
|
|
28
|
+
* When not provided, the default formatter is used: primitives use
|
|
29
|
+
* `String()`, valid `Date` values use `.toISOString()`, and plain
|
|
30
|
+
* objects use `JSON.stringify()`. All other objects (arrays, class
|
|
31
|
+
* instances, etc.) use `String()`.
|
|
32
|
+
*
|
|
33
|
+
* @param value The parsed value to format.
|
|
34
|
+
* @returns A string representation of the value.
|
|
35
|
+
* @since 1.0.0
|
|
36
|
+
*/
|
|
37
|
+
readonly format?: (value: T) => string;
|
|
19
38
|
/**
|
|
20
39
|
* Custom error messages for Zod validation failures.
|
|
21
40
|
*/
|
|
@@ -46,7 +65,8 @@ interface ZodParserOptions {
|
|
|
46
65
|
*
|
|
47
66
|
* @template T The output type of the Zod schema.
|
|
48
67
|
* @param schema A Zod schema to validate input against.
|
|
49
|
-
* @param options
|
|
68
|
+
* @param options Configuration for the parser, including a required
|
|
69
|
+
* `placeholder` value used during deferred prompt resolution.
|
|
50
70
|
* @returns A value parser that validates inputs using the provided schema.
|
|
51
71
|
*
|
|
52
72
|
* @example Basic string validation
|
|
@@ -55,7 +75,9 @@ interface ZodParserOptions {
|
|
|
55
75
|
* import { zod } from "@optique/zod";
|
|
56
76
|
* import { option } from "@optique/core/primitives";
|
|
57
77
|
*
|
|
58
|
-
* const email = option("--email",
|
|
78
|
+
* const email = option("--email",
|
|
79
|
+
* zod(z.string().email(), { placeholder: "" }),
|
|
80
|
+
* );
|
|
59
81
|
* ```
|
|
60
82
|
*
|
|
61
83
|
* @example Number validation with coercion
|
|
@@ -66,7 +88,7 @@ interface ZodParserOptions {
|
|
|
66
88
|
*
|
|
67
89
|
* // Use z.coerce for non-string types since CLI args are always strings
|
|
68
90
|
* const port = option("-p", "--port",
|
|
69
|
-
* zod(z.coerce.number().int().min(1024).max(65535))
|
|
91
|
+
* zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
|
|
70
92
|
* );
|
|
71
93
|
* ```
|
|
72
94
|
*
|
|
@@ -77,7 +99,7 @@ interface ZodParserOptions {
|
|
|
77
99
|
* import { option } from "@optique/core/primitives";
|
|
78
100
|
*
|
|
79
101
|
* const logLevel = option("--log-level",
|
|
80
|
-
* zod(z.enum(["debug", "info", "warn", "error"]))
|
|
102
|
+
* zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
|
|
81
103
|
* );
|
|
82
104
|
* ```
|
|
83
105
|
*
|
|
@@ -88,17 +110,25 @@ interface ZodParserOptions {
|
|
|
88
110
|
* import { message } from "@optique/core/message";
|
|
89
111
|
* import { option } from "@optique/core/primitives";
|
|
90
112
|
*
|
|
91
|
-
* const email = option("--email",
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* }
|
|
113
|
+
* const email = option("--email",
|
|
114
|
+
* zod(z.string().email(), {
|
|
115
|
+
* placeholder: "",
|
|
116
|
+
* metavar: "EMAIL",
|
|
117
|
+
* errors: {
|
|
118
|
+
* zodError: (error, input) =>
|
|
119
|
+
* message`Please provide a valid email address, got ${input}.`
|
|
120
|
+
* },
|
|
121
|
+
* }),
|
|
122
|
+
* );
|
|
98
123
|
* ```
|
|
99
124
|
*
|
|
125
|
+
* @throws {TypeError} If `options` is missing, not an object, or does not
|
|
126
|
+
* include `placeholder`.
|
|
127
|
+
* @throws {TypeError} If the resolved `metavar` is an empty string.
|
|
128
|
+
* @throws {TypeError} If the schema contains async refinements or other async
|
|
129
|
+
* operations that cannot be executed synchronously.
|
|
100
130
|
* @since 0.7.0
|
|
101
131
|
*/
|
|
102
|
-
declare function zod<T>(schema: z.Schema<T>, options
|
|
132
|
+
declare function zod<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"sync", T>;
|
|
103
133
|
//#endregion
|
|
104
134
|
export { ZodParserOptions, zod };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,134 @@
|
|
|
1
|
-
import { message } from "@optique/core/message";
|
|
1
|
+
import { message, valueSet } from "@optique/core/message";
|
|
2
|
+
import { ensureNonEmptyString } from "@optique/core/nonempty";
|
|
3
|
+
import { ZodError } from "zod";
|
|
2
4
|
|
|
3
5
|
//#region src/index.ts
|
|
4
6
|
/**
|
|
7
|
+
* Checks whether the given error is a Zod async-parse error.
|
|
8
|
+
*
|
|
9
|
+
* - **Zod v4** throws a dedicated `$ZodAsyncError` class.
|
|
10
|
+
* - **Zod v3** (3.25+) throws a plain `Error` whose message starts with
|
|
11
|
+
* `"Async refinement encountered during synchronous parse operation"` for
|
|
12
|
+
* async refinements, or `"Asynchronous transform encountered during
|
|
13
|
+
* synchronous parse operation"` for async transforms.
|
|
14
|
+
*/
|
|
15
|
+
function isZodAsyncError(error) {
|
|
16
|
+
if (error.constructor.name === "$ZodAsyncError") return true;
|
|
17
|
+
if (error.message === "Async refinement encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Synchronous parse encountered promise.") return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const BOOL_TRUE_LITERALS = [
|
|
21
|
+
"true",
|
|
22
|
+
"1",
|
|
23
|
+
"yes",
|
|
24
|
+
"on"
|
|
25
|
+
];
|
|
26
|
+
const BOOL_FALSE_LITERALS = [
|
|
27
|
+
"false",
|
|
28
|
+
"0",
|
|
29
|
+
"no",
|
|
30
|
+
"off"
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Analyzes whether the given Zod schema represents a boolean type,
|
|
34
|
+
* unwrapping all known Zod wrappers. Also determines whether it is
|
|
35
|
+
* safe to expose `choices` and `suggest()` — wrappers that can narrow
|
|
36
|
+
* the accepted domain (effects, catch) suppress choice exposure.
|
|
37
|
+
*/
|
|
38
|
+
function analyzeBooleanSchema(schema) {
|
|
39
|
+
const result = analyzeBooleanInner(schema, true, /* @__PURE__ */ new WeakSet());
|
|
40
|
+
if (!result.isBoolean) return {
|
|
41
|
+
isBoolean: false,
|
|
42
|
+
exposeChoices: false,
|
|
43
|
+
isCoerced: false
|
|
44
|
+
};
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function analyzeBooleanInner(schema, canExposeChoices, visited) {
|
|
48
|
+
if (visited.has(schema)) return {
|
|
49
|
+
isBoolean: false,
|
|
50
|
+
exposeChoices: false,
|
|
51
|
+
isCoerced: false
|
|
52
|
+
};
|
|
53
|
+
visited.add(schema);
|
|
54
|
+
const def = schema._def;
|
|
55
|
+
if (!def) return {
|
|
56
|
+
isBoolean: false,
|
|
57
|
+
exposeChoices: false,
|
|
58
|
+
isCoerced: false
|
|
59
|
+
};
|
|
60
|
+
const typeName = def.typeName ?? def.type;
|
|
61
|
+
if (typeName === "ZodBoolean" || typeName === "boolean") {
|
|
62
|
+
const hasCustomChecks = Array.isArray(def.checks) && def.checks.some((c) => c.kind === "custom" || c.type === "custom" || c._zod?.def?.check === "custom");
|
|
63
|
+
return {
|
|
64
|
+
isBoolean: true,
|
|
65
|
+
exposeChoices: canExposeChoices && !hasCustomChecks,
|
|
66
|
+
isCoerced: def.coerce === true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default" || typeName === "ZodReadonly" || typeName === "readonly" || typeName === "prefault" || typeName === "nonoptional") {
|
|
70
|
+
const innerType = def.innerType;
|
|
71
|
+
if (innerType != null) return analyzeBooleanInner(innerType, canExposeChoices, visited);
|
|
72
|
+
}
|
|
73
|
+
if (typeName === "ZodLazy" || typeName === "lazy") {
|
|
74
|
+
if (typeof def.getter === "function") return analyzeBooleanInner(def.getter(), canExposeChoices, visited);
|
|
75
|
+
}
|
|
76
|
+
if (typeName === "ZodEffects" || typeName === "effects") {
|
|
77
|
+
if (def.effect?.type === "preprocess") return {
|
|
78
|
+
isBoolean: false,
|
|
79
|
+
exposeChoices: false,
|
|
80
|
+
isCoerced: false
|
|
81
|
+
};
|
|
82
|
+
const innerSchema = def.schema;
|
|
83
|
+
if (innerSchema != null) return analyzeBooleanInner(innerSchema, false, visited);
|
|
84
|
+
}
|
|
85
|
+
if (typeName === "ZodCatch" || typeName === "catch") {
|
|
86
|
+
const innerType = def.innerType;
|
|
87
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
88
|
+
}
|
|
89
|
+
if (typeName === "ZodBranded" || typeName === "branded") {
|
|
90
|
+
const innerType = def.innerType ?? (typeof def.type === "object" && def.type != null ? def.type : void 0);
|
|
91
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
92
|
+
}
|
|
93
|
+
if (typeName === "pipe" || typeName === "ZodPipeline") {
|
|
94
|
+
const inSchema = def.in;
|
|
95
|
+
if (inSchema != null) return analyzeBooleanInner(inSchema, false, visited);
|
|
96
|
+
const innerType = def.innerType;
|
|
97
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
98
|
+
}
|
|
99
|
+
if (typeName === "pipeline") {
|
|
100
|
+
const innerType = def.innerType;
|
|
101
|
+
if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
isBoolean: false,
|
|
105
|
+
exposeChoices: false,
|
|
106
|
+
isCoerced: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Pre-converts a CLI string input to an actual boolean value using
|
|
111
|
+
* CLI-friendly literals (true/false, 1/0, yes/no, on/off).
|
|
112
|
+
*/
|
|
113
|
+
function preConvertBoolean(input) {
|
|
114
|
+
const normalized = input.trim().toLowerCase();
|
|
115
|
+
if (BOOL_TRUE_LITERALS.includes(normalized)) return {
|
|
116
|
+
success: true,
|
|
117
|
+
value: true
|
|
118
|
+
};
|
|
119
|
+
if (BOOL_FALSE_LITERALS.includes(normalized)) return {
|
|
120
|
+
success: true,
|
|
121
|
+
value: false
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: message`Invalid Boolean value: ${input}. Expected one of ${valueSet([...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS], {
|
|
126
|
+
fallback: "",
|
|
127
|
+
locale: "en-US"
|
|
128
|
+
})}.`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
5
132
|
* Infers an appropriate metavar string from a Zod schema.
|
|
6
133
|
*
|
|
7
134
|
* This function analyzes the Zod schema's internal structure to determine
|
|
@@ -24,7 +151,7 @@ import { message } from "@optique/core/message";
|
|
|
24
151
|
function inferMetavar(schema) {
|
|
25
152
|
const def = schema._def;
|
|
26
153
|
if (!def) return "VALUE";
|
|
27
|
-
const typeName = def.typeName
|
|
154
|
+
const typeName = def.typeName ?? def.type;
|
|
28
155
|
if (typeName === "ZodString" || typeName === "string") {
|
|
29
156
|
if (Array.isArray(def.checks)) for (const check of def.checks) {
|
|
30
157
|
const kind = check.kind || check.format;
|
|
@@ -53,8 +180,15 @@ function inferMetavar(schema) {
|
|
|
53
180
|
}
|
|
54
181
|
if (typeName === "ZodBoolean" || typeName === "boolean") return "BOOLEAN";
|
|
55
182
|
if (typeName === "ZodDate" || typeName === "date") return "DATE";
|
|
56
|
-
if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return "CHOICE";
|
|
57
|
-
if (typeName === "
|
|
183
|
+
if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return inferChoices(schema) != null ? "CHOICE" : "VALUE";
|
|
184
|
+
if (typeName === "ZodLiteral" || typeName === "literal") {
|
|
185
|
+
if (inferChoices(schema) != null) return "CHOICE";
|
|
186
|
+
return "VALUE";
|
|
187
|
+
}
|
|
188
|
+
if (typeName === "ZodUnion" || typeName === "union") {
|
|
189
|
+
if (inferChoices(schema) != null) return "CHOICE";
|
|
190
|
+
return "VALUE";
|
|
191
|
+
}
|
|
58
192
|
if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable") {
|
|
59
193
|
const innerType = def.innerType;
|
|
60
194
|
if (innerType != null) return inferMetavar(innerType);
|
|
@@ -68,6 +202,70 @@ function inferMetavar(schema) {
|
|
|
68
202
|
return "VALUE";
|
|
69
203
|
}
|
|
70
204
|
/**
|
|
205
|
+
* Extracts valid choices from a Zod schema that represents a fixed set of
|
|
206
|
+
* values (enum, literal, or union of literals).
|
|
207
|
+
*
|
|
208
|
+
* @param schema A Zod schema to analyze.
|
|
209
|
+
* @returns An array of string representations of valid choices, or `undefined`
|
|
210
|
+
* if the schema does not represent a fixed set of values.
|
|
211
|
+
*/
|
|
212
|
+
function inferChoices(schema) {
|
|
213
|
+
const def = schema._def;
|
|
214
|
+
if (!def) return void 0;
|
|
215
|
+
const typeName = def.typeName ?? def.type;
|
|
216
|
+
if (typeName === "ZodEnum" || typeName === "enum") {
|
|
217
|
+
const values = def.values;
|
|
218
|
+
if (Array.isArray(values)) return values.map(String);
|
|
219
|
+
const entries = def.entries;
|
|
220
|
+
if (entries != null && typeof entries === "object") {
|
|
221
|
+
const result = /* @__PURE__ */ new Set();
|
|
222
|
+
for (const val of Object.values(entries)) if (typeof val === "string") result.add(val);
|
|
223
|
+
else return void 0;
|
|
224
|
+
return result.size > 0 ? [...result] : void 0;
|
|
225
|
+
}
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
if (typeName === "ZodNativeEnum" || typeName === "nativeEnum") {
|
|
229
|
+
const values = def.values;
|
|
230
|
+
if (values != null && typeof values === "object" && !Array.isArray(values)) {
|
|
231
|
+
const result = /* @__PURE__ */ new Set();
|
|
232
|
+
for (const val of Object.values(values)) if (typeof val === "string") result.add(val);
|
|
233
|
+
else return void 0;
|
|
234
|
+
return result.size > 0 ? [...result] : void 0;
|
|
235
|
+
}
|
|
236
|
+
return void 0;
|
|
237
|
+
}
|
|
238
|
+
if (typeName === "ZodLiteral" || typeName === "literal") {
|
|
239
|
+
const value = def.value;
|
|
240
|
+
if (typeof value === "string") return [value];
|
|
241
|
+
const values = def.values;
|
|
242
|
+
if (Array.isArray(values)) {
|
|
243
|
+
const result = [];
|
|
244
|
+
for (const v of values) if (typeof v === "string") result.push(v);
|
|
245
|
+
else return void 0;
|
|
246
|
+
return result.length > 0 ? result : void 0;
|
|
247
|
+
}
|
|
248
|
+
return void 0;
|
|
249
|
+
}
|
|
250
|
+
if (typeName === "ZodUnion" || typeName === "union") {
|
|
251
|
+
const options = def.options;
|
|
252
|
+
if (!Array.isArray(options)) return void 0;
|
|
253
|
+
const allChoices = /* @__PURE__ */ new Set();
|
|
254
|
+
for (const opt of options) {
|
|
255
|
+
const sub = inferChoices(opt);
|
|
256
|
+
if (sub == null) return void 0;
|
|
257
|
+
for (const choice of sub) allChoices.add(choice);
|
|
258
|
+
}
|
|
259
|
+
return allChoices.size > 0 ? [...allChoices] : void 0;
|
|
260
|
+
}
|
|
261
|
+
if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default") {
|
|
262
|
+
const innerType = def.innerType;
|
|
263
|
+
if (innerType != null) return inferChoices(innerType);
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
return void 0;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
71
269
|
* Creates a value parser from a Zod schema.
|
|
72
270
|
*
|
|
73
271
|
* This parser validates CLI argument strings using Zod schemas, enabling
|
|
@@ -84,7 +282,8 @@ function inferMetavar(schema) {
|
|
|
84
282
|
*
|
|
85
283
|
* @template T The output type of the Zod schema.
|
|
86
284
|
* @param schema A Zod schema to validate input against.
|
|
87
|
-
* @param options
|
|
285
|
+
* @param options Configuration for the parser, including a required
|
|
286
|
+
* `placeholder` value used during deferred prompt resolution.
|
|
88
287
|
* @returns A value parser that validates inputs using the provided schema.
|
|
89
288
|
*
|
|
90
289
|
* @example Basic string validation
|
|
@@ -93,7 +292,9 @@ function inferMetavar(schema) {
|
|
|
93
292
|
* import { zod } from "@optique/zod";
|
|
94
293
|
* import { option } from "@optique/core/primitives";
|
|
95
294
|
*
|
|
96
|
-
* const email = option("--email",
|
|
295
|
+
* const email = option("--email",
|
|
296
|
+
* zod(z.string().email(), { placeholder: "" }),
|
|
297
|
+
* );
|
|
97
298
|
* ```
|
|
98
299
|
*
|
|
99
300
|
* @example Number validation with coercion
|
|
@@ -104,7 +305,7 @@ function inferMetavar(schema) {
|
|
|
104
305
|
*
|
|
105
306
|
* // Use z.coerce for non-string types since CLI args are always strings
|
|
106
307
|
* const port = option("-p", "--port",
|
|
107
|
-
* zod(z.coerce.number().int().min(1024).max(65535))
|
|
308
|
+
* zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
|
|
108
309
|
* );
|
|
109
310
|
* ```
|
|
110
311
|
*
|
|
@@ -115,7 +316,7 @@ function inferMetavar(schema) {
|
|
|
115
316
|
* import { option } from "@optique/core/primitives";
|
|
116
317
|
*
|
|
117
318
|
* const logLevel = option("--log-level",
|
|
118
|
-
* zod(z.enum(["debug", "info", "warn", "error"]))
|
|
319
|
+
* zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
|
|
119
320
|
* );
|
|
120
321
|
* ```
|
|
121
322
|
*
|
|
@@ -126,49 +327,139 @@ function inferMetavar(schema) {
|
|
|
126
327
|
* import { message } from "@optique/core/message";
|
|
127
328
|
* import { option } from "@optique/core/primitives";
|
|
128
329
|
*
|
|
129
|
-
* const email = option("--email",
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* }
|
|
330
|
+
* const email = option("--email",
|
|
331
|
+
* zod(z.string().email(), {
|
|
332
|
+
* placeholder: "",
|
|
333
|
+
* metavar: "EMAIL",
|
|
334
|
+
* errors: {
|
|
335
|
+
* zodError: (error, input) =>
|
|
336
|
+
* message`Please provide a valid email address, got ${input}.`
|
|
337
|
+
* },
|
|
338
|
+
* }),
|
|
339
|
+
* );
|
|
136
340
|
* ```
|
|
137
341
|
*
|
|
342
|
+
* @throws {TypeError} If `options` is missing, not an object, or does not
|
|
343
|
+
* include `placeholder`.
|
|
344
|
+
* @throws {TypeError} If the resolved `metavar` is an empty string.
|
|
345
|
+
* @throws {TypeError} If the schema contains async refinements or other async
|
|
346
|
+
* operations that cannot be executed synchronously.
|
|
138
347
|
* @since 0.7.0
|
|
139
348
|
*/
|
|
140
|
-
function zod(schema, options
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
349
|
+
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.");
|
|
352
|
+
const choices = inferChoices(schema);
|
|
353
|
+
const boolInfo = analyzeBooleanSchema(schema);
|
|
354
|
+
const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
|
|
355
|
+
ensureNonEmptyString(metavar);
|
|
356
|
+
function doSafeParse(input, rawInput) {
|
|
357
|
+
let result;
|
|
358
|
+
try {
|
|
359
|
+
result = schema.safeParse(input);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error instanceof Error && isZodAsyncError(error)) throw new TypeError("Async Zod schemas (e.g., async refinements) are not supported by zod(). Use synchronous schemas instead.");
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
if (result.success) return {
|
|
365
|
+
success: true,
|
|
366
|
+
value: result.data
|
|
367
|
+
};
|
|
368
|
+
if (options.errors?.zodError) return {
|
|
369
|
+
success: false,
|
|
370
|
+
error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, rawInput) : options.errors.zodError
|
|
371
|
+
};
|
|
372
|
+
const zodModule = schema;
|
|
373
|
+
if (typeof zodModule.constructor?.prettifyError === "function") try {
|
|
374
|
+
const pretty = zodModule.constructor.prettifyError(result.error);
|
|
375
|
+
return {
|
|
376
|
+
success: false,
|
|
377
|
+
error: message`${pretty}`
|
|
149
378
|
};
|
|
150
|
-
|
|
379
|
+
} catch {}
|
|
380
|
+
const firstError = result.error.issues[0];
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
error: message`${firstError?.message ?? "Validation failed"}`
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Handles a failed boolean literal pre-conversion.
|
|
388
|
+
*
|
|
389
|
+
* - *Non-coerced* (`z.boolean()`): falls through to `doSafeParse`
|
|
390
|
+
* so that catch/default, custom errors, and async detection all
|
|
391
|
+
* work. This is safe because `safeParse(string)` fails at the
|
|
392
|
+
* type level before any refinements execute.
|
|
393
|
+
* - *Coerced* (`z.coerce.boolean()`): runs the lazy async probe
|
|
394
|
+
* (if not yet completed), then returns the pre-conversion error
|
|
395
|
+
* or delegates to the custom `zodError` callback.
|
|
396
|
+
*/
|
|
397
|
+
function handleBooleanLiteralError(boolResult, rawInput) {
|
|
398
|
+
if (!boolInfo.isCoerced) return doSafeParse(rawInput, rawInput);
|
|
399
|
+
if (options.errors?.zodError) {
|
|
400
|
+
if (typeof options.errors.zodError !== "function") return {
|
|
151
401
|
success: false,
|
|
152
|
-
error:
|
|
402
|
+
error: options.errors.zodError
|
|
153
403
|
};
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
};
|
|
161
|
-
} catch {}
|
|
162
|
-
const firstError = result.error.issues[0];
|
|
404
|
+
const zodError = new ZodError([{
|
|
405
|
+
code: "invalid_type",
|
|
406
|
+
expected: "boolean",
|
|
407
|
+
message: `Invalid Boolean value: ${rawInput}`,
|
|
408
|
+
path: []
|
|
409
|
+
}]);
|
|
163
410
|
return {
|
|
164
411
|
success: false,
|
|
165
|
-
error:
|
|
412
|
+
error: options.errors.zodError(zodError, rawInput)
|
|
166
413
|
};
|
|
414
|
+
}
|
|
415
|
+
return boolResult;
|
|
416
|
+
}
|
|
417
|
+
const parser = {
|
|
418
|
+
mode: "sync",
|
|
419
|
+
metavar,
|
|
420
|
+
placeholder: options.placeholder,
|
|
421
|
+
...boolInfo.exposeChoices ? {
|
|
422
|
+
choices: Object.freeze([true, false]),
|
|
423
|
+
suggest(prefix) {
|
|
424
|
+
const allLiterals = [...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS];
|
|
425
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
426
|
+
return allLiterals.filter((lit) => lit.startsWith(normalizedPrefix)).map((lit) => ({
|
|
427
|
+
kind: "literal",
|
|
428
|
+
text: lit
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
} : choices != null && choices.length > 0 ? {
|
|
432
|
+
choices: Object.freeze(choices),
|
|
433
|
+
*suggest(prefix) {
|
|
434
|
+
for (const c of choices) if (c.startsWith(prefix)) yield {
|
|
435
|
+
kind: "literal",
|
|
436
|
+
text: c
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
} : {},
|
|
440
|
+
parse(input) {
|
|
441
|
+
if (boolInfo.isBoolean) {
|
|
442
|
+
const boolResult = preConvertBoolean(input);
|
|
443
|
+
if (!boolResult.success) return handleBooleanLiteralError(boolResult, input);
|
|
444
|
+
return doSafeParse(boolResult.value, input);
|
|
445
|
+
}
|
|
446
|
+
return doSafeParse(input, input);
|
|
167
447
|
},
|
|
168
448
|
format(value) {
|
|
169
|
-
return
|
|
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;
|
|
170
460
|
}
|
|
171
461
|
};
|
|
462
|
+
return parser;
|
|
172
463
|
}
|
|
173
464
|
|
|
174
465
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optique/zod",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
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.0.0
|
|
60
|
+
"@optique/core": "1.0.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^20.19.9",
|