Skip to content

Commit 01208b4

Browse files
committed
feat: dynamically load export converters from jsdelivr CDN
1 parent ac4302e commit 01208b4

12 files changed

Lines changed: 448 additions & 78 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: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
declare global {
127+
var tscircuitDynamicModules: Record<string, unknown> | undefined
128+
}
129+
130+
const converterModulePromises = new Map<
131+
ConverterPackageName,
132+
Promise<ConverterModules[ConverterPackageName]>
133+
>()
134+
135+
const importConverterModule: ConverterModuleImporter = (specifier) =>
136+
import(/* @vite-ignore */ specifier)
137+
138+
export const getConverterCdnUrl = (packageName: ConverterPackageName) =>
139+
CONVERTER_CDN_URLS[packageName]
140+
141+
export const clearConverterModuleCache = () => {
142+
converterModulePromises.clear()
143+
}
144+
145+
const registerConverterModule = <TName extends ConverterPackageName>(
146+
packageName: TName,
147+
module: ConverterModules[TName],
148+
) => {
149+
globalThis.tscircuitDynamicModules ??= {}
150+
globalThis.tscircuitDynamicModules[packageName] = module
151+
return module
152+
}
153+
154+
export const loadConverterModule = async <TName extends ConverterPackageName>(
155+
packageName: TName,
156+
importer: ConverterModuleImporter = importConverterModule,
157+
): Promise<ConverterModules[TName]> => {
158+
let modulePromise = converterModulePromises.get(packageName) as
159+
| Promise<ConverterModules[TName]>
160+
| undefined
161+
162+
if (!modulePromise) {
163+
modulePromise = importer<ConverterModules[TName]>(
164+
getConverterCdnUrl(packageName),
165+
)
166+
.then((module) => registerConverterModule(packageName, module))
167+
.catch((error) => {
168+
// Clear cache on failure so next call retries
169+
converterModulePromises.delete(packageName)
170+
throw error
171+
})
172+
converterModulePromises.set(
173+
packageName,
174+
modulePromise as Promise<ConverterModules[ConverterPackageName]>,
175+
)
176+
}
177+
178+
return modulePromise
179+
}
180+
181+
// ---------------------------------------------------------------------------
182+
// Convenience loaders (one per converter)
183+
// ---------------------------------------------------------------------------
184+
185+
export const loadGerberConverter = () =>
186+
loadConverterModule("circuit-json-to-gerber")
187+
188+
export const loadBomCsvConverter = () =>
189+
loadConverterModule("circuit-json-to-bom-csv")
190+
191+
export const loadPnpCsvConverter = () =>
192+
loadConverterModule("circuit-json-to-pnp-csv")
193+
194+
export const loadKicadConverter = () =>
195+
loadConverterModule("circuit-json-to-kicad")
196+
197+
export const loadGltfConverter = () =>
198+
loadConverterModule("circuit-json-to-gltf")
199+
200+
export const loadStepConverter = () =>
201+
loadConverterModule("circuit-json-to-step")
202+
203+
export const loadLbrnConverter = () =>
204+
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: 10 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 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()
@@ -45,10 +45,16 @@ export const createKicadProjectZip = async ({
4545
projectName,
4646
model3dSourcePaths: pcbConverter.getModel3dSourcePaths(),
4747
fetch,
48-
onModelFile: ({ outputPath, content }) => {
48+
onModelFile: ({
49+
outputPath,
50+
content,
51+
}: {
52+
outputPath: string
53+
content: string | ArrayBuffer | Blob | Uint8Array
54+
}) => {
4955
zip.file(outputPath, content)
5056
},
51-
onError: ({ sourcePath }) => {
57+
onError: ({ sourcePath }: { sourcePath: string }) => {
5258
console.warn(`Failed to load 3D model from ${sourcePath}`)
5359
},
5460
})

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:

0 commit comments

Comments
 (0)