Skip to content

Commit daf41c6

Browse files
Astro 6 astrojs/rss fix for Zod 4 (#15283)
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
1 parent 8983f17 commit daf41c6

File tree

7 files changed

+25
-39
lines changed

7 files changed

+25
-39
lines changed

.changeset/solid-sloths-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/rss': patch
3+
---
4+
5+
Updates validation to use Zod v4

packages/astro-rss/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@
2828
},
2929
"devDependencies": {
3030
"@types/xml2js": "^0.4.14",
31-
"astro": "workspace:*",
3231
"astro-scripts": "workspace:*",
33-
"zod": "^3.25.76",
3432
"xml2js": "0.6.2"
3533
},
3634
"dependencies": {
3735
"fast-xml-parser": "^5.3.3",
38-
"piccolore": "^0.1.3"
36+
"piccolore": "^0.1.3",
37+
"zod": "^4.0.0"
3938
}
4039
}

packages/astro-rss/src/index.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { z } from 'zod/v3';
1+
import { z } from 'zod';
22
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
33
import colors from 'piccolore';
44
import { rssSchema } from './schema.js';
5-
import { createCanonicalURL, errorMap, isValidURL } from './util.js';
5+
import { createCanonicalURL, isValidURL } from './util.js';
66

77
export { rssSchema };
88

@@ -13,7 +13,7 @@ export type RSSOptions = {
1313
description: z.infer<typeof rssOptionsValidator>['description'];
1414
/**
1515
* Specify the base URL to use for RSS feed links.
16-
* We recommend using the [endpoint context object](https://docs.astro.build/en/reference/api-reference/#site),
16+
* We recommend using the [endpoint context object](https://docs.astro.build/en/reference/api-reference/#contextsite),
1717
* which includes the `site` configured in your project's `astro.config.*`
1818
*/
1919
site: z.infer<typeof rssOptionsValidator>['site'] | URL;
@@ -59,7 +59,7 @@ type ValidatedRSSFeedItem = z.infer<typeof rssSchema>;
5959
type ValidatedRSSOptions = z.infer<typeof rssOptionsValidator>;
6060
type GlobResult = z.infer<typeof globResultValidator>;
6161

62-
const globResultValidator = z.record(z.function().returns(z.promise(z.any())));
62+
const globResultValidator = z.record(z.string(), z.function({ input: [], output: z.promise(z.any()) }));
6363

6464
const rssOptionsValidator = z.object({
6565
title: z.string(),
@@ -72,14 +72,14 @@ const rssOptionsValidator = z.object({
7272
if (!Array.isArray(items)) {
7373
console.warn(
7474
colors.yellow(
75-
'[RSS] Passing a glob result directly has been deprecated. Please migrate to the `pagesGlobToRssItems()` helper: https://docs.astro.build/en/recipes/rss/',
75+
'[RSS] Passing a glob result directly has been deprecated. Please migrate to the `pagesGlobToRssItems()` helper: https://docs.astro.build/en/guides/rss/',
7676
),
7777
);
7878
return pagesGlobToRssItems(items);
7979
}
8080
return items;
8181
}),
82-
xmlns: z.record(z.string()).optional(),
82+
xmlns: z.record(z.string(), z.unknown()).optional(),
8383
stylesheet: z.union([z.string(), z.boolean()]).optional(),
8484
customData: z.string().optional(),
8585
trailingSlash: z.boolean().default(true),
@@ -100,14 +100,14 @@ export async function getRssString(rssOptions: RSSOptions): Promise<string> {
100100
}
101101

102102
async function validateRssOptions(rssOptions: RSSOptions) {
103-
const parsedResult = await rssOptionsValidator.safeParseAsync(rssOptions, { errorMap });
103+
const parsedResult = await rssOptionsValidator.safeParseAsync(rssOptions);
104104
if (parsedResult.success) {
105105
return parsedResult.data;
106106
}
107107
const formattedError = new Error(
108108
[
109109
`[RSS] Invalid or missing options:`,
110-
...parsedResult.error.errors.map((zodError) => {
110+
...parsedResult.error.issues.map((zodError) => {
111111
const path = zodError.path.join('.');
112112
const message = `${zodError.message} (${path})`;
113113
const code = zodError.code;
@@ -116,7 +116,7 @@ async function validateRssOptions(rssOptions: RSSOptions) {
116116
return [
117117
message,
118118
`The \`items\` property requires at least the \`title\` or \`description\` key. They must be properly typed, as well as \`pubDate\` and \`link\` keys if provided.`,
119-
`Check your collection's schema, and visit https://docs.astro.build/en/recipes/rss/#generating-items for more info.`,
119+
`Check your collection's schema, and visit https://docs.astro.build/en/guides/rss/#generating-items for more info.`,
120120
].join('\n');
121121
}
122122

@@ -133,23 +133,23 @@ export function pagesGlobToRssItems(items: GlobResult): Promise<ValidatedRSSFeed
133133
const { url, frontmatter } = await getInfo();
134134
if (url === undefined || url === null) {
135135
throw new Error(
136-
`[RSS] You can only glob entries within 'src/pages/' when passing import.meta.glob() directly. Consider mapping the result to an array of RSSFeedItems. See the RSS docs for usage examples: https://docs.astro.build/en/recipes/rss/`,
136+
`[RSS] You can only glob entries within 'src/pages/' when passing import.meta.glob() directly. Consider mapping the result to an array of RSSFeedItems. See the RSS docs for usage examples: https://docs.astro.build/en/guides/rss/#2-list-of-rss-feed-objects`,
137137
);
138138
}
139139
const parsedResult = rssSchema
140140
.refine((val) => val.title || val.description, {
141141
message: 'At least title or description must be provided.',
142142
path: ['title', 'description'],
143143
})
144-
.safeParse({ ...frontmatter, link: url }, { errorMap });
144+
.safeParse({ ...frontmatter, link: url });
145145

146146
if (parsedResult.success) {
147147
return parsedResult.data;
148148
}
149149
const formattedError = new Error(
150150
[
151151
`[RSS] ${filePath} has invalid or missing frontmatter.\nFix the following properties:`,
152-
...parsedResult.error.errors.map((zodError) => zodError.message),
152+
...parsedResult.error.issues.map((zodError) => zodError.message),
153153
].join('\n'),
154154
);
155155
(formattedError as any).file = filePath;

packages/astro-rss/src/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { z } from 'zod/v3';
1+
import { z } from 'zod';
22

33
export const rssSchema = z.object({
44
title: z.string().optional(),
@@ -22,4 +22,4 @@ export const rssSchema = z.object({
2222
.optional(),
2323
link: z.string().optional(),
2424
content: z.string().optional(),
25-
});
25+
});

packages/astro-rss/src/util.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { z } from 'zod/v3';
21
import type { RSSOptions } from './index.js';
32

43
/** Normalize URL to its canonical form */
@@ -37,17 +36,3 @@ function getUrlExtension(url: string) {
3736
const lastSlash = url.lastIndexOf('/');
3837
return lastDot > lastSlash ? url.slice(lastDot + 1) : '';
3938
}
40-
41-
const flattenErrorPath = (errorPath: (string | number)[]) => errorPath.join('.');
42-
43-
export const errorMap: z.ZodErrorMap = (error, ctx) => {
44-
if (error.code === 'invalid_type') {
45-
const badKeyPath = JSON.stringify(flattenErrorPath(error.path));
46-
if (error.received === 'undefined') {
47-
return { message: `${badKeyPath} is required.` };
48-
} else {
49-
return { message: `${badKeyPath} should be ${error.expected}, not ${error.received}.` };
50-
}
51-
}
52-
return { message: ctx.defaultError };
53-
};

packages/astro-rss/test/rss.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'node:assert/strict';
22
import { describe, it } from 'node:test';
33

4-
import { z } from 'astro/zod';
4+
import { z } from 'zod';
55
import rss, { getRssString } from '../dist/index.js';
66
import { rssSchema } from '../dist/schema.js';
77
import {

pnpm-lock.yaml

Lines changed: 3 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)