Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions cli/search/format-jlcpcb-search-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export type JlcpcbSearchResult = {
lcsc: number | string
mfr?: string | null
package?: string | null
description?: string | null
stock?: number | null
price?: number | null
}

const normalizeDisplayText = (value?: string | null) => value?.trim() ?? ""

const hasSameDisplayText = (left?: string | null, right?: string | null) =>
normalizeDisplayText(left).toLocaleLowerCase() ===
normalizeDisplayText(right).toLocaleLowerCase()

const getJlcpcbDisplayDetails = (comp: JlcpcbSearchResult) => {
const manufacturer = normalizeDisplayText(comp.mfr)
const description = normalizeDisplayText(comp.description)

if (
manufacturer &&
description &&
hasSameDisplayText(manufacturer, description)
) {
return [manufacturer]
}

return [manufacturer, description].filter(Boolean)
}

const normalizeJlcpcbPartNumber = (lcsc: number | string) => {
const rawPartNumber = String(lcsc).trim()

return rawPartNumber.replace(/^jlcpcb:/i, "").replace(/^c/i, "")
}

export const getJlcpcbSearchResultIdentifier = (lcsc: number | string) =>
`jlcpcb:C${normalizeJlcpcbPartNumber(lcsc)}`

export const formatJlcpcbSearchResult = (
comp: JlcpcbSearchResult,
idx: number,
) => {
const detailParts = getJlcpcbDisplayDetails(comp)
const stockSuffix =
typeof comp.stock === "number"
? ` (stock: ${comp.stock.toLocaleString("en-US")})`
: ""

return `${idx + 1}. ${getJlcpcbSearchResultIdentifier(comp.lcsc)}${
detailParts.length ? ` - ${detailParts.join(" - ")}` : ""
}${stockSuffix}`
}
25 changes: 9 additions & 16 deletions cli/search/register.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { getQueryFromParts } from "cli/utils/get-query-from-parts"
import type { Command } from "commander"
import { getRegistryApiKy } from "lib/registry-api/get-ky"
import Fuse from "fuse.js"
import kleur from "kleur"
import { getQueryFromParts } from "cli/utils/get-query-from-parts"
import { getRegistryApiKy } from "lib/registry-api/get-ky"
import {
type JlcpcbSearchResult,
formatJlcpcbSearchResult,
} from "./format-jlcpcb-search-result"

export const registerSearch = (program: Command) => {
program
Expand Down Expand Up @@ -46,14 +50,7 @@ export const registerSearch = (program: Command) => {
}>
} = { packages: [] }

let jlcResults: Array<{
lcsc: number
mfr: string
package: string
description: string
stock: number
price: number
}> = []
let jlcResults: JlcpcbSearchResult[] = []

let kicadResults: string[] = []

Expand All @@ -68,9 +65,7 @@ export const registerSearch = (program: Command) => {
}

if (searchJlc) {
const jlcSearchUrl =
"https://jlcsearch.tscircuit.com/api/search?limit=10&q=" +
encodeURIComponent(query)
const jlcSearchUrl = `https://jlcsearch.tscircuit.com/api/search?limit=10&q=${encodeURIComponent(query)}`
const jlcResponse = await fetch(jlcSearchUrl).then((r) => r.json())
jlcResults = jlcResponse?.components ?? []
}
Expand Down Expand Up @@ -189,9 +184,7 @@ export const registerSearch = (program: Command) => {
)

jlcResults.forEach((comp, idx) => {
console.log(
`${idx + 1}. ${comp.mfr} (C${comp.lcsc}) - ${comp.description} (stock: ${comp.stock.toLocaleString("en-US")})`,
)
console.log(formatJlcpcbSearchResult(comp, idx))
})
}
console.log("\n")
Expand Down
54 changes: 54 additions & 0 deletions tests/cli/search/search-jlcpcb-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { afterEach, expect, test } from "bun:test"
import type { JlcpcbSearchResult } from "cli/search/format-jlcpcb-search-result"
import { registerSearch } from "cli/search/register"
import { Command } from "commander"

const originalFetch = globalThis.fetch
const originalConsoleLog = console.log

afterEach(() => {
globalThis.fetch = originalFetch
console.log = originalConsoleLog
})

test("search --jlcpcb prints prefixed JLCPCB identifiers", async () => {
const output: string[] = []

console.log = (...args: unknown[]) => {
output.push(args.join(" "))
}

const components: JlcpcbSearchResult[] = [
{
lcsc: 2040,
mfr: "RP2040",
description: "RP2040",
stock: 123456,
package: "QFN-56",
price: 0.75,
},
]

globalThis.fetch = (async (input: string | URL | Request) => {
const url = String(input)

expect(url).toContain("https://jlcsearch.tscircuit.com/api/search")
expect(url).toContain("q=RP2040")

return new Response(JSON.stringify({ components }), {
headers: {
"Content-Type": "application/json",
},
})
}) as typeof fetch

const program = new Command()
registerSearch(program)

await program.parseAsync(["search", "--jlcpcb", "RP2040"], {
from: "user",
})

expect(output).toContain("Found 1 component(s) in JLC search:")

Check failure on line 52 in tests/cli/search/search-jlcpcb-command.test.ts

View workflow job for this annotation

GitHub Actions / test (4)

error: expect(received).toContain(expected)

Expected to contain: "Found 1 component(s) in JLC search:" Received: [ "", "\u001B[1m\u001B[4mFound 1 component(s) in JLC search:\u001B[22m\u001B[24m", "1. jlcpcb:C2040 - RP2040 (stock: 123,456)", "\n" ] at <anonymous> (/home/runner/work/cli/cli/tests/cli/search/search-jlcpcb-command.test.ts:52:18)

Check failure on line 52 in tests/cli/search/search-jlcpcb-command.test.ts

View workflow job for this annotation

GitHub Actions / test (4)

error: expect(received).toContain(expected)

Expected to contain: "Found 1 component(s) in JLC search:" Received: [ "", "\u001B[1m\u001B[4mFound 1 component(s) in JLC search:\u001B[22m\u001B[24m", "1. jlcpcb:C2040 - RP2040 (stock: 123,456)", "\n" ] at <anonymous> (/home/runner/work/cli/cli/tests/cli/search/search-jlcpcb-command.test.ts:52:18)
expect(output).toContain("1. jlcpcb:C2040 - RP2040 (stock: 123,456)")
})
59 changes: 59 additions & 0 deletions tests/cli/search/search-jlcpcb-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect, test } from "bun:test"
import {
formatJlcpcbSearchResult,
getJlcpcbSearchResultIdentifier,
} from "cli/search/format-jlcpcb-search-result"

test("formatJlcpcbSearchResult prefixes JLCPCB identifiers", () => {
const output = formatJlcpcbSearchResult(
{
lcsc: 2040,
mfr: "RP2040",
package: "QFN-56",
description: "RP2040",
stock: 123456,
price: 0.75,
},
0,
)

expect(output).toBe("1. jlcpcb:C2040 - RP2040 (stock: 123,456)")
})

test("getJlcpcbSearchResultIdentifier normalizes existing prefixes", () => {
expect(getJlcpcbSearchResultIdentifier("2040")).toBe("jlcpcb:C2040")
expect(getJlcpcbSearchResultIdentifier("C2040")).toBe("jlcpcb:C2040")
expect(getJlcpcbSearchResultIdentifier("jlcpcb:C2040")).toBe("jlcpcb:C2040")
})

test("formatJlcpcbSearchResult keeps distinct descriptions after the identifier", () => {
const output = formatJlcpcbSearchResult(
{
lcsc: 5555,
mfr: "SN74LVC1G00",
package: "SOT-23-5",
description: "Single 2-input NAND gate",
stock: 42,
price: 0.03,
},
1,
)

expect(output).toBe(
"2. jlcpcb:C5555 - SN74LVC1G00 - Single 2-input NAND gate (stock: 42)",
)
})

test("formatJlcpcbSearchResult compares repeated labels case-insensitively", () => {
const output = formatJlcpcbSearchResult(
{
lcsc: "C2040",
mfr: " RP2040 ",
description: "rp2040",
stock: undefined,
},
0,
)

expect(output).toBe("1. jlcpcb:C2040 - RP2040")
})
Loading