UNPKG

@conform-to/zod

Version:

Conform helpers for integrating with Zod

327 lines (314 loc) 12.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js'); var zod = require('zod'); /** * Helpers for coercing string value * Modify the value only if it's a string, otherwise return the value as-is */ function stripEmptyString(value) { if (typeof value !== 'string') { return value; } if (value === '') { return undefined; } return value; } /** * Helpers for coercing file * Modify the value only if it's a file, otherwise return the value as-is */ function stripEmptyFile(file) { if (typeof File !== 'undefined' && file instanceof File && file.name === '' && file.size === 0) { return undefined; } 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; } var date = new Date(value); // z.date() does not expose a quick way to set invalid_date error // This gets around it by returning the original string if it's invalid // See https://github.com/colinhacks/zod/issues/1526 if (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 (_unused) { return value; } } /** * A file schema is usually defined with `z.instanceof(File)` * which is implemented with `z.custom()` based on ZodAny with a `superRefine` check * See https://github.com/colinhacks/zod/blob/eea05ae3dab628e7a834397414e5145e935e418b/src/types.ts#L5250-L5285 */ function isFileSchema(schema) { if (typeof File === 'undefined') { return false; } return schema._def.effect.type === 'refinement' && schema.innerType()._def.typeName === 'ZodAny' && schema.safeParse(new File([], '')).success && !schema.safeParse('').success; } function compose(a, b) { var c = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : i => i; return value => c(b(a(value))); } function selectDefaultCoercion(type, defaultCoercion) { var def = type._def; if (def.typeName === 'ZodString' || def.typeName === 'ZodEnum' || def.typeName === 'ZodNativeEnum') { return defaultCoercion.string; } else if (def.typeName === 'ZodLiteral') { var literalValue = type.value; if (typeof literalValue === 'number') { return defaultCoercion.number; } if (typeof literalValue === 'boolean') { return defaultCoercion.boolean; } if (typeof literalValue === 'bigint') { return defaultCoercion.bigint; } return defaultCoercion.string; } else if (def.typeName === 'ZodEffects' && isFileSchema(type)) { return defaultCoercion.file; } else if (def.typeName === 'ZodNumber') { return defaultCoercion.number; } else if (def.typeName === 'ZodBoolean') { return defaultCoercion.boolean; } else if (def.typeName === 'ZodDate') { return defaultCoercion.date; } else if (def.typeName === 'ZodBigInt') { return defaultCoercion.bigint; } return null; } /** * Reconstruct the provided schema with additional preprocessing steps * This strips empty values to undefined and coerces string to the correct type */ function enableTypeCoercion(type, options) { var result = options.cache.get(type); // Return the cached schema if it's already processed // This is to prevent infinite recursion caused by z.lazy() if (result) { return result; } var schema = type; var def = type._def; var coercion = options.coerce(type); if (coercion) { schema = zod.any().transform(coercion).pipe(type); } else if (def.typeName === 'ZodArray') { schema = zod.any().transform(value => { // No preprocess needed if the value is already an array if (Array.isArray(value)) { return value; } if (typeof value === 'undefined' || typeof options.stripEmptyValue(value) === 'undefined') { return []; } // Wrap it in an array otherwise return [value]; }).pipe(new zod.ZodArray(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { type: enableTypeCoercion(def.type, options) }))); } else if (def.typeName === 'ZodObject') { schema = zod.any().transform(value => { if (typeof value === 'undefined') { // Defaults it to an empty object return {}; } return value; }).pipe(new zod.ZodObject(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { shape: () => Object.fromEntries(Object.entries(def.shape()).map(_ref => { var [key, def] = _ref; return [key, // @ts-expect-error see message above enableTypeCoercion(def, options)]; })) }))); } else if (def.typeName === 'ZodEffects') { schema = new zod.ZodEffects(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { schema: enableTypeCoercion(def.schema, options) })); } else if (def.typeName === 'ZodOptional') { schema = zod.any().transform(options.stripEmptyValue).pipe(new zod.ZodOptional(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType: enableTypeCoercion(def.innerType, options) }))); } else if (def.typeName === 'ZodDefault') { var defaultValue = def.defaultValue(); schema = zod.any().transform(options.stripEmptyValue).pipe(new zod.ZodDefault(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType: defaultValue !== '' ? enableTypeCoercion(def.innerType, options) : def.innerType }))); } else if (def.typeName === 'ZodCatch') { schema = new zod.ZodCatch(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType: enableTypeCoercion(def.innerType, options) })); } else if (def.typeName === 'ZodIntersection') { schema = new zod.ZodIntersection(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { left: enableTypeCoercion(def.left, options), right: enableTypeCoercion(def.right, options) })); } else if (def.typeName === 'ZodUnion') { schema = new zod.ZodUnion(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { options: def.options.map(option => enableTypeCoercion(option, options)) })); } else if (def.typeName === 'ZodDiscriminatedUnion') { schema = zod.any().transform(value => { if (typeof value === 'undefined') { // Defaults it to an empty object return {}; } return value; }).pipe(new zod.ZodDiscriminatedUnion(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { options: def.options.map(option => enableTypeCoercion(option, options)), optionsMap: new Map(Array.from(def.optionsMap.entries()).map(_ref2 => { var [discriminator, option] = _ref2; return [discriminator, enableTypeCoercion(option, options)]; })) }))); } else if (def.typeName === 'ZodBranded') { schema = new zod.ZodBranded(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { type: enableTypeCoercion(def.type, options) })); } else if (def.typeName === 'ZodTuple') { schema = new zod.ZodTuple(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { items: def.items.map(item => enableTypeCoercion(item, options)) })); } else if (def.typeName === 'ZodNullable') { schema = new zod.ZodNullable(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType: enableTypeCoercion(def.innerType, options) })); } else if (def.typeName === 'ZodPipeline') { schema = new zod.ZodPipeline(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { in: enableTypeCoercion(def.in, options), out: enableTypeCoercion(def.out, options) })); } else if (def.typeName === 'ZodLazy') { var inner = def.getter(); schema = zod.lazy(() => enableTypeCoercion(inner, options)); } if (type !== schema) { options.cache.set(type, schema); } return schema; } /** * A helper that enhance the zod schema to strip empty value and coerce form value to the expected type with option to customize type coercion. * **Example:** * * ```tsx * import { coerceFormValue } from '@conform-to/zod/v3/future'; // Or import `@conform-to/zod/v4/future`. * import { z } from 'zod'; * * // Coerce the form value with default behaviour * const schema = coerceFormValue( * z.object({ * // ... * }) * ); * * // Coerce the form value with default coercion overrided * const schema = coerceFormValue( * z.object({ * ref: z.number() * date: z.date(), * amount: z.number(), * confirm: z.boolean(), * }), * { * // Trim the value for all string-based fields * // e.g. `z.string()`, `z.number()` or `z.boolean()` * string: (value) => { * if (typeof value !== 'string') { * return value; * } * * const result = value.trim(); * * // Treat it as `undefined` if the value is empty * if (result === '') { * return undefined; * } * * return result; * }, * * // Override the default coercion with `z.number()` * number: (value) => { * // Pass the value as is if it's not a string * if (typeof value !== 'string') { * return value; * } * * // Trim and remove commas before casting it to number * return Number(value.trim().replace(/,/g, '')); * }, * * // Disable coercion for `z.boolean()` * boolean: false, * }, * ); * ``` */ function coerceFormValue(type, options) { var getCoercion = (type, fallbackCoercion) => { var _options$defaultCoerc; var providedCoercion = options === null || options === void 0 || (_options$defaultCoerc = options.defaultCoercion) === null || _options$defaultCoerc === void 0 ? void 0 : _options$defaultCoerc[type]; if (typeof providedCoercion === 'function') { return providedCoercion; } // If the user explicitly disabled the coercion or no fallback coercion, return a noop function if (providedCoercion === false || !fallbackCoercion) { return value => value; } return fallbackCoercion; }; var defaultCoercion = { string: compose(stripEmptyString, getCoercion('string')), file: compose(stripEmptyFile, getCoercion('file')), number: compose(stripEmptyString, getCoercion('string'), getCoercion('number', coerceNumber)), boolean: compose(stripEmptyString, getCoercion('string'), getCoercion('boolean', coerceBoolean)), date: compose(stripEmptyString, getCoercion('string'), getCoercion('date', coerceDate)), bigint: compose(stripEmptyString, getCoercion('string'), getCoercion('bigint', coerceBigInt)) }; return enableTypeCoercion(type, { cache: new Map(), stripEmptyValue: compose(defaultCoercion.string, defaultCoercion.file), coerce: type => { var _options$customize, _options$customize2; var coercion = (_options$customize = options === null || options === void 0 || (_options$customize2 = options.customize) === null || _options$customize2 === void 0 ? void 0 : _options$customize2.call(options, type)) !== null && _options$customize !== void 0 ? _options$customize : null; if (coercion === null) { coercion = selectDefaultCoercion(type, defaultCoercion); } return coercion; } }); } exports.coerceFormValue = coerceFormValue; exports.enableTypeCoercion = enableTypeCoercion;