UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

125 lines (124 loc) • 6.08 kB
export const errorMap = (issue) => { const baseErrorPath = flattenErrorPath(issue.path ?? []); if (issue.code === "invalid_union") { // Optimization: Combine type and literal errors for keys that are common across ALL union types // Ex. a union between `{ key: z.literal('tutorial') }` and `{ key: z.literal('blog') }` will // raise a single error when `key` does not match: // > Did not match union. // > key: Expected `'tutorial' | 'blog'`, received 'foo' let typeOrLiteralErrByPath = new Map(); for (const unionError of issue.errors.flat()) { if (unionError.code === "invalid_type") { const flattenedErrorPath = flattenErrorPath(unionError.path); if (typeOrLiteralErrByPath.has(flattenedErrorPath)) { typeOrLiteralErrByPath.get(flattenedErrorPath).expected.push(unionError.expected); } else { typeOrLiteralErrByPath.set(flattenedErrorPath, { code: unionError.code, received: unionError.received, expected: [unionError.expected], message: unionError.message }); } } } const messages = [prefix(baseErrorPath, "Did not match union.")]; const details = [...typeOrLiteralErrByPath.entries()].filter(([, error]) => error.expected.length === issue.errors.flat().length).map(([key, error]) => key === baseErrorPath ? `> ${getTypeOrLiteralMsg(error)}` : `> ${prefix(key, getTypeOrLiteralMsg(error))}`); if (details.length === 0) { // For discriminated unions, show the expected discriminator options if ("discriminator" in issue && issue.discriminator && "options" in issue) { const options = issue.options; if (Array.isArray(options)) { details.push(`> Expected \`${issue.discriminator}\` to be ${options.map((o) => `\`${stringify(o)}\``).join(" | ")}`); details.push("> Received `" + stringify(issue.input) + "`"); } } } if (details.length === 0) { const expectedShapes = []; for (const unionErrors of issue.errors) { const expectedShape = []; for (const _issue of unionErrors) { // If the issue is a nested union error, show the associated error message instead of the // base error message. if (_issue.code === "invalid_union") { return errorMap(_issue); } const relativePath = flattenErrorPath(_issue.path).replace(baseErrorPath, "").replace(leadingPeriod, ""); if (_issue.code === "custom" && _issue.message && _issue.message.includes("security.csp")) { expectedShape.push(_issue.message); } else if ("expected" in _issue && typeof _issue.expected === "string") { expectedShape.push(relativePath ? `${relativePath}: ${_issue.expected}` : _issue.expected); } else if ("values" in _issue) { expectedShape.push(..._issue.values.filter((v) => typeof v === "string").map((v) => `"${v}"`)); } else if (relativePath) { expectedShape.push(relativePath); } } if (expectedShape.length === 1 && !expectedShape[0]?.includes(":")) { // In this case the expected shape is not an object, but probably a literal type, e.g. `['string']`. expectedShapes.push(expectedShape.join("")); } else if (expectedShape.length > 0) { expectedShapes.push(`{ ${expectedShape.join("; ")} }`); } } if (expectedShapes.length) { details.push("> Expected type `" + expectedShapes.join(" | ") + "`"); details.push("> Received `" + stringify(issue.input) + "`"); } } return { message: messages.concat(details).join("\n") }; } else if (issue.code === "invalid_key") { // Zod 4.4.0+: Record key validation failures are now structured `invalid_key` issues // with nested `issues` array containing the actual validation errors. const keyIssues = issue.issues; if (Array.isArray(keyIssues) && keyIssues.length > 0) { const firstIssue = keyIssues[0]; const msg = firstIssue.message || "Invalid key in record"; return { message: prefix(baseErrorPath, msg) }; } return { message: prefix(baseErrorPath, "Invalid key in record") }; } else if (issue.code === "invalid_element") { // Zod 4.4.0+: Map/Set element validation failures are now structured `invalid_element` issues. const elementIssues = issue.issues; if (Array.isArray(elementIssues) && elementIssues.length > 0) { const firstIssue = elementIssues[0]; const msg = firstIssue.message || "Invalid element"; return { message: prefix(baseErrorPath, msg) }; } return { message: prefix(baseErrorPath, "Invalid element") }; } else if (issue.code === "invalid_type") { return { message: prefix(baseErrorPath, getTypeOrLiteralMsg({ code: issue.code, received: typeof issue.input, expected: [issue.expected], message: issue.message })) }; } else if (issue.message) { return { message: prefix(baseErrorPath, issue.message) }; } }; const getTypeOrLiteralMsg = (error) => { // received could be `undefined` or the string `'undefined'` if (typeof error.received === "undefined" || error.received === "undefined") return error.message ?? "Required"; const expectedDeduped = new Set(error.expected); switch (error.code) { case "invalid_type": return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received \`${stringify(error.received)}\``; case "invalid_literal": return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received \`${stringify(error.received)}\``; } }; const prefix = (key, msg) => key.length ? `**${key}**: ${msg}` : msg; const unionExpectedVals = (expectedVals) => [...expectedVals].map((expectedVal) => stringify(expectedVal)).join(" | "); const flattenErrorPath = (errorPath) => errorPath.join("."); /** `JSON.stringify()` a value with spaces around object/array entries. */ const stringify = (val) => { // `JSON.stringify` returns `undefined` for `undefined`/functions/symbols. This can happen for // nested union issues where `issue.input` is not populated, so fall back to `String()`. const json = JSON.stringify(val, null, 1); if (json === undefined) { return String(val); } return json.split(newlinePlusWhitespace).join(" "); }; const newlinePlusWhitespace = /\n\s*/; const leadingPeriod = /^\./;