From f590e19d85df7bf4ddb62abdce185bd13e8d040c Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Wed, 18 Mar 2026 15:25:25 -0700 Subject: [PATCH] Make websafe bundle avoid core dependency --- ...ert-easyeda-json-to-tscircuit-soup-json.ts | 2 +- lib/index.ts | 2 +- lib/utils/normalize-pin-labels.ts | 83 +++++++++++++++++++ tests/websafe/browser-bundle-websafe.test.ts | 20 +++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 lib/utils/normalize-pin-labels.ts create mode 100644 tests/websafe/browser-bundle-websafe.test.ts diff --git a/lib/convert-easyeda-json-to-tscircuit-soup-json.ts b/lib/convert-easyeda-json-to-tscircuit-soup-json.ts index 31d0eb93..79f22031 100644 --- a/lib/convert-easyeda-json-to-tscircuit-soup-json.ts +++ b/lib/convert-easyeda-json-to-tscircuit-soup-json.ts @@ -33,7 +33,7 @@ import { import { compose, scale, translate, applyToPoint } from "transformation-matrix" import { mm } from "@tscircuit/mm" import { mil10ToMm } from "./utils/easyeda-unit-to-mm" -import { normalizePinLabels } from "@tscircuit/core" +import { normalizePinLabels } from "./utils/normalize-pin-labels" import { DEFAULT_PCB_THICKNESS_MM } from "./constants" import { normalizeSymbolName } from "./utils/normalize-symbol-name" diff --git a/lib/index.ts b/lib/index.ts index 4a222de9..5e1cd503 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -4,4 +4,4 @@ export { convertRawEasyToTsx as convertRawEasyEdaToTs } from "./websafe/convert- export { normalizeManufacturerPartNumber } from "./utils/normalize-manufacturer-part-number" export * from "./schemas/easy-eda-json-schema" export { convertEasyEdaJsonToVariousFormats } from "./convert-easyeda-json-to-various-formats" -export { normalizePinLabels } from "@tscircuit/core" +export { normalizePinLabels } from "./utils/normalize-pin-labels" diff --git a/lib/utils/normalize-pin-labels.ts b/lib/utils/normalize-pin-labels.ts new file mode 100644 index 00000000..670aebf9 --- /dev/null +++ b/lib/utils/normalize-pin-labels.ts @@ -0,0 +1,83 @@ +export const normalizePinLabels = (pinLabelSets: string[][]): string[][] => { + const normalizedLabels = pinLabelSets.map((labels) => + labels.filter( + (label) => typeof label === "string" && label.trim().length > 0, + ), + ) + + const requestedPinNumbers = normalizedLabels.map((labels) => { + const numericLabel = labels.find((label) => /^\d+$/.test(label)) + return numericLabel ? Number.parseInt(numericLabel, 10) : null + }) + + const firstOccurrenceByPinNumber = new Map() + const reservedPinNumbers = new Set() + for (const [index, requestedPinNumber] of requestedPinNumbers.entries()) { + if (requestedPinNumber == null) continue + reservedPinNumbers.add(requestedPinNumber) + if (!firstOccurrenceByPinNumber.has(requestedPinNumber)) { + firstOccurrenceByPinNumber.set(requestedPinNumber, index) + } + } + + const nonNumericTotals = new Map() + for (const labels of normalizedLabels) { + for (const label of labels) { + if (/^\d+$/.test(label)) continue + nonNumericTotals.set(label, (nonNumericTotals.get(label) ?? 0) + 1) + } + } + + const generatedPinNumbers = new Set() + const duplicateNumericCounts = new Map() + const nonNumericSeenCounts = new Map() + let nextGeneratedPinNumber = 1 + + const getNextAvailablePinNumber = () => { + while ( + reservedPinNumbers.has(nextGeneratedPinNumber) || + generatedPinNumbers.has(nextGeneratedPinNumber) + ) { + nextGeneratedPinNumber += 1 + } + + const pinNumber = nextGeneratedPinNumber + generatedPinNumbers.add(pinNumber) + nextGeneratedPinNumber += 1 + return pinNumber + } + + return normalizedLabels.map((labels, index) => { + const requestedPinNumber = requestedPinNumbers[index] + const primaryPinNumber = + requestedPinNumber != null && + firstOccurrenceByPinNumber.get(requestedPinNumber) === index + ? requestedPinNumber + : getNextAvailablePinNumber() + + const outputLabels = [`pin${primaryPinNumber}`] + + if (requestedPinNumber != null && primaryPinNumber !== requestedPinNumber) { + const duplicateIndex = + (duplicateNumericCounts.get(requestedPinNumber) ?? 0) + 1 + duplicateNumericCounts.set(requestedPinNumber, duplicateIndex) + outputLabels.push(`pin${requestedPinNumber}_alt${duplicateIndex}`) + } + + for (const label of labels) { + if (/^\d+$/.test(label)) continue + + const totalOccurrences = nonNumericTotals.get(label) ?? 0 + if (totalOccurrences <= 1) { + outputLabels.push(label) + continue + } + + const occurrenceIndex = (nonNumericSeenCounts.get(label) ?? 0) + 1 + nonNumericSeenCounts.set(label, occurrenceIndex) + outputLabels.push(`${label}${occurrenceIndex}`) + } + + return outputLabels + }) +} diff --git a/tests/websafe/browser-bundle-websafe.test.ts b/tests/websafe/browser-bundle-websafe.test.ts new file mode 100644 index 00000000..290f15cf --- /dev/null +++ b/tests/websafe/browser-bundle-websafe.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test" +import { readFile } from "node:fs/promises" + +const forbiddenBundlePatterns = [ + { pattern: /_tty:\s*function/, description: "TTY require shim" }, + { pattern: /node_modules\/pretty\/index\.js/, description: "pretty package" }, + { pattern: /graphics-debug/, description: "graphics-debug dependency" }, +] + +describe("browser bundle websafe guard", () => { + test("browser build does not include known non-websafe runtime baggage", async () => { + await Bun.$`bun run build`.quiet() + + const browserBundle = await readFile("dist/browser/index.js", "utf8") + + for (const { pattern, description } of forbiddenBundlePatterns) { + expect(browserBundle).not.toMatch(pattern) + } + }, 30_000) +})