conform-to-valibot
Version:
Conform helpers for integrating with Valibot
500 lines (496 loc) • 14.5 kB
JavaScript
// constraint.ts
var keys = [
"required",
"minLength",
"maxLength",
"min",
"max",
"step",
"multiple",
"pattern"
];
function getValibotConstraint(schema) {
function updateConstraint(schema2, data, name = "") {
if (name !== "" && !data[name]) {
data[name] = { required: true };
}
const constraint = name !== "" ? data[name] : {};
if (schema2.type === "object") {
for (const key in schema2.entries) {
updateConstraint(
// @ts-expect-error
schema2.entries[key],
data,
name ? `${name}.${key}` : key
);
}
} else if (schema2.type === "intersect") {
for (const option of schema2.options) {
const result2 = {};
updateConstraint(option, result2, name);
Object.assign(data, result2);
}
} else if (schema2.type === "union" || schema2.type === "variant") {
Object.assign(
data,
// @ts-expect-error
schema2.options.map((option) => {
const result2 = {};
updateConstraint(option, result2, name);
return result2;
}).reduce((prev, next) => {
const list = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
const result2 = {};
for (const name2 of list) {
const prevConstraint = prev[name2];
const nextConstraint = next[name2];
if (prevConstraint && nextConstraint) {
const constraint2 = {};
result2[name2] = constraint2;
for (const key of keys) {
if (typeof prevConstraint[key] !== "undefined" && typeof nextConstraint[key] !== "undefined" && prevConstraint[key] === nextConstraint[key]) {
constraint2[key] = prevConstraint[key];
}
}
} else {
result2[name2] = {
...prevConstraint,
...nextConstraint,
required: false
};
}
}
return result2;
})
);
} else if (name === "") {
throw new Error("Unsupported schema");
} else if (schema2.type === "array") {
constraint.multiple = true;
updateConstraint(schema2.item, data, `${name}[]`);
} else if (schema2.type === "string") {
const minLength = schema2.pipe?.find(
// @ts-expect-error
(v) => "type" in v && v.type === "min_length"
);
if (minLength && "requirement" in minLength) {
constraint.minLength = minLength.requirement;
}
const maxLength = schema2.pipe?.find(
// @ts-expect-error
(v) => "type" in v && v.type === "max_length"
);
if (maxLength && "requirement" in maxLength) {
constraint.maxLength = maxLength.requirement;
}
} else if (schema2.type === "optional") {
constraint.required = false;
updateConstraint(schema2.wrapped, data, name);
} else if (schema2.type === "nullish") {
constraint.required = false;
updateConstraint(schema2.wrapped, data, name);
} else if (schema2.type === "number") {
const minValue = schema2.pipe?.find(
// @ts-expect-error
(v) => "type" in v && v.type === "min_value"
);
if (minValue && "requirement" in minValue) {
constraint.min = minValue.requirement;
}
const maxValue = schema2.pipe?.find(
// @ts-expect-error
(v) => "type" in v && v.type === "max_value"
);
if (maxValue && "requirement" in maxValue) {
constraint.max = maxValue.requirement;
}
} else if (schema2.type === "enum") {
constraint.pattern = Object.entries(schema2.enum).map(
([_, option]) => (
// To escape unsafe characters on regex
typeof option === "string" ? option.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d") : option
)
).join("|");
} else if (schema2.type === "tuple") {
for (let i = 0; i < schema2.items.length; i++) {
updateConstraint(schema2.items[i], data, `${name}[${i}]`);
}
} else {
}
}
const result = {};
updateConstraint(schema, result);
return result;
}
// parse.ts
import {
parse as baseParse,
formatPaths
} from "@conform-to/dom";
import {
safeParse,
safeParseAsync
} from "valibot";
// coercion.ts
import {
pipe,
pipeAsync,
transform as vTransform,
unknown as valibotUnknown
} from "valibot";
function coerce(type, transformFn) {
const unknown = { ...valibotUnknown(), expects: type.expects };
const transformAction = vTransform(transformFn);
const schema = type.async ? pipeAsync(unknown, transformAction, type) : pipe(unknown, transformAction, type);
return { transformAction, schema };
}
function stripEmptyString(value) {
if (typeof value !== "string") {
return value;
}
if (value === "") {
return void 0;
}
return value;
}
function stripEmptyFile(file) {
if (typeof File !== "undefined" && file instanceof File && file.name === "" && file.size === 0) {
return void 0;
}
return file;
}
function coerceNumber(value) {
if (typeof value !== "string") {
return value;
}
return value.trim() === "" ? value : Number(value);
}
function coerceBoolean(value) {
if (typeof value !== "string") {
return value;
}
return value === "on" ? true : value;
}
function coerceDate(value) {
if (typeof value !== "string") {
return value;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date;
}
function coerceBigInt(value) {
if (typeof value !== "string") {
return value;
}
if (value.trim() === "") {
return value;
}
try {
return BigInt(value);
} catch {
return value;
}
}
function coerceArray(type) {
const unknown = { ...valibotUnknown(), expects: type.expects };
const transformFunction = (output) => {
if (Array.isArray(output)) {
return output;
}
if (typeof output === "undefined" || typeof stripEmptyFile(stripEmptyString(output)) === "undefined") {
return [];
}
return [output];
};
if (type.async) {
return pipeAsync(unknown, vTransform(transformFunction), type);
}
return pipe(unknown, vTransform(transformFunction), type);
}
function compose(a, b) {
return (value) => b(a(value));
}
function generateWrappedSchema(type, options, rewrap = false) {
const unknown = { ...valibotUnknown(), expects: type.expects };
const { transformAction, schema: wrapSchema } = enableTypeCoercion(
// @ts-expect-error
type.wrapped,
options
);
if (transformAction) {
const schema2 = type.async ? pipeAsync(unknown, transformAction, type) : pipe(unknown, transformAction, type);
if (rewrap) {
const default_ = "default" in type ? type.default : void 0;
return {
transformAction: void 0,
schema: type.reference(schema2, default_)
};
}
return { transformAction, schema: schema2 };
}
const wrappedSchema = {
...type,
wrapped: wrapSchema
};
const transformActionForStripEmptyString = vTransform(stripEmptyString);
const schema = wrappedSchema.async ? pipeAsync(unknown, transformActionForStripEmptyString, wrappedSchema) : pipe(unknown, transformActionForStripEmptyString, wrappedSchema);
return {
transformAction: void 0,
schema
};
}
function enableTypeCoercion(type, options) {
if ("pipe" in type) {
const { transformAction, schema: coercedSchema } = enableTypeCoercion(
type.pipe[0],
options
);
const schema = type.async ? pipeAsync(coercedSchema, ...type.pipe.slice(1)) : (
// @ts-expect-error `coercedSchema` must be sync here but TypeScript can't infer that.
pipe(coercedSchema, ...type.pipe.slice(1))
);
return { transformAction, schema };
}
const customizeFn = options.customize(type);
if (customizeFn) {
return coerce(type, customizeFn);
}
switch (type.type) {
case "string":
case "literal":
case "enum":
case "undefined": {
return coerce(type, options.defaultCoercion.string);
}
case "number": {
return coerce(type, options.defaultCoercion.number);
}
case "boolean": {
return coerce(type, options.defaultCoercion.boolean);
}
case "date": {
return coerce(type, options.defaultCoercion.date);
}
case "bigint": {
return coerce(type, options.defaultCoercion.bigint);
}
case "file":
case "blob": {
return coerce(type, options.defaultCoercion.file);
}
case "array": {
const arraySchema = {
...type,
// @ts-expect-error
item: enableTypeCoercion(type.item, options).schema
};
return {
transformAction: void 0,
schema: coerceArray(arraySchema)
};
}
case "exact_optional": {
const { schema: wrapSchema } = enableTypeCoercion(type.wrapped, options);
const exactOptionalSchema = {
...type,
wrapped: wrapSchema
};
return {
transformAction: void 0,
schema: exactOptionalSchema
};
}
case "nullish":
case "optional": {
return generateWrappedSchema(type, options, true);
}
case "undefinedable":
case "nullable":
case "non_optional":
case "non_nullish":
case "non_nullable": {
return generateWrappedSchema(type, options);
}
case "union":
case "intersect": {
const unionSchema = {
...type,
// @ts-expect-error
options: type.options.map(
// @ts-expect-error
(option) => enableTypeCoercion(option, options).schema
)
};
return {
transformAction: void 0,
schema: unionSchema
};
}
case "variant": {
const variantSchema = {
...type,
// @ts-expect-error
options: type.options.map(
// @ts-expect-error
(option) => enableTypeCoercion(option, options).schema
)
};
return {
transformAction: void 0,
schema: variantSchema
};
}
case "tuple": {
const tupleSchema = {
...type,
// @ts-expect-error
items: type.items.map(
// @ts-expect-error
(option) => enableTypeCoercion(option, options).schema
)
};
return {
transformAction: void 0,
schema: tupleSchema
};
}
case "tuple_with_rest": {
const tupleWithRestSchema = {
...type,
// @ts-expect-error
items: type.items.map(
// @ts-expect-error
(option) => enableTypeCoercion(option, options).schema
),
// @ts-expect-error
rest: enableTypeCoercion(type.rest, options).schema
};
return {
transformAction: void 0,
schema: tupleWithRestSchema
};
}
case "loose_object":
case "strict_object":
case "object": {
const objectSchema = {
...type,
entries: Object.fromEntries(
// @ts-expect-error
Object.entries(type.entries).map(([key, def]) => [
key,
enableTypeCoercion(def, options).schema
])
)
};
return {
transformAction: void 0,
schema: objectSchema
};
}
case "object_with_rest": {
const objectWithRestSchema = {
...type,
entries: Object.fromEntries(
// @ts-expect-error
Object.entries(type.entries).map(([key, def]) => [
key,
enableTypeCoercion(def, options).schema
])
),
// @ts-expect-error
rest: enableTypeCoercion(type.rest, options).schema
};
return {
transformAction: void 0,
schema: objectWithRestSchema
};
}
}
return coerce(type, (value) => value);
}
function coerceFormValue(type, options) {
return enableTypeCoercion(type, {
defaultCoercion: {
string: compose(
stripEmptyString,
getCoercion(options?.defaultCoercion?.string)
),
file: compose(
stripEmptyFile,
getCoercion(options?.defaultCoercion?.file)
),
number: compose(
stripEmptyString,
getCoercion(options?.defaultCoercion?.number, coerceNumber)
),
boolean: compose(
stripEmptyString,
getCoercion(options?.defaultCoercion?.boolean, coerceBoolean)
),
date: compose(
stripEmptyString,
getCoercion(options?.defaultCoercion?.date, coerceDate)
),
bigint: compose(
stripEmptyString,
getCoercion(options?.defaultCoercion?.bigint, coerceBigInt)
)
},
customize: options?.customize ?? (() => null)
}).schema;
}
var getCoercion = (providedCoercion, fallbackCoercion) => {
if (typeof providedCoercion === "function") {
return providedCoercion;
}
if (providedCoercion === false || fallbackCoercion === void 0) {
return (value) => value;
}
return fallbackCoercion;
};
// parse.ts
var conformValibotMessage = {
VALIDATION_SKIPPED: "__skipped__",
VALIDATION_UNDEFINED: "__undefined__"
};
function parseWithValibot(payload, config) {
return baseParse(payload, {
resolve(payload2, intent) {
const baseSchema = typeof config.schema === "function" ? config.schema(intent) : config.schema;
const schema = config.disableAutoCoercion ? baseSchema : coerceFormValue(baseSchema);
const resolveResult = (result) => {
if (result.success) {
return {
value: result.output
};
}
return {
error: result.issues.reduce((result2, e) => {
if (result2 === null || e.message === conformValibotMessage.VALIDATION_UNDEFINED) {
return null;
}
const name = formatPaths(
e.path?.map((d) => d.key) ?? []
);
result2[name] = result2[name] === null || e.message === conformValibotMessage.VALIDATION_SKIPPED ? null : [...result2[name] ?? [], e.message];
return result2;
}, {})
};
};
if (schema.async === true) {
return safeParseAsync(schema, payload2, config.info).then(resolveResult);
}
return resolveResult(safeParse(schema, payload2, config.info));
}
});
}
export {
conformValibotMessage,
getValibotConstraint,
parseWithValibot,
coerceFormValue as unstable_coerceFormValue
};