import { z } from "zod" import { parse, type TemplateVariable } from "./parser" import { renderDir, type RenderOptions } from "./render" export type { RenderOptions, ReverseMapFile, ReverseMapManifest, ReverseMapToken } from "./render" export { reverseDir, type ReverseOptions, type ReverseResult, type ReverseWarning } from "./reverse" interface Stringable { toString: () => string } interface Issue { message: string, path: Stringable } export class SchemaMismatchError extends Error { constructor(issue: Issue) { super(`Schema doesn't match used template variables: ${issue.path}: ${issue.message}` ) this.name = "SchemaMismatchError" } } function zodTypeName(schema: z.ZodType): string { if (schema instanceof z.ZodObject) return "object" if (schema instanceof z.ZodString) return "string" if (schema instanceof z.ZodBoolean) return "boolean" if (schema instanceof z.ZodNumber) return "number" return schema.constructor.name } function zodDisplayName(schema: z.ZodType): string { const type = zodTypeName(schema) return type.startsWith("Zod") ? type : `z.${type}()` } function setExpectedType(expected: Map, path: string, type: string) { const existing = expected.get(path) if (existing && existing !== type) { throw new SchemaMismatchError({ path, message: `conflicting template variable types: expected both z.${existing}() and z.${type}()`, }) } expected.set(path, type) } function validateSchemaMatchesTemplates(userSchema: z.ZodType, variables: TemplateVariable[]) { if (!(userSchema instanceof z.ZodObject)) { throw new SchemaMismatchError({ path: "", message: "Schema must be a z.object()" }) } // Collect all expected paths: leaf variables + intermediate segments must be objects const expected = new Map() for (const v of variables) { const segments = v.path.split(".") for (let i = 0; i < segments.length - 1; i++) { const intermediate = segments.slice(0, i + 1).join(".") setExpectedType(expected, intermediate, "object") } setExpectedType(expected, v.path, v.type) } for (const [path, expectedType] of expected) { const segments = path.split(".") let current: z.ZodType = userSchema let currentPath = "" for (const seg of segments) { currentPath = currentPath ? `${currentPath}.${seg}` : seg if (!(current instanceof z.ZodObject)) { throw new SchemaMismatchError({ path: currentPath, message: `expected z.object() but schema has ${zodDisplayName(current)}` }) } const shape = current.shape if (!(seg in shape)) { throw new SchemaMismatchError({ path: currentPath, message: `missing in schema` }) } current = shape[seg]! } const actual = zodTypeName(current) if (actual !== expectedType) { throw new SchemaMismatchError({ path, message: `expected z.${expectedType}() but schema has ${zodDisplayName(current)}` }) } } } export const initRenderer = (dirPath: string) => { const variables = parse(dirPath) const createRenderer = (userSchema: S) => { validateSchemaMatchesTemplates(userSchema, variables) return (targetPath: string, context: z.infer, options?: RenderOptions) => { userSchema.parse(context) renderDir(dirPath, targetPath, context as Record, options) } } return createRenderer }