From 87caaaf9d9f3726286a717f5b21204b5a5c38e47 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 00:51:57 +0900 Subject: [PATCH 1/6] 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 | 12 +++++------ 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, 159 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 8525406d5..1a5bda2b8 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 11a6c4b60..ae1e07798 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.393", "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 3e6f71a0a..5fc152848 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 691bc498b..ace297d8d 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 700c47c2e..3e3a893c0 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 a7d6c8a8b..27d238933 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 2cc983692..dd7b201fc 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 bb6898fb3..a3aae2b40 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 9d3f84d5b..18acee13d 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 a1099f175..5bfac9ec6 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 de4dbdeb6..9ae5d1562 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 1d7cab982..3ac40de76 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 7ccf2bae3..b9aa736c2 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 b794c27ab..f4c74952b 100644 --- a/src/fn/pinrow.ts +++ b/src/fn/pinrow.ts @@ -30,12 +30,12 @@ export const pinrow_def = base_def .transform((val) => Number(val)) .optional() .describe("number of nominal columns in a sparse grid"), - p: length.default("0.1in").describe("pitch"), + p: length.prefault("0.1in").describe("pitch"), py: length.optional().describe("vertical row pitch"), - id: length.default("1.0mm").describe("inner diameter"), - od: length.default("1.5mm").describe("outer diameter"), + id: length.prefault("1.0mm").describe("inner diameter"), + od: length.prefault("1.5mm").describe("outer diameter"), missing: function_call - .default([]) + .prefault([]) .describe("row-major nominal grid positions to omit"), male: z.boolean().optional().describe("for male pin headers"), female: z.boolean().optional().describe("for female pin headers"), @@ -45,8 +45,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 f069063c9..c14be3524 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 22f7ed90a..e7e29bb74 100644 --- a/src/fn/quad.ts +++ b/src/fn/quad.ts @@ -42,7 +42,7 @@ export const base_quad_def = base_def.extend({ topbottompins: side_pin_count, w: length.optional(), h: length.optional(), - p: length.default(length.parse("0.5mm")), + p: length.prefault(length.parse("0.5mm")), px: length.optional().describe("top and bottom side pad pitch"), py: length.optional().describe("left and right side pad pitch"), pw: length.optional(), diff --git a/src/fn/radial.ts b/src/fn/radial.ts index 8d2f83037..a51b572c4 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 eddb9f61d..064a315c1 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 e7106fe5f..7e575eae5 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 7de00911a..9e2d5f71d 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 6e12f4bb0..ae6fa0e2d 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 abb99abfc..c3257faf3 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 3cbe0a86e..6ad0c51f7 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 67320ea4c..6d61d7a0b 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 bd2d67e9f..0b43f0771 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 b7ad33b54..635b65ec6 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 699d57353..124ebc5f3 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 1dec60906..7f306cdca 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 6163bb2a8..9cfc21f45 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 71656f11a..b7677af81 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 873d55eee..bfed2ac90 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 bef0d4868..3dab34920 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 1fbde123d..160b13bb1 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 e9b3fdf44..7680e876c 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 1c0f568c1..c5f46ab4e 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 ff1c2d9c6..917e36c16 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 e67b3f3b656c4050e28fc3ab71447b3341f81b91 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 01:32:52 +0900 Subject: [PATCH 2/6] 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 1a5bda2b8..72f335faa 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 ae1e07798..cfeaa4f17 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.393", "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 d0cd405cff49add6f06f542b49701a22a2a5149d Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 14:48:39 +0900 Subject: [PATCH 3/6] ci: retrigger after circuit-json@0.0.455 release From 1d19f2bcbf6eea9a3797b003843f4dbd4c9221f4 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 21:53:11 +0900 Subject: [PATCH 4/6] 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 cfeaa4f17..a62ca0f87 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 3b5ea463240af1a6421659af71c4745fc991d3e2 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sun, 26 Jul 2026 15:12:51 +0900 Subject: [PATCH 5/6] 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 c7672f973..a47469293 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), From 9a91970b0cec60b7ced3f493efc2852db44cedcf Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Mon, 27 Jul 2026 12:16:52 +0900 Subject: [PATCH 6/6] chore: migrate the three defs that landed after this branch was opened Rebasing onto current main surfaced a real break rather than just a conflict. fpc, led2835 and led5050 were merged into main after this branch was cut, so their length fields still use .default() while the rest of the codebase moved to .prefault(). Under zod v4 that is not a style difference. .default() short-circuits the transform, so the declared value is handed back verbatim: before {"x": null, "y": null, "w": "2mm", "h": "1.1mm"} after {"x": -2.4, "y": 1.7, "w": 2, "h": 1.1} Coordinates came out null and dimensions as strings, which is why the led5050/led2835 tests and both KiCad parity tests failed on the rebased branch but pass on main. Verified the branch now fails exactly what main fails and nothing more: main 5 fail (diode0603, diode1210, melf, sod323fl, sod923) this PR 5 fail (identical list) 479 pass on both. --- src/fn/fpc.ts | 12 ++++++------ src/fn/led2835.ts | 14 +++++++------- src/fn/led5050.ts | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/fn/fpc.ts b/src/fn/fpc.ts index 692426f5a..31deff883 100644 --- a/src/fn/fpc.ts +++ b/src/fn/fpc.ts @@ -12,9 +12,9 @@ import { base_def } from "../helpers/zod/base_def" export const fpc_def = base_def.extend({ fn: z.literal("fpc"), num_pins: z.coerce.number().int().min(2), - p: length.default("0.5mm").describe("contact pad pitch"), - pw: length.default("0.3mm").describe("contact pad width"), - pl: length.default("1.25mm").describe("contact pad length"), + p: length.prefault("0.5mm").describe("contact pad pitch"), + pw: length.prefault("0.3mm").describe("contact pad width"), + pl: length.prefault("1.25mm").describe("contact pad length"), staggered: z .boolean() .default(false) @@ -23,7 +23,7 @@ export const fpc_def = base_def.extend({ .boolean() .default(false) .describe("place pin 1 on the upper row when staggered"), - py: length.default("2.4mm").describe("staggered row center pitch"), + py: length.prefault("2.4mm").describe("staggered row center pitch"), toppl: length.optional().describe("upper-row contact pad length"), bottompl: length.optional().describe("lower-row contact pad length"), mpx: length.optional().describe("mounting pad center pitch"), @@ -34,8 +34,8 @@ export const fpc_def = base_def.extend({ .boolean() .default(false) .describe("place the mounting pads above the contact-row center"), - mpw: length.default("2mm").describe("mounting pad width"), - mpl: length.default("2.5mm").describe("mounting pad length"), + mpw: length.prefault("2mm").describe("mounting pad width"), + mpl: length.prefault("2.5mm").describe("mounting pad length"), }) type PadBounds = { diff --git a/src/fn/led2835.ts b/src/fn/led2835.ts index 2f1fede5e..530af8369 100644 --- a/src/fn/led2835.ts +++ b/src/fn/led2835.ts @@ -14,13 +14,13 @@ export const led2835_def = base_def.extend({ num_pins: z.literal(2).default(2), // Anode (pin 1) is the wide pad, cathode (pin 2) the narrow pad, matching // the KiCad LED_PLCC_2835 land pattern - p1w: length.default("2.2mm").describe("pin 1 (anode) pad width"), - p2w: length.default("1.25mm").describe("pin 2 (cathode) pad width"), - ph: length.default("2.2mm").describe("pad height"), - p1x: length.default("-0.9mm").describe("pin 1 pad center x"), - p2x: length.default("1.375mm").describe("pin 2 pad center x"), - w: length.default("3.5mm").describe("package body width"), - h: length.default("2.8mm").describe("package body height"), + p1w: length.prefault("2.2mm").describe("pin 1 (anode) pad width"), + p2w: length.prefault("1.25mm").describe("pin 2 (cathode) pad width"), + ph: length.prefault("2.2mm").describe("pad height"), + p1x: length.prefault("-0.9mm").describe("pin 1 pad center x"), + p2x: length.prefault("1.375mm").describe("pin 2 pad center x"), + w: length.prefault("3.5mm").describe("package body width"), + h: length.prefault("2.8mm").describe("package body height"), }) export const led2835 = ( diff --git a/src/fn/led5050.ts b/src/fn/led5050.ts index daa332be1..0e9cfe576 100644 --- a/src/fn/led5050.ts +++ b/src/fn/led5050.ts @@ -12,12 +12,12 @@ import { base_def } from "../helpers/zod/base_def" export const led5050_def = base_def.extend({ fn: z.literal("led5050"), num_pins: z.literal(6).default(6), - p: length.default("1.7mm").describe("pin pitch along each column"), - rowspan: length.default("4.8mm").describe("distance between pad columns"), - pl: length.default("2mm").describe("pad length across the package edge"), - pw: length.default("1.1mm").describe("pad width along the pin column"), - w: length.default("5mm").describe("package body width"), - h: length.default("5mm").describe("package body height"), + p: length.prefault("1.7mm").describe("pin pitch along each column"), + rowspan: length.prefault("4.8mm").describe("distance between pad columns"), + pl: length.prefault("2mm").describe("pad length across the package edge"), + pw: length.prefault("1.1mm").describe("pad width along the pin column"), + w: length.prefault("5mm").describe("package body width"), + h: length.prefault("5mm").describe("package body height"), }) export const led5050 = (