UNPKG

zod

Version:

TypeScript-first schema declaration and validation library with static type inference

361 lines (359 loc) • 14.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.$ZodCyclicError = void 0; exports.isRecursiveSchema = isRecursiveSchema; exports.memoizer = memoizer; exports.isBackEdge = isBackEdge; const util = __importStar(require("./util.cjs")); class $ZodCyclicError extends Error { constructor() { super(`Cannot parse a reference cycle that closes through a transform`); this.name = "ZodCyclicError"; } } exports.$ZodCyclicError = $ZodCyclicError; /** Keyed off the context object every schema in one parse call already shares. */ const STATE = "~memo"; const NO_ISSUES = []; // a value a cycle can close through function isRef(value) { return value !== null && typeof value === "object"; } // Receivers prefix paths in place, so the cache and every hand-out need their own copies. function cloneIssues(issues) { return issues.map((iss) => (iss.path ? { ...iss, path: iss.path.slice() } : { ...iss })); } const recursive = /*@__PURE__*/ new WeakMap(); /** What the walk established, in order of certainty: ordered so the strongest answer among children wins. */ const NONE = 0; const ASSUMED = 1; const PROVEN = 2; /** Whether this schema's subtree contains a cycle, so one parse can re-enter it. */ function isRecursive(inst, stack, resolve) { const cached = recursive.get(inst); if (cached !== undefined) return cached ? PROVEN : NONE; // Relative to the walk in progress, so not cached. if (stack.has(inst)) return PROVEN; stack.add(inst); let result = NONE; const check = (child) => { if (result !== PROVEN && child?._zod) { const answer = isRecursive(child, stack, resolve); if (answer > result) result = answer; } }; // `Reflect.ownKeys` rather than `Object.keys`, so a cycle through a declared symbol key is still seen const shape = (sh, spread) => { let answer = NONE; for (const key of Reflect.ownKeys(sh)) { const desc = Object.getOwnPropertyDescriptor(sh, key); // an object resolves its shape by spread, so a key it does not enumerate is never parsed; `z.properties` reads every own key and so keeps them all if (spread && !desc.enumerable) continue; // resolving runs user code, and a factory mints a fresh subtree per read, so an edge the walk can't follow counts as a cycle const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE; if (child > answer) answer = child; } return answer; }; const merge = (answer) => { if (answer > result) result = answer; }; const def = inst._zod.def; const kind = def.type; switch (kind) { case "object": { const raw = util.rawShape(def); // a def with no raw shape answers `shape` from an accessor of its own, and running that can mint a whole fresh subtree merge(raw ? shape(raw, true) : ASSUMED); check(def.catchall); break; } case "array": check(def.element); break; case "tuple": for (const el of def.items) check(el); check(def.rest); break; case "record": case "map": check(def.keyType); check(def.valueType); break; case "set": check(def.valueType); break; case "union": for (const el of def.options) check(el); break; case "intersection": check(def.left); check(def.right); break; case "optional": case "nullable": case "default": case "prefault": case "catch": case "readonly": case "nonoptional": case "promise": case "success": check(def.innerType); break; case "pipe": check(def.in); check(def.out); break; case "function": check(def.input); check(def.output); break; // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly case "lazy": { const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : undefined); // walked with resolution off: one hop sees past the deferral, and a lazy that yields only another unresolved lazy is generative, so it stops there merge(inner ? isRecursive(inner, stack, false) : ASSUMED); break; } // a leaf by choice: `parts` are regex fragments, not data positions case "template_literal": // leaves case "string": case "number": case "int": case "boolean": case "bigint": case "symbol": case "undefined": case "null": case "void": case "never": case "any": case "unknown": case "date": case "nan": case "enum": case "literal": case "file": case "transform": case "custom": break; default: { // a new built-in kind becomes a compile error here kind; // a user-defined kind can still hold children, and only its author knows where, so fall back to scanning the def — skipping accessors, since reading one can run user code for (const key in def) { const desc = Object.getOwnPropertyDescriptor(def, key); if (!desc || desc.get) continue; const value = desc.value; if (!value || typeof value !== "object") continue; if (value._zod) check(value); else if (Array.isArray(value)) for (const el of value) check(el); } } } stack.delete(inst); return settle(inst, result); } /** An assumed answer must not outlive the resolution that settles it, so only a certain one is cached. */ function settle(inst, answer) { if (answer !== ASSUMED) recursive.set(inst, answer === PROVEN); return answer; } /** * Whether one parse can re-enter this schema, i.e. its subtree contains a cycle. * Exported for `z.compile`, which refuses to compile such a schema: cycle * breaking is driven from here off state keyed on the parse context, and a * generated fast path has no context to key on. */ function isRecursiveSchema(inst) { // z.compile never parses, so nothing would ever resolve a lazy for it; it runs once and already treats a throw here as recursive return isRecursive(inst, new Set(), true) !== NONE; } function bucketFor(state, inst) { let bucket = state.buckets.get(inst); if (!bucket) { bucket = new WeakMap(); state.buckets.set(inst, bucket); } return bucket; } // Set immediately before delegating to core and cleared immediately after, so `alloc` registers only for a visit this module is driving. let handoff; // Allocated but unfinished entries. `alloc` and the matching pop both happen in the synchronous part of a parse, so they nest even when children are async, and one stack serves every schema. const open = []; const memo = { alloc(_inst, payload, empty) { const bucket = handoff; if (!bucket) return empty; handoff = undefined; const entry = { value: empty, issues: null }; bucket.set(payload.value, entry); open.push(entry); return empty; }, guard(inst) { var _a; (_a = inst._zod).deferred ?? (_a.deferred = []); inst._zod.deferred.push(() => { const base = inst._zod.parse; const wrapped = (payload, ctx) => { // The value is a placeholder a back-edge is still waiting on, so the cycle closes through this transform. Its output can't exist in time to bind. if (ctx.direction !== "backward" && isBackEdge(ctx, payload.value)) throw new $ZodCyclicError(); return base(payload, ctx); }; inst._zod.parse = wrapped; if (inst._zod.run === base) inst._zod.run = wrapped; }); }, attach(inst) { var _a; let isRecursiveInst; let rechecked = false; // a recursive schema is re-entered many times per parse and its bucket never changes let lastCtx; let lastBucket; // Wraps `parse` in a deferred so it sees the container's final parse. Core's own deferred copies `parse` into `run` when there are no checks, and it ran first, so `run` is patched to match; with checks, `run` reads `parse` dynamically. (_a = inst._zod).deferred ?? (_a.deferred = []); inst._zod.deferred.push(() => { const base = inst._zod.parse; const wrapped = (payload, ctx) => { if (isRecursiveInst === undefined) { const walked = isRecursive(inst, new Set(), false); if (walked === NONE) { // Nothing here can ever fire, so take it back out. inst._zod.parse = base; if (inst._zod.run === wrapped) inst._zod.run = base; return base(payload, ctx); } // this parse resolves the deferred edges on its own path, so ask once more before latching if (walked === PROVEN || rechecked) isRecursiveInst = true; else rechecked = true; } const input = payload.value; if (!isRef(input)) return base(payload, ctx); let state = ctx[STATE]; if (!state) { state = { buckets: new WeakMap(), backEdges: undefined }; ctx[STATE] = state; } let bucket; if (lastCtx === ctx) { bucket = lastBucket; } else { bucket = bucketFor(state, inst); lastCtx = ctx; lastBucket = bucket; } const hit = bucket.get(input); if (hit) { payload.value = hit.value; if (hit.issues) { if (hit.issues.length) payload.issues.push(...cloneIssues(hit.issues)); } else { // Still being parsed: its own checks cover it, so skip them here. payload.memo = true; state.backEdges ?? (state.backEdges = new WeakSet()); state.backEdges.add(hit.value); } return payload; } handoff = bucket; const depth = open.length; const result = base(payload, ctx); handoff = undefined; // A container that rejected its input outright allocated nothing. const entry = open.length > depth ? open.pop() : undefined; // Both paths written out so the sync one allocates no closure. It runs once per node, and capturing here cost more than everything else combined. if (result instanceof Promise) { return result.then((r) => { if (entry) entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES; return r; }); } if (entry) entry.issues = result.issues.length ? cloneIssues(result.issues) : NO_ISSUES; return result; }; inst._zod.parse = wrapped; if (inst._zod.run === base) inst._zod.run = wrapped; }); }, }; /** The memoizer that gives containers cycle support. `zod` installs it by default; `zod/mini` opts in with `config({ memoizer: memoizer() })`. */ function memoizer() { return memo; } /** Whether this value is a node a back-edge resolved to before it finished. */ function isBackEdge(ctx, value) { const backEdges = ctx[STATE]?.backEdges; return backEdges !== undefined && isRef(value) && backEdges.has(value); } // seal-cjs-exports (function () { var keys = Object.getOwnPropertyNames(exports); for (var i = 0; i < keys.length; i++) { var desc = Object.getOwnPropertyDescriptor(exports, keys[i]); if (!desc || !desc.get || !desc.configurable) continue; var value; try { value = desc.get(); } catch (e) { continue; } // a circular require may not have settled this one yet, so leave it live if (value === undefined) continue; Object.defineProperty(exports, keys[i], { value: value, writable: false, enumerable: desc.enumerable, configurable: false }); } Object.freeze(exports); })();