diff --git a/lib/components/base-components/PrimitiveComponent.ts b/lib/components/base-components/PrimitiveComponent.ts index 5b856c3..719da92 100644 --- a/lib/components/base-components/PrimitiveComponent.ts +++ b/lib/components/base-components/PrimitiveComponent.ts @@ -18,6 +18,7 @@ import { import { isMatchingSelector } from "lib/utils/selector-matching" import type { LayoutBuilder } from "@tscircuit/layout" import type { LayerRef } from "circuit-json" +import { InvalidProps } from "lib/errors/InvalidProps" export interface BaseComponentConfig { componentName: string @@ -96,9 +97,16 @@ export abstract class PrimitiveComponent< this.childrenPendingRemoval = [] this.props = props ?? {} this.externallyAddedAliases = [] - this._parsedProps = this.config.zodProps.parse( - props ?? {}, - ) as z.infer + const parsePropsResult = this.config.zodProps.safeParse(props ?? {}) + if (parsePropsResult.success) { + this._parsedProps = parsePropsResult.data as z.infer + } else { + throw new InvalidProps( + this.lowercaseComponentName, + this.props, + parsePropsResult.error.format(), + ) + } } setProps(props: Partial>) { @@ -271,9 +279,7 @@ export abstract class PrimitiveComponent< const { config } = this if (!config.schematicSymbolName) return null return ( - symbols[ - `${config.schematicSymbolName}` as keyof typeof symbols - ] ?? null + symbols[`${config.schematicSymbolName}` as keyof typeof symbols] ?? null ) } diff --git a/lib/errors/InvalidProps.ts b/lib/errors/InvalidProps.ts new file mode 100644 index 0000000..5e45b80 --- /dev/null +++ b/lib/errors/InvalidProps.ts @@ -0,0 +1,36 @@ +import type { ZodFormattedError } from "zod" +export class InvalidProps extends Error { + constructor( + public readonly componentName: string, + public readonly originalProps: any, + public readonly formattedError: ZodFormattedError, + ) { + let message: string + + const propsWithError = Object.keys(formattedError).filter( + (k) => k !== "_errors", + ) + + const propMessage = propsWithError + .map((k) => { + if ((formattedError as any)[k]._errors[0]) { + return `${k} (${(formattedError as any)[k]._errors[0]})` + } + return `${k} (${JSON.stringify((formattedError as any)[k])})` + }) + .join(", ") + + if ("name" in originalProps) { + message = `Invalid props for ${componentName} "${originalProps.name}": ${propMessage}` + } else if ( + "footprint" in originalProps && + typeof originalProps.footprint === "string" + ) { + message = `Invalid props for ${componentName} (unnamed ${originalProps.footprint} component): ${propMessage}` + } else { + message = `Invalid props for ${componentName} (unnamed): ${propMessage}` + } + + super(message) + } +} diff --git a/lib/errors/index.ts b/lib/errors/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 044eea8..42cb940 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tscircuit/core", "type": "module", - "version": "0.0.99", + "version": "0.0.101", "types": "dist/index.d.ts", "main": "dist/index.js", "module": "dist/index.js", diff --git a/tests/components/normal-components/resistor-without-resistance.test.tsx b/tests/components/normal-components/resistor-without-resistance.test.tsx new file mode 100644 index 0000000..7cbec0b --- /dev/null +++ b/tests/components/normal-components/resistor-without-resistance.test.tsx @@ -0,0 +1,25 @@ +import { test, expect } from "bun:test" +import { InvalidProps } from "lib/errors/InvalidProps" +import { getTestFixture } from "tests/fixtures/get-test-fixture" +import { ZodError } from "zod" + +test("resistor without resistance throws well-formed error", () => { + const { project } = getTestFixture() + + try { + project.add( + + {/* @ts-ignore */} + + , + ) + + throw new Error( + "Should not be able to render circuit where resistor has no resistance", + ) + } catch (e: unknown) { + expect(e).toBeInstanceOf(InvalidProps) + expect((e as InvalidProps).message).toContain("R1") + expect((e as InvalidProps).message).toContain("resistance") + } +})