diff options
author | Chris Swithinbank | 2024-02-23 23:36:35 +0100 |
---|---|---|
committer | GitHub | 2024-02-23 23:36:35 +0100 |
commit | b3b7a6069952d5f27a49b2fd097aa4db065e1718 (patch) | |
tree | 4fdf747cec4c55aef37662cd1ddd9ca9263c65b0 | |
parent | 1043052f3890a577a73276472f3773924909406b (diff) | |
download | IT.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.md | 5 | ||||
-rw-r--r-- | packages/starlight/__tests__/basics/config-errors.test.ts | 189 | ||||
-rw-r--r-- | packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts | 28 | ||||
-rw-r--r-- | packages/starlight/__tests__/basics/starlight-page-route-data.test.ts | 34 | ||||
-rw-r--r-- | packages/starlight/__tests__/snapshot-serializer-astro-error.ts | 22 | ||||
-rw-r--r-- | packages/starlight/__tests__/test-config.ts | 3 | ||||
-rw-r--r-- | packages/starlight/package.json | 4 | ||||
-rw-r--r-- | packages/starlight/utils/error-map.ts | 110 | ||||
-rw-r--r-- | packages/starlight/utils/plugins.ts | 43 | ||||
-rw-r--r-- | packages/starlight/utils/starlight-page.ts | 30 | ||||
-rw-r--r-- | packages/tailwind/package.json | 4 | ||||
-rw-r--r-- | pnpm-lock.yaml | 87 |
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 |