tscircuit/ti
ti tscircuit library
- Version
- 1.0.195
- License
- unset
- Stars
- 0
system-block-ui/src/model/catalog.ts
import {
CURATED_ADAPTER_BY_COMPONENT,
CURATED_SUBCIRCUIT_ADAPTERS,
} from "./subcircuit-adapters";
import type { SubcircuitDefinition } from "./types";
type RawSourceModules = Record<string, string>;
const discoverRawSubcircuitSources = (): RawSourceModules => {
try {
// Vite expands this literal glob at build time. The try/catch keeps the
// pure model importable by Bun tests, where import.meta.glob is absent.
return import.meta.glob("../../../lib/subcircuits/*.circuit.tsx", {
eager: true,
import: "default",
query: "?raw",
}) as RawSourceModules;
} catch {
return {};
}
};
const componentNameFromSource = (fileName: string, source: string): string => {
const namedExport = source.match(
/export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=/,
)?.[1];
return namedExport ?? fileName.replace(/\.circuit\.tsx$/, "");
};
const titleFromComponentName = (componentName: string): string =>
componentName
.replace(/_/g, " ")
.replace(/([a-z\d])([A-Z])/g, "$1 $2")
.replace(/\s+/g, " ")
.trim();
const idFromComponentName = (componentName: string): string =>
componentName
.replace(/([a-z\d])([A-Z])/g, "$1-$2")
.replace(/_/g, "-")
.replace(/[^A-Za-z\d-]+/g, "-")
.replace(/-{2,}/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase();
const categoryFromComponentName = (componentName: string): string => {
const prefix = componentName.split("_")[0];
const categories: Readonly<Record<string, string>> = {
AudioAmplifier: "Audio",
BatteryCharging: "Power",
BatteryManagement: "Power",
BluetoothAudioHost: "Processors",
BluetoothController: "Wireless",
BoostConverter: "Power",
BuckBoostConverter: "Power",
BuckConverter: "Power",
ClockBuffer: "Timing",
CommunicationInterface: "Interfaces",
ElectrochromicMirrorDriver: "Drivers",
EnvironmentalSensor: "Sensors",
FlashMemory: "Memory",
InputOutputProtection: "Protection",
InputPowerProtection: "Power",
IsolatedRS485: "Interfaces",
LEDDriver: "Drivers",
LampDriver: "Drivers",
LevelShifter: "Interfaces",
LightSensor: "Sensors",
LoadSwitch: "Power",
LogicBuffer: "Logic",
LVDSDriver: "Interfaces",
Microcontroller: "Processors",
MotorDriver: "Motor Control",
OutputUserInterface: "User Interface",
PowerManagement: "Power",
PowerModule: "Power",
PowerMonitor: "Power",
RFIDReader: "Wireless",
RealTimeClock: "Timing",
TargetSocket: "Development",
TemperatureSensor: "Sensors",
USBC: "Power",
WirelessAntenna: "Wireless",
WirelessMCU: "Wireless",
};
return categories[prefix] ?? "Other";
};
const fileNameFromGlobPath = (path: string): string =>
path.slice(path.lastIndexOf("/") + 1);
const componentAcceptsProps = (
componentName: string,
source: string,
): boolean => {
const signature = source.match(
new RegExp(
`export\\s+const\\s+${componentName}\\s*(?::[^=]+)?=\\s*\\(([^)]*)\\)\\s*=>`,
),
);
// Unknown declaration styles stay available; the standard zero-argument
// arrow form is the only shape known to reject generated instance props.
return signature ? signature[1].trim().length > 0 : true;
};
const genericDefinition = (
path: string,
source: string,
): SubcircuitDefinition => {
const fileName = fileNameFromGlobPath(path);
const componentName = componentNameFromSource(fileName, source);
const acceptsProps = componentAcceptsProps(componentName, source);
return {
id: idFromComponentName(componentName),
title: titleFromComponentName(componentName),
category: categoryFromComponentName(componentName),
componentName,
importPath: "@tsci/tscircuit.ti",
sourcePath: `lib/subcircuits/${fileName}`,
source,
ports: [],
canInstantiate: acceptsProps,
warning: !acceptsProps
? "This subcircuit does not accept instance props, so a generated name cannot be applied safely."
: "Automatic connections are not mapped for this subcircuit yet.",
};
};
export const createSubcircuitCatalog = (
rawSources: RawSourceModules,
): readonly SubcircuitDefinition[] => {
const byComponent = new Map<string, SubcircuitDefinition>();
for (const definition of CURATED_SUBCIRCUIT_ADAPTERS) {
byComponent.set(definition.componentName, definition);
}
for (const [path, source] of Object.entries(rawSources)) {
const discovered = genericDefinition(path, source);
const curated = CURATED_ADAPTER_BY_COMPONENT.get(discovered.componentName);
byComponent.set(
discovered.componentName,
curated
? {
...curated,
sourcePath: discovered.sourcePath,
source,
}
: discovered,
);
}
return [...byComponent.values()].sort((a, b) =>
a.title.localeCompare(b.title, "en"),
);
};
const RAW_SUBCIRCUIT_SOURCES = discoverRawSubcircuitSources();
/** Full Vite-discovered catalog, enriched by the curated electrical adapters. */
export const SUBCIRCUIT_CATALOG = createSubcircuitCatalog(
RAW_SUBCIRCUIT_SOURCES,
);
export const getSubcircuitCatalog = (): readonly SubcircuitDefinition[] =>
SUBCIRCUIT_CATALOG;
export const getSubcircuitDefinition = (
id: string,
catalog: readonly SubcircuitDefinition[] = SUBCIRCUIT_CATALOG,
): SubcircuitDefinition | undefined =>
catalog.find(
(definition) => definition.id === id || definition.componentName === id,
);
export const searchSubcircuits = (
query: string,
catalog: readonly SubcircuitDefinition[] = SUBCIRCUIT_CATALOG,
): readonly SubcircuitDefinition[] => {
const normalized = query.trim().toLowerCase();
if (!normalized) return catalog;
return catalog.filter((definition) =>
[
definition.title,
definition.componentName,
definition.category,
...(definition.tags ?? []),
].some((value) => value.toLowerCase().includes(normalized)),
);
};