UNPKG

conform-to-valibot

Version:

Conform helpers for integrating with Valibot

500 lines (496 loc) 14.5 kB
// 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 };