From 321ee9ee768ae1e4470eb70d5c4983bda6205db4 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Mon, 13 Oct 2025 10:37:24 -0700 Subject: [PATCH] Add circuit JSON download endpoint for debug page --- endpoint.ts | 28 ++++++++++++++++++++++++++++ lib/getDebugHtml.ts | 31 +++++++++++++++++++++++++++++++ tests/debug-page.test.ts | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/endpoint.ts b/endpoint.ts index b241226..f33f129 100644 --- a/endpoint.ts +++ b/endpoint.ts @@ -16,6 +16,7 @@ import { pinoutPngHandler } from "./handlers/pinout-png" import { threeDSvgHandler } from "./handlers/three-d-svg" import { threeDPngHandler } from "./handlers/three-d-png" import { getDebugHtml } from "./lib/getDebugHtml" +import { getCircuitJsonFromContext } from "./lib/getCircuitJson" export default async (req: Request) => { const url = new URL(req.url.replace("/api", "/")) @@ -47,6 +48,33 @@ export default async (req: Request) => { } const ctx = ctxOrError + if (url.pathname === "/circuit_json") { + const circuitSource = url.searchParams.get("circuit_source") + const preferCompressedCode = + circuitSource === "code" && ctx.compressedCode != null + const ctxForDownload = preferCompressedCode + ? { ...ctx, fsMap: undefined } + : ctx + + try { + const circuitJson = await getCircuitJsonFromContext(ctxForDownload) + return new Response(JSON.stringify(circuitJson, null, 2), { + headers: { + "Content-Type": "application/json", + "Content-Disposition": 'attachment; filename="circuit.json"', + }, + }) + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Failed to generate circuit JSON" + const status = errorMessage === "No circuit data provided" ? 400 : 500 + return new Response(JSON.stringify({ ok: false, error: errorMessage }), { + status, + headers: { "Content-Type": "application/json" }, + }) + } + } + if (url.searchParams.has("debug")) { return new Response(getDebugHtml(ctx), { headers: { "Content-Type": "text/html" }, diff --git a/lib/getDebugHtml.ts b/lib/getDebugHtml.ts index 5337654..1ff3ef7 100644 --- a/lib/getDebugHtml.ts +++ b/lib/getDebugHtml.ts @@ -33,6 +33,13 @@ export function getDebugHtml(ctx: RequestContext): string { ([key, value]) => ({ key, value }), ) + const circuitJsonDownloadUrl = new URL(ctx.url.toString()) + circuitJsonDownloadUrl.pathname = "/circuit_json" + circuitJsonDownloadUrl.searchParams.delete("debug") + if (ctx.compressedCode && ctx.fsMap) { + circuitJsonDownloadUrl.searchParams.set("circuit_source", "code") + } + let decodedFsMap: Record | null = null let decompressedCode: string | null = null let decompressionError: string | null = null @@ -132,6 +139,20 @@ export function getDebugHtml(ctx: RequestContext): string { section { margin-bottom: 32px; } + .download-link { + display: inline-block; + margin-top: 8px; + padding: 8px 12px; + background: #1e1e1e; + border-radius: 4px; + color: #4dabf7; + text-decoration: none; + border: 1px solid #333; + } + .download-link:hover { + background: #262626; + border-color: #4dabf7; + } table { width: 100%; border-collapse: collapse; @@ -194,6 +215,16 @@ export function getDebugHtml(ctx: RequestContext): string { +
+

Downloads

+ + Download Circuit JSON + +

Context Summary

${escapeHtml(JSON.stringify(contextSummary, null, 2))}
diff --git a/tests/debug-page.test.ts b/tests/debug-page.test.ts index c182eb0..dcf8a34 100644 --- a/tests/debug-page.test.ts +++ b/tests/debug-page.test.ts @@ -14,8 +14,12 @@ export default () => ( `) const fsMap = { - "index.tsx": - "export const Example = () => {\n return
Hello
\n}\n", + "index.tsx": `export default () => ( + + + +) +`, } const response = await fetch( @@ -35,4 +39,33 @@ export default () => ( expect(html).toContain("Decoded fs_map") expect(html).toContain("index.tsx") expect(html).toContain("newline-symbol") + expect(html).toContain("Download Circuit JSON") + + const downloadLinkMatch = html.match( + /href=\"([^\"]+)\"[^>]*>\s*Download Circuit JSON/, + ) + + expect(downloadLinkMatch).not.toBeNull() + + const downloadHref = downloadLinkMatch![1].replace(/&/g, "&") + const downloadUrl = new URL(downloadHref, serverUrl) + const circuitJsonResponse = await fetch(downloadUrl) + + if (circuitJsonResponse.status !== 200) { + const errorBody = await circuitJsonResponse.text() + throw new Error( + `circuit_json download failed: ${circuitJsonResponse.status} ${errorBody}`, + ) + } + + expect(circuitJsonResponse.headers.get("content-type")).toBe( + "application/json", + ) + expect( + circuitJsonResponse.headers.get("content-disposition") ?? "", + ).toContain("circuit.json") + + const circuitJson = await circuitJsonResponse.json() + expect(Array.isArray(circuitJson)).toBe(true) + expect(circuitJson.length).toBeGreaterThan(0) })