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
28 changes: 28 additions & 0 deletions endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", "/"))
Expand Down Expand Up @@ -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" },
Expand Down
31 changes: 31 additions & 0 deletions lib/getDebugHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Comment on lines +36 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve /api prefix in circuit JSON download link

The download URL is built from ctx.url and then its pathname is overwritten with /circuit_json. Because ctx.url has already had the /api prefix stripped for internal routing, this produces a link to /circuit_json instead of /api/circuit_json. In the deployed Next.js environment the API handler only lives under /api, so clicking the link rendered on /api?debug=1 will 404 and never reach the new endpoint, even though the Bun test passes (it serves the handler at /). The generated URL should retain the original request’s path prefix or explicitly add /api/circuit_json so the download works in production.

Useful? React with 👍 / 👎.


let decodedFsMap: Record<string, string> | null = null
let decompressedCode: string | null = null
let decompressionError: string | null = null
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -194,6 +215,16 @@ export function getDebugHtml(ctx: RequestContext): string {
</tbody>
</table>
</section>
<section>
<h2>Downloads</h2>
<a
class="download-link"
href="${escapeHtml(circuitJsonDownloadUrl.toString())}"
download="circuit.json"
>
Download Circuit JSON
</a>
</section>
<section>
<h2>Context Summary</h2>
<pre>${escapeHtml(JSON.stringify(contextSummary, null, 2))}</pre>
Expand Down
37 changes: 35 additions & 2 deletions tests/debug-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ export default () => (
`)

const fsMap = {
"index.tsx":
"export const Example = () => {\n return <div>Hello</div>\n}\n",
"index.tsx": `export default () => (
<board width="10mm" height="10mm">
<resistor resistance="1k" footprint="0402" name="R1" />
</board>
)
`,
}

const response = await fetch(
Expand All @@ -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(/&amp;/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)
})