From 3be98b2aa5a7c8705620643c12f9fd3f95870195 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 00:51:57 +0900 Subject: [PATCH 1/5] chore: migrate footprinter to zod v4 Keep transformed footprint defaults on the input path and align Circuit JSON schema composition on Zod v4. --- README.md | 5 +++++ package.json | 11 +++++----- src/fn/axial.ts | 6 +++--- src/fn/bga.ts | 2 +- src/fn/breakoutheaders.ts | 16 +++++++-------- src/fn/crystal.ts | 8 ++++---- src/fn/dfn.ts | 6 +++++- src/fn/diode.ts | 4 ++-- src/fn/dip.ts | 2 +- src/fn/electrolytic.ts | 8 ++++---- src/fn/hc49.ts | 10 ++++----- src/fn/lga.ts | 6 +++--- src/fn/mountedpcbmodule.ts | 10 ++++----- src/fn/pinrow.ts | 10 ++++----- src/fn/pushbutton.ts | 8 ++++---- src/fn/quad.ts | 2 +- src/fn/radial.ts | 6 +++--- src/fn/smdpushbutton.ts | 8 ++++---- src/fn/soic.ts | 8 ++++---- src/fn/ssop.ts | 4 ++-- src/fn/stampboard.ts | 20 +++++++++--------- src/fn/stampreceiver.ts | 18 ++++++++--------- src/fn/to220.ts | 10 ++++----- src/fn/to220f.ts | 10 ++++----- src/fn/tssop.ts | 8 ++++---- src/fn/usbcmidmount.ts | 38 +++++++++++++++++------------------ src/fn/vson.ts | 4 ++-- src/fn/wson.ts | 16 +++++++-------- src/helpers/res0402-array2.ts | 6 +++--- src/helpers/res0402-array4.ts | 6 +++--- src/helpers/res0603-array2.ts | 6 +++--- src/helpers/res0603-array4.ts | 6 +++--- src/helpers/res0606-array2.ts | 6 +++--- src/helpers/res1206-array4.ts | 6 +++--- tests/smtpad.test.ts | 2 ++ tests/to92s.test.ts | 13 +----------- 36 files changed, 158 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 418a467b..29f2dd17 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Footprinter is tscircuit's DSL and micro-builder for creating footprints. +> **Zod v4 migration:** Footprinter now requires `zod@^4` and +> `circuit-json@^0.0.454` as peer dependencies so generated Circuit JSON can be +> composed with consumer schemas. Consumers that inspect raw parsing errors +> should update their handling for Zod v4's error format. + ![image](https://github.com/user-attachments/assets/24f7a9ba-47ef-4dd9-9a66-9536159a8ff9) You can create very custom footprints using the `` element, but the diff --git a/package.json b/package.json index 19ad4b99..aadce4ee 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@types/bun": "^1.2.2", "@types/node": "^20.12.13", "bun-match-svg": "^0.0.12", - "circuit-json": "^0.0.411", + "circuit-json": "^0.0.454", "circuit-json-to-connectivity-map": "^0.0.18", "circuit-to-svg": "^0.0.371", "esbuild": "^0.21.4", @@ -37,13 +37,14 @@ "schematic-symbols": "^0.0.230", "ts-node": "^10.9.2", "tsup": "^8.0.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "zod": "^4.0.0" }, "dependencies": { - "@tscircuit/mm": "^0.0.8", - "zod": "^3.23.8" + "@tscircuit/mm": "^0.0.8" }, "peerDependencies": { - "circuit-json": "*" + "circuit-json": "^0.0.454", + "zod": "^4.0.0" } } diff --git a/src/fn/axial.ts b/src/fn/axial.ts index 3e6f71a0..5fc15284 100644 --- a/src/fn/axial.ts +++ b/src/fn/axial.ts @@ -14,9 +14,9 @@ import { createRectUnionOutline } from "src/helpers/rect-union-outline" export const axial_def = base_def.extend({ fn: z.string(), - p: length.optional().default("2.54mm"), - id: length.optional().default("0.7mm"), - od: length.optional().default("1.4mm"), + p: length.optional().prefault("2.54mm"), + id: length.optional().prefault("0.7mm"), + od: length.optional().prefault("1.4mm"), }) export type AxialDef = z.input diff --git a/src/fn/bga.ts b/src/fn/bga.ts index 691bc498..ace297d8 100644 --- a/src/fn/bga.ts +++ b/src/fn/bga.ts @@ -21,7 +21,7 @@ export const bga_def = base_def fn: z.string(), num_pins: z.number().optional().default(64), grid: dim2d.optional(), - p: distance.default("0.8mm"), + p: distance.prefault("0.8mm"), w: length.optional(), h: length.optional(), ball: length.optional().describe("ball diameter"), diff --git a/src/fn/breakoutheaders.ts b/src/fn/breakoutheaders.ts index 700c47c2..3e3a893c 100644 --- a/src/fn/breakoutheaders.ts +++ b/src/fn/breakoutheaders.ts @@ -12,15 +12,15 @@ import { base_def } from "../helpers/zod/base_def" export const breakoutheaders_def = base_def.extend({ fn: z.string(), - w: length.default("10mm"), + w: length.prefault("10mm"), h: length.optional(), - left: length.optional().default(20), - right: length.optional().default(20), - top: length.optional().default(0), - bottom: length.optional().default(0), - p: length.default(length.parse("2.54mm")), - id: length.optional().default(length.parse("1mm")), - od: length.optional().default(length.parse("1.5mm")), + left: length.optional().prefault(20), + right: length.optional().prefault(20), + top: length.optional().prefault(0), + bottom: length.optional().prefault(0), + p: length.prefault(length.parse("2.54mm")), + id: length.optional().prefault(length.parse("1mm")), + od: length.optional().prefault(length.parse("1.5mm")), }) export type breakoutheaders_def = z.input diff --git a/src/fn/crystal.ts b/src/fn/crystal.ts index a7d6c8a8..27d23893 100644 --- a/src/fn/crystal.ts +++ b/src/fn/crystal.ts @@ -12,10 +12,10 @@ import { base_def } from "../helpers/zod/base_def" export const crystal_def = base_def.extend({ fn: z.literal("crystal"), num_pins: z.literal(4).default(4), - px: length.default("2.2mm").describe("horizontal pad center pitch"), - py: length.default("1.7mm").describe("vertical pad center pitch"), - pw: length.default("1.4mm").describe("pad width"), - ph: length.default("1.2mm").describe("pad height"), + px: length.prefault("2.2mm").describe("horizontal pad center pitch"), + py: length.prefault("1.7mm").describe("vertical pad center pitch"), + pw: length.prefault("1.4mm").describe("pad width"), + ph: length.prefault("1.2mm").describe("pad height"), }) export const crystal = ( diff --git a/src/fn/dfn.ts b/src/fn/dfn.ts index 2cc98369..dd7b201f 100644 --- a/src/fn/dfn.ts +++ b/src/fn/dfn.ts @@ -27,7 +27,11 @@ export const dfn = ( raw_params: DfnInput, ): { circuitJson: AnyCircuitElement[]; parameters: any } => { const missing = function_call.parse(raw_params.missing ?? []) - if (missing.some((position) => typeof position !== "number")) { + if ( + !missing.every( + (position): position is number => typeof position === "number", + ) + ) { throw new Error("DFN missing positions must be pad numbers") } const missingPositions = [...new Set(missing)] diff --git a/src/fn/diode.ts b/src/fn/diode.ts index bb6898fb..a3aae2b4 100644 --- a/src/fn/diode.ts +++ b/src/fn/diode.ts @@ -9,14 +9,14 @@ const getCopperBounds = (circuitJson: AnySoupElement[]) => { let maxY = Number.NEGATIVE_INFINITY for (const element of circuitJson) { - if (element.type === "pcb_smtpad") { + if (element.type === "pcb_smtpad" && element.shape === "rect") { minX = Math.min(minX, element.x - element.width / 2) maxX = Math.max(maxX, element.x + element.width / 2) minY = Math.min(minY, element.y - element.height / 2) maxY = Math.max(maxY, element.y + element.height / 2) } - if (element.type === "pcb_plated_hole") { + if (element.type === "pcb_plated_hole" && element.shape === "circle") { minX = Math.min(minX, element.x - element.outer_diameter / 2) maxX = Math.max(maxX, element.x + element.outer_diameter / 2) minY = Math.min(minY, element.y - element.outer_diameter / 2) diff --git a/src/fn/dip.ts b/src/fn/dip.ts index 9d3f84d5..18acee13 100644 --- a/src/fn/dip.ts +++ b/src/fn/dip.ts @@ -37,7 +37,7 @@ export const extendDipDef = (newDefaults: { w?: string; p?: string }) => wide: z.boolean().optional(), narrow: z.boolean().optional(), w: lengthInMm.optional(), - p: lengthInMm.default(newDefaults.p ?? "2.54mm"), + p: lengthInMm.prefault(newDefaults.p ?? "2.54mm"), id: lengthInMm.optional(), od: lengthInMm.optional(), nosquareplating: z diff --git a/src/fn/electrolytic.ts b/src/fn/electrolytic.ts index a1099f17..5bfac9ec 100644 --- a/src/fn/electrolytic.ts +++ b/src/fn/electrolytic.ts @@ -12,10 +12,10 @@ import { base_def } from "../helpers/zod/base_def" const electrolytic_def = base_def.extend({ fn: z.string(), - p: length.optional().default("7.5mm"), - id: length.optional().default("1mm"), - od: length.optional().default("2mm"), - d: length.optional().default("10.5mm"), + p: length.optional().prefault("7.5mm"), + id: length.optional().prefault("1mm"), + od: length.optional().prefault("2mm"), + d: length.optional().prefault("10.5mm"), }) export default electrolytic_def diff --git a/src/fn/hc49.ts b/src/fn/hc49.ts index de4dbdeb..9ae5d156 100644 --- a/src/fn/hc49.ts +++ b/src/fn/hc49.ts @@ -27,11 +27,11 @@ const generate_u_curve = ( export const hc49_def = base_def.extend({ fn: z.string(), - p: length.optional().default("4.88mm"), - id: length.optional().default("0.8mm"), - od: length.optional().default("1.5mm"), - w: length.optional().default("5.6mm"), - h: length.optional().default("3.5mm"), + p: length.optional().prefault("4.88mm"), + id: length.optional().prefault("0.8mm"), + od: length.optional().prefault("1.5mm"), + w: length.optional().prefault("5.6mm"), + h: length.optional().prefault("3.5mm"), }) export type Hc49Def = z.input diff --git a/src/fn/lga.ts b/src/fn/lga.ts index 1d7cab98..3ac40de7 100644 --- a/src/fn/lga.ts +++ b/src/fn/lga.ts @@ -15,11 +15,11 @@ export const lga_def = base_def.extend({ fn: z.string(), num_pins: z.number().int().positive().optional().default(14), grid: dim2d.optional(), - p: length.default(length.parse("0.5mm")), + p: length.prefault(length.parse("0.5mm")), w: length.optional(), h: length.optional(), - pw: length.default(length.parse("0.28mm")), - pl: length.default(length.parse("0.7mm")), + pw: length.prefault(length.parse("0.28mm")), + pl: length.prefault(length.parse("0.7mm")), pillpads: z.boolean().optional().default(false), }) diff --git a/src/fn/mountedpcbmodule.ts b/src/fn/mountedpcbmodule.ts index 7ccf2bae..b9aa736c 100644 --- a/src/fn/mountedpcbmodule.ts +++ b/src/fn/mountedpcbmodule.ts @@ -19,9 +19,9 @@ export const mountedpcbmodule_def = base_def .optional() .default(1) .describe("number of rows"), - p: length.default("2.54mm").describe("pitch"), - id: length.default("1.0mm").describe("inner diameter"), - od: length.default("1.5mm").describe("outer diameter"), + p: length.prefault("2.54mm").describe("pitch"), + id: length.prefault("1.0mm").describe("inner diameter"), + od: length.prefault("1.5mm").describe("outer diameter"), male: z.boolean().optional().describe("the module uses male headers"), nopin: z .boolean() @@ -78,7 +78,7 @@ export const mountedpcbmodule_def = base_def .optional(), width: length.optional(), height: length.optional(), - pinRowHoleEdgeToEdgeDist: length.default("2mm"), + pinRowHoleEdgeToEdgeDist: length.prefault("2mm"), holes: z .union([z.string(), z.array(z.string())]) .optional() @@ -95,7 +95,7 @@ export const mountedpcbmodule_def = base_def }), holeXDist: length.optional(), holeYDist: length.optional(), - holeInset: length.default("1mm"), + holeInset: length.prefault("1mm"), pinrow: z.union([z.string(), z.number()]).optional(), usbposition: z .enum(["left", "right", "top", "bottom"]) diff --git a/src/fn/pinrow.ts b/src/fn/pinrow.ts index 4c2cc389..f5673618 100644 --- a/src/fn/pinrow.ts +++ b/src/fn/pinrow.ts @@ -24,9 +24,9 @@ export const pinrow_def = base_def .optional() .default(1) .describe("number of rows"), - p: length.default("0.1in").describe("pitch"), - id: length.default("1.0mm").describe("inner diameter"), - od: length.default("1.5mm").describe("outer diameter"), + p: length.prefault("0.1in").describe("pitch"), + id: length.prefault("1.0mm").describe("inner diameter"), + od: length.prefault("1.5mm").describe("outer diameter"), male: z.boolean().optional().describe("for male pin headers"), female: z.boolean().optional().describe("for female pin headers"), smd: z.boolean().optional().describe("surface mount device"), @@ -35,8 +35,8 @@ export const pinrow_def = base_def .optional() .describe("surface mount device (verbose)"), rightangle: z.boolean().optional().describe("right angle"), - pw: length.optional().default("1.0mm").describe("pad width for SMD"), - pl: length.optional().default("2.0mm").describe("pad length for SMD"), + pw: length.optional().prefault("1.0mm").describe("pad width for SMD"), + pl: length.optional().prefault("2.0mm").describe("pad length for SMD"), pinlabeltextalignleft: z.boolean().optional().default(false), pinlabeltextaligncenter: z.boolean().optional().default(false), pinlabeltextalignright: z.boolean().optional().default(false), diff --git a/src/fn/pushbutton.ts b/src/fn/pushbutton.ts index f069063c..c14be352 100644 --- a/src/fn/pushbutton.ts +++ b/src/fn/pushbutton.ts @@ -11,10 +11,10 @@ import { base_def } from "../helpers/zod/base_def" export const pushbutton_def = base_def.extend({ fn: z.literal("pushbutton"), - w: length.default(4.5), - h: length.default(6.5), - id: length.default(1), - od: length.default(1.2), + w: length.prefault(4.5), + h: length.prefault(6.5), + id: length.prefault(1), + od: length.prefault(1.2), }) export const pushbutton = ( diff --git a/src/fn/quad.ts b/src/fn/quad.ts index 7111d353..9858c817 100644 --- a/src/fn/quad.ts +++ b/src/fn/quad.ts @@ -28,7 +28,7 @@ export const base_quad_def = base_def.extend({ num_pins: z.number().optional().default(64), w: length.optional(), h: length.optional(), - p: length.default(length.parse("0.5mm")), + p: length.prefault(length.parse("0.5mm")), pw: length.optional(), pl: length.optional(), thermalpad: z.union([z.literal(true), dim2d]).optional(), diff --git a/src/fn/radial.ts b/src/fn/radial.ts index 8d2f8303..a51b572c 100644 --- a/src/fn/radial.ts +++ b/src/fn/radial.ts @@ -13,9 +13,9 @@ import { base_def } from "../helpers/zod/base_def" export const radial_def = base_def.extend({ fn: z.string(), - p: length.optional().default("5mm"), - id: length.optional().default("0.8mm"), - od: length.optional().default("1.6mm"), + p: length.optional().prefault("5mm"), + id: length.optional().prefault("0.8mm"), + od: length.optional().prefault("1.6mm"), ceramic: z.boolean().optional(), electrolytic: z.boolean().optional(), diff --git a/src/fn/smdpushbutton.ts b/src/fn/smdpushbutton.ts index eddb9f61..064a315c 100644 --- a/src/fn/smdpushbutton.ts +++ b/src/fn/smdpushbutton.ts @@ -12,10 +12,10 @@ import { base_def } from "../helpers/zod/base_def" export const smdpushbutton_def = base_def.extend({ fn: z.literal("smdpushbutton"), num_pins: z.literal(4).default(4), - px: length.default("4.2mm").describe("horizontal pad center pitch"), - py: length.default("2.15mm").describe("vertical pad center pitch"), - pw: length.default("1.05mm").describe("pad width"), - ph: length.default("0.7mm").describe("pad height"), + px: length.prefault("4.2mm").describe("horizontal pad center pitch"), + py: length.prefault("2.15mm").describe("vertical pad center pitch"), + pw: length.prefault("1.05mm").describe("pad width"), + ph: length.prefault("0.7mm").describe("pad height"), }) export const smdpushbutton = ( diff --git a/src/fn/soic.ts b/src/fn/soic.ts index e7106fe5..7e575eae 100644 --- a/src/fn/soic.ts +++ b/src/fn/soic.ts @@ -28,10 +28,10 @@ export const extendSoicDef = (newDefaults: { .extend({ fn: z.string(), num_pins: z.number().optional().default(8), - w: length.default(length.parse(newDefaults.w ?? "5.3mm")), - p: length.default(length.parse(newDefaults.p ?? "1.27mm")), - pw: length.default(length.parse(newDefaults.pw ?? "0.6mm")), - pl: length.default(length.parse(newDefaults.pl ?? "1.0mm")), + w: length.prefault(length.parse(newDefaults.w ?? "5.3mm")), + p: length.prefault(length.parse(newDefaults.p ?? "1.27mm")), + pw: length.prefault(length.parse(newDefaults.pw ?? "0.6mm")), + pl: length.prefault(length.parse(newDefaults.pl ?? "1.0mm")), legsoutside: z .boolean() .optional() diff --git a/src/fn/ssop.ts b/src/fn/ssop.ts index 7de00911..9e2d5f71 100644 --- a/src/fn/ssop.ts +++ b/src/fn/ssop.ts @@ -19,8 +19,8 @@ export const ssop_def = base_def .extend({ fn: z.string(), num_pins: z.number().optional().default(8), - w: length.default(length.parse("3.9mm")), - p: length.default(length.parse("1.27mm")), + w: length.prefault(length.parse("3.9mm")), + p: length.prefault(length.parse("1.27mm")), pw: length.optional(), pl: length.optional(), legsoutside: z.boolean().optional().default(false), diff --git a/src/fn/stampboard.ts b/src/fn/stampboard.ts index 6e12f4bb..ae6fa0e2 100644 --- a/src/fn/stampboard.ts +++ b/src/fn/stampboard.ts @@ -13,19 +13,19 @@ import { base_def } from "../helpers/zod/base_def" export const stampboard_def = base_def.extend({ fn: z.string(), - w: length.default("22.58mm"), + w: length.prefault("22.58mm"), h: length.optional(), - left: length.optional().default(20), - right: length.optional().default(20), - top: length.optional().default(2), - bottom: length.optional().default(2), - p: length.default(length.parse("2.54mm")), - pw: length.default(length.parse("1.6mm")), - pl: length.default(length.parse("2.4mm")), + left: length.optional().prefault(20), + right: length.optional().prefault(20), + top: length.optional().prefault(2), + bottom: length.optional().prefault(2), + p: length.prefault(length.parse("2.54mm")), + pw: length.prefault(length.parse("1.6mm")), + pl: length.prefault(length.parse("2.4mm")), innerhole: z.boolean().default(false), - innerholeedgedistance: length.default(length.parse("1.61mm")), + innerholeedgedistance: length.prefault(length.parse("1.61mm")), silkscreenlabels: z.boolean().default(false), - silkscreenlabelmargin: length.default(length.parse("0.1mm")), + silkscreenlabelmargin: length.prefault(length.parse("0.1mm")), }) export type Stampboard_def = z.input diff --git a/src/fn/stampreceiver.ts b/src/fn/stampreceiver.ts index abb99abf..c3257faf 100644 --- a/src/fn/stampreceiver.ts +++ b/src/fn/stampreceiver.ts @@ -13,17 +13,17 @@ import { base_def } from "../helpers/zod/base_def" export const stampreceiver_def = base_def.extend({ fn: z.string(), - w: length.default("22.58mm"), + w: length.prefault("22.58mm"), h: length.optional(), - left: length.optional().default(20), - right: length.optional().default(20), - top: length.optional().default(2), - bottom: length.optional().default(2), - p: length.default(length.parse("2.54mm")), - pw: length.default(length.parse("1.6mm")), - pl: length.default(length.parse("3.2mm")), + left: length.optional().prefault(20), + right: length.optional().prefault(20), + top: length.optional().prefault(2), + bottom: length.optional().prefault(2), + p: length.prefault(length.parse("2.54mm")), + pw: length.prefault(length.parse("1.6mm")), + pl: length.prefault(length.parse("3.2mm")), innerhole: z.boolean().default(false), - innerholeedgedistance: length.default(length.parse("1.61mm")), + innerholeedgedistance: length.prefault(length.parse("1.61mm")), }) export type Stampreceiver_def = z.input diff --git a/src/fn/to220.ts b/src/fn/to220.ts index 3cbe0a86..6ad0c51f 100644 --- a/src/fn/to220.ts +++ b/src/fn/to220.ts @@ -11,11 +11,11 @@ import { base_def } from "../helpers/zod/base_def" export const to220_def = base_def.extend({ fn: z.string(), - p: length.optional().default("5.0mm"), - id: length.optional().default("1.0mm"), - od: length.optional().default("1.9mm"), - w: length.optional().default("13mm"), - h: length.optional().default("7mm"), + p: length.optional().prefault("5.0mm"), + id: length.optional().prefault("1.0mm"), + od: length.optional().prefault("1.9mm"), + w: length.optional().prefault("13mm"), + h: length.optional().prefault("7mm"), num_pins: z.number().optional(), string: z.string().optional(), }) diff --git a/src/fn/to220f.ts b/src/fn/to220f.ts index 67320ea4..6d61d7a0 100644 --- a/src/fn/to220f.ts +++ b/src/fn/to220f.ts @@ -13,11 +13,11 @@ const TO220F_PITCH_MM = 2.54 export const to220f_def = base_def.extend({ fn: z.string(), // KiCad TO-220F-3_Vertical: hole=1.2mm, pad=1.905×2.0mm, pitch=2.54mm - id: length.optional().default("1.2mm"), - od: length.optional().default("1.905mm"), - ph: length.optional().default("2mm"), - w: length.optional().default("13mm"), - h: length.optional().default("7mm"), + id: length.optional().prefault("1.2mm"), + od: length.optional().prefault("1.905mm"), + ph: length.optional().prefault("2mm"), + w: length.optional().prefault("13mm"), + h: length.optional().prefault("7mm"), num_pins: z.number().optional(), string: z.string().optional(), }) diff --git a/src/fn/tssop.ts b/src/fn/tssop.ts index bd2d67e9..0b43f077 100644 --- a/src/fn/tssop.ts +++ b/src/fn/tssop.ts @@ -16,10 +16,10 @@ import { createThermalPad } from "src/helpers/create-thermal-pad" export const tssop_def = base_def.extend({ fn: z.string(), num_pins: z.number().optional().default(8), - w: length.default(length.parse("3.0mm")), - p: length.default(length.parse("0.5mm")), - pw: length.default(length.parse("0.30mm")), - pl: length.default(length.parse("1.45mm")), + w: length.prefault(length.parse("3.0mm")), + p: length.prefault(length.parse("0.5mm")), + pw: length.prefault(length.parse("0.30mm")), + pl: length.prefault(length.parse("1.45mm")), legsoutside: z.boolean().optional().default(true), thermalpad: dim2d.optional(), silkscreen_stroke_width: z.number().optional().default(0.1), diff --git a/src/fn/usbcmidmount.ts b/src/fn/usbcmidmount.ts index b7ad33b5..635b65ec 100644 --- a/src/fn/usbcmidmount.ts +++ b/src/fn/usbcmidmount.ts @@ -21,27 +21,27 @@ export const usbcmidmount_def = base_def.extend({ .default(false) .describe("number contact lands right-to-left"), noholes: z.boolean().default(false).describe("omit the two locator holes"), - rowy: length.default("2.125mm").describe("contact pad row y position"), - ph: length.default("1.1mm").describe("contact pad height"), - pw: length.default("0.3mm").describe("signal contact pad width"), - powerpw: length.default("0.55mm").describe("merged power pad width"), - powerx: length.default("3.2mm").describe("outer merged power pad x"), - shellx: length.default("4.325mm").describe("shell tab x position"), - topy: length.default("1.575mm").describe("upper shell tab y position"), + rowy: length.prefault("2.125mm").describe("contact pad row y position"), + ph: length.prefault("1.1mm").describe("contact pad height"), + pw: length.prefault("0.3mm").describe("signal contact pad width"), + powerpw: length.prefault("0.55mm").describe("merged power pad width"), + powerx: length.prefault("3.2mm").describe("outer merged power pad x"), + shellx: length.prefault("4.325mm").describe("shell tab x position"), + topy: length.prefault("1.575mm").describe("upper shell tab y position"), bottomy: length - .default("2.625mm") + .prefault("2.625mm") .describe("lower shell tab negative y magnitude"), - tophw: length.default("0.6mm").describe("upper shell slot width"), - tophh: length.default("1.5mm").describe("upper shell slot height"), - topring: length.default("0.25mm").describe("upper shell annular ring"), - bottomhw: length.default("0.6mm").describe("lower shell slot width"), - bottomhh: length.default("1.2mm").describe("lower shell slot height"), - bottomring: length.default("0.3mm").describe("lower shell annular ring"), - holex: length.default("2.89mm").describe("locator hole x position"), - holey: length.default("1.055mm").describe("locator hole y position"), - holed: length.default("0.7mm").describe("locator hole diameter"), + tophw: length.prefault("0.6mm").describe("upper shell slot width"), + tophh: length.prefault("1.5mm").describe("upper shell slot height"), + topring: length.prefault("0.25mm").describe("upper shell annular ring"), + bottomhw: length.prefault("0.6mm").describe("lower shell slot width"), + bottomhh: length.prefault("1.2mm").describe("lower shell slot height"), + bottomring: length.prefault("0.3mm").describe("lower shell annular ring"), + holex: length.prefault("2.89mm").describe("locator hole x position"), + holey: length.prefault("1.055mm").describe("locator hole y position"), + holed: length.prefault("0.7mm").describe("locator hole diameter"), bodybottom: length - .default("5.225mm") + .prefault("5.225mm") .describe("connector body negative y extent"), }) @@ -185,7 +185,7 @@ export const usbcmidmount = ( index < 2 || index >= signalXs.length - 2 ? powerpw : pw, ) const signalPads = signalXs.map((x, index) => - rectpad(pinStart + 4 + index, x, rowy, signalWidths[index], ph), + rectpad(pinStart + 4 + index, x, rowy, signalWidths[index]!, ph), ) const silkX = Math.min(shellx - Math.max(tophw, bottomhw) / 2 - 0.2, 4.5) const silkscreen = [ diff --git a/src/fn/vson.ts b/src/fn/vson.ts index 699d5735..124ebc5f 100644 --- a/src/fn/vson.ts +++ b/src/fn/vson.ts @@ -20,10 +20,10 @@ export const vson_def = base_def.extend({ w: length.describe("width between vertical rows of pins"), grid: dim2d.describe("width and height of the border of the footprint"), ep: dim2d - .default("0x0mm") + .prefault("0x0mm") .describe("width and height of the central exposed thermal pad"), epx: length - .default("0mm") + .prefault("0mm") .describe("x offset of the center of the central exposed thermal pad"), pinw: length.describe("width of the pin pads"), pinh: length.describe("height of the pin pads"), diff --git a/src/fn/wson.ts b/src/fn/wson.ts index 1dec6090..7f306cdc 100644 --- a/src/fn/wson.ts +++ b/src/fn/wson.ts @@ -12,15 +12,15 @@ import { base_def } from "../helpers/zod/base_def" export const wson_def = base_def.extend({ fn: z.literal("wson"), num_pins: z.literal(8).default(8), - p: length.default("0.5mm").describe("pin pitch"), - rowspan: length.default("3.015mm").describe("distance between pad rows"), - pl: length.default("0.6mm").describe("pad length across the package edge"), - pw: length.default("0.28mm").describe("pad width along the pin row"), + p: length.prefault("0.5mm").describe("pin pitch"), + rowspan: length.prefault("3.015mm").describe("distance between pad rows"), + pl: length.prefault("0.6mm").describe("pad length across the package edge"), + pw: length.prefault("0.28mm").describe("pad width along the pin row"), ep: z.boolean().default(true).describe("include the exposed pad"), - epw: length.default("1.7mm").describe("exposed pad width"), - eph: length.default("0.3mm").describe("exposed pad height"), - w: length.default("2mm").describe("package body width"), - h: length.default("3mm").describe("package body height"), + epw: length.prefault("1.7mm").describe("exposed pad width"), + eph: length.prefault("0.3mm").describe("exposed pad height"), + w: length.prefault("2mm").describe("package body width"), + h: length.prefault("3mm").describe("package body height"), }) export const wson = ( diff --git a/src/helpers/res0402-array2.ts b/src/helpers/res0402-array2.ts index 6163bb2a..9cfc21f4 100644 --- a/src/helpers/res0402-array2.ts +++ b/src/helpers/res0402-array2.ts @@ -6,9 +6,9 @@ import { base_def } from "./zod/base_def" import mm from "@tscircuit/mm" export const res0402Array2_def = base_def.extend({ - pw: length.default("0.5mm"), - ph: length.default("0.4mm"), - p: length.default("0.7mm"), + pw: length.prefault("0.5mm"), + ph: length.prefault("0.4mm"), + p: length.prefault("0.7mm"), textbottom: z.boolean().optional(), convex: z.boolean().optional(), concave: z.boolean().optional(), diff --git a/src/helpers/res0402-array4.ts b/src/helpers/res0402-array4.ts index 71656f11..b7677af8 100644 --- a/src/helpers/res0402-array4.ts +++ b/src/helpers/res0402-array4.ts @@ -6,9 +6,9 @@ import { base_def } from "./zod/base_def" import mm from "@tscircuit/mm" export const res0402Array4_def = base_def.extend({ - pw: length.default("0.5mm"), - ph: length.default("0.32mm"), - p: length.default("0.5mm"), + pw: length.prefault("0.5mm"), + ph: length.prefault("0.32mm"), + p: length.prefault("0.5mm"), textbottom: z.boolean().optional(), convex: z.boolean().optional(), concave: z.boolean().optional(), diff --git a/src/helpers/res0603-array2.ts b/src/helpers/res0603-array2.ts index 873d55ee..bfed2ac9 100644 --- a/src/helpers/res0603-array2.ts +++ b/src/helpers/res0603-array2.ts @@ -6,9 +6,9 @@ import { base_def } from "./zod/base_def" import mm from "@tscircuit/mm" export const res0603Array2_def = base_def.extend({ - pw: length.default("0.9mm"), - ph: length.default("0.4mm"), - p: length.default("0.8mm"), + pw: length.prefault("0.9mm"), + ph: length.prefault("0.4mm"), + p: length.prefault("0.8mm"), textbottom: z.boolean().optional(), convex: z.boolean().optional(), concave: z.boolean().optional(), diff --git a/src/helpers/res0603-array4.ts b/src/helpers/res0603-array4.ts index bef0d486..3dab3492 100644 --- a/src/helpers/res0603-array4.ts +++ b/src/helpers/res0603-array4.ts @@ -6,9 +6,9 @@ import { base_def } from "./zod/base_def" import mm from "@tscircuit/mm" export const res0603Array4_def = base_def.extend({ - pw: length.default("0.9mm"), - ph: length.default("0.4mm"), - p: length.default("0.8mm"), + pw: length.prefault("0.9mm"), + ph: length.prefault("0.4mm"), + p: length.prefault("0.8mm"), textbottom: z.boolean().optional(), convex: z.boolean().optional(), concave: z.boolean().optional(), diff --git a/src/helpers/res0606-array2.ts b/src/helpers/res0606-array2.ts index 1fbde123..160b13bb 100644 --- a/src/helpers/res0606-array2.ts +++ b/src/helpers/res0606-array2.ts @@ -6,9 +6,9 @@ import { base_def } from "./zod/base_def" import mm from "@tscircuit/mm" export const res0606Array2_def = base_def.extend({ - pw: length.default("0.7mm"), - ph: length.default("0.64mm"), - p: length.default("0.94mm"), + pw: length.prefault("0.7mm"), + ph: length.prefault("0.64mm"), + p: length.prefault("0.94mm"), textbottom: z.boolean().optional(), convex: z.boolean().optional(), concave: z.boolean().optional(), diff --git a/src/helpers/res1206-array4.ts b/src/helpers/res1206-array4.ts index e9b3fdf4..7680e876 100644 --- a/src/helpers/res1206-array4.ts +++ b/src/helpers/res1206-array4.ts @@ -6,9 +6,9 @@ import { base_def } from "./zod/base_def" import mm from "@tscircuit/mm" export const res1206Array4_def = base_def.extend({ - pw: length.default("0.9mm"), - ph: length.default("0.9mm"), - p: length.default("1.34mm"), + pw: length.prefault("0.9mm"), + ph: length.prefault("0.9mm"), + p: length.prefault("1.34mm"), textbottom: z.boolean().optional(), convex: z.boolean().optional(), concave: z.boolean().optional(), diff --git a/tests/smtpad.test.ts b/tests/smtpad.test.ts index 1c0f568c..c5f46ab4 100644 --- a/tests/smtpad.test.ts +++ b/tests/smtpad.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test" +import { any_circuit_element } from "circuit-json" import { fp } from "../src/footprinter" import { getTestFixture } from "./fixtures/get-test-fixture" @@ -46,6 +47,7 @@ test("smtpad rect w/h", async () => { const { snapshotSoup } = await getTestFixture("smtpad_rect_w2_h1") const circuitJson = fp().smtpad().rect().w("2mm").h("1mm").circuitJson() expectPad(circuitJson, "rect", { width: 2, height: 1 }) + expect(() => any_circuit_element.array().parse(circuitJson)).not.toThrow() snapshotSoup(circuitJson) }) diff --git a/tests/to92s.test.ts b/tests/to92s.test.ts index ff1c2d9c..917e36c1 100644 --- a/tests/to92s.test.ts +++ b/tests/to92s.test.ts @@ -25,16 +25,5 @@ test("to92s_3", () => { }) // invalid test test("to92s_4", () => { - try { - const circuitJson = fp.string("to92s_4").circuitJson() - const svgContent = convertCircuitJsonToPcbSvg(circuitJson, { - showCourtyards: true, - }) - expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to92s_4") - } catch (error) { - const e = error as Error - expect(e).toBeInstanceOf(Error) - expect(e.message).toContain("Invalid literal value, expected 3") - expect(e.message).toContain("Invalid literal value, expected 2") - } + expect(() => fp.string("to92s_4").circuitJson()).toThrow() }) From 50f710642b88c6d94ce501684b3bfe92a586799a Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 01:32:52 +0900 Subject: [PATCH 2/5] chore: require circuit-json zod v4 release --- README.md | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 29f2dd17..f01f5384 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Footprinter is tscircuit's DSL and micro-builder for creating footprints. > **Zod v4 migration:** Footprinter now requires `zod@^4` and -> `circuit-json@^0.0.454` as peer dependencies so generated Circuit JSON can be +> `circuit-json@^0.0.455` as peer dependencies so generated Circuit JSON can be > composed with consumer schemas. Consumers that inspect raw parsing errors > should update their handling for Zod v4's error format. diff --git a/package.json b/package.json index aadce4ee..4444d5bc 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@types/bun": "^1.2.2", "@types/node": "^20.12.13", "bun-match-svg": "^0.0.12", - "circuit-json": "^0.0.454", + "circuit-json": "^0.0.455", "circuit-json-to-connectivity-map": "^0.0.18", "circuit-to-svg": "^0.0.371", "esbuild": "^0.21.4", @@ -44,7 +44,7 @@ "@tscircuit/mm": "^0.0.8" }, "peerDependencies": { - "circuit-json": "^0.0.454", + "circuit-json": "^0.0.455", "zod": "^4.0.0" } } From 37eaa8ef7eb68de1829eb68ab08f20cb68efb973 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 14:48:39 +0900 Subject: [PATCH 3/5] ci: retrigger after circuit-json@0.0.455 release From 0f3f074073ae7f7400d94bf06a5183889597a8d1 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 21:53:11 +0900 Subject: [PATCH 4/5] chore: override circuit-json-util's zod 3 peer so npm installs resolve --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 4444d5bc..aab4fa44 100644 --- a/package.json +++ b/package.json @@ -46,5 +46,10 @@ "peerDependencies": { "circuit-json": "^0.0.455", "zod": "^4.0.0" + }, + "overrides": { + "@tscircuit/circuit-json-util": { + "zod": "^4.0.0" + } } } From 5208b294bcbcd87dd1cc9f09883be8f24237c379 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sun, 26 Jul 2026 15:12:51 +0900 Subject: [PATCH 5/5] chore: migrate smdpinheader defaults to prefault smdpinheader landed on main after this branch's zod v4 migration, so its `length.default(...)` calls were never converted. On zod v4 `.default()` applies to the OUTPUT type, so the transformed length values stayed as raw strings ("2.54mm") instead of numbers, failing the parity assertion. `.prefault()` applies on the input side, matching every other length field in this branch. Literal booleans keep `.default()`. --- src/fn/smdpinheader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fn/smdpinheader.ts b/src/fn/smdpinheader.ts index c7672f97..a4746929 100644 --- a/src/fn/smdpinheader.ts +++ b/src/fn/smdpinheader.ts @@ -13,13 +13,13 @@ export const smdpinheader_def = base_def.extend({ fn: z.literal("smdpinheader"), num_pins: z.number().int().positive().default(6), // Defaults follow the Harwin M20-877R recommended vertical SMT land pattern. - p: length.default("2.54mm").describe("contact pitch"), + p: length.prefault("2.54mm").describe("contact pitch"), py: length - .default("3.31mm") + .prefault("3.31mm") .describe("center-to-center distance between alternating pad rows"), - pw: length.default("1mm").describe("pad width"), - ph: length.default("2.51mm").describe("pad height"), - bh: length.default("2.5mm").describe("plastic body height"), + pw: length.prefault("1mm").describe("pad width"), + ph: length.prefault("2.51mm").describe("pad height"), + bh: length.prefault("2.5mm").describe("plastic body height"), male: z.literal(true).default(true), female: z.literal(false).default(false), smd: z.literal(true).default(true),