Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/strict-regions-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astrojs/sitemap': patch
'@astrojs/rss': patch
'@astrojs/db': patch
---

Updates zod to v4
2 changes: 1 addition & 1 deletion packages/astro-rss/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import * as z from 'zod/v4';
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
import colors from 'piccolore';
import { rssSchema } from './schema.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/astro-rss/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import * as z from 'zod/v4';

export const rssSchema = z.object({
title: z.string().optional(),
Expand Down
3 changes: 1 addition & 2 deletions packages/astro-rss/test/rss.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { z } from 'zod';
import * as z from 'zod/v4';
import rss, { getRssString } from '../dist/index.js';
import { rssSchema } from '../dist/schema.js';
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/session/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import z from 'zod/v4';
import * as z from 'zod/v4';

export const SessionDriverConfigSchema = z.object({
config: z.record(z.string(), z.any()).optional(),
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/test/types/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it } from 'node:test';
import { expectTypeOf } from 'expect-type';
import type z from 'zod';
import type * as z from 'zod/v4';
import { type FontProviderSchema, FontFamilySchema } from '../../src/assets/fonts/config.js';
import type { FontProvider, FontFamily } from '../../src/assets/fonts/types.js';
import type { SessionDriverConfigSchema } from '../../dist/core/session/config.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"piccolore": "^0.1.3",
"prompts": "^2.4.2",
"yargs-parser": "^22.0.0",
"zod": "^3.25.76"
"zod": "^4.3.6"
},
"devDependencies": {
"@types/deep-diff": "^1.0.5",
Expand Down
6 changes: 3 additions & 3 deletions packages/db/src/core/db-client/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Config as LibSQLConfig } from '@libsql/client';
import z from 'zod/v3';
import * as z from 'zod/v4';

const rawLibSQLOptions = z.record(z.string());
const rawLibSQLOptions = z.record(z.string(), z.string());

const parseNumber = (value: string) => z.coerce.number().parse(value);
const parseBoolean = (value: string) => z.coerce.boolean().parse(value);
Expand Down Expand Up @@ -50,7 +50,7 @@ export const parseLibSQLConfig = (config: Record<string, string>): Partial<LibSQ
return libSQLConfigTransformed.parse(config);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid LibSQL config: ${error.errors.map((e) => e.message).join(', ')}`);
throw new Error(`Invalid LibSQL config: ${error.issues.map((e) => e.message).join(', ')}`);
}
throw error;
}
Expand Down
144 changes: 86 additions & 58 deletions packages/db/src/core/integration/error-map.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,132 @@
/**
* 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 'zod/v3';
import type { $ZodErrorMap } from 'zod/v4/core';

interface TypeOrLiteralErrByPathEntry {
type TypeOrLiteralErrByPathEntry = {
code: 'invalid_type' | 'invalid_literal';
received: unknown;
expected: unknown[];
}
message: string | undefined;
};

export const errorMap: z.ZodErrorMap = (baseError, ctx) => {
const baseErrorPath = flattenErrorPath(baseError.path);
if (baseError.code === 'invalid_union') {
export const errorMap: $ZodErrorMap = (issue) => {
const baseErrorPath = flattenErrorPath(issue.path ?? []);
if (issue.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'
const typeOrLiteralErrByPath = new Map<string, TypeOrLiteralErrByPathEntry>();
for (const unionError of baseError.unionErrors.flatMap((e) => e.errors)) {
if (unionError.code === 'invalid_type' || unionError.code === 'invalid_literal') {
let typeOrLiteralErrByPath = new Map<string, TypeOrLiteralErrByPathEntry>();
for (const unionError of issue.errors.flat()) {
if (unionError.code === 'invalid_type') {
const flattenedErrorPath = flattenErrorPath(unionError.path);
const typeOrLiteralErr = typeOrLiteralErrByPath.get(flattenedErrorPath);
if (typeOrLiteralErr) {
typeOrLiteralErr.expected.push(unionError.expected);
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],
message: unionError.message,
});
}
}
}
const 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 === issue.errors.flat().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 unionErrors of issue.errors) {
const expectedShape: string[] = [];
for (const _issue of unionErrors) {
// 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 as any);
}
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 if ('values' in _issue) {
expectedShape.push(
..._issue.values.filter((v) => typeof v === 'string').map((v) => `"${v}"`),
);
} else if (relativePath) {
expectedShape.push(relativePath);
}
}
if (expectedShape.length === 1 && !expectedShape[0]?.includes(':')) {
// In this case the expected shape is not an object, but probably a literal type, e.g. `['string']`.
expectedShapes.push(expectedShape.join(''));
} else if (expectedShape.length > 0) {
expectedShapes.push(`{ ${expectedShape.join('; ')} }`);
}
}
if (expectedShapes.length) {
details.push('> Expected type `' + expectedShapes.join(' | ') + '`');
details.push('> Received `' + stringify(issue.input) + '`');
}
}

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]) =>
// Avoid printing the key again if it's a base error
key === baseErrorPath
? `> ${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 (issue.code === 'invalid_type') {
return {
message: prefix(
baseErrorPath,
getTypeOrLiteralMsg({
code: baseError.code,
received: (baseError as any).received,
expected: [baseError.expected],
code: issue.code,
received: typeof issue.input,
expected: [issue.expected],
message: issue.message,
}),
),
};
} else if (baseError.message) {
return { message: prefix(baseErrorPath, baseError.message) };
} else {
return { message: prefix(baseErrorPath, ctx.defaultError) };
} else if (issue.message) {
return { message: prefix(baseErrorPath, issue.message) };
}
};

const getTypeOrLiteralMsg = (error: TypeOrLiteralErrByPathEntry): string => {
if (error.received === 'undefined') return 'Required';
// received could be `undefined` or the string `'undefined'`
if (typeof error.received === 'undefined' || error.received === 'undefined')
return error.message ?? 'Required';
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 | symbol)[]) => errorPath.join('.');

const flattenErrorPath = (errorPath: Array<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 = /^\./;
4 changes: 2 additions & 2 deletions packages/db/src/core/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type ViteDevServer,
} from 'vite';
import parseArgs from 'yargs-parser';
import { z } from 'zod/v3';
import * as z from 'zod/v4';
import { AstroDbError, isDbError } from '../../runtime/utils.js';
import { CONFIG_FILE_NAMES, DB_PATH, VIRTUAL_MODULE_ID } from '../consts.js';
import { EXEC_DEFAULT_EXPORT_ERROR, EXEC_ERROR } from '../errors.js';
Expand Down Expand Up @@ -46,7 +46,7 @@ const astroDBConfigSchema = z
.default('node'),
})
.optional()
.default({});
.prefault({});

export type AstroDBConfig = z.infer<typeof astroDBConfigSchema>;

Expand Down
4 changes: 2 additions & 2 deletions packages/db/src/core/load-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function resolveDbConfig({
integrations,
}: Pick<AstroConfig, 'root' | 'integrations'>) {
const { mod, dependencies } = await loadUserConfigFile(root);
const userDbConfig = dbConfigSchema.parse(mod?.default ?? {}, { errorMap });
const userDbConfig = dbConfigSchema.parse(mod?.default ?? {}, { error: errorMap });
/** Resolved `astro:db` config including tables provided by integrations. */
const dbConfig = { tables: userDbConfig.tables ?? {} };

Expand All @@ -46,7 +46,7 @@ export async function resolveDbConfig({
// TODO: config file dependencies are not tracked for integrations for now.
const loadedConfig = await loadIntegrationConfigFile(root, configEntrypoint);
const integrationDbConfig = dbConfigSchema.parse(loadedConfig.mod?.default ?? {}, {
errorMap,
error: errorMap,
});
for (const key in integrationDbConfig.tables) {
if (key in dbConfig.tables) {
Expand Down
Loading
Loading