Skip to content

Commit ae3e5e3

Browse files
committed
feat: dynamically load export converters from jsdelivr CDN
1 parent a539877 commit ae3e5e3

12 files changed

Lines changed: 405 additions & 76 deletions

File tree

bun.lock

Lines changed: 8 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/components/BomTable/BomTable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { convertCircuitJsonToBomRows } from "circuit-json-to-bom-csv"
21
import { useEffect, useState } from "react"
32
import type React from "react"
3+
import { loadBomCsvConverter } from "../../optional-features/exporting/dynamic-converters"
44
import { getBomCellDescriptors, getBomMetadata } from "./bom-table.columns"
55
import type { BomRow, BomTableProps } from "./bom-table.types"
66

@@ -16,6 +16,7 @@ export const BomTable: React.FC<BomTableProps> = ({ circuitJson }) => {
1616
setError(null)
1717
setRows(null)
1818

19+
const { convertCircuitJsonToBomRows } = await loadBomCsvConverter()
1920
const bomRows = await convertCircuitJsonToBomRows({
2021
circuitJson,
2122
})

lib/components/BomTable/bom-table.types.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import type { AnyCircuitElement } from "circuit-json"
2-
import type { convertCircuitJsonToBomRows } from "circuit-json-to-bom-csv"
32
import type React from "react"
43

54
export interface BomTableProps {
65
circuitJson: AnyCircuitElement[]
76
}
87

9-
export type BomRow = Awaited<
10-
ReturnType<typeof convertCircuitJsonToBomRows>
11-
>[number]
8+
export type BomRow = {
9+
designator?: string
10+
comment?: string
11+
value?: string
12+
footprint?: string
13+
supplier_part_number_columns?: Record<string, string>
14+
extra_columns?: Record<string, string>
15+
manufacturer_mpn_pairs?: Array<{
16+
manufacturer: string
17+
mpn: string
18+
}>
19+
}
1220

1321
export type BomMetadata = {
1422
extraColumnNames: string[]
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Centralized dynamic converter loader.
3+
*
4+
* Instead of bundling heavy converter packages, each converter is lazy-loaded
5+
* on demand from the jsdelivr CDN when the user actually requests an export.
6+
* Modules are cached after first successful load; failed loads are cleared
7+
* so a retry can succeed.
8+
*/
9+
10+
// ---------------------------------------------------------------------------
11+
// Module type declarations
12+
// ---------------------------------------------------------------------------
13+
14+
type GerberConverterModule = {
15+
stringifyGerberCommandLayers: (...args: any[]) => Record<string, string>
16+
convertSoupToGerberCommands: (...args: any[]) => any
17+
convertSoupToExcellonDrillCommands: (...args: any[]) => any
18+
stringifyExcellonDrill: (...args: any[]) => string
19+
}
20+
21+
type BomCsvConverterModule = {
22+
convertCircuitJsonToBomRows: (...args: any[]) => Promise<any[]> | any[]
23+
convertBomRowsToCsv: (...args: any[]) => Promise<string> | string
24+
}
25+
26+
type PnpCsvConverterModule = {
27+
convertCircuitJsonToPickAndPlaceCsv: (
28+
...args: any[]
29+
) => Promise<string> | string
30+
}
31+
32+
type KicadConverterModule = {
33+
CircuitJsonToKicadPcbConverter: new (
34+
...args: any[]
35+
) => {
36+
runUntilFinished: () => void
37+
getOutputString: () => string
38+
getModel3dSourcePaths: () => string[]
39+
}
40+
CircuitJsonToKicadSchConverter: new (
41+
...args: any[]
42+
) => {
43+
runUntilFinished: () => void
44+
getOutputString: () => string
45+
}
46+
CircuitJsonToKicadProConverter: new (
47+
...args: any[]
48+
) => {
49+
runUntilFinished: () => void
50+
getOutputString: () => string
51+
}
52+
CircuitJsonToKicadLibraryConverter: new (
53+
...args: any[]
54+
) => {
55+
runUntilFinished: () => void
56+
getOutput: () => {
57+
kicadSymString: string
58+
footprints: Array<{ footprintName: string; kicadModString: string }>
59+
model3dSourcePaths: string[]
60+
fpLibTableString: string
61+
symLibTableString: string
62+
}
63+
}
64+
resolveAndLoadKicad3dModelFiles: (...args: any[]) => Promise<void>
65+
}
66+
67+
type GltfConverterModule = {
68+
convertCircuitJsonToGltf: (
69+
...args: any[]
70+
) => Promise<ArrayBuffer> | ArrayBuffer
71+
}
72+
73+
type StepConverterModule = {
74+
circuitJsonToStep: (...args: any[]) => Promise<string> | string
75+
}
76+
77+
type LbrnConverterModule = {
78+
convertCircuitJsonToLbrn: (...args: any[]) => Promise<{ toXml: () => string }>
79+
}
80+
81+
// ---------------------------------------------------------------------------
82+
// Package → type mapping
83+
// ---------------------------------------------------------------------------
84+
85+
type ConverterModules = {
86+
"circuit-json-to-gerber": GerberConverterModule
87+
"circuit-json-to-bom-csv": BomCsvConverterModule
88+
"circuit-json-to-pnp-csv": PnpCsvConverterModule
89+
"circuit-json-to-kicad": KicadConverterModule
90+
"circuit-json-to-gltf": GltfConverterModule
91+
"circuit-json-to-step": StepConverterModule
92+
"circuit-json-to-lbrn": LbrnConverterModule
93+
}
94+
95+
export type ConverterPackageName = keyof ConverterModules
96+
97+
// ---------------------------------------------------------------------------
98+
// Pinned CDN URLs (jsdelivr ESM)
99+
// ---------------------------------------------------------------------------
100+
101+
const CONVERTER_CDN_URLS: Record<ConverterPackageName, string> = {
102+
"circuit-json-to-gerber":
103+
"https://cdn.jsdelivr.net/npm/circuit-json-to-gerber@0.0.76/+esm",
104+
"circuit-json-to-bom-csv":
105+
"https://cdn.jsdelivr.net/npm/circuit-json-to-bom-csv@0.0.9/+esm",
106+
"circuit-json-to-pnp-csv":
107+
"https://cdn.jsdelivr.net/npm/circuit-json-to-pnp-csv@0.0.8/+esm",
108+
"circuit-json-to-kicad":
109+
"https://cdn.jsdelivr.net/npm/circuit-json-to-kicad@0.0.147/+esm",
110+
"circuit-json-to-gltf":
111+
"https://cdn.jsdelivr.net/npm/circuit-json-to-gltf@0.0.102/+esm",
112+
"circuit-json-to-step":
113+
"https://cdn.jsdelivr.net/npm/circuit-json-to-step@0.0.33/+esm",
114+
"circuit-json-to-lbrn":
115+
"https://cdn.jsdelivr.net/npm/circuit-json-to-lbrn@0.0.82/+esm",
116+
}
117+
118+
// ---------------------------------------------------------------------------
119+
// Loader with singleton-promise cache + failure retry
120+
// ---------------------------------------------------------------------------
121+
122+
export type ConverterModuleImporter = <TModule>(
123+
specifier: string,
124+
) => Promise<TModule>
125+
126+
const converterModulePromises = new Map<
127+
ConverterPackageName,
128+
Promise<ConverterModules[ConverterPackageName]>
129+
>()
130+
131+
const importConverterModule: ConverterModuleImporter = (specifier) =>
132+
import(/* @vite-ignore */ specifier)
133+
134+
export const getConverterCdnUrl = (packageName: ConverterPackageName) =>
135+
CONVERTER_CDN_URLS[packageName]
136+
137+
export const clearConverterModuleCache = () => {
138+
converterModulePromises.clear()
139+
}
140+
141+
export const loadConverterModule = async <TName extends ConverterPackageName>(
142+
packageName: TName,
143+
importer: ConverterModuleImporter = importConverterModule,
144+
): Promise<ConverterModules[TName]> => {
145+
let modulePromise = converterModulePromises.get(packageName) as
146+
| Promise<ConverterModules[TName]>
147+
| undefined
148+
149+
if (!modulePromise) {
150+
modulePromise = importer<ConverterModules[TName]>(
151+
getConverterCdnUrl(packageName),
152+
).catch((error) => {
153+
// Clear cache on failure so next call retries
154+
converterModulePromises.delete(packageName)
155+
throw error
156+
})
157+
converterModulePromises.set(
158+
packageName,
159+
modulePromise as Promise<ConverterModules[ConverterPackageName]>,
160+
)
161+
}
162+
163+
return modulePromise
164+
}
165+
166+
// ---------------------------------------------------------------------------
167+
// Convenience loaders (one per converter)
168+
// ---------------------------------------------------------------------------
169+
170+
export const loadGerberConverter = () =>
171+
loadConverterModule("circuit-json-to-gerber")
172+
173+
export const loadBomCsvConverter = () =>
174+
loadConverterModule("circuit-json-to-bom-csv")
175+
176+
export const loadPnpCsvConverter = () =>
177+
loadConverterModule("circuit-json-to-pnp-csv")
178+
179+
export const loadKicadConverter = () =>
180+
loadConverterModule("circuit-json-to-kicad")
181+
182+
export const loadGltfConverter = () =>
183+
loadConverterModule("circuit-json-to-gltf")
184+
185+
export const loadStepConverter = () =>
186+
loadConverterModule("circuit-json-to-step")
187+
188+
export const loadLbrnConverter = () =>
189+
loadConverterModule("circuit-json-to-lbrn")

lib/optional-features/exporting/formats/export-fabrication-files.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { AnyCircuitElement } from "circuit-json"
22
import JSZip from "jszip"
3-
import importer from "@tscircuit/internal-dynamic-import"
43
import {
5-
convertCircuitJsonToBomRows,
6-
convertBomRowsToCsv,
7-
} from "circuit-json-to-bom-csv"
8-
import { convertCircuitJsonToPickAndPlaceCsv } from "circuit-json-to-pnp-csv"
4+
loadGerberConverter,
5+
loadBomCsvConverter,
6+
loadPnpCsvConverter,
7+
} from "../dynamic-converters"
98
import { openForDownload } from "../open-for-download"
109

1110
export const exportFabricationFiles = async ({
@@ -17,12 +16,17 @@ export const exportFabricationFiles = async ({
1716
}) => {
1817
const zip = new JSZip()
1918

19+
const [gerberMod, bomMod, pnpMod] = await Promise.all([
20+
loadGerberConverter(),
21+
loadBomCsvConverter(),
22+
loadPnpCsvConverter(),
23+
])
2024
const {
2125
stringifyGerberCommandLayers,
2226
convertSoupToGerberCommands,
2327
convertSoupToExcellonDrillCommands,
2428
stringifyExcellonDrill,
25-
} = await importer("circuit-json-to-gerber")
29+
} = gerberMod
2630

2731
// Filter out error and warning elements for gerber/drill generation
2832
const filteredCircuitJson = circuitJson.filter(
@@ -49,12 +53,12 @@ export const exportFabricationFiles = async ({
4953
zip.file("gerber/drill.drl", drillFileContents)
5054

5155
// Generate BOM CSV
52-
const bomRows = await convertCircuitJsonToBomRows({ circuitJson })
53-
const bomCsv = await convertBomRowsToCsv(bomRows)
56+
const bomRows = await bomMod.convertCircuitJsonToBomRows({ circuitJson })
57+
const bomCsv = await bomMod.convertBomRowsToCsv(bomRows)
5458
zip.file("bom.csv", bomCsv)
5559

5660
// Generate Pick and Place CSV
57-
const pnpCsv = await convertCircuitJsonToPickAndPlaceCsv(circuitJson)
61+
const pnpCsv = await pnpMod.convertCircuitJsonToPickAndPlaceCsv(circuitJson)
5862
zip.file("pick_and_place.csv", pnpCsv)
5963

6064
// Generate and download the zip file

lib/optional-features/exporting/formats/export-glb.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CircuitJson } from "circuit-json"
22
import { openForDownload } from "../open-for-download"
3-
import importer from "@tscircuit/internal-dynamic-import"
3+
import { loadGltfConverter } from "../dynamic-converters"
44

55
export const exportGlb = async ({
66
circuitJson,
@@ -11,7 +11,7 @@ export const exportGlb = async ({
1111
}) => {
1212
let blob: Blob
1313
try {
14-
const { convertCircuitJsonToGltf } = await importer("circuit-json-to-gltf")
14+
const { convertCircuitJsonToGltf } = await loadGltfConverter()
1515

1616
console.log("convertCircuitJsonToGltf", convertCircuitJsonToGltf)
1717

lib/optional-features/exporting/formats/export-kicad-library.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CircuitJson } from "circuit-json"
22
import JSZip from "jszip"
3-
import importer from "@tscircuit/internal-dynamic-import"
3+
import { loadKicadConverter } from "../dynamic-converters"
44
import { openForDownload } from "../open-for-download"
55

66
export const createKicadLibraryZip = async ({
@@ -10,9 +10,7 @@ export const createKicadLibraryZip = async ({
1010
circuitJson: CircuitJson
1111
libraryName: string
1212
}) => {
13-
const { CircuitJsonToKicadLibraryConverter } = await importer(
14-
"circuit-json-to-kicad",
15-
)
13+
const { CircuitJsonToKicadLibraryConverter } = await loadKicadConverter()
1614
const libConverter = new CircuitJsonToKicadLibraryConverter(
1715
circuitJson as any,
1816
{

lib/optional-features/exporting/formats/export-kicad-project.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CircuitJson } from "circuit-json"
22
import JSZip from "jszip"
3-
import importer from "@tscircuit/internal-dynamic-import"
3+
import { loadKicadConverter } from "../dynamic-converters"
44
import { openForDownload } from "../open-for-download"
55

66
export const createKicadProjectZip = async ({
@@ -15,7 +15,7 @@ export const createKicadProjectZip = async ({
1515
CircuitJsonToKicadSchConverter,
1616
CircuitJsonToKicadProConverter,
1717
resolveAndLoadKicad3dModelFiles,
18-
} = await importer("circuit-json-to-kicad")
18+
} = await loadKicadConverter()
1919
const schConverter = new CircuitJsonToKicadSchConverter(circuitJson as any)
2020
schConverter.runUntilFinished()
2121
const schContent = schConverter.getOutputString()

lib/optional-features/exporting/formats/export-lbrn.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CircuitJson } from "circuit-json"
22
import { openForDownload } from "../open-for-download"
3-
import importer from "@tscircuit/internal-dynamic-import"
3+
import { loadLbrnConverter } from "../dynamic-converters"
44
import { toast } from "lib/utils/toast"
55

66
export interface LbrnExportOptions {
@@ -19,7 +19,7 @@ export const exportLbrn = async ({
1919
}) => {
2020
try {
2121
// Convert Circuit JSON to LBRN format
22-
const { convertCircuitJsonToLbrn } = await importer("circuit-json-to-lbrn")
22+
const { convertCircuitJsonToLbrn } = await loadLbrnConverter()
2323
const lbrnProject = await convertCircuitJsonToLbrn(circuitJson, {
2424
includeSilkscreen: options.includeSilkscreen ?? false,
2525
includeOxidationCleaningLayer:

lib/optional-features/exporting/formats/export-step.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CircuitJson } from "circuit-json"
22
import { openForDownload } from "../open-for-download"
33
import { toast } from "lib/utils/toast"
4-
import importer from "@tscircuit/internal-dynamic-import"
4+
import { loadStepConverter } from "../dynamic-converters"
55

66
export const exportStep = async ({
77
circuitJson,
@@ -10,7 +10,7 @@ export const exportStep = async ({
1010
circuitJson: CircuitJson
1111
projectName: string
1212
}) => {
13-
const { circuitJsonToStep } = await importer("circuit-json-to-step")
13+
const { circuitJsonToStep } = await loadStepConverter()
1414
// Extract board dimensions from circuit JSON
1515
const pcbBoard = circuitJson.find((el) => el.type === "pcb_board")
1616

0 commit comments

Comments
 (0)