@tscircuit/parts-engine 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Transforms a KiCad footprint string into a generic "footprinter string".
3
+ * For now, this is a simplified conversion to a standard package name.
4
+ * e.g. "kicad:Resistor_SMD:R_0603_1608Metric" -> "0603"
5
+ */
6
+ export const getFootprinterStringFromKicad = (
7
+ kicadFootprint: string,
8
+ ): string | undefined => {
9
+ // kicad:Resistor_SMD:R_0603_1608Metric -> 0603
10
+ let match = kicadFootprint.match(/:[RC]_(\d{4})_/)
11
+ if (match) return match[1]
12
+
13
+ // kicad:Package_SO:SOIC-8_3.9x4.9mm_P1.27mm -> SOIC-8
14
+ // kicad:Package_TO_SOT_SMD:SOT-23 -> SOT-23
15
+ match = kicadFootprint.match(
16
+ /:(SOIC-\d+|SOT-\d+|SOD-\d+|SSOP-\d+|TSSOP-\d+|QFP-\d+|QFN-\d+)/,
17
+ )
18
+ if (match) return match[1]
19
+
20
+ return undefined
21
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Transforms a generic "footprinter string" into a JLC-compatible package name.
3
+ * e.g. "cap0603" -> "0603"
4
+ */
5
+ export const getJlcPackageFromFootprinterString = (
6
+ footprinterString: string,
7
+ ): string => {
8
+ if (footprinterString.includes("cap")) {
9
+ return footprinterString.replace(/cap/g, "")
10
+ }
11
+ return footprinterString
12
+ }
@@ -0,0 +1,25 @@
1
+ import { getFootprinterStringFromKicad } from "./get-footprinter-string-from-kicad"
2
+ import { getJlcPackageFromFootprinterString } from "./get-jlc-package-from-footprinter-string"
3
+
4
+ /**
5
+ * Get a JLC-compatible package name from a footprint string, which could be
6
+ * a KiCad footprint or a generic "footprinter string".
7
+ */
8
+ export const getJlcpcbPackageName = (
9
+ footprint: string | undefined,
10
+ ): string | undefined => {
11
+ if (!footprint) return undefined
12
+
13
+ if (footprint.startsWith("kicad:")) {
14
+ const footprinterString = getFootprinterStringFromKicad(footprint)
15
+ if (footprinterString) {
16
+ return getJlcPackageFromFootprinterString(footprinterString)
17
+ }
18
+
19
+ // Fallback for un-matched KiCad strings
20
+ return footprint
21
+ }
22
+
23
+ // Not a KiCad string, assume it's a footprinter string
24
+ return getJlcPackageFromFootprinterString(footprint)
25
+ }
@@ -1,6 +1,7 @@
1
1
  import type { PartsEngine, SupplierPartNumbers } from "@tscircuit/props"
2
+ import { getJlcpcbPackageName } from "./footprint-translators/index"
2
3
 
3
- const cache = new Map<string, any>()
4
+ export const cache = new Map<string, any>()
4
5
 
5
6
  const getJlcPartsCached = async (name: any, params: any) => {
6
7
  const paramString = new URLSearchParams({
@@ -18,37 +19,47 @@ const getJlcPartsCached = async (name: any, params: any) => {
18
19
  return responseJson
19
20
  }
20
21
 
22
+ const withBasicPartPreference = (parts: any[] | undefined) => {
23
+ if (!parts) return []
24
+ return [...parts].sort(
25
+ (a, b) => Number(b.is_basic ?? false) - Number(a.is_basic ?? false),
26
+ )
27
+ }
28
+
21
29
  export const jlcPartsEngine: PartsEngine = {
22
30
  findPart: async ({
23
31
  sourceComponent,
24
32
  footprinterString,
25
33
  }): Promise<SupplierPartNumbers> => {
34
+ const jlcpcbPackage = getJlcpcbPackageName(footprinterString)
35
+
26
36
  if (
27
37
  sourceComponent.type === "source_component" &&
28
38
  sourceComponent.ftype === "simple_resistor"
29
39
  ) {
30
40
  const { resistors } = await getJlcPartsCached("resistors", {
31
41
  resistance: sourceComponent.resistance,
32
- package: footprinterString,
42
+ package: jlcpcbPackage,
33
43
  })
34
44
 
35
45
  return {
36
- jlcpcb: (resistors ?? []).map((r: any) => `C${r.lcsc}`).slice(0, 3),
46
+ jlcpcb: withBasicPartPreference(resistors)
47
+ .map((r: any) => `C${r.lcsc}`)
48
+ .slice(0, 3),
37
49
  }
38
50
  } else if (
39
51
  sourceComponent.type === "source_component" &&
40
52
  sourceComponent.ftype === "simple_capacitor"
41
53
  ) {
42
- if (footprinterString?.includes("cap")) {
43
- footprinterString = footprinterString.replace("cap", "")
44
- }
45
54
  const { capacitors } = await getJlcPartsCached("capacitors", {
46
55
  capacitance: sourceComponent.capacitance,
47
- package: footprinterString,
56
+ package: jlcpcbPackage,
48
57
  })
49
58
 
50
59
  return {
51
- jlcpcb: (capacitors ?? []).map((c: any) => `C${c.lcsc}`).slice(0, 3),
60
+ jlcpcb: withBasicPartPreference(capacitors)
61
+ .map((c: any) => `C${c.lcsc}`)
62
+ .slice(0, 3),
52
63
  }
53
64
  } else if (
54
65
  sourceComponent.type === "source_component" &&
@@ -72,7 +83,9 @@ export const jlcPartsEngine: PartsEngine = {
72
83
  },
73
84
  )
74
85
  return {
75
- jlcpcb: (headers ?? []).map((h: any) => `C${h.lcsc}`).slice(0, 3),
86
+ jlcpcb: withBasicPartPreference(headers)
87
+ .map((h: any) => `C${h.lcsc}`)
88
+ .slice(0, 3),
76
89
  }
77
90
  } else if (
78
91
  sourceComponent.type === "source_component" &&
@@ -80,10 +93,10 @@ export const jlcPartsEngine: PartsEngine = {
80
93
  ) {
81
94
  const { potentiometers } = await getJlcPartsCached("potentiometers", {
82
95
  resistance: sourceComponent.max_resistance,
83
- package: footprinterString,
96
+ package: jlcpcbPackage,
84
97
  })
85
98
  return {
86
- jlcpcb: (potentiometers ?? [])
99
+ jlcpcb: withBasicPartPreference(potentiometers)
87
100
  .map((p: any) => `C${p.lcsc}`)
88
101
  .slice(0, 3),
89
102
  }
@@ -92,31 +105,37 @@ export const jlcPartsEngine: PartsEngine = {
92
105
  sourceComponent.ftype === "simple_diode"
93
106
  ) {
94
107
  const { diodes } = await getJlcPartsCached("diodes", {
95
- package: footprinterString,
108
+ package: jlcpcbPackage,
96
109
  })
97
110
  return {
98
- jlcpcb: (diodes ?? []).map((d: any) => `C${d.lcsc}`).slice(0, 3),
111
+ jlcpcb: withBasicPartPreference(diodes)
112
+ .map((d: any) => `C${d.lcsc}`)
113
+ .slice(0, 3),
99
114
  }
100
115
  } else if (
101
116
  sourceComponent.type === "source_component" &&
102
117
  sourceComponent.ftype === "simple_chip"
103
118
  ) {
104
119
  const { chips } = await getJlcPartsCached("chips", {
105
- package: footprinterString,
120
+ package: jlcpcbPackage,
106
121
  })
107
122
  return {
108
- jlcpcb: (chips ?? []).map((c: any) => `C${c.lcsc}`).slice(0, 3),
123
+ jlcpcb: withBasicPartPreference(chips)
124
+ .map((c: any) => `C${c.lcsc}`)
125
+ .slice(0, 3),
109
126
  }
110
127
  } else if (
111
128
  sourceComponent.type === "source_component" &&
112
129
  sourceComponent.ftype === "simple_transistor"
113
130
  ) {
114
131
  const { transistors } = await getJlcPartsCached("transistors", {
115
- package: footprinterString,
132
+ package: jlcpcbPackage,
116
133
  transistor_type: sourceComponent.transistor_type,
117
134
  })
118
135
  return {
119
- jlcpcb: (transistors ?? []).map((t: any) => `C${t.lcsc}`).slice(0, 3),
136
+ jlcpcb: withBasicPartPreference(transistors)
137
+ .map((t: any) => `C${t.lcsc}`)
138
+ .slice(0, 3),
120
139
  }
121
140
  } else if (
122
141
  sourceComponent.type === "source_component" &&
@@ -124,10 +143,12 @@ export const jlcPartsEngine: PartsEngine = {
124
143
  ) {
125
144
  const { power_sources } = await getJlcPartsCached("power_sources", {
126
145
  voltage: sourceComponent.voltage,
127
- package: footprinterString,
146
+ package: jlcpcbPackage,
128
147
  })
129
148
  return {
130
- jlcpcb: (power_sources ?? []).map((p: any) => `C${p.lcsc}`).slice(0, 3),
149
+ jlcpcb: withBasicPartPreference(power_sources)
150
+ .map((p: any) => `C${p.lcsc}`)
151
+ .slice(0, 3),
131
152
  }
132
153
  } else if (
133
154
  sourceComponent.type === "source_component" &&
@@ -135,10 +156,12 @@ export const jlcPartsEngine: PartsEngine = {
135
156
  ) {
136
157
  const { inductors } = await getJlcPartsCached("inductors", {
137
158
  inductance: sourceComponent.inductance,
138
- package: footprinterString,
159
+ package: jlcpcbPackage,
139
160
  })
140
161
  return {
141
- jlcpcb: (inductors ?? []).map((i: any) => `C${i.lcsc}`).slice(0, 3),
162
+ jlcpcb: withBasicPartPreference(inductors)
163
+ .map((i: any) => `C${i.lcsc}`)
164
+ .slice(0, 3),
142
165
  }
143
166
  } else if (
144
167
  sourceComponent.type === "source_component" &&
@@ -147,22 +170,26 @@ export const jlcPartsEngine: PartsEngine = {
147
170
  const { crystals } = await getJlcPartsCached("crystals", {
148
171
  frequency: sourceComponent.frequency,
149
172
  load_capacitance: sourceComponent.load_capacitance,
150
- package: footprinterString,
173
+ package: jlcpcbPackage,
151
174
  })
152
175
  return {
153
- jlcpcb: (crystals ?? []).map((c: any) => `C${c.lcsc}`).slice(0, 3),
176
+ jlcpcb: withBasicPartPreference(crystals)
177
+ .map((c: any) => `C${c.lcsc}`)
178
+ .slice(0, 3),
154
179
  }
155
180
  } else if (
156
181
  sourceComponent.type === "source_component" &&
157
182
  sourceComponent.ftype === "simple_mosfet"
158
183
  ) {
159
184
  const { mosfets } = await getJlcPartsCached("mosfets", {
160
- package: footprinterString,
185
+ package: jlcpcbPackage,
161
186
  mosfet_mode: sourceComponent.mosfet_mode,
162
187
  channel_type: sourceComponent.channel_type,
163
188
  })
164
189
  return {
165
- jlcpcb: (mosfets ?? []).map((m: any) => `C${m.lcsc}`).slice(0, 3),
190
+ jlcpcb: withBasicPartPreference(mosfets)
191
+ .map((m: any) => `C${m.lcsc}`)
192
+ .slice(0, 3),
166
193
  }
167
194
  } else if (
168
195
  sourceComponent.type === "source_component" &&
@@ -170,10 +197,12 @@ export const jlcPartsEngine: PartsEngine = {
170
197
  ) {
171
198
  const { resonators } = await getJlcPartsCached("resonators", {
172
199
  frequency: sourceComponent.frequency,
173
- package: footprinterString,
200
+ package: jlcpcbPackage,
174
201
  })
175
202
  return {
176
- jlcpcb: (resonators ?? []).map((r: any) => `C${r.lcsc}`).slice(0, 3),
203
+ jlcpcb: withBasicPartPreference(resonators)
204
+ .map((r: any) => `C${r.lcsc}`)
205
+ .slice(0, 3),
177
206
  }
178
207
  } else if (
179
208
  sourceComponent.type === "source_component" &&
@@ -181,30 +210,36 @@ export const jlcPartsEngine: PartsEngine = {
181
210
  ) {
182
211
  const { switches } = await getJlcPartsCached("switches", {
183
212
  switch_type: sourceComponent.type,
184
- package: footprinterString,
213
+ package: jlcpcbPackage,
185
214
  })
186
215
  return {
187
- jlcpcb: (switches ?? []).map((s: any) => `C${s.lcsc}`).slice(0, 3),
216
+ jlcpcb: withBasicPartPreference(switches)
217
+ .map((s: any) => `C${s.lcsc}`)
218
+ .slice(0, 3),
188
219
  }
189
220
  } else if (
190
221
  sourceComponent.type === "source_component" &&
191
222
  sourceComponent.ftype === "simple_led"
192
223
  ) {
193
224
  const { leds } = await getJlcPartsCached("leds", {
194
- package: footprinterString,
225
+ package: jlcpcbPackage,
195
226
  })
196
227
  return {
197
- jlcpcb: (leds ?? []).map((l: any) => `C${l.lcsc}`).slice(0, 3),
228
+ jlcpcb: withBasicPartPreference(leds)
229
+ .map((l: any) => `C${l.lcsc}`)
230
+ .slice(0, 3),
198
231
  }
199
232
  } else if (
200
233
  sourceComponent.type === "source_component" &&
201
234
  sourceComponent.ftype === "simple_fuse"
202
235
  ) {
203
236
  const { fuses } = await getJlcPartsCached("fuses", {
204
- package: footprinterString,
237
+ package: jlcpcbPackage,
205
238
  })
206
239
  return {
207
- jlcpcb: (fuses ?? []).map((l: any) => `C${l.lcsc}`).slice(0, 3),
240
+ jlcpcb: withBasicPartPreference(fuses)
241
+ .map((l: any) => `C${l.lcsc}`)
242
+ .slice(0, 3),
208
243
  }
209
244
  }
210
245
  return {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/parts-engine",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "devDependencies": {
@@ -1,8 +1,9 @@
1
1
  import { describe, test, expect, beforeEach } from "bun:test"
2
- import { jlcPartsEngine } from "../lib/jlc-parts-engine"
2
+ import { jlcPartsEngine, cache } from "../lib/jlc-parts-engine"
3
3
  import type { AnySourceComponent } from "circuit-json"
4
4
  describe("jlcPartsEngine", () => {
5
5
  beforeEach(() => {
6
+ cache.clear()
6
7
  // Reset fetch fake between tests
7
8
  globalThis.fetch = (async (url: string) => {
8
9
  if (url.includes("/resistors/")) {
@@ -127,6 +128,25 @@ describe("jlcPartsEngine", () => {
127
128
  })
128
129
  })
129
130
 
131
+ test("should find resistor parts with kicad footprint", async () => {
132
+ const resistor: AnySourceComponent = {
133
+ type: "source_component",
134
+ ftype: "simple_resistor",
135
+ resistance: 10000,
136
+ source_component_id: "source_component_0",
137
+ name: "R1",
138
+ }
139
+
140
+ const result = await jlcPartsEngine.findPart({
141
+ sourceComponent: resistor,
142
+ footprinterString: "kicad:Resistor_SMD:R_0603_1608Metric",
143
+ })
144
+
145
+ expect(result).toEqual({
146
+ jlcpcb: ["C1234", "C5678", "C9012"],
147
+ })
148
+ })
149
+
130
150
  test("should find capacitor parts", async () => {
131
151
  const capacitor: AnySourceComponent = {
132
152
  type: "source_component",
@@ -138,7 +158,7 @@ describe("jlcPartsEngine", () => {
138
158
 
139
159
  const result = await jlcPartsEngine.findPart({
140
160
  sourceComponent: capacitor,
141
- footprinterString: "0603cap",
161
+ footprinterString: "cap0603",
142
162
  })
143
163
 
144
164
  expect(result).toEqual({
@@ -222,6 +242,24 @@ describe("jlcPartsEngine", () => {
222
242
  })
223
243
  })
224
244
 
245
+ test("should find chip parts with kicad footprint", async () => {
246
+ const chip: AnySourceComponent = {
247
+ type: "source_component",
248
+ ftype: "simple_chip",
249
+ source_component_id: "source_component_0",
250
+ name: "U1",
251
+ }
252
+
253
+ const result = await jlcPartsEngine.findPart({
254
+ sourceComponent: chip,
255
+ footprinterString: "kicad:Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
256
+ })
257
+
258
+ expect(result).toEqual({
259
+ jlcpcb: ["C5678", "C9012", "C3456"],
260
+ })
261
+ })
262
+
225
263
  test("should find transistor parts", async () => {
226
264
  const transistor: AnySourceComponent = {
227
265
  type: "source_component",
@@ -394,4 +432,42 @@ describe("jlcPartsEngine", () => {
394
432
 
395
433
  expect(result).toEqual({ jlcpcb: [] })
396
434
  })
435
+
436
+ test("should prefer basic parts when available", async () => {
437
+ // Override the global fetch mock for this specific test
438
+ globalThis.fetch = (async (url: string) => {
439
+ if (url.includes("/resistors/")) {
440
+ return {
441
+ json: async () => ({
442
+ resistors: [
443
+ { lcsc: "1111" }, // is_basic is undefined, treated as false
444
+ { lcsc: "2222", is_basic: true },
445
+ { lcsc: "3333", is_basic: false },
446
+ { lcsc: "4444", is_basic: true },
447
+ { lcsc: "5555" },
448
+ ],
449
+ }),
450
+ } as Response
451
+ }
452
+ return {} as Response
453
+ }) as unknown as typeof fetch
454
+
455
+ const resistor: AnySourceComponent = {
456
+ type: "source_component",
457
+ ftype: "simple_resistor",
458
+ resistance: 10000,
459
+ source_component_id: "source_component_0",
460
+ name: "R1",
461
+ }
462
+
463
+ const result = await jlcPartsEngine.findPart({
464
+ sourceComponent: resistor,
465
+ footprinterString: "0603",
466
+ })
467
+
468
+ // Expecting basic parts to be prioritized
469
+ expect(result).toEqual({
470
+ jlcpcb: ["C2222", "C4444", "C1111"],
471
+ })
472
+ })
397
473
  })