summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorReuben Tier2023-05-30 23:44:36 +0100
committerGitHub2023-05-31 00:44:36 +0200
commit6a2c0df4d5586b70b46c854061df67b028e73630 (patch)
tree7af8cae6bd6c02b441d71e135c98eaf647149989
parentfe0a9b3fb12db06ddcbc01eae629f5837409ada3 (diff)
downloadIT.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.md5
-rw-r--r--packages/starlight/index.ts12
-rw-r--r--packages/starlight/utils/error-map.ts104
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('.');