UNPKG

@conform-to/zod

Version:

Conform helpers for integrating with Zod

495 lines (483 loc) 18.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js'); var v4 = require('zod/v4'); var _excluded = ["_zod"]; function coerceFile(file) { if (typeof File !== 'undefined' && file instanceof File && file.name === '' && file.size === 0) { return undefined; } return file; } function defaultConvertNumber(text) { return text.trim() === '' ? NaN : Number(text); } function defaultConvertBoolean(text) { if (text === 'on') { return true; } throw new Error('Not a boolean'); } function defaultConvertDate(text) { var date = new Date(text); if (isNaN(date.getTime())) { throw new Error('Invalid date'); } return date; } function defaultConvertBigInt(text) { if (text.trim() === '') { throw new Error('Empty bigint'); } return BigInt(text); } function coerceString(value, fn) { if (typeof value !== 'string') { return value; } return fn(value); } /** * strip → undefined stops, else convert, catch → original string */ function createValidationCoercion(strip, convert) { return value => coerceString(value, text => { var stripped = strip(text); if (stripped === undefined) { return undefined; } try { return convert(stripped); } catch (_unused) { return stripped; } }); } /** * On error returns the sentinel (e.g. NaN, false) so typeCheckTransform accepts it. */ function createStructuralCoercion(convert, emptySentinel) { return value => coerceString(value, text => { try { return convert(text); } catch (_unused2) { return emptySentinel; } }); } function selectCoercion(type, defaultCoercion) { var def = type._zod.def; if (def.type === 'string' || def.type === 'enum' // || def.type === 'nativeEnum' ) { return defaultCoercion.string; } else if (def.type === 'literal') { if (!('values' in def)) { return defaultCoercion.string; } var literalValue = [...def.values][0]; 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.type === 'file') { return defaultCoercion.file; } else if (def.type === 'number') { return defaultCoercion.number; } else if (def.type === 'boolean') { return defaultCoercion.boolean; } else if (def.type === 'date') { return defaultCoercion.date; } else if (def.type === 'bigint') { return defaultCoercion.bigint; } } /** * Constrained types (enum, literal) should still be validated in structural * mode because their values come from constrained UI (select, radio, hidden * input) and an invalid value indicates a developer error. */ function isConstrainedType(type) { var def = type._zod.def; return def.type === 'enum' || def.type === 'literal'; } /** * Accepts sentinel values like NaN and Invalid Date since * `typeof NaN === 'number'` and `Invalid Date instanceof Date`. */ function getTypeCheckTarget(type) { switch (type._zod.def.type) { case 'string': case 'number': case 'boolean': case 'date': case 'bigint': return typeCheckTransform(type._zod.def.type); default: return type; } } function typeCheckTransform(defType) { var check = defType === 'date' ? value => value instanceof Date : value => typeof value === defType; return v4.transform((value, ctx) => { if (!check(value)) { ctx.issues.push({ input: value, code: 'invalid_type', expected: defType, received: typeof value, message: "Invalid input: expected ".concat(defType, ", received ").concat(typeof value) }); } return value; }); } function pipeWithOptionality(coercion, target) { var schema = v4.pipe(v4.transform(coercion), target); if (target._zod.optin) { schema._zod.optin = target._zod.optin; } if (target._zod.optout) { schema._zod.optout = target._zod.optout; } return schema; } function materializesMissingValue(type) { var def = type._zod.def; if (def.type === 'optional' || def.type === 'nonoptional' || def.type === 'default' || def.type === 'prefault' || def.type === 'catch' || def.type === 'nullable') { return materializesMissingValue(def.innerType); } if (def.type === 'pipe') { var pipeDef = def; if (pipeDef.in._zod.def.type === 'transform') { return materializesMissingValue(pipeDef.out); } return materializesMissingValue(pipeDef.in); } return def.type === 'array' || def.type === 'object'; } function withOptionalInput(type) { var schema = Object.create(Object.getPrototypeOf(type)); var _Object$getOwnPropert = Object.getOwnPropertyDescriptors(type), descriptors = _rollupPluginBabelHelpers.objectWithoutProperties(_Object$getOwnPropert, _excluded); Object.defineProperties(schema, descriptors); Object.defineProperty(schema, '_zod', { value: Object.create(type._zod, { optin: { value: 'optional' } }) }); return schema; } function coerceObjectShapeEntry(type, options) { var schema = coerceType(type, options); if (type._zod.optin !== 'optional' && materializesMissingValue(type)) { return withOptionalInput(schema); } return schema; } /** * Reconstruct the provided schema with additional preprocessing steps * that strip empty values and coerce strings to the correct type. */ function coerceType(type, options) { var _; var result = options.resolved.get(type); if (result) { // Prevent infinite recursion from z.lazy() return result; } // Detect re-entrant calls caused by getter-based recursive schemas // (Zod v4's recommended pattern for recursive types). // Return a lazy wrapper to break the recursion; the inner call is // deferred to parse time, by which point the schema will be cached. if (options.resolving.has(type)) { return v4.lazy(() => coerceType(type, options)); } options.resolving.add(type); var schema = type; var zod = type._zod; var def = zod.def; var constr = zod.constr; var coercion = options.coerce(type); var target = options.skipValidation && !isConstrainedType(type) ? getTypeCheckTarget(type) : type; if (coercion) { schema = pipeWithOptionality(coercion, target); } else if (target !== type) { schema = target; } else if (def.type === 'array') { var arrayDef = options.skipValidation ? _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { checks: undefined }) : def; schema = pipeWithOptionality(value => { if (Array.isArray(value)) { return value; } if (typeof value === 'undefined' || options.stripEmptyValue && typeof options.stripEmptyValue(value) === 'undefined') { return []; } return [value]; }, new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, arrayDef), {}, { element: coerceType(def.element, options) }))); } else if (def.type === 'object') { var objectDef = options.skipValidation ? _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { catchall: undefined }) : def; schema = pipeWithOptionality(value => { if (typeof value === 'undefined') { return {}; } return value; }, new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, objectDef), {}, { shape: Object.fromEntries(Object.entries(def.shape).map(_ref => { var [key, def] = _ref; return [key, coerceObjectShapeEntry(def, options)]; })) }))); } else if (def.type === 'optional' || def.type === 'nonoptional') { var innerType = coerceType(def.innerType, options); var wrapped = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType })); schema = options.stripEmptyValue ? pipeWithOptionality(options.stripEmptyValue, wrapped) : wrapped; } else if (def.type === 'default' || def.type === 'prefault') { if (options.skipDefaults) { schema = v4.optional(coerceType(def.innerType, options)); } else { var defaultValue = def.defaultValue; var _innerType = defaultValue !== '' // Don't strip the empty string that IS the default ? coerceType(def.innerType, options) : def.innerType; var _wrapped = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType: _innerType })); schema = options.stripEmptyValue ? pipeWithOptionality(options.stripEmptyValue, _wrapped) : _wrapped; } } else if (def.type === 'catch') { schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType: coerceType(def.innerType, options) })); } else if (def.type === 'intersection') { schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { left: coerceType(def.left, options), right: coerceType(def.right, options) })); // The `type` of `discriminatedUnion` is defined as 'union', so it can be determined from the class name used. } else if (def.type === 'union' && (_ = [...zod.traits][0]) !== null && _ !== void 0 && _.includes('DiscriminatedUnion')) { schema = v4.pipe(v4.transform(value => { if (typeof value === 'undefined') { return {}; } return value; }), new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { options: def.options.map(item => { var objectDef = item._zod.def; var setOriginalPropValues = object => { // The discriminate key is obtained from the defined Object. // If you regenerate the Object schema, the `propValues` property disappears. Therefore, set the one obtained from the original Object. // https://github.com/colinhacks/zod/blob/22ab436bc214d86d740e78f33ae6834d28ddc152/packages/zod/src/v4/core/schemas.ts#L1949-L1963 object._zod.propValues = item._zod.propValues; // @ts-expect-error: The `disc` property was used up to version 3.25.34, but was changed to the `propValues` property from version 3.25.35 onwards. object._zod.disc = item._zod.disc; return object; }; if (objectDef.type !== 'object') { return setOriginalPropValues(coerceType(item, options)); } var innerDef = options.skipValidation ? _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, objectDef), {}, { catchall: undefined }) : objectDef; var object = new item._zod.constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, innerDef), {}, { shape: Object.fromEntries(Object.entries(objectDef.shape).map(_ref2 => { var [key, def] = _ref2; return [key, coerceObjectShapeEntry(def, options)]; })) })); return setOriginalPropValues(object); }) }))); } else if (def.type === 'union') { schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { options: def.options.map(item => coerceType(item, options)) })); } else if (def.type === 'tuple') { schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { items: def.items.map(item => coerceType(item, options)) })); } else if (def.type === 'nullable') { schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { innerType: coerceType(def.innerType, options) })); } else if (def.type === 'pipe') { if (options.skipTransforms) { schema = coerceType(def.in, options); } else { schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, { in: coerceType(def.in, options), out: coerceType(def.out, options) })); } } else if (def.type === 'lazy') { var inner = def.getter(); schema = v4.lazy(() => coerceType(inner, options)); } options.resolving.delete(type); if (type !== schema) { options.resolved.set(type, schema); } return schema; } /** * Creates configured coercion functions for form value parsing. * * **Example:** * * ```tsx * import { configureCoercion } from '@conform-to/zod/v4/future'; * import { z } from 'zod'; * * const { coerceFormValue, coerceStructure } = configureCoercion({ * // Trim whitespace and treat whitespace-only as empty * stripEmptyString: (value) => { * const trimmed = value.trim(); * return trimmed === '' ? undefined : trimmed; * }, * type: { * // Custom number parsing: strip commas * number: (text) => Number(text.replace(/,/g, '')), * }, * }); * * const schema = z.object({ age: z.number(), name: z.string() }); * const validationSchema = coerceFormValue(schema); * const structuralSchema = coerceStructure(schema); * ``` */ function configureCoercion(config) { var _config$stripEmptyStr, _config$type$number, _config$type, _config$type$boolean, _config$type2, _config$type$date, _config$type3; var stripEmptyString = (_config$stripEmptyStr = config === null || config === void 0 ? void 0 : config.stripEmptyString) !== null && _config$stripEmptyStr !== void 0 ? _config$stripEmptyStr : value => value === '' ? undefined : value; var convertNumber = (_config$type$number = config === null || config === void 0 || (_config$type = config.type) === null || _config$type === void 0 ? void 0 : _config$type.number) !== null && _config$type$number !== void 0 ? _config$type$number : defaultConvertNumber; var convertBoolean = (_config$type$boolean = config === null || config === void 0 || (_config$type2 = config.type) === null || _config$type2 === void 0 ? void 0 : _config$type2.boolean) !== null && _config$type$boolean !== void 0 ? _config$type$boolean : defaultConvertBoolean; var convertDate = (_config$type$date = config === null || config === void 0 || (_config$type3 = config.type) === null || _config$type3 === void 0 ? void 0 : _config$type3.date) !== null && _config$type$date !== void 0 ? _config$type$date : defaultConvertDate; var validationMap = { string: value => coerceString(value, stripEmptyString), file: coerceFile, number: createValidationCoercion(stripEmptyString, convertNumber), boolean: createValidationCoercion(stripEmptyString, convertBoolean), date: createValidationCoercion(stripEmptyString, convertDate), bigint: createValidationCoercion(stripEmptyString, defaultConvertBigInt) }; var structuralMap = { number: createStructuralCoercion(convertNumber, NaN), boolean: createStructuralCoercion(convertBoolean, false), date: createStructuralCoercion(convertDate, new Date('')), bigint: createStructuralCoercion(defaultConvertBigInt, 0n) }; var coerce = (type, coercionMap) => { var _selectCoercion; if (config !== null && config !== void 0 && config.customize) { var customFn = config.customize(type); if (customFn !== null) { return customFn; } } return (_selectCoercion = selectCoercion(type, coercionMap)) !== null && _selectCoercion !== void 0 ? _selectCoercion : null; }; var validationCache = new WeakMap(); var structureCache = new WeakMap(); return { /** * Enhances a schema to coerce form values and strip empty values before validation. * This configured helper uses the options passed to `configureCoercion`. * * Results are cached per schema, so this can be called inline. * * **Example:** * * ```tsx * const schema = coerceFormValue(z.object({ * age: z.number().optional(), * subscribe: z.boolean(), * })); * * schema.parse({ age: '', subscribe: 'on' }); * // { age: undefined, subscribe: true } * ``` */ coerceFormValue(type) { var result = validationCache.get(type); if (!result) { result = coerceType(type, { resolved: new Map(), resolving: new Set(), stripEmptyValue(value) { return coerceFile(coerceString(value, stripEmptyString)); }, coerce(type) { return coerce(type, validationMap); } }); validationCache.set(type, result); } return result; }, /** * Enhances a schema to coerce form values without running validation. * This configured helper is useful for reading current form values as typed data. * * It skips validation, defaults, transforms, and refinements, and does not strip * empty strings to `undefined`. * * For number, boolean, date, and bigint schemas, empty strings and other failed * string coercions still become fallback values: * * - `z.number()` -> `NaN` * - `z.boolean()` -> `false` * - `z.date()` -> `Invalid Date` * - `z.bigint()` -> `0n` * * Results are cached per schema, so this can be called inline. * * **Example:** * * ```tsx * const schema = coerceStructure(z.object({ * age: z.number().min(10), * })); * * schema.parse({ age: '3' }); * // { age: 3 } * ``` */ coerceStructure(type) { var result = structureCache.get(type); if (!result) { result = coerceType(type, { resolved: new Map(), resolving: new Set(), coerce(type) { return coerce(type, structuralMap); }, skipValidation: true, skipDefaults: true, skipTransforms: true }); structureCache.set(type, result); } return result; } }; } var { coerceFormValue, coerceStructure } = configureCoercion(); exports.coerceFormValue = coerceFormValue; exports.coerceStructure = coerceStructure; exports.configureCoercion = configureCoercion;