summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2024-02-23 23:36:35 +0100
committerGitHub2024-02-23 23:36:35 +0100
commitb3b7a6069952d5f27a49b2fd097aa4db065e1718 (patch)
tree4fdf747cec4c55aef37662cd1ddd9ca9263c65b0
parent1043052f3890a577a73276472f3773924909406b (diff)
downloadIT.starlight-b3b7a6069952d5f27a49b2fd097aa4db065e1718.tar.gz
IT.starlight-b3b7a6069952d5f27a49b2fd097aa4db065e1718.tar.bz2
IT.starlight-b3b7a6069952d5f27a49b2fd097aa4db065e1718.zip
Improve Zod errors (#1542)
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
-rw-r--r--.changeset/itchy-dots-act.md5
-rw-r--r--packages/starlight/__tests__/basics/config-errors.test.ts189
-rw-r--r--packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts28
-rw-r--r--packages/starlight/__tests__/basics/starlight-page-route-data.test.ts34
-rw-r--r--packages/starlight/__tests__/snapshot-serializer-astro-error.ts22
-rw-r--r--packages/starlight/__tests__/test-config.ts3
-rw-r--r--packages/starlight/package.json4
-rw-r--r--packages/starlight/utils/error-map.ts110
-rw-r--r--packages/starlight/utils/plugins.ts43
-rw-r--r--packages/starlight/utils/starlight-page.ts30
-rw-r--r--packages/tailwind/package.json4
-rw-r--r--pnpm-lock.yaml87
12 files changed, 414 insertions, 145 deletions
diff --git a/.changeset/itchy-dots-act.md b/.changeset/itchy-dots-act.md
new file mode 100644
index 00000000..2c5f0534
--- /dev/null
+++ b/.changeset/itchy-dots-act.md
@@ -0,0 +1,5 @@
+---
+"@astrojs/starlight": patch
+---
+
+Improves error messages shown by Starlight for configuration errors.
diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts
new file mode 100644
index 00000000..dae9b599
--- /dev/null
+++ b/packages/starlight/__tests__/basics/config-errors.test.ts
@@ -0,0 +1,189 @@
+import { expect, test } from 'vitest';
+import { parseWithFriendlyErrors } from '../../utils/error-map';
+import { StarlightConfigSchema, type StarlightUserConfig } from '../../utils/user-config';
+
+function parseStarlightConfigWithFriendlyErrors(config: StarlightUserConfig) {
+ return parseWithFriendlyErrors(
+ StarlightConfigSchema,
+ config,
+ 'Invalid config passed to starlight integration'
+ );
+}
+
+test('parses valid config successfully', () => {
+ const data = parseStarlightConfigWithFriendlyErrors({ title: '' });
+ expect(data).toMatchInlineSnapshot(`
+ {
+ "components": {
+ "Banner": "@astrojs/starlight/components/Banner.astro",
+ "ContentPanel": "@astrojs/starlight/components/ContentPanel.astro",
+ "EditLink": "@astrojs/starlight/components/EditLink.astro",
+ "FallbackContentNotice": "@astrojs/starlight/components/FallbackContentNotice.astro",
+ "Footer": "@astrojs/starlight/components/Footer.astro",
+ "Head": "@astrojs/starlight/components/Head.astro",
+ "Header": "@astrojs/starlight/components/Header.astro",
+ "Hero": "@astrojs/starlight/components/Hero.astro",
+ "LanguageSelect": "@astrojs/starlight/components/LanguageSelect.astro",
+ "LastUpdated": "@astrojs/starlight/components/LastUpdated.astro",
+ "MarkdownContent": "@astrojs/starlight/components/MarkdownContent.astro",
+ "MobileMenuFooter": "@astrojs/starlight/components/MobileMenuFooter.astro",
+ "MobileMenuToggle": "@astrojs/starlight/components/MobileMenuToggle.astro",
+ "MobileTableOfContents": "@astrojs/starlight/components/MobileTableOfContents.astro",
+ "PageFrame": "@astrojs/starlight/components/PageFrame.astro",
+ "PageSidebar": "@astrojs/starlight/components/PageSidebar.astro",
+ "PageTitle": "@astrojs/starlight/components/PageTitle.astro",
+ "Pagination": "@astrojs/starlight/components/Pagination.astro",
+ "Search": "@astrojs/starlight/components/Search.astro",
+ "Sidebar": "@astrojs/starlight/components/Sidebar.astro",
+ "SiteTitle": "@astrojs/starlight/components/SiteTitle.astro",
+ "SkipLink": "@astrojs/starlight/components/SkipLink.astro",
+ "SocialIcons": "@astrojs/starlight/components/SocialIcons.astro",
+ "TableOfContents": "@astrojs/starlight/components/TableOfContents.astro",
+ "ThemeProvider": "@astrojs/starlight/components/ThemeProvider.astro",
+ "ThemeSelect": "@astrojs/starlight/components/ThemeSelect.astro",
+ "TwoColumnContent": "@astrojs/starlight/components/TwoColumnContent.astro",
+ },
+ "customCss": [],
+ "defaultLocale": {
+ "dir": "ltr",
+ "label": "English",
+ "lang": "en",
+ "locale": undefined,
+ },
+ "disable404Route": false,
+ "editLink": {},
+ "favicon": {
+ "href": "/favicon.svg",
+ "type": "image/svg+xml",
+ },
+ "head": [],
+ "isMultilingual": false,
+ "lastUpdated": false,
+ "locales": undefined,
+ "pagefind": true,
+ "pagination": true,
+ "tableOfContents": {
+ "maxHeadingLevel": 3,
+ "minHeadingLevel": 2,
+ },
+ "title": "",
+ "titleDelimiter": "|",
+ }
+ `);
+});
+
+test('errors if title is missing', () => {
+ expect(() =>
+ parseStarlightConfigWithFriendlyErrors({} as any)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ "[AstroUserError]:
+ Invalid config passed to starlight integration
+ Hint:
+ **title**: Required"
+ `
+ );
+});
+
+test('errors if title value is not a string', () => {
+ expect(() =>
+ parseStarlightConfigWithFriendlyErrors({ title: 5 } as any)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ "[AstroUserError]:
+ Invalid config passed to starlight integration
+ Hint:
+ **title**: Expected type \`"string"\`, received \`"number"\`"
+ `
+ );
+});
+
+test('errors with bad social icon config', () => {
+ expect(() =>
+ parseStarlightConfigWithFriendlyErrors({ title: 'Test', social: { unknown: '' } as any })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ "[AstroUserError]:
+ Invalid config passed to starlight integration
+ Hint:
+ **social.unknown**: Invalid enum value. Expected 'twitter' | 'mastodon' | 'github' | 'gitlab' | 'bitbucket' | 'discord' | 'gitter' | 'codeberg' | 'codePen' | 'youtube' | 'threads' | 'linkedin' | 'twitch' | 'microsoftTeams' | 'instagram' | 'stackOverflow' | 'x.com' | 'telegram' | 'rss' | 'facebook' | 'email' | 'reddit' | 'patreon' | 'slack' | 'matrix' | 'openCollective', received 'unknown'
+ **social.unknown**: Invalid url"
+ `
+ );
+});
+
+test('errors with bad logo config', () => {
+ expect(() =>
+ parseStarlightConfigWithFriendlyErrors({ title: 'Test', logo: { html: '' } as any })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ "[AstroUserError]:
+ Invalid config passed to starlight integration
+ Hint:
+ **logo**: Did not match union.
+ > Expected type \`{ src: string } | { dark: string; light: string }\`
+ > Received \`{ "html": "" }\`"
+ `
+ );
+});
+
+test('errors with bad head config', () => {
+ expect(() =>
+ parseStarlightConfigWithFriendlyErrors({
+ title: 'Test',
+ head: [{ tag: 'unknown', attrs: { prop: null }, content: 20 } as any],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ "[AstroUserError]:
+ Invalid config passed to starlight integration
+ Hint:
+ **head.0.tag**: Invalid enum value. Expected 'title' | 'base' | 'link' | 'style' | 'meta' | 'script' | 'noscript' | 'template', received 'unknown'
+ **head.0.attrs.prop**: Did not match union.
+ > Expected type \`"string" | "boolean" | "undefined"\`, received \`"null"\`
+ **head.0.content**: Expected type \`"string"\`, received \`"number"\`"
+ `
+ );
+});
+
+test('errors with bad sidebar config', () => {
+ expect(() =>
+ parseStarlightConfigWithFriendlyErrors({
+ title: 'Test',
+ sidebar: [{ label: 'Example', href: '/' } as any],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ "[AstroUserError]:
+ Invalid config passed to starlight integration
+ Hint:
+ **sidebar.0**: Did not match union.
+ > Expected type \`{ link: string } | { items: array } | { autogenerate: object }\`
+ > Received \`{ "label": "Example", "href": "/" }\`"
+ `
+ );
+});
+
+test('errors with bad nested sidebar config', () => {
+ expect(() =>
+ parseStarlightConfigWithFriendlyErrors({
+ title: 'Test',
+ sidebar: [
+ {
+ label: 'Example',
+ items: [
+ { label: 'Nested Example 1', link: '/' },
+ { label: 'Nested Example 2', link: true },
+ ],
+ } as any,
+ ],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Invalid config passed to starlight integration
+ Hint:
+ **sidebar.0.items.1**: Did not match union.
+ > Expected type \`{ link: string } | { items: array } | { autogenerate: object }\`
+ > Received \`{ "label": "Example", "items": [ { "label": "Nested Example 1", "link": "/" }, { "label": "Nested Example 2", "link": true } ] }\`"
+ `);
+});
diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts
index 39e60955..3ab5b8f5 100644
--- a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts
+++ b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts
@@ -21,25 +21,19 @@ const starlightPageProps: StarlightPageProps = {
};
test('throws a validation error if a built-in field required by the user schema is not passed down', async () => {
- expect.assertions(3);
-
- try {
- await generateStarlightPageRouteData({
+ // The first line should be a user-friendly error message describing the exact issue and the second line should be
+ // the missing description field.
+ expect(() =>
+ generateStarlightPageRouteData({
props: starlightPageProps,
url: new URL('https://example.com/test-slug'),
- });
- } catch (error) {
- assert(error instanceof Error);
- const lines = error.message.split('\n');
- // The first line should be a user-friendly error message describing the exact issue and the second line should be
- // the missing description field.
- expect(lines).toHaveLength(2);
- const [message, missingField] = lines;
- expect(message).toMatchInlineSnapshot(
- `"Invalid frontmatter props passed to the \`<StarlightPage/>\` component."`
- );
- expect(missingField).toMatchInlineSnapshot(`"**description**: Required"`);
- }
+ })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Invalid frontmatter props passed to the \`<StarlightPage/>\` component.
+ Hint:
+ **description**: Required"
+ `);
});
test('returns new field defined in the user schema', async () => {
diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
index 064e261a..95f611e2 100644
--- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
+++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
@@ -225,8 +225,38 @@ test('throws error if sidebar is malformated', async () => {
url: starlightPageUrl,
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
- [Error: Invalid sidebar prop passed to the \`<StarlightPage/>\` component.
- **0**: Did not match union:]
+ "[AstroUserError]:
+ Invalid sidebar prop passed to the \`<StarlightPage/>\` component.
+ Hint:
+ **0**: Did not match union.
+ > Expected type \`{ href: string } | { entries: array }\`
+ > Received \`{ "label": "Custom link 1", "href": 5 }\`"
+ `);
+});
+
+test('throws error if sidebar uses wrong literal for entry type', async () => {
+ // This test also makes sure we show a helpful error for incorrect literals.
+ expect(() =>
+ generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ sidebar: [
+ {
+ //@ts-expect-error Intentionally bad type to cause error.
+ type: 'typo',
+ label: 'Custom link 1',
+ href: '/',
+ },
+ ],
+ },
+ url: starlightPageUrl,
+ })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Invalid sidebar prop passed to the \`<StarlightPage/>\` component.
+ Hint:
+ **0**: Did not match union.
+ > **0.type**: Expected \`"link" | "group"\`, received \`"typo"\`"
`);
});
diff --git a/packages/starlight/__tests__/snapshot-serializer-astro-error.ts b/packages/starlight/__tests__/snapshot-serializer-astro-error.ts
new file mode 100644
index 00000000..a9c64264
--- /dev/null
+++ b/packages/starlight/__tests__/snapshot-serializer-astro-error.ts
@@ -0,0 +1,22 @@
+import { AstroError } from 'astro/errors';
+import type { SnapshotSerializer } from 'vitest';
+
+export default {
+ /** Check if a value should be handled by this serializer, i.e. if it is an `AstroError`. */
+ test(val) {
+ return !!val && AstroError.is(val);
+ },
+ /** Customize serialization of Astro errors to include the `hint`. Vitest only uses `message` by default. */
+ serialize({ name, message, hint }: AstroError, config, indentation, depth, refs, printer) {
+ const prettyError = `[${name}]:\n${indent(message)}\nHint:\n${indent(hint)}`;
+ return printer(prettyError, config, indentation, depth, refs);
+ },
+} satisfies SnapshotSerializer;
+
+/** Indent each line in `string` with a given character. */
+function indent(string = '', indentation = '\t') {
+ return string
+ .split('\n')
+ .map((line) => indentation + line)
+ .join('\n');
+}
diff --git a/packages/starlight/__tests__/test-config.ts b/packages/starlight/__tests__/test-config.ts
index e0cfe701..1f3de592 100644
--- a/packages/starlight/__tests__/test-config.ts
+++ b/packages/starlight/__tests__/test-config.ts
@@ -23,5 +23,8 @@ export async function defineVitestConfig(
plugins: [
vitePluginStarlightUserConfig(starlightConfig, { root, srcDir, build, trailingSlash }),
],
+ test: {
+ snapshotSerializers: ['./snapshot-serializer-astro-error.ts'],
+ },
});
}
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index 78d89c8d..da3338d7 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -176,9 +176,9 @@
"devDependencies": {
"@astrojs/markdown-remark": "^4.2.1",
"@types/node": "^18.16.19",
- "@vitest/coverage-v8": "^1.2.2",
+ "@vitest/coverage-v8": "^1.3.1",
"astro": "^4.3.5",
- "vitest": "^1.2.2"
+ "vitest": "^1.3.1"
},
"dependencies": {
"@astrojs/mdx": "^2.1.1",
diff --git a/packages/starlight/utils/error-map.ts b/packages/starlight/utils/error-map.ts
index fd5c3660..45a4923a 100644
--- a/packages/starlight/utils/error-map.ts
+++ b/packages/starlight/utils/error-map.ts
@@ -3,6 +3,7 @@
* source: https://github.com/withastro/astro/blob/main/packages/astro/src/content/error-map.ts
*/
+import { AstroError } from 'astro/errors';
import type { z } from 'astro:content';
type TypeOrLiteralErrByPathEntry = {
@@ -11,11 +12,27 @@ type TypeOrLiteralErrByPathEntry = {
expected: unknown[];
};
-export function throwValidationError(error: z.ZodError, message: string): never {
- throw new Error(`${message}\n${error.issues.map((i) => i.message).join('\n')}`);
+/**
+ * Parse data with a Zod schema and throw a nicely formatted error if it is invalid.
+ *
+ * @param schema The Zod schema to use to parse the input.
+ * @param input Input data that should match the schema.
+ * @param message Error message preamble to use if the input fails to parse.
+ * @returns Validated data parsed by Zod.
+ */
+export function parseWithFriendlyErrors<T extends z.Schema>(
+ schema: T,
+ input: z.input<T>,
+ message: string
+): z.output<T> {
+ const parsedConfig = schema.safeParse(input, { errorMap });
+ if (!parsedConfig.success) {
+ throw new AstroError(message, parsedConfig.error.issues.map((i) => i.message).join('\n'));
+ }
+ return parsedConfig.data;
}
-export const errorMap: z.ZodErrorMap = (baseError, ctx) => {
+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
@@ -38,30 +55,51 @@ export const errorMap: z.ZodErrorMap = (baseError, ctx) => {
}
}
}
- let messages: string[] = [
- prefix(
- baseErrorPath,
- typeOrLiteralErrByPath.size ? 'Did not match union:' : 'Did not match union.'
- ),
- ];
+ const messages: string[] = [prefix(baseErrorPath, 'Did not match union.')];
+ const details: string[] = [...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))}`
+ );
+
+ if (details.length === 0) {
+ const expectedShapes: string[] = [];
+ for (const unionError of baseError.unionErrors) {
+ const expectedShape: string[] = [];
+ for (const issue of unionError.issues) {
+ // 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, ctx);
+ }
+ const relativePath = flattenErrorPath(issue.path)
+ .replace(baseErrorPath, '')
+ .replace(leadingPeriod, '');
+ if ('expected' in issue && typeof issue.expected === 'string') {
+ expectedShape.push(
+ relativePath ? `${relativePath}: ${issue.expected}` : issue.expected
+ );
+ } else {
+ expectedShape.push(relativePath);
+ }
+ }
+ expectedShapes.push(`{ ${expectedShape.join('; ')} }`);
+ }
+ if (expectedShapes.length) {
+ details.push('> Expected type `' + expectedShapes.join(' | ') + '`');
+ details.push('> Received `' + stringify(ctx.data) + '`');
+ }
+ }
+
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'),
+ message: messages.concat(details).join('\n'),
};
- }
- if (baseError.code === 'invalid_literal' || baseError.code === 'invalid_type') {
+ } else if (baseError.code === 'invalid_literal' || baseError.code === 'invalid_type') {
return {
message: prefix(
baseErrorPath,
@@ -84,25 +122,25 @@ const getTypeOrLiteralMsg = (error: TypeOrLiteralErrByPathEntry): string => {
const expectedDeduped = new Set(error.expected);
switch (error.code) {
case 'invalid_type':
- return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify(
+ return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received \`${stringify(
error.received
- )}`;
+ )}\``;
case 'invalid_literal':
- return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify(
+ return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received \`${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('');
+ [...expectedVals].map((expectedVal) => stringify(expectedVal)).join(' | ');
const flattenErrorPath = (errorPath: (string | number)[]) => errorPath.join('.');
+
+/** `JSON.stringify()` a value with spaces around object/array entries. */
+const stringify = (val: unknown) =>
+ JSON.stringify(val, null, 1).split(newlinePlusWhitespace).join(' ');
+const newlinePlusWhitespace = /\n\s*/;
+const leadingPeriod = /^\./;
diff --git a/packages/starlight/utils/plugins.ts b/packages/starlight/utils/plugins.ts
index bca72087..94f07db2 100644
--- a/packages/starlight/utils/plugins.ts
+++ b/packages/starlight/utils/plugins.ts
@@ -1,7 +1,7 @@
import type { AstroIntegration } from 'astro';
import { z } from 'astro/zod';
import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
-import { errorMap, throwValidationError } from '../utils/error-map';
+import { parseWithFriendlyErrors } from '../utils/error-map';
/**
* Runs Starlight plugins in the order that they are configured after validating the user-provided
@@ -15,23 +15,19 @@ export async function runPlugins(
) {
// Validate the user-provided configuration.
let userConfig = starlightUserConfig;
- let starlightConfig = StarlightConfigSchema.safeParse(userConfig, { errorMap });
- if (!starlightConfig.success) {
- throwValidationError(starlightConfig.error, 'Invalid config passed to starlight integration');
- }
+ let starlightConfig = parseWithFriendlyErrors(
+ StarlightConfigSchema,
+ userConfig,
+ 'Invalid config passed to starlight integration'
+ );
// Validate the user-provided plugins configuration.
- const pluginsConfig = starlightPluginsConfigSchema.safeParse(pluginsUserConfig, {
- errorMap,
- });
-
- if (!pluginsConfig.success) {
- throwValidationError(
- pluginsConfig.error,
- 'Invalid plugins config passed to starlight integration'
- );
- }
+ const pluginsConfig = parseWithFriendlyErrors(
+ starlightPluginsConfigSchema,
+ pluginsUserConfig,
+ 'Invalid plugins config passed to starlight integration'
+ );
// A list of Astro integrations added by the various plugins.
const integrations: AstroIntegration[] = [];
@@ -39,7 +35,7 @@ export async function runPlugins(
for (const {
name,
hooks: { setup },
- } of pluginsConfig.data) {
+ } of pluginsConfig) {
await setup({
config: pluginsUserConfig ? { ...userConfig, plugins: pluginsUserConfig } : userConfig,
updateConfig(newConfig) {
@@ -52,14 +48,11 @@ export async function runPlugins(
// If the plugin is updating the user config, re-validate it.
const mergedUserConfig = { ...userConfig, ...newConfig };
- const mergedConfig = StarlightConfigSchema.safeParse(mergedUserConfig, { errorMap });
-
- if (!mergedConfig.success) {
- throwValidationError(
- mergedConfig.error,
- `Invalid config update provided by the '${name}' plugin`
- );
- }
+ const mergedConfig = parseWithFriendlyErrors(
+ StarlightConfigSchema,
+ mergedUserConfig,
+ `Invalid config update provided by the '${name}' plugin`
+ );
// If the updated config is valid, keep track of both the user config and parsed config.
userConfig = mergedUserConfig;
@@ -79,7 +72,7 @@ export async function runPlugins(
});
}
- return { integrations, starlightConfig: starlightConfig.data };
+ return { integrations, starlightConfig };
}
// https://github.com/withastro/astro/blob/910eb00fe0b70ca80bd09520ae100e8c78b675b5/packages/astro/src/core/config/schema.ts#L113
diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts
index 5bdb4e9d..88fbef87 100644
--- a/packages/starlight/utils/starlight-page.ts
+++ b/packages/starlight/utils/starlight-page.ts
@@ -1,7 +1,7 @@
import { z } from 'astro/zod';
import { type ContentConfig, type SchemaContext } from 'astro:content';
import config from 'virtual:starlight/user-config';
-import { errorMap, throwValidationError } from './error-map';
+import { parseWithFriendlyErrors } from './error-map';
import { stripLeadingAndTrailingSlashes } from './path';
import { getToC, type PageProps, type StarlightRouteData } from './route-data';
import type { StarlightDocsEntry } from './routing';
@@ -138,14 +138,11 @@ type StarlightPageSidebarUserConfig = z.input<typeof StarlightPageSidebarSchema>
const normalizeSidebarProp = (
sidebarProp: StarlightPageSidebarUserConfig
): StarlightRouteData['sidebar'] => {
- const sidebar = StarlightPageSidebarSchema.safeParse(sidebarProp, { errorMap });
- if (!sidebar.success) {
- throwValidationError(
- sidebar.error,
- 'Invalid sidebar prop passed to the `<StarlightPage/>` component.'
- );
- }
- return sidebar.data;
+ return parseWithFriendlyErrors(
+ StarlightPageSidebarSchema,
+ sidebarProp,
+ 'Invalid sidebar prop passed to the `<StarlightPage/>` component.'
+ );
};
/**
@@ -267,16 +264,11 @@ async function getStarlightPageFrontmatter(frontmatter: StarlightPageFrontmatter
}),
});
- const pageFrontmatter = schema.safeParse(frontmatter, { errorMap });
-
- if (!pageFrontmatter.success) {
- throwValidationError(
- pageFrontmatter.error,
- 'Invalid frontmatter props passed to the `<StarlightPage/>` component.'
- );
- }
-
- return pageFrontmatter.data;
+ return parseWithFriendlyErrors(
+ schema,
+ frontmatter,
+ 'Invalid frontmatter props passed to the `<StarlightPage/>` component.'
+ );
}
/** Returns the user docs schema and falls back to the default schema if needed. */
diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json
index 275b7ca7..a143a103 100644
--- a/packages/tailwind/package.json
+++ b/packages/tailwind/package.json
@@ -23,9 +23,9 @@
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
- "@vitest/coverage-v8": "^1.2.2",
+ "@vitest/coverage-v8": "^1.3.1",
"postcss": "^8.4.33",
- "vitest": "^1.2.2"
+ "vitest": "^1.3.1"
},
"peerDependencies": {
"@astrojs/starlight": ">=0.9.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 089fcb0c..ea420609 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -191,14 +191,14 @@ importers:
specifier: ^18.16.19
version: 18.16.19
'@vitest/coverage-v8':
- specifier: ^1.2.2
- version: 1.2.2(vitest@1.2.2)
+ specifier: ^1.3.1
+ version: 1.3.1(vitest@1.3.1)
astro:
specifier: ^4.3.5
version: 4.3.5(@types/node@18.16.19)
vitest:
- specifier: ^1.2.2
- version: 1.2.2(@types/node@18.16.19)
+ specifier: ^1.3.1
+ version: 1.3.1(@types/node@18.16.19)
packages/tailwind:
dependencies:
@@ -213,14 +213,14 @@ importers:
version: 3.4.1
devDependencies:
'@vitest/coverage-v8':
- specifier: ^1.2.2
- version: 1.2.2(vitest@1.2.2)
+ specifier: ^1.3.1
+ version: 1.3.1(vitest@1.3.1)
postcss:
specifier: ^8.4.33
version: 8.4.33
vitest:
- specifier: ^1.2.2
- version: 1.2.2(@types/node@18.16.19)
+ specifier: ^1.3.1
+ version: 1.3.1(@types/node@18.16.19)
packages:
@@ -1647,10 +1647,10 @@ packages:
/@ungap/structured-clone@1.2.0:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
- /@vitest/coverage-v8@1.2.2(vitest@1.2.2):
- resolution: {integrity: sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==}
+ /@vitest/coverage-v8@1.3.1(vitest@1.3.1):
+ resolution: {integrity: sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==}
peerDependencies:
- vitest: ^1.0.0
+ vitest: 1.3.1
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
@@ -1665,43 +1665,43 @@ packages:
std-env: 3.7.0
test-exclude: 6.0.0
v8-to-istanbul: 9.2.0
- vitest: 1.2.2(@types/node@18.16.19)
+ vitest: 1.3.1(@types/node@18.16.19)
transitivePeerDependencies:
- supports-color
dev: true
- /@vitest/expect@1.2.2:
- resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==}
+ /@vitest/expect@1.3.1:
+ resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==}
dependencies:
- '@vitest/spy': 1.2.2
- '@vitest/utils': 1.2.2
+ '@vitest/spy': 1.3.1
+ '@vitest/utils': 1.3.1
chai: 4.4.1
dev: true
- /@vitest/runner@1.2.2:
- resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==}
+ /@vitest/runner@1.3.1:
+ resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==}
dependencies:
- '@vitest/utils': 1.2.2
+ '@vitest/utils': 1.3.1
p-limit: 5.0.0
pathe: 1.1.2
dev: true
- /@vitest/snapshot@1.2.2:
- resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==}
+ /@vitest/snapshot@1.3.1:
+ resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==}
dependencies:
magic-string: 0.30.5
pathe: 1.1.2
pretty-format: 29.7.0
dev: true
- /@vitest/spy@1.2.2:
- resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==}
+ /@vitest/spy@1.3.1:
+ resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==}
dependencies:
tinyspy: 2.2.0
dev: true
- /@vitest/utils@1.2.2:
- resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==}
+ /@vitest/utils@1.3.1:
+ resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==}
dependencies:
diff-sequences: 29.6.3
estree-walker: 3.0.3
@@ -4012,6 +4012,10 @@ packages:
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ /js-tokens@8.0.3:
+ resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==}
+ dev: true
+
/js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
@@ -6314,10 +6318,10 @@ packages:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
- /strip-literal@1.3.0:
- resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==}
+ /strip-literal@2.0.0:
+ resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==}
dependencies:
- acorn: 8.11.3
+ js-tokens: 8.0.3
dev: true
/style-to-object@0.4.1:
@@ -6798,8 +6802,8 @@ packages:
unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2
- /vite-node@1.2.2(@types/node@18.16.19):
- resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
+ /vite-node@1.3.1(@types/node@18.16.19):
+ resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
dependencies:
@@ -6864,15 +6868,15 @@ packages:
dependencies:
vite: 5.0.12(@types/node@18.16.19)
- /vitest@1.2.2(@types/node@18.16.19):
- resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
+ /vitest@1.3.1(@types/node@18.16.19):
+ resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/node': ^18.0.0 || >=20.0.0
- '@vitest/browser': ^1.0.0
- '@vitest/ui': ^1.0.0
+ '@vitest/browser': 1.3.1
+ '@vitest/ui': 1.3.1
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
@@ -6890,13 +6894,12 @@ packages:
optional: true
dependencies:
'@types/node': 18.16.19
- '@vitest/expect': 1.2.2
- '@vitest/runner': 1.2.2
- '@vitest/snapshot': 1.2.2
- '@vitest/spy': 1.2.2
- '@vitest/utils': 1.2.2
+ '@vitest/expect': 1.3.1
+ '@vitest/runner': 1.3.1
+ '@vitest/snapshot': 1.3.1
+ '@vitest/spy': 1.3.1
+ '@vitest/utils': 1.3.1
acorn-walk: 8.3.2
- cac: 6.7.14
chai: 4.4.1
debug: 4.3.4
execa: 8.0.1
@@ -6905,11 +6908,11 @@ packages:
pathe: 1.1.2
picocolors: 1.0.0
std-env: 3.7.0
- strip-literal: 1.3.0
+ strip-literal: 2.0.0
tinybench: 2.6.0
tinypool: 0.8.2
vite: 5.0.12(@types/node@18.16.19)
- vite-node: 1.2.2(@types/node@18.16.19)
+ vite-node: 1.3.1(@types/node@18.16.19)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less