diff --git a/packages/core/src/catalog.test.ts b/packages/core/src/catalog.test.ts index 19237777..d09541b2 100644 --- a/packages/core/src/catalog.test.ts +++ b/packages/core/src/catalog.test.ts @@ -385,6 +385,68 @@ describe("generateSystemPrompt", () => { expect(prompt).toContain("phoneNumber"); expect(prompt).toContain("zipCode"); }); + + it("formats discriminatedUnion props with correct literal types", () => { + const catalog = createCatalog({ + components: { + Container: { + props: z.object({ + content: z.discriminatedUnion("type", [ + z.object({ + type: z.literal("text"), + text: z.string(), + variant: z.enum(["default", "heading", "caption"]).optional(), + }), + z.object({ + type: z.literal("image"), + url: z.string(), + alt: z.string().optional(), + }), + ]), + }), + description: "A container", + }, + }, + }); + + const prompt = generateSystemPrompt(catalog); + + // discriminator field should show literal values, not "undefined" + expect(prompt).toContain('type: "text"'); + expect(prompt).toContain('type: "image"'); + expect(prompt).not.toContain("type: undefined"); + + // other fields should still render correctly + expect(prompt).toContain("text: string"); + expect(prompt).toContain("url: string"); + expect(prompt).toContain("alt?: string"); + expect(prompt).toContain('"default" | "heading" | "caption"'); + }); + + it("formats nativeEnum props correctly", () => { + enum Status { + Active = "active", + Inactive = "inactive", + Pending = "pending", + } + + const catalog = createCatalog({ + components: { + Badge: { + props: z.object({ + status: z.enum(Status), + label: z.string().optional(), + }), + description: "Status badge", + }, + }, + }); + + const prompt = generateSystemPrompt(catalog); + + expect(prompt).toContain('"active" | "inactive" | "pending"'); + expect(prompt).toContain("label?: string"); + }); }); describe("defineCatalog (new schema API)", () => { diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 8db379cc..9d201851 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -298,11 +298,12 @@ export type InferCatalogComponentProps< * Internal Zod definition type for introspection */ interface ZodDefInternal { - typeName?: string; + type?: string; value?: unknown; values?: unknown; - type?: z.ZodTypeAny; - shape?: () => Record; + entries?: Record; + element?: z.ZodTypeAny; + shape?: Record; innerType?: z.ZodTypeAny; options?: z.ZodTypeAny[]; } @@ -312,77 +313,72 @@ interface ZodDefInternal { */ function formatZodType(schema: z.ZodTypeAny, isOptional = false): string { const def = schema._def as unknown as ZodDefInternal; - const typeName = def.typeName ?? ""; + const typeName = def.type ?? ""; let result: string; switch (typeName) { - case "ZodString": + case "string": result = "string"; break; - case "ZodNumber": + case "number": result = "number"; break; - case "ZodBoolean": + case "boolean": result = "boolean"; break; - case "ZodLiteral": - result = JSON.stringify(def.value); + case "literal": + result = (def.values as unknown[]) + .map((v) => JSON.stringify(v)) + .join(" | "); break; - case "ZodEnum": - result = (def.values as string[]).map((v) => `"${v}"`).join("|"); - break; - case "ZodNativeEnum": - result = Object.values(def.values as Record) + case "enum": + result = Object.values(def.entries as Record) .map((v) => `"${v}"`) - .join("|"); + .join(" | "); break; - case "ZodArray": - result = def.type - ? `Array<${formatZodType(def.type)}>` + case "array": + result = def.element + ? `Array<${formatZodType(def.element)}>` : "Array"; break; - case "ZodObject": { + case "object": { if (!def.shape) { result = "object"; break; } - const shape = def.shape(); - const props = Object.entries(shape) + const props = Object.entries(def.shape) .map(([key, value]) => { const innerDef = value._def as unknown as ZodDefInternal; const innerOptional = - innerDef.typeName === "ZodOptional" || - innerDef.typeName === "ZodNullable"; + innerDef.type === "optional" || innerDef.type === "nullable"; return `${key}${innerOptional ? "?" : ""}: ${formatZodType(value)}`; }) .join(", "); result = `{ ${props} }`; break; } - case "ZodOptional": - return def.innerType ? formatZodType(def.innerType, true) : "unknown?"; - case "ZodNullable": - return def.innerType ? formatZodType(def.innerType, true) : "unknown?"; - case "ZodDefault": + case "optional": + case "nullable": + case "default": return def.innerType ? formatZodType(def.innerType, isOptional) : "unknown"; - case "ZodUnion": + case "union": result = def.options - ? def.options.map((opt) => formatZodType(opt)).join("|") + ? def.options.map((opt) => formatZodType(opt)).join(" | ") : "unknown"; break; - case "ZodNull": + case "null": result = "null"; break; - case "ZodUndefined": + case "undefined": result = "undefined"; break; - case "ZodAny": + case "any": result = "any"; break; - case "ZodUnknown": + case "unknown": result = "unknown"; break; default: @@ -399,18 +395,15 @@ function extractPropsFromSchema( schema: z.ZodTypeAny, ): Array<{ name: string; type: string; optional: boolean }> { const def = schema._def as unknown as ZodDefInternal; - const typeName = def.typeName ?? ""; - if (typeName !== "ZodObject" || !def.shape) { + if (def.type !== "object" || !def.shape) { return []; } - const shape = def.shape(); - return Object.entries(shape).map(([name, value]) => { + return Object.entries(def.shape).map(([name, value]) => { const innerDef = value._def as unknown as ZodDefInternal; const optional = - innerDef.typeName === "ZodOptional" || - innerDef.typeName === "ZodNullable"; + innerDef.type === "optional" || innerDef.type === "nullable"; return { name, type: formatZodType(value),