summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2025-04-16 11:28:11 +0200
committerGitHub2025-04-16 11:28:11 +0200
commit77a110461dffacd1d3ee3b8934fd48b20111f3c4 (patch)
treef6d7d3ce9c3c94cdf0f91a60ddad5a04f75bd818
parent3a087d8fbcd946336f8a0423203967e53e5678fe (diff)
downloadIT.starlight-77a110461dffacd1d3ee3b8934fd48b20111f3c4.tar.gz
IT.starlight-77a110461dffacd1d3ee3b8934fd48b20111f3c4.tar.bz2
IT.starlight-77a110461dffacd1d3ee3b8934fd48b20111f3c4.zip
Fix image metadata validation in StarlightPage schema (#3118)
-rw-r--r--.changeset/warm-adults-wash.md5
-rw-r--r--packages/starlight/__tests__/basics/starlight-page-route-data.test.ts91
-rw-r--r--packages/starlight/utils/starlight-page.ts35
3 files changed, 110 insertions, 21 deletions
diff --git a/.changeset/warm-adults-wash.md b/.changeset/warm-adults-wash.md
new file mode 100644
index 00000000..15bf8ddb
--- /dev/null
+++ b/.changeset/warm-adults-wash.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': patch
+---
+
+Fixes passing imported SVGs to the `frontmatter` prop of the `<StarlightPage>` component in Astro ≥5.7.0
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 950eeab4..2c5f5363 100644
--- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
+++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
@@ -1,11 +1,12 @@
+import type { ImageMetadata } from 'astro';
import { expect, test, vi } from 'vitest';
-import { getRouteDataTestContext } from '../test-utils';
-import { generateRouteData } from '../../utils/routing/data';
import { routes } from '../../utils/routing';
+import { generateRouteData } from '../../utils/routing/data';
import {
generateStarlightPageRouteData,
type StarlightPageProps,
} from '../../utils/starlight-page';
+import { getRouteDataTestContext } from '../test-utils';
vi.mock('virtual:starlight/collection-config', async () =>
(await import('../test-utils')).mockedCollectionConfig()
@@ -523,3 +524,89 @@ test('generates data with a similar root shape to regular route data', async ()
expect(Object.keys(data).sort()).toEqual(Object.keys(starlightPageData).sort());
});
+
+test('parses an ImageMetadata object successfully', async () => {
+ const fakeImportedImage: ImageMetadata = {
+ src: '/image-src.png',
+ width: 100,
+ height: 100,
+ format: 'png',
+ };
+ const data = await generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ frontmatter: {
+ ...starlightPageProps.frontmatter,
+ hero: {
+ image: { file: fakeImportedImage },
+ },
+ },
+ },
+ context: getRouteDataTestContext(starlightPagePathname),
+ });
+ expect(data.entry.data.hero?.image).toBeDefined();
+ // @ts-expect-error — image’s type can be different shapes but we know it’s this one here
+ expect(data.entry.data.hero?.image!['file']).toMatchInlineSnapshot(`
+ {
+ "format": "png",
+ "height": 100,
+ "src": "/image-src.png",
+ "width": 100,
+ }
+ `);
+});
+
+test('parses an image that is also a function successfully', async () => {
+ const fakeImportedSvg = (() => {}) as unknown as ImageMetadata;
+ Object.assign(fakeImportedSvg, { src: '/image-src.svg', width: 100, height: 100, format: 'svg' });
+ const data = await generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ frontmatter: {
+ ...starlightPageProps.frontmatter,
+ hero: {
+ image: { file: fakeImportedSvg },
+ },
+ },
+ },
+ context: getRouteDataTestContext(starlightPagePathname),
+ });
+ expect(data.entry.data.hero?.image).toBeDefined();
+ // @ts-expect-error — image’s type can be different shapes but we know it’s this one here
+ expect(data.entry.data.hero?.image!['file']).toMatchInlineSnapshot(`[Function]`);
+ // @ts-expect-error
+ expect(data.entry.data.hero?.image!['file']).toHaveProperty('src');
+ // @ts-expect-error
+ expect(data.entry.data.hero?.image!['file']).toHaveProperty('width');
+ // @ts-expect-error
+ expect(data.entry.data.hero?.image!['file']).toHaveProperty('height');
+ // @ts-expect-error
+ expect(data.entry.data.hero?.image!['file']).toHaveProperty('format');
+});
+
+test('fails to parse an image without the expected metadata properties', async () => {
+ await expect(() =>
+ generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ frontmatter: {
+ ...starlightPageProps.frontmatter,
+ hero: {
+ image: {
+ // @ts-expect-error intentionally incorrect input
+ file: () => {},
+ },
+ },
+ },
+ },
+ context: getRouteDataTestContext(starlightPagePathname),
+ })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Invalid frontmatter props passed to the \`<StarlightPage/>\` component.
+ Hint:
+ **hero.image**: Did not match union.
+ > Expected type \`file | { dark; light } | { html: string }\`
+ > Received \`{}\`"
+ `);
+});
diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts
index 7a297bf3..01fe7dd7 100644
--- a/packages/starlight/utils/starlight-page.ts
+++ b/packages/starlight/utils/starlight-page.ts
@@ -1,5 +1,5 @@
import { z } from 'astro/zod';
-import { type ContentConfig, type SchemaContext } from 'astro:content';
+import { type ContentConfig, type ImageFunction, type SchemaContext } from 'astro:content';
import project from 'virtual:starlight/project-context';
import config from 'virtual:starlight/user-config';
import { getCollectionPathFromRoot } from './collection';
@@ -179,25 +179,22 @@ export async function generateStarlightPageRouteData({
/** Validates the Starlight page frontmatter properties from the props received by a Starlight page. */
async function getStarlightPageFrontmatter(frontmatter: StarlightPageFrontmatter) {
- // This needs to be in sync with ImageMetadata.
- // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32
const schema = await StarlightPageFrontmatterSchema({
- image: () =>
- z.object({
- src: z.string(),
- width: z.number(),
- height: z.number(),
- format: z.union([
- z.literal('png'),
- z.literal('jpg'),
- z.literal('jpeg'),
- z.literal('tiff'),
- z.literal('webp'),
- z.literal('gif'),
- z.literal('svg'),
- z.literal('avif'),
- ]),
- }),
+ image: (() =>
+ // Mock validator for ImageMetadata.
+ // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32
+ // It uses a custom validation approach because imported SVGs have a type of `function` as
+ // well as containing the metadata properties and this ensures we handle those correctly.
+ z.custom(
+ (value) =>
+ value &&
+ (typeof value === 'function' || typeof value === 'object') &&
+ 'src' in value &&
+ 'width' in value &&
+ 'height' in value &&
+ 'format' in value,
+ 'Invalid image passed to `<StarlightPage>` component. Expected imported `ImageMetadata` object.'
+ )) as ImageFunction,
});
// Starting with Astro 4.14.0, a frontmatter schema that contains collection references will