diff --git a/bun.lock b/bun.lock index 34b161407..4a8b4966f 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@tscircuit/check-shorts": "https://jscdn.tscircuit.com/@tscircuit/check-shorts/0.0.19.tgz", "@tscircuit/circuit-json-placement-analysis": "^0.0.9", "@tscircuit/circuit-json-routing-analysis": "^0.0.8", - "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#db2cd1ca2ce51490e3230e53c3d7b361eeb14446", + "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#690e01c161f54a72f24c848e96a74c2584690f86", "@tscircuit/circuit-json-util": "^0.0.112", "@tscircuit/eval": "^0.0.1016", "@tscircuit/fake-snippets": "^0.0.182", @@ -396,7 +396,7 @@ "@tscircuit/circuit-json-routing-analysis": ["@tscircuit/circuit-json-routing-analysis@0.0.8", "", { "peerDependencies": { "circuit-json": "*", "typescript": "^5" } }, "sha512-mZxRSUbqVb6iZ47ZhNL3mDL7BY9kUcbLg7o4f8Jn5amkDGvmTo52547JQ5ui/6WSJHXmaW7hmRVTEj6PlivskQ=="], - "@tscircuit/circuit-json-schematic-placement-analysis": ["@tscircuit/circuit-json-schematic-placement-analysis@github:tscircuit/circuit-json-schematic-placement-analysis#db2cd1c", { "dependencies": { "@tscircuit/circuit-json-util": "^0.0.94" }, "peerDependencies": { "circuit-json": "*", "typescript": "^5" } }, "tscircuit-circuit-json-schematic-placement-analysis-db2cd1c", "sha512-nKAqBijEMIHwsU5+4lq3TS26TU+wy0oB/cyaoW3HTNRzFMHSDhM0artGNxu2FrLx9/YABeIBe9C8x0OWwF9SIg=="], + "@tscircuit/circuit-json-schematic-placement-analysis": ["@tscircuit/circuit-json-schematic-placement-analysis@github:tscircuit/circuit-json-schematic-placement-analysis#690e01c", { "dependencies": { "@tscircuit/circuit-json-util": "^0.0.94", "circuit-json-to-connectivity-map": "^0.0.23", "circuit-to-svg": "^0.0.370", "stack-svgs": "^0.0.1" }, "peerDependencies": { "circuit-json": "*", "typescript": "^5" } }, "tscircuit-circuit-json-schematic-placement-analysis-690e01c"], "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.112", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-3NvAa6VWWJ1W+7y/9Pf5/nn82sdq97ON++Dr66oyoyXdu6qkZY51wKqh019WHZHSAUoVudHaR78C7KZ5pH5GlA=="], @@ -1294,6 +1294,10 @@ "@tscircuit/circuit-json-schematic-placement-analysis/@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.94", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-kEYV6LzcZbRuw43IxsZ1cZL2pUx4nF07MYAHHhY9s90UzKYaIYfZ1q11s+F2wNwKecCcSyTUoAwWeqazLQEyVQ=="], + "@tscircuit/circuit-json-schematic-placement-analysis/circuit-json-to-connectivity-map": ["circuit-json-to-connectivity-map@0.0.23", "", { "dependencies": { "@tscircuit/math-utils": "^0.0.9" }, "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-DSOiXaXOTvjU+7et8ITXb2LjgKto6cQzLv3hReYdXuUNtLw2GVnpOly1G83VcIBcSQ4hRVHI4VMKRyZB3XVzdg=="], + + "@tscircuit/circuit-json-schematic-placement-analysis/circuit-to-svg": ["circuit-to-svg@0.0.370", "", { "dependencies": { "@types/node": "^22.5.5", "bun-types": "^1.1.40", "calculate-elbow": "0.0.12", "debug": "^4.4.3", "svg-path-commander": "^2.1.11", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" }, "peerDependencies": { "@tscircuit/alphabet": "*" } }, "sha512-kepu8upgDmC38Y/wce9WayP9zau8a55MQPZwide7dcuXXMReSkrqbs6Xu9j0c7DIReI7xJKlLWAPfh2+gJ3KSg=="], + "@tscircuit/copper-pour-solver/@tscircuit/manifold-2d": ["@tscircuit/manifold-2d@0.0.6", "", {}, "sha512-kYny1hDwPHOwRfMQtbfLh0nvCQ7nM91kkHy7pnj1UrqM6rcgDymZVCZB7ib5Cx1aZFIs8xCCF7lA51tq+giDcw=="], "@tscircuit/core/calculate-packing": ["calculate-packing@0.0.77", "", { "peerDependencies": { "@tscircuit/circuit-json-util": "*", "typescript": "^5" } }, "sha512-TGFUN5Di0hZTua+ReW1Kx1CjmN4KYSUl3aS0QW0jrgxcs1Ww45lwSGvAxFrXE06PyohKz4uQRz7j5R/UIv7PsA=="], @@ -1458,6 +1462,10 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@tscircuit/circuit-json-schematic-placement-analysis/circuit-json-to-connectivity-map/@tscircuit/math-utils": ["@tscircuit/math-utils@0.0.9", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-sPzfXndijet8z29X6f5vnSZddiso2tRg7m6rB+268bVj60mxnxUMD14rKuMlLn6n84fMOpD/X7pRTZUfi6M+Tg=="], + + "@tscircuit/circuit-json-schematic-placement-analysis/circuit-to-svg/@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], + "@tscircuit/create-fdm-enclosure/graphics-debug/transformation-matrix": ["transformation-matrix@3.1.0", "", {}, "sha512-oYubRWTi2tYFHAL2J8DLvPIqIYcYZ0fSOi2vmSy042Ho4jBW2ce6VP7QfD44t65WQz6bw5w1Pk22J7lcUpaTKA=="], "@tscircuit/runframe/@tscircuit/core/calculate-cell-boundaries": ["calculate-cell-boundaries@0.0.21", "", { "dependencies": { "@tscircuit/solver-utils": "^0.0.19", "lucide-react": "^1.17.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-/D7ND2cl8gyBWp/1bih562JDKeGymSnBQrwhjsSJ32f39c9/Qa4N4zR4lD9zH2Aqy9qYlPWa7ToRJw68HiangA=="], @@ -1572,6 +1580,8 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@tscircuit/circuit-json-schematic-placement-analysis/circuit-to-svg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@tscircuit/runframe/@tscircuit/core/calculate-cell-boundaries/@tscircuit/solver-utils": ["@tscircuit/solver-utils@0.0.19", "", { "peerDependencies": { "graphics-debug": "*", "typescript": "^5" } }, "sha512-UPUVgRRIrylSMJfKw7QwhSCvlODD+uAAwXc9jpOG06ipqteeeLRkj/HtHns31KQfLjRCSsGTvxMYBjmhaDDLGg=="], "@tscircuit/runframe/@tscircuit/core/calculate-cell-boundaries/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], diff --git a/cli/check/schematic-placement/register.ts b/cli/check/schematic-placement/register.ts index 7f307b8d0..1d0960414 100644 --- a/cli/check/schematic-placement/register.ts +++ b/cli/check/schematic-placement/register.ts @@ -1,4 +1,9 @@ -import { analyzeSchematicPlacement } from "@tscircuit/circuit-json-schematic-placement-analysis" +import { mkdir, readdir, rm, writeFile } from "node:fs/promises" +import path from "node:path" +import { + analyzeSchematicPlacement, + createSchematicPlacementIssueArtifacts, +} from "@tscircuit/circuit-json-schematic-placement-analysis" import type { PlatformConfig } from "@tscircuit/props" import type { AnyCircuitElement, CircuitJson } from "circuit-json" import type { Command } from "commander" @@ -16,7 +21,14 @@ export const checkSchematicPlacement = async (file?: string) => { allowPrebuiltCircuitJson: true, })) as AnyCircuitElement[] - return analyzeSchematicPlacement(circuitJson as CircuitJson).getString() + const analysis = analyzeSchematicPlacement(circuitJson as CircuitJson) + return { + output: analysis.getString(), + artifacts: createSchematicPlacementIssueArtifacts( + circuitJson as CircuitJson, + { analysis }, + ), + } } export const registerCheckSchematicPlacement = (program: Command) => { @@ -27,8 +39,28 @@ export const registerCheckSchematicPlacement = (program: Command) => { .argument("[file]", "Path to the entry file") .action(async (file?: string) => { try { - const output = await checkSchematicPlacement(file) + const { output, artifacts } = await checkSchematicPlacement(file) console.log(output) + const outputDir = path.resolve("dist", "schematic-placement") + // Remove only files generated by this command, so resolved issues from + // previous runs do not remain alongside the current reports. + const previousFiles = await readdir(outputDir, { + withFileTypes: true, + }).catch((error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") return [] + throw error + }) + for (const entry of previousFiles) { + if (entry.isFile() && /^issue-\d+-[A-Za-z]+\.svg$/.test(entry.name)) { + await rm(path.join(outputDir, entry.name)) + } + } + if (artifacts.length > 0) await mkdir(outputDir, { recursive: true }) + for (const artifact of artifacts) { + const outputPath = path.join(outputDir, artifact.fileName) + await writeFile(outputPath, artifact.content) + console.log(`Schematic placement artifact written to ${outputPath}`) + } } catch (error) { console.error(error instanceof Error ? error.message : String(error)) process.exit(1) diff --git a/package.json b/package.json index 854dca97a..faa221245 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@tscircuit/check-shorts": "https://jscdn.tscircuit.com/@tscircuit/check-shorts/0.0.19.tgz", "@tscircuit/circuit-json-placement-analysis": "^0.0.9", "@tscircuit/circuit-json-routing-analysis": "^0.0.8", - "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#db2cd1ca2ce51490e3230e53c3d7b361eeb14446", + "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#690e01c161f54a72f24c848e96a74c2584690f86", "@tscircuit/circuit-json-util": "^0.0.112", "@tscircuit/eval": "^0.0.1016", "@tscircuit/fake-snippets": "^0.0.182", diff --git a/tests/cli/check/check-schematic-placement.test.ts b/tests/cli/check/check-schematic-placement.test.ts index 73dd50179..94c9e1390 100644 --- a/tests/cli/check/check-schematic-placement.test.ts +++ b/tests/cli/check/check-schematic-placement.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { rm, writeFile } from "node:fs/promises" +import { readFile, readdir, rm, writeFile } from "node:fs/promises" import path from "node:path" import { checkSchematicPlacement } from "../../../cli/check/schematic-placement/register" import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture" @@ -100,7 +100,7 @@ const misalignedPinPairsCircuitJson = [ ] test("tsci check schematic-placement prints schematic placement analysis", async () => { - const { runCommand } = await getCliTestFixture() + const { runCommand, tmpDir } = await getCliTestFixture() const circuitPath = path.join( process.cwd(), `tmp-check-schematic-placement-${Date.now()}-${Math.random().toString(36).slice(2)}.tsx`, @@ -117,10 +117,37 @@ test("tsci check schematic-placement prints schematic placement analysis", async expect(exitCode).toBe(0) expect(stderr).toBe("") - expect(stdout.trim()).toContain(expected) + expect(stdout.trim()).toContain(expected.output) expect(stdout).toContain("") expect(stdout).toContain('componentName="R1"') expect(stdout).toContain('componentName="C1"') + const outputDir = path.join(tmpDir, "dist", "schematic-placement") + expect((await readdir(outputDir)).sort()).toEqual( + expected.artifacts.map((artifact) => artifact.fileName).sort(), + ) + expect(expected.artifacts.length).toBeGreaterThan(0) + for (const artifact of expected.artifacts) { + const svg = await readFile( + path.join(outputDir, artifact.fileName), + "utf8", + ) + expect(svg).toBe(artifact.content) + expect(svg.match(/data-issue-index=/g)).toHaveLength(1) + expect(stdout).toContain(path.join(outputDir, artifact.fileName)) + } + // A later clean result removes stale generated reports but keeps user files. + await writeFile(path.join(outputDir, "notes.svg"), "keep this") + const cleanCircuitPath = path.join(tmpDir, "clean.circuit.json") + await writeFile(cleanCircuitPath, "[]") + const clean = await runCommand( + `tsci check schematic-placement ${cleanCircuitPath}`, + ) + expect(clean.exitCode).toBe(0) + expect(clean.stderr).toBe("") + expect(await readdir(outputDir)).toEqual(["notes.svg"]) + expect(await readFile(path.join(outputDir, "notes.svg"), "utf8")).toBe( + "keep this", + ) } finally { await rm(circuitPath, { force: true }) } @@ -141,3 +168,20 @@ test("tsci check schematic-placement reports a better vertical pin alignment", a ' { + const { runCommand, tmpDir } = await getCliTestFixture() + const circuitPath = path.join(tmpDir, "clean.circuit.json") + await writeFile(circuitPath, "[]") + const { stdout, stderr, exitCode } = await runCommand( + `tsci check schematic-placement ${circuitPath}`, + ) + expect(exitCode).toBe(0) + expect(stderr).toBe("") + expect(stdout.trim()).toBe("") + expect( + await readdir(path.join(tmpDir, "dist", "schematic-placement")).catch( + (error) => error.code, + ), + ).toBe("ENOENT") +})