gokul/macro-six-usbc
USB-powered ATmega32U4 controller board with protected/switched 5 V power, six tactile keys, rotary encoder, reset/boot/ISP interfaces, power LED, and six daisy-chained SK6812 RGB LEDs driven through MOSFETs.
- Version
- 0.1.6
- License
- MIT
- Stars
- 0
scripts/check-availability.ts
import { mkdirSync } from "node:fs"
type SupplierPartNumber = string
interface CatalogPart {
lcsc: number
stock?: number
price?: number | string
}
interface CatalogResponse { components: CatalogPart[] }
interface SupplierAttributes {
componentCode?: string
componentModelEn?: string
assemblyMode?: string
fixtureFlag?: boolean
needAuditFlag?: boolean
orderInstructionEnglish?: string
moistureSensitivityLevelEn?: string
lossNumber?: number
preMinPurchaseNum?: number
}
const folder = "outputs/release"
mkdirSync(`${folder}/supplier-evidence`, { recursive: true })
const bom = await Bun.file(`${folder}/BOM.csv`).text()
const supplierPartNumbers = [...bom.matchAll(/"(C\d+)"$/gm)].map(match => match[1] as SupplierPartNumber)
async function checkPart(supplierPartNumber: SupplierPartNumber) {
const checkedAt = new Date().toISOString()
const sourceUrl = `https://jlcpcb.com/partdetail/${supplierPartNumber}`
let attributes: SupplierAttributes = {}
let pcbaType = "unconfirmed"
let stock: number | null = null
let price: number | string | null = null
const lookupErrors: string[] = []
try {
const response = await fetch(sourceUrl, { signal: AbortSignal.timeout(30000) })
if (!response.ok) throw new Error(`Public part page HTTP ${response.status}`)
const html = await response.text()
const readablePage = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ")
pcbaType = readablePage.match(/PCBA Type\s+(Economic and Standard|Standard Only)/)?.[1] ?? "unconfirmed"
await Bun.write(`${folder}/supplier-evidence/${supplierPartNumber}.html`, html)
const streams: string[] = []
for (const match of html.matchAll(/self\.__next_f\.push\((\[.*?\])\)<\/script>/g)) {
const segment = JSON.parse(match[1]) as [number, string | null]
if (typeof segment[1] === "string") streams.push(segment[1])
}
const stream = streams.join("\n")
for (const match of stream.matchAll(/"componentInfo":(\{)/g)) {
const start = match.index + '"componentInfo":'.length
let depth = 0, quoted = false, escaped = false
for (let end = start; end < stream.length; end++) {
const character = stream[end]
if (quoted) {
if (escaped) escaped = false
else if (character === "\\") escaped = true
else if (character === '"') quoted = false
} else if (character === '"') quoted = true
else if (character === "{") depth++
else if (character === "}" && --depth === 0) {
const supplier = JSON.parse(stream.slice(start, end + 1)) as SupplierAttributes
if (supplier.componentCode === supplierPartNumber) attributes = { ...attributes, ...supplier }
break
}
}
}
} catch (error) { lookupErrors.push(String(error)) }
try {
// Read-only public catalog lookup; no account, order, or board upload.
const response = await fetch(`https://jlcsearch.tscircuit.com/api/search?q=${supplierPartNumber}&limit=5`, { signal: AbortSignal.timeout(15000) })
if (!response.ok) throw new Error(`Catalog HTTP ${response.status}`)
const catalog = await response.json() as CatalogResponse
await Bun.write(`${folder}/supplier-evidence/${supplierPartNumber}-catalog.json`, JSON.stringify(catalog, null, 2))
const exact = catalog.components.find(part => `C${part.lcsc}` === supplierPartNumber)
stock = exact?.stock ?? null
price = exact?.price ?? null
} catch (error) { lookupErrors.push(String(error)) }
const bomRow = bom.split("\n").find(line => line.endsWith(`"${supplierPartNumber}"`))!
const designators = bomRow.match(/^"[^"]*","([^"]*)"/)![1].split(",")
return { supplierPartNumber, checkedAt, sourceUrl, stock, price, stockStatus: stock === null ? "unconfirmed" : stock === 0 ? "out_of_stock" : "listed_in_stock", quantityPerBoard: designators.length, designators, pcbaType, model: attributes.componentModelEn ?? null, assemblyMode: attributes.assemblyMode ?? null, fixtureRequired: attributes.fixtureFlag ?? null, supplierAuditRequired: attributes.needAuditFlag ?? null, supplierInstructions: attributes.orderInstructionEnglish ?? "", moistureSensitivity: attributes.moistureSensitivityLevelEn ?? null, procurementMinimum: attributes.preMinPurchaseNum ?? null, assemblyAttrition: attributes.lossNumber ?? null, lookupErrors }
}
const rows = []
for (const supplierPartNumber of supplierPartNumbers) {
rows.push(await checkPart(supplierPartNumber))
await Bun.sleep(500)
}
await Bun.write(`${folder}/supplier-availability-snapshot.json`, JSON.stringify(rows, null, 2))
for (const row of rows) console.log(row.supplierPartNumber, row.stockStatus, row.stock, row.assemblyMode, row.supplierInstructions)
console.log("Catalog stock is indicative, not reserved. Order quantity and supplier approval remain required.")