diff options
author | Reuben Tier | 2023-05-30 23:44:36 +0100 |
---|---|---|
committer | GitHub | 2023-05-31 00:44:36 +0200 |
commit | 6a2c0df4d5586b70b46c854061df67b028e73630 (patch) | |
tree | 7af8cae6bd6c02b441d71e135c98eaf647149989 | |
parent | fe0a9b3fb12db06ddcbc01eae629f5837409ada3 (diff) | |
download | IT.starlight-6a2c0df4d5586b70b46c854061df67b028e73630.tar.gz IT.starlight-6a2c0df4d5586b70b46c854061df67b028e73630.tar.bz2 IT.starlight-6a2c0df4d5586b70b46c854061df67b028e73630.zip |
Add ZodErrorMap (#101)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r-- | .changeset/hot-dragons-sin.md | 5 | ||||
-rw-r--r-- | packages/starlight/index.ts | 12 | ||||
-rw-r--r-- | packages/starlight/utils/error-map.ts | 104 |
3 files changed, 120 insertions, 1 deletions
diff --git a/.changeset/hot-dragons-sin.md b/.changeset/hot-dragons-sin.md new file mode 100644 index 00000000..c144df2c --- /dev/null +++ b/.changeset/hot-dragons-sin.md @@ -0,0 +1,5 @@ +--- +"@astrojs/starlight": patch +--- + +Add better error messages for starlight config diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts index e3a5f3bf..c110f9cd 100644 --- a/packages/starlight/index.ts +++ b/packages/starlight/index.ts @@ -15,11 +15,21 @@ import { StarlightConfig, StarlightConfigSchema, } from './utils/user-config'; +import { errorMap } from './utils/error-map'; export default function StarlightIntegration( opts: StarlightUserConfig ): AstroIntegration[] { - const userConfig = StarlightConfigSchema.parse(opts); + const parsedConfig = StarlightConfigSchema.safeParse(opts, { errorMap }); + + if (!parsedConfig.success) { + throw new Error( + 'Invalid config passed to starlight integration\n' + + parsedConfig.error.issues.map((i) => i.message).join('\n') + ); + } + + const userConfig = parsedConfig.data; const Starlight: AstroIntegration = { name: '@astrojs/starlight', diff --git a/packages/starlight/utils/error-map.ts b/packages/starlight/utils/error-map.ts new file mode 100644 index 00000000..d1006d35 --- /dev/null +++ b/packages/starlight/utils/error-map.ts @@ -0,0 +1,104 @@ +/** + * This is a modified version of Astro's error map. + * source: https://github.com/withastro/astro/blob/main/packages/astro/src/content/error-map.ts + */ + +import type { z } from 'astro:content'; + +type TypeOrLiteralErrByPathEntry = { + code: 'invalid_type' | 'invalid_literal'; + received: unknown; + expected: unknown[]; +}; + +export const errorMap: z.ZodErrorMap = (baseError, ctx) => { + const baseErrorPath = flattenErrorPath(baseError.path); + if (baseError.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: Map<string, TypeOrLiteralErrByPathEntry> = new Map(); + for (const unionError of baseError.unionErrors.map((e) => e.errors).flat()) { + if (unionError.code === 'invalid_type' || unionError.code === 'invalid_literal') { + 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 as any).received, + expected: [unionError.expected], + }); + } + } + } + let messages: string[] = [ + prefix( + baseErrorPath, + typeOrLiteralErrByPath.size ? 'Did not match union:' : 'Did not match union.' + ), + ]; + return { + message: messages + .concat( + [...typeOrLiteralErrByPath.entries()] + // If type or literal error isn't common to ALL union types, + // filter it out. Can lead to confusing noise. + .filter(([, error]) => error.expected.length === baseError.unionErrors.length) + .map(([key, error]) => + key === baseErrorPath + ? // Avoid printing the key again if it's a base error + `> ${getTypeOrLiteralMsg(error)}` + : `> ${prefix(key, getTypeOrLiteralMsg(error))}` + ) + ) + .join('\n'), + }; + } + if (baseError.code === 'invalid_literal' || baseError.code === 'invalid_type') { + return { + message: prefix( + baseErrorPath, + getTypeOrLiteralMsg({ + code: baseError.code, + received: (baseError as any).received, + expected: [baseError.expected], + }) + ), + }; + } else if (baseError.message) { + return { message: prefix(baseErrorPath, baseError.message) }; + } else { + return { message: prefix(baseErrorPath, ctx.defaultError) }; + } +}; + +const getTypeOrLiteralMsg = (error: TypeOrLiteralErrByPathEntry): string => { + if (error.received === 'undefined') return 'Required'; + const expectedDeduped = new Set(error.expected); + switch (error.code) { + case 'invalid_type': + return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify( + error.received + )}`; + case 'invalid_literal': + return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify( + error.received + )}`; + } +}; + +const prefix = (key: string, msg: string) => (key.length ? `**${key}**: ${msg}` : msg); + +const unionExpectedVals = (expectedVals: Set<unknown>) => + [...expectedVals] + .map((expectedVal, idx) => { + if (idx === 0) return JSON.stringify(expectedVal); + const sep = ' | '; + return `${sep}${JSON.stringify(expectedVal)}`; + }) + .join(''); + +const flattenErrorPath = (errorPath: (string | number)[]) => errorPath.join('.'); |