@optique/valibot 1.0.0-dev.1354 → 1.0.0-dev.1359

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -27,6 +27,148 @@ const valibot = __toESM(require("valibot"));
27
27
 
28
28
  //#region src/index.ts
29
29
  /**
30
+ * Valibot transformation action types that are known to be non-rejecting and
31
+ * type-preserving (they transform a string into a string without ever adding
32
+ * issues). All other transformation types (transform, raw_transform,
33
+ * parse_json, to_number, to_boolean, etc.) may reject input or change the
34
+ * value type.
35
+ */
36
+ const SAFE_TRANSFORMATION_TYPES = new Set([
37
+ "trim",
38
+ "to_lower_case",
39
+ "to_upper_case",
40
+ "normalize",
41
+ "to_min_value",
42
+ "to_max_value",
43
+ "trim_start",
44
+ "trim_end",
45
+ "readonly",
46
+ "brand",
47
+ "flavor"
48
+ ]);
49
+ /**
50
+ * Checks whether a schema synchronously accepts every possible input value.
51
+ * This includes:
52
+ * - `v.unknown()`, `v.any()` (accept everything regardless of input type)
53
+ * - Bare `v.string()` without a pipe (accepts every string)
54
+ * - Any wrapper with a `wrapped` field pointing to a catch-all schema
55
+ * (e.g., `v.optional()`, `v.nullable()`, `v.nonOptional()`, etc.)
56
+ *
57
+ * Piped schemas are considered catch-all only when the base type is
58
+ * `string`/`unknown`/`any` and every pipe action is a non-rejecting
59
+ * transformation (not a validation or nested schema).
60
+ *
61
+ * @param afterTransform When true, only type-agnostic catch-alls
62
+ * (`v.unknown()`, `v.any()`) are recognized. String-based catch-alls
63
+ * are not trusted since the input type may no longer be a string.
64
+ */
65
+ function isCatchAllSchema(schema, afterTransform = false) {
66
+ const s = schema;
67
+ if (s.async) return false;
68
+ if ("fallback" in s) return true;
69
+ if (s.type === "unknown" || s.type === "any") {
70
+ if (!s.pipe) return true;
71
+ return s.pipe.slice(1).every((action) => {
72
+ const a = action;
73
+ if (a.kind === "validation") return false;
74
+ if (a.kind === "schema") return isCatchAllSchema(action, afterTransform);
75
+ return SAFE_TRANSFORMATION_TYPES.has(a.type ?? "");
76
+ });
77
+ }
78
+ if (!afterTransform && s.type === "string") {
79
+ if (!s.pipe) return true;
80
+ return s.pipe.slice(1).every((action) => {
81
+ const a = action;
82
+ if (a.kind === "validation") return false;
83
+ if (a.kind === "schema") return isCatchAllSchema(action, afterTransform);
84
+ return SAFE_TRANSFORMATION_TYPES.has(a.type ?? "");
85
+ });
86
+ }
87
+ if (s.wrapped && !s.pipe) return isCatchAllSchema(s.wrapped, afterTransform);
88
+ return false;
89
+ }
90
+ /**
91
+ * Recursively checks whether a Valibot schema contains any async parts
92
+ * (e.g., `pipeAsync`, `checkAsync`). Wrapper schemas such as `optional()`,
93
+ * `nullable()`, `nullish()`, and `union()` keep `async === false` on the
94
+ * outer layer even when they wrap async inner schemas, so a shallow check
95
+ * on the top-level `async` property is not sufficient.
96
+ *
97
+ * Known limitations:
98
+ * - `v.variant()` arms are treated like union arms, but the catch-all
99
+ * detection does not recognize object-shaped variant arms. Variant
100
+ * schemas with async arms after a broad discriminator will be
101
+ * conservatively rejected.
102
+ * - `v.lazy()` schemas are not inspected because the getter depends on
103
+ * actual parse input, making static analysis unreliable.
104
+ *
105
+ * @param afterTransform When true, a preceding `v.transform()` may have
106
+ * changed the value type. Container members become reachable and
107
+ * string-based union catch-all arms are no longer trusted.
108
+ */
109
+ function containsAsyncSchema(schema, visited = /* @__PURE__ */ new WeakMap(), afterTransform = false) {
110
+ const prev = visited.get(schema);
111
+ if (prev !== void 0 && (prev || !afterTransform)) return false;
112
+ visited.set(schema, (prev ?? false) || afterTransform);
113
+ const s = schema;
114
+ if (s.async) return true;
115
+ if (s.wrapped && !s.pipe) return containsAsyncSchema(s.wrapped, visited, afterTransform);
116
+ if (s.options && Array.isArray(s.options)) {
117
+ if (s.type === "union") for (const option of s.options) {
118
+ if (typeof option !== "object" || option == null) continue;
119
+ if (isCatchAllSchema(option, afterTransform)) break;
120
+ if (containsAsyncSchema(option, visited, afterTransform)) return true;
121
+ }
122
+ else if (s.type === "variant") {
123
+ if (afterTransform) {
124
+ for (const option of s.options) if (typeof option === "object" && option != null) {
125
+ if (containsAsyncSchema(option, visited, true)) return true;
126
+ }
127
+ }
128
+ } else for (const option of s.options) if (typeof option === "object" && option != null) {
129
+ if (containsAsyncSchema(option, visited, afterTransform)) return true;
130
+ }
131
+ }
132
+ if (s.pipe && Array.isArray(s.pipe)) {
133
+ let seenTransform = afterTransform;
134
+ for (const action of s.pipe) {
135
+ if (action.async) return true;
136
+ const a = action;
137
+ if (a.kind === "transformation" && !SAFE_TRANSFORMATION_TYPES.has(a.type ?? "")) seenTransform = true;
138
+ if (a.kind === "schema") {
139
+ if (containsAsyncSchema(action, visited, seenTransform)) return true;
140
+ if (!seenTransform) {
141
+ const innerPipe = action.pipe;
142
+ if (innerPipe && Array.isArray(innerPipe)) for (const innerAction of innerPipe) {
143
+ const ia = innerAction;
144
+ if (ia.kind === "transformation" && !SAFE_TRANSFORMATION_TYPES.has(ia.type ?? "")) {
145
+ seenTransform = true;
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ if (afterTransform) {
154
+ if (s.entries) {
155
+ for (const entry of Object.values(s.entries)) if (containsAsyncSchema(entry, visited, true)) return true;
156
+ }
157
+ if (s.item && containsAsyncSchema(s.item, visited, true)) return true;
158
+ if (s.items && Array.isArray(s.items)) {
159
+ for (const item of s.items) if (containsAsyncSchema(item, visited, true)) return true;
160
+ }
161
+ if (s.key && typeof s.key === "object" && containsAsyncSchema(s.key, visited, true)) return true;
162
+ if (s.value && containsAsyncSchema(s.value, visited, true)) return true;
163
+ if (s.rest && containsAsyncSchema(s.rest, visited, true)) return true;
164
+ if (s.type === "promise") {
165
+ const promiseInner = schema.message;
166
+ if (typeof promiseInner === "object" && promiseInner != null && "kind" in promiseInner && containsAsyncSchema(promiseInner, visited, true)) return true;
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+ /**
30
172
  * Infers an appropriate metavar string from a Valibot schema.
31
173
  *
32
174
  * This function analyzes the Valibot schema's internal structure to determine
@@ -221,9 +363,12 @@ function inferChoices(schema) {
221
363
  * ```
222
364
  *
223
365
  * @throws {TypeError} If the resolved `metavar` is an empty string.
366
+ * @throws {TypeError} If the schema contains async validations that cannot be
367
+ * executed synchronously.
224
368
  * @since 0.7.0
225
369
  */
226
370
  function valibot$1(schema, options = {}) {
371
+ if (containsAsyncSchema(schema)) throw new TypeError("Async Valibot schemas (e.g., async validations) are not supported by valibot(). Use synchronous schemas instead.");
227
372
  const choices = inferChoices(schema);
228
373
  const metavar = options.metavar ?? inferMetavar(schema);
229
374
  (0, __optique_core_nonempty.ensureNonEmptyString)(metavar);
package/dist/index.d.cts CHANGED
@@ -107,6 +107,8 @@ interface ValibotParserOptions {
107
107
  * ```
108
108
  *
109
109
  * @throws {TypeError} If the resolved `metavar` is an empty string.
110
+ * @throws {TypeError} If the schema contains async validations that cannot be
111
+ * executed synchronously.
110
112
  * @since 0.7.0
111
113
  */
112
114
  declare function valibot<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>, options?: ValibotParserOptions): ValueParser<"sync", T>;
package/dist/index.d.ts CHANGED
@@ -107,6 +107,8 @@ interface ValibotParserOptions {
107
107
  * ```
108
108
  *
109
109
  * @throws {TypeError} If the resolved `metavar` is an empty string.
110
+ * @throws {TypeError} If the schema contains async validations that cannot be
111
+ * executed synchronously.
110
112
  * @since 0.7.0
111
113
  */
112
114
  declare function valibot<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>, options?: ValibotParserOptions): ValueParser<"sync", T>;
package/dist/index.js CHANGED
@@ -4,6 +4,148 @@ import { safeParse } from "valibot";
4
4
 
5
5
  //#region src/index.ts
6
6
  /**
7
+ * Valibot transformation action types that are known to be non-rejecting and
8
+ * type-preserving (they transform a string into a string without ever adding
9
+ * issues). All other transformation types (transform, raw_transform,
10
+ * parse_json, to_number, to_boolean, etc.) may reject input or change the
11
+ * value type.
12
+ */
13
+ const SAFE_TRANSFORMATION_TYPES = new Set([
14
+ "trim",
15
+ "to_lower_case",
16
+ "to_upper_case",
17
+ "normalize",
18
+ "to_min_value",
19
+ "to_max_value",
20
+ "trim_start",
21
+ "trim_end",
22
+ "readonly",
23
+ "brand",
24
+ "flavor"
25
+ ]);
26
+ /**
27
+ * Checks whether a schema synchronously accepts every possible input value.
28
+ * This includes:
29
+ * - `v.unknown()`, `v.any()` (accept everything regardless of input type)
30
+ * - Bare `v.string()` without a pipe (accepts every string)
31
+ * - Any wrapper with a `wrapped` field pointing to a catch-all schema
32
+ * (e.g., `v.optional()`, `v.nullable()`, `v.nonOptional()`, etc.)
33
+ *
34
+ * Piped schemas are considered catch-all only when the base type is
35
+ * `string`/`unknown`/`any` and every pipe action is a non-rejecting
36
+ * transformation (not a validation or nested schema).
37
+ *
38
+ * @param afterTransform When true, only type-agnostic catch-alls
39
+ * (`v.unknown()`, `v.any()`) are recognized. String-based catch-alls
40
+ * are not trusted since the input type may no longer be a string.
41
+ */
42
+ function isCatchAllSchema(schema, afterTransform = false) {
43
+ const s = schema;
44
+ if (s.async) return false;
45
+ if ("fallback" in s) return true;
46
+ if (s.type === "unknown" || s.type === "any") {
47
+ if (!s.pipe) return true;
48
+ return s.pipe.slice(1).every((action) => {
49
+ const a = action;
50
+ if (a.kind === "validation") return false;
51
+ if (a.kind === "schema") return isCatchAllSchema(action, afterTransform);
52
+ return SAFE_TRANSFORMATION_TYPES.has(a.type ?? "");
53
+ });
54
+ }
55
+ if (!afterTransform && s.type === "string") {
56
+ if (!s.pipe) return true;
57
+ return s.pipe.slice(1).every((action) => {
58
+ const a = action;
59
+ if (a.kind === "validation") return false;
60
+ if (a.kind === "schema") return isCatchAllSchema(action, afterTransform);
61
+ return SAFE_TRANSFORMATION_TYPES.has(a.type ?? "");
62
+ });
63
+ }
64
+ if (s.wrapped && !s.pipe) return isCatchAllSchema(s.wrapped, afterTransform);
65
+ return false;
66
+ }
67
+ /**
68
+ * Recursively checks whether a Valibot schema contains any async parts
69
+ * (e.g., `pipeAsync`, `checkAsync`). Wrapper schemas such as `optional()`,
70
+ * `nullable()`, `nullish()`, and `union()` keep `async === false` on the
71
+ * outer layer even when they wrap async inner schemas, so a shallow check
72
+ * on the top-level `async` property is not sufficient.
73
+ *
74
+ * Known limitations:
75
+ * - `v.variant()` arms are treated like union arms, but the catch-all
76
+ * detection does not recognize object-shaped variant arms. Variant
77
+ * schemas with async arms after a broad discriminator will be
78
+ * conservatively rejected.
79
+ * - `v.lazy()` schemas are not inspected because the getter depends on
80
+ * actual parse input, making static analysis unreliable.
81
+ *
82
+ * @param afterTransform When true, a preceding `v.transform()` may have
83
+ * changed the value type. Container members become reachable and
84
+ * string-based union catch-all arms are no longer trusted.
85
+ */
86
+ function containsAsyncSchema(schema, visited = /* @__PURE__ */ new WeakMap(), afterTransform = false) {
87
+ const prev = visited.get(schema);
88
+ if (prev !== void 0 && (prev || !afterTransform)) return false;
89
+ visited.set(schema, (prev ?? false) || afterTransform);
90
+ const s = schema;
91
+ if (s.async) return true;
92
+ if (s.wrapped && !s.pipe) return containsAsyncSchema(s.wrapped, visited, afterTransform);
93
+ if (s.options && Array.isArray(s.options)) {
94
+ if (s.type === "union") for (const option of s.options) {
95
+ if (typeof option !== "object" || option == null) continue;
96
+ if (isCatchAllSchema(option, afterTransform)) break;
97
+ if (containsAsyncSchema(option, visited, afterTransform)) return true;
98
+ }
99
+ else if (s.type === "variant") {
100
+ if (afterTransform) {
101
+ for (const option of s.options) if (typeof option === "object" && option != null) {
102
+ if (containsAsyncSchema(option, visited, true)) return true;
103
+ }
104
+ }
105
+ } else for (const option of s.options) if (typeof option === "object" && option != null) {
106
+ if (containsAsyncSchema(option, visited, afterTransform)) return true;
107
+ }
108
+ }
109
+ if (s.pipe && Array.isArray(s.pipe)) {
110
+ let seenTransform = afterTransform;
111
+ for (const action of s.pipe) {
112
+ if (action.async) return true;
113
+ const a = action;
114
+ if (a.kind === "transformation" && !SAFE_TRANSFORMATION_TYPES.has(a.type ?? "")) seenTransform = true;
115
+ if (a.kind === "schema") {
116
+ if (containsAsyncSchema(action, visited, seenTransform)) return true;
117
+ if (!seenTransform) {
118
+ const innerPipe = action.pipe;
119
+ if (innerPipe && Array.isArray(innerPipe)) for (const innerAction of innerPipe) {
120
+ const ia = innerAction;
121
+ if (ia.kind === "transformation" && !SAFE_TRANSFORMATION_TYPES.has(ia.type ?? "")) {
122
+ seenTransform = true;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ if (afterTransform) {
131
+ if (s.entries) {
132
+ for (const entry of Object.values(s.entries)) if (containsAsyncSchema(entry, visited, true)) return true;
133
+ }
134
+ if (s.item && containsAsyncSchema(s.item, visited, true)) return true;
135
+ if (s.items && Array.isArray(s.items)) {
136
+ for (const item of s.items) if (containsAsyncSchema(item, visited, true)) return true;
137
+ }
138
+ if (s.key && typeof s.key === "object" && containsAsyncSchema(s.key, visited, true)) return true;
139
+ if (s.value && containsAsyncSchema(s.value, visited, true)) return true;
140
+ if (s.rest && containsAsyncSchema(s.rest, visited, true)) return true;
141
+ if (s.type === "promise") {
142
+ const promiseInner = schema.message;
143
+ if (typeof promiseInner === "object" && promiseInner != null && "kind" in promiseInner && containsAsyncSchema(promiseInner, visited, true)) return true;
144
+ }
145
+ }
146
+ return false;
147
+ }
148
+ /**
7
149
  * Infers an appropriate metavar string from a Valibot schema.
8
150
  *
9
151
  * This function analyzes the Valibot schema's internal structure to determine
@@ -198,9 +340,12 @@ function inferChoices(schema) {
198
340
  * ```
199
341
  *
200
342
  * @throws {TypeError} If the resolved `metavar` is an empty string.
343
+ * @throws {TypeError} If the schema contains async validations that cannot be
344
+ * executed synchronously.
201
345
  * @since 0.7.0
202
346
  */
203
347
  function valibot(schema, options = {}) {
348
+ if (containsAsyncSchema(schema)) throw new TypeError("Async Valibot schemas (e.g., async validations) are not supported by valibot(). Use synchronous schemas instead.");
204
349
  const choices = inferChoices(schema);
205
350
  const metavar = options.metavar ?? inferMetavar(schema);
206
351
  ensureNonEmptyString(metavar);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/valibot",
3
- "version": "1.0.0-dev.1354+b0af7b96",
3
+ "version": "1.0.0-dev.1359+1fa707c2",
4
4
  "description": "Valibot value parsers for Optique",
5
5
  "keywords": [
6
6
  "CLI",
@@ -57,7 +57,7 @@
57
57
  "valibot": "^1.2.0"
58
58
  },
59
59
  "dependencies": {
60
- "@optique/core": "1.0.0-dev.1354+b0af7b96"
60
+ "@optique/core": "1.0.0-dev.1359+1fa707c2"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^20.19.9",