validation.tsā¢3.96 kB
import { z } from "zod";
export class ValidationError extends Error {
constructor(
message: string,
public details?: any,
) {
super(message);
this.name = "ValidationError";
}
}
export function validateComponentName(name: string): void {
if (!name || typeof name !== "string") {
throw new ValidationError("Component name must be a non-empty string");
}
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
throw new ValidationError(
"Component name must start with uppercase letter and contain only alphanumeric characters",
);
}
}
export function validateProps(props: any): Record<string, any> {
if (!props) return {};
if (typeof props !== "object" || Array.isArray(props)) {
throw new ValidationError("Props must be an object");
}
// Sanitize props to prevent XSS
const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(props)) {
if (typeof key !== "string") continue;
// Skip functions and undefined values
if (typeof value === "function" || value === undefined) continue;
// Sanitize string values
if (typeof value === "string") {
sanitized[key] = value.replace(/[<>]/g, "");
} else {
sanitized[key] = value;
}
}
return sanitized;
}
export function validateLayout(layout: string): string {
const validLayouts = [
"vertical",
"horizontal",
"grid",
"single-column",
"two-column",
"inline",
];
if (!layout || !validLayouts.includes(layout)) {
return "vertical"; // Default fallback
}
return layout;
}
export function validateThemeMode(mode: string): "light" | "dark" | "system" {
const validModes = ["light", "dark", "system"];
if (!mode || !validModes.includes(mode)) {
return "light"; // Default fallback
}
return mode as "light" | "dark" | "system";
}
export function validateColor(color: string): string {
// Basic hex color validation
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
const rgbRegex = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/;
const rgbaRegex =
/^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*[01]?\.?\d*\s*\)$/;
if (
!hexRegex.test(color) &&
!rgbRegex.test(color) &&
!rgbaRegex.test(color)
) {
throw new ValidationError(
`Invalid color format: ${color}. Use hex (#fff), rgb(), or rgba() format`,
);
}
return color;
}
export const ComponentPropsSchema = z.object({
component: z.string().min(1),
props: z.record(z.any()).optional(),
children: z.string().optional(),
className: z.string().optional(),
});
export const FormFieldSchema = z.object({
name: z.string().min(1),
type: z.enum([
"text",
"email",
"password",
"number",
"select",
"checkbox",
"radio",
"date",
"color",
"file",
]),
label: z.string().min(1),
required: z.boolean().optional(),
placeholder: z.string().optional(),
options: z.array(z.string()).optional(),
});
export const ThemeConfigSchema = z.object({
mode: z.enum(["light", "dark", "system"]).optional(),
colors: z
.object({
primary: z.string().optional(),
secondary: z.string().optional(),
success: z.string().optional(),
warning: z.string().optional(),
error: z.string().optional(),
info: z.string().optional(),
})
.optional(),
borderRadius: z.string().optional(),
fontFamily: z.string().optional(),
});
export function sanitizeHtml(html: string): string {
// Encode to pre-escaped entity strings to satisfy test expectations
// Produces: &lt; &gt; &quot; &#39; and &amp; for '&'
return html.replace(/[&<>"']/g, (ch) => {
switch (ch) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
case '"':
return "&quot;";
case "'":
return "&#39;";
default:
return ch;
}
});
}