feat: implement component metadata overrides and enhance property controls
This commit is contained in:
parent
e489a10255
commit
4f3236437a
|
|
@ -0,0 +1,384 @@
|
|||
# Component Metadata Generator
|
||||
|
||||
Documentation of the component metadata auto-generation system and the override mechanism that preserves custom property configurations across regenerations.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [How It Works](#how-it-works)
|
||||
3. [The Problem: Lost Customizations](#the-problem-lost-customizations)
|
||||
4. [The Solution: component-overrides.json](#the-solution-component-overridesjson)
|
||||
5. [Override File Format](#override-file-format)
|
||||
6. [How Overrides Are Applied](#how-overrides-are-applied)
|
||||
7. [Current Overrides](#current-overrides)
|
||||
8. [Adding New Overrides](#adding-new-overrides)
|
||||
9. [Generator Internals](#generator-internals)
|
||||
10. [File Reference](#file-reference)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Velxio uses 48+ electronic components from the [wokwi-elements](https://github.com/wokwi/wokwi-elements) library. Each component needs metadata (properties, categories, pin counts, controls) for the UI to render property dialogs, component picker, and simulation logic.
|
||||
|
||||
Instead of maintaining this metadata manually, a **generator script** scans the wokwi-elements TypeScript source files, extracts `@property` decorators, `@customElement` tag names, and `pinInfo` getters, then produces `frontend/public/components-metadata.json`.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
wokwi-libs/wokwi-elements/src/*-element.ts
|
||||
│
|
||||
▼
|
||||
scripts/generate-component-metadata.ts ←── reads TypeScript AST
|
||||
│
|
||||
├── Extracts @customElement('wokwi-led') → id: "led"
|
||||
├── Extracts @property() color = 'red' → { name: "color", defaultValue: "red" }
|
||||
├── Counts pinInfo entries → pinCount: 2
|
||||
├── Reads .stories.ts for display name → name: "LED"
|
||||
├── Maps id to category via CATEGORY_MAP → category: "output"
|
||||
│
|
||||
├── Applies overrides from component-overrides.json ← NEW
|
||||
│
|
||||
▼
|
||||
frontend/public/components-metadata.json
|
||||
```
|
||||
|
||||
**Running the generator:**
|
||||
|
||||
```bash
|
||||
cd scripts
|
||||
npx ts-node generate-component-metadata.ts
|
||||
```
|
||||
|
||||
Or if `ts-node` is not available:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/generate-component-metadata.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Problem: Lost Customizations
|
||||
|
||||
The generator reads property types from the wokwi-elements source and infers a UI control:
|
||||
|
||||
| TypeScript Type | Inferred Control |
|
||||
|-----------------|-----------------|
|
||||
| `boolean` | `boolean` |
|
||||
| `number` | `range` |
|
||||
| `string` | `text` |
|
||||
|
||||
This is a reasonable default, but some components need **richer controls** that can't be inferred from the TypeScript source:
|
||||
|
||||
- **LED `color`**: The wokwi-elements source declares `@property() color = 'red'` (a plain string). But in the UI, we want a **dropdown select** with predefined colors (`red`, `green`, `blue`, etc.), not a free-text input.
|
||||
|
||||
- **SSD1306 `protocol`**: The wokwi-elements source has no `protocol` property at all — it's a Velxio-specific concept for choosing between I2C and SPI communication modes. We need to **inject an entirely new property** that doesn't exist in wokwi-elements.
|
||||
|
||||
Previously, these customizations were made by hand-editing `components-metadata.json`. But every time the generator ran (e.g., after updating wokwi-libs), it would **overwrite the entire file** and the customizations would be lost.
|
||||
|
||||
---
|
||||
|
||||
## The Solution: component-overrides.json
|
||||
|
||||
A separate JSON file at `scripts/component-overrides.json` stores all custom property overrides. The generator reads this file **after** scanning wokwi-elements and applies the overrides before writing the output.
|
||||
|
||||
```
|
||||
Auto-generated from wokwi-elements + component-overrides.json
|
||||
(properties, defaults, pin counts) (custom controls, new properties)
|
||||
│ │
|
||||
└──────────────┬─────────────────────────┘
|
||||
▼
|
||||
components-metadata.json
|
||||
(final merged output)
|
||||
```
|
||||
|
||||
Since `component-overrides.json` is a separate file that the generator **reads but never writes**, customizations are preserved across any number of regenerations.
|
||||
|
||||
---
|
||||
|
||||
## Override File Format
|
||||
|
||||
```json
|
||||
{
|
||||
"$comment": "Custom property overrides applied AFTER auto-generation...",
|
||||
"<component-id>": {
|
||||
"properties": {
|
||||
"<property-name>": {
|
||||
// Fields to patch on an existing property, or full definition for a new one
|
||||
}
|
||||
},
|
||||
"defaultValues": {
|
||||
"<property-name>": "<default-value>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Patching an Existing Property
|
||||
|
||||
To change the control type of an existing property (one that the generator already extracts from wokwi-elements):
|
||||
|
||||
```json
|
||||
{
|
||||
"led": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"control": "select",
|
||||
"options": ["red", "green", "blue", "yellow", "orange", "white", "purple"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This finds the existing `color` property on the LED component and patches it with `control: "select"` and `options: [...]`. All other fields (`name`, `type`, `defaultValue`) are kept from the auto-generated version.
|
||||
|
||||
### Adding a New Property
|
||||
|
||||
To inject a property that doesn't exist in the wokwi-elements source:
|
||||
|
||||
```json
|
||||
{
|
||||
"ssd1306": {
|
||||
"properties": {
|
||||
"protocol": {
|
||||
"name": "protocol",
|
||||
"type": "string",
|
||||
"defaultValue": "i2c",
|
||||
"control": "select",
|
||||
"description": "Communication protocol",
|
||||
"options": ["i2c", "spi"]
|
||||
}
|
||||
},
|
||||
"defaultValues": {
|
||||
"protocol": "i2c"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since no existing property named `protocol` exists on the SSD1306, the generator **appends** it to the properties array. The `defaultValues` merge ensures the default is set.
|
||||
|
||||
---
|
||||
|
||||
## How Overrides Are Applied
|
||||
|
||||
The `applyOverrides()` method in the generator works as follows:
|
||||
|
||||
```
|
||||
For each component in the generated metadata:
|
||||
1. Check if component-overrides.json has an entry for this component ID
|
||||
2. If yes, for each property override:
|
||||
a. Find existing property by name in the component's properties array
|
||||
b. If found → Object.assign(existing, patch) (merge/overwrite fields)
|
||||
c. If not found → push(patch) to properties array (add new property)
|
||||
3. Merge any defaultValues from the override into the component's defaultValues
|
||||
```
|
||||
|
||||
The key behavior:
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Override property exists in wokwi-elements | Patch: only overridden fields change, rest preserved |
|
||||
| Override property does NOT exist in wokwi-elements | Add: full property definition appended |
|
||||
| Override has `defaultValues` | Merge: `{ ...autoGenerated, ...override }` |
|
||||
|
||||
---
|
||||
|
||||
## Current Overrides
|
||||
|
||||
### LED Color Selector
|
||||
|
||||
**Component:** `led`
|
||||
**Property:** `color`
|
||||
**Change:** `control: "text"` → `control: "select"` with 7 color options
|
||||
|
||||
The wokwi LED element accepts any CSS color string, but in practice users want to pick from standard LED colors. The select dropdown provides:
|
||||
- `red`, `green`, `blue`, `yellow`, `orange`, `white`, `purple`
|
||||
|
||||
**UI result:** When clicking on an LED in the simulator, the property dialog shows a dropdown instead of a text input.
|
||||
|
||||
### SSD1306 Protocol Selector
|
||||
|
||||
**Component:** `ssd1306`
|
||||
**Property:** `protocol` (new, not in wokwi-elements)
|
||||
**Control:** `select` with `["i2c", "spi"]`
|
||||
**Default:** `"i2c"`
|
||||
|
||||
The real SSD1306 OLED display supports both I2C and SPI communication. In Velxio, the simulation logic reads this property to decide which bus to attach:
|
||||
|
||||
- **I2C mode**: Registers as an `I2CDevice` on the simulator's I2C bus. Responds to address `0x3C`. Uses control byte (`0x00` = command, `0x40` = data).
|
||||
- **SPI mode**: Hooks into the SPI `onByte` callback. Uses the DC (Data/Command) pin to distinguish commands from data.
|
||||
|
||||
The simulation code in `PartSimulationRegistry` reads the protocol at attachment time:
|
||||
|
||||
```typescript
|
||||
PartSimulationRegistry.register('ssd1306', {
|
||||
attachEvents: (element, simulator, getPin, componentId) => {
|
||||
const comp = useSimulatorStore.getState().components.find(c => c.id === componentId);
|
||||
const protocol = (comp?.properties?.protocol as string) ?? 'i2c';
|
||||
if (protocol === 'spi') return attachSSD1306SPI(element, simulator, getPin);
|
||||
// I2C default...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**UI result:** The property dialog shows an I2C/SPI dropdown. A colored badge (blue = I2C, orange = SPI) appears next to the component label on the canvas.
|
||||
|
||||
---
|
||||
|
||||
## Adding New Overrides
|
||||
|
||||
### Step 1: Edit component-overrides.json
|
||||
|
||||
Add a new entry keyed by the component's `id` (the part after `wokwi-` in the tag name):
|
||||
|
||||
```json
|
||||
{
|
||||
"resistor": {
|
||||
"properties": {
|
||||
"value": {
|
||||
"control": "select",
|
||||
"options": ["220", "330", "1000", "4700", "10000"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Regenerate metadata
|
||||
|
||||
```bash
|
||||
npx tsx scripts/generate-component-metadata.ts
|
||||
```
|
||||
|
||||
The generator will log:
|
||||
|
||||
```
|
||||
🔧 Applied overrides for resistor
|
||||
|
||||
🔧 Applied overrides to 1 component(s)
|
||||
```
|
||||
|
||||
### Step 3: Verify
|
||||
|
||||
Open `frontend/public/components-metadata.json` and search for the component. The override fields should be present.
|
||||
|
||||
### Common override patterns
|
||||
|
||||
**Change a text input to a dropdown:**
|
||||
```json
|
||||
{
|
||||
"<component-id>": {
|
||||
"properties": {
|
||||
"<prop-name>": {
|
||||
"control": "select",
|
||||
"options": ["option1", "option2", "option3"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Add a description to an existing property:**
|
||||
```json
|
||||
{
|
||||
"<component-id>": {
|
||||
"properties": {
|
||||
"<prop-name>": {
|
||||
"description": "Human-readable description shown in the property dialog"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Add a completely new property with a default:**
|
||||
```json
|
||||
{
|
||||
"<component-id>": {
|
||||
"properties": {
|
||||
"newProp": {
|
||||
"name": "newProp",
|
||||
"type": "string",
|
||||
"defaultValue": "default",
|
||||
"control": "select",
|
||||
"description": "Description",
|
||||
"options": ["default", "alt1", "alt2"]
|
||||
}
|
||||
},
|
||||
"defaultValues": {
|
||||
"newProp": "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Generator Internals
|
||||
|
||||
### TypeScript AST Parsing
|
||||
|
||||
The generator uses the TypeScript compiler API (`ts.createSourceFile`) to parse each `*-element.ts` file without executing it. It extracts:
|
||||
|
||||
1. **Tag name**: From `@customElement('wokwi-led')` decorator → `wokwi-led`
|
||||
2. **Properties**: From `@property() color = 'red'` decorators → `{ name: "color", type: "string", defaultValue: "red" }`
|
||||
3. **Pin count**: By counting object literals `{ name: ... }` inside the `pinInfo` getter body
|
||||
4. **Display name**: From the corresponding `.stories.ts` file's `title:` field
|
||||
|
||||
### Category Assignment
|
||||
|
||||
Components don't declare their own category. The generator uses a hardcoded `CATEGORY_MAP`:
|
||||
|
||||
```typescript
|
||||
const CATEGORY_MAP: Record<string, ComponentCategory> = {
|
||||
'led': 'output',
|
||||
'pushbutton': 'input',
|
||||
'ssd1306': 'displays',
|
||||
'servo': 'motors',
|
||||
'resistor': 'passive',
|
||||
// ... etc
|
||||
};
|
||||
```
|
||||
|
||||
Components not in the map get `category: "other"`.
|
||||
|
||||
### Control Inference
|
||||
|
||||
The generator infers a UI control from the TypeScript property type:
|
||||
|
||||
```typescript
|
||||
private inferControl(tsType: string): 'text' | 'range' | 'color' | 'boolean' | 'select' {
|
||||
if (tsType.includes('boolean')) return 'boolean';
|
||||
if (tsType.includes('number')) return 'range';
|
||||
return 'text'; // strings and everything else
|
||||
}
|
||||
```
|
||||
|
||||
This is why custom overrides are needed — the generator can't know that `color: string` should be a `select` with specific options.
|
||||
|
||||
### Tag Generation
|
||||
|
||||
Each component gets search tags derived from its ID and display name:
|
||||
|
||||
```
|
||||
id: "led-bar-graph" → tags: ["led-bar-graph", "led bar graph", "led", "bar", "graph"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Reference
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `scripts/generate-component-metadata.ts` | Generator script — scans wokwi-elements, applies overrides, writes JSON |
|
||||
| `scripts/component-overrides.json` | Custom property overrides (survives regeneration) |
|
||||
| `frontend/public/components-metadata.json` | Generated output — consumed by the frontend at runtime |
|
||||
| `frontend/src/types/component-metadata.ts` | TypeScript interface for `ComponentMetadata` |
|
||||
| `frontend/src/services/ComponentRegistry.ts` | Loads and indexes the metadata JSON at runtime |
|
||||
| `wokwi-libs/wokwi-elements/src/*-element.ts` | Source files scanned by the generator |
|
||||
|
|
@ -247,9 +247,19 @@
|
|||
"name": "imageData",
|
||||
"type": "string",
|
||||
"control": "text"
|
||||
},
|
||||
{
|
||||
"name": "protocol",
|
||||
"type": "string",
|
||||
"defaultValue": "i2c",
|
||||
"control": "select",
|
||||
"description": "Communication protocol",
|
||||
"options": ["i2c", "spi"]
|
||||
}
|
||||
],
|
||||
"defaultValues": {},
|
||||
"defaultValues": {
|
||||
"protocol": "i2c"
|
||||
},
|
||||
"pinCount": 0,
|
||||
"tags": [
|
||||
"ssd1306"
|
||||
|
|
@ -1326,7 +1336,8 @@
|
|||
"name": "color",
|
||||
"type": "string",
|
||||
"defaultValue": "red",
|
||||
"control": "text"
|
||||
"control": "select",
|
||||
"options": ["red", "green", "blue", "yellow", "orange", "white", "purple"]
|
||||
},
|
||||
{
|
||||
"name": "lightColor",
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
|
|||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
// Calculate dialog position on mount
|
||||
// Calculate dialog position on mount — clamp within canvas viewport
|
||||
useEffect(() => {
|
||||
if (!dialogRef.current) return;
|
||||
|
||||
const dialogWidth = 220;
|
||||
const dialogWidth = dialogRef.current.offsetWidth || 220;
|
||||
const dialogHeight = dialogRef.current.offsetHeight || 200;
|
||||
const canvasElement = document.querySelector('.canvas-content');
|
||||
if (!canvasElement) return;
|
||||
|
|
@ -47,19 +47,20 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
|
|||
const canvasWidth = canvasElement.clientWidth;
|
||||
const canvasHeight = canvasElement.clientHeight;
|
||||
|
||||
// Try positioning to the right of component
|
||||
let x = position.x + 150; // Approximate component width + gap
|
||||
// Position to the right of the component (screen coords already include pan+zoom)
|
||||
let x = position.x + 120;
|
||||
let y = position.y;
|
||||
|
||||
// If off-screen right, position to left
|
||||
// If off-screen right, position to the left
|
||||
if (x + dialogWidth > canvasWidth) {
|
||||
x = Math.max(10, position.x - dialogWidth - 10);
|
||||
}
|
||||
|
||||
// Keep within vertical bounds
|
||||
if (y + dialogHeight > canvasHeight) {
|
||||
y = Math.max(10, canvasHeight - dialogHeight - 10);
|
||||
}
|
||||
// Clamp horizontal
|
||||
x = Math.max(10, Math.min(x, canvasWidth - dialogWidth - 10));
|
||||
|
||||
// Clamp vertical — ensure dialog stays fully visible
|
||||
y = Math.max(10, Math.min(y, canvasHeight - dialogHeight - 10));
|
||||
|
||||
setDialogPosition({ x, y });
|
||||
}, [position]);
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ export const SimulatorCanvas = () => {
|
|||
const [sensorControlComponentId, setSensorControlComponentId] = useState<string | null>(null);
|
||||
const [sensorControlMetadataId, setSensorControlMetadataId] = useState<string | null>(null);
|
||||
|
||||
// Board built-in LED states (pin 13 for AVR, GPIO25 for RP2040, etc.)
|
||||
// Tracks directly from pinManager — independent of any led-builtin component.
|
||||
const [boardLedStates, setBoardLedStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Board context menu (right-click)
|
||||
const [boardContextMenu, setBoardContextMenu] = useState<{ boardId: string; x: number; y: number } | null>(null);
|
||||
// Board removal confirmation dialog
|
||||
|
|
@ -555,7 +559,10 @@ export const SimulatorCanvas = () => {
|
|||
setSensorControlMetadataId(component.metadataId);
|
||||
} else {
|
||||
setPropertyDialogComponentId(touchId);
|
||||
setPropertyDialogPosition({ x: component.x, y: component.y });
|
||||
setPropertyDialogPosition({
|
||||
x: component.x * zoomRef.current + panRef.current.x,
|
||||
y: component.y * zoomRef.current + panRef.current.y,
|
||||
});
|
||||
setShowPropertyDialog(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -707,6 +714,38 @@ export const SimulatorCanvas = () => {
|
|||
};
|
||||
}, [components, wires, boards, pinManager, updateComponentState]);
|
||||
|
||||
// Board built-in LED: subscribe directly to pinManager for the LED pin of each board.
|
||||
// This works even when no external led-builtin component exists (e.g. basic Blink example).
|
||||
useEffect(() => {
|
||||
if (!pinManager) return;
|
||||
const unsubs: (() => void)[] = [];
|
||||
|
||||
boards.forEach((board) => {
|
||||
// Determine which GPIO pin drives the board's built-in LED
|
||||
let ledPin: number;
|
||||
switch (board.boardKind) {
|
||||
case 'raspberry-pi-pico':
|
||||
case 'pi-pico-w':
|
||||
case 'nano-rp2040':
|
||||
ledPin = 25; // GPIO25
|
||||
break;
|
||||
default:
|
||||
ledPin = 13; // Pin 13 for Arduino Uno/Nano/Mega, ATtiny85, etc.
|
||||
}
|
||||
|
||||
unsubs.push(
|
||||
pinManager.onPinChange(ledPin, (_pin, state) => {
|
||||
setBoardLedStates((prev) => {
|
||||
if (prev[board.id] === state) return prev;
|
||||
return { ...prev, [board.id]: state };
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return () => unsubs.forEach((u) => u());
|
||||
}, [boards, pinManager]);
|
||||
|
||||
// ESP32 input components: forward button presses and potentiometer values to QEMU
|
||||
useEffect(() => {
|
||||
const cleanups: (() => void)[] = [];
|
||||
|
|
@ -968,7 +1007,10 @@ export const SimulatorCanvas = () => {
|
|||
setSensorControlMetadataId(component.metadataId);
|
||||
} else {
|
||||
setPropertyDialogComponentId(draggedComponentId);
|
||||
setPropertyDialogPosition({ x: component.x, y: component.y });
|
||||
setPropertyDialogPosition({
|
||||
x: component.x * zoomRef.current + panRef.current.x,
|
||||
y: component.y * zoomRef.current + panRef.current.y,
|
||||
});
|
||||
setShowPropertyDialog(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1463,7 +1505,7 @@ export const SimulatorCanvas = () => {
|
|||
board={board}
|
||||
running={running}
|
||||
isActive={board.id === activeBoardId}
|
||||
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
|
||||
led13={Boolean(boardLedStates[board.id])}
|
||||
onMouseDown={(e) => {
|
||||
setClickStartTime(Date.now());
|
||||
setClickStartPos({ x: e.clientX, y: e.clientY });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$comment": "Custom property overrides applied AFTER auto-generation from wokwi-elements. Add entries here to customize controls, add new properties, or change defaults. These survive metadata regeneration.",
|
||||
"led": {
|
||||
"properties": {
|
||||
"color": {
|
||||
"control": "select",
|
||||
"options": ["red", "green", "blue", "yellow", "orange", "white", "purple"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ssd1306": {
|
||||
"properties": {
|
||||
"protocol": {
|
||||
"name": "protocol",
|
||||
"type": "string",
|
||||
"defaultValue": "i2c",
|
||||
"control": "select",
|
||||
"description": "Communication protocol",
|
||||
"options": ["i2c", "spi"]
|
||||
}
|
||||
},
|
||||
"defaultValues": {
|
||||
"protocol": "i2c"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,10 +93,12 @@ interface ParsedComponent {
|
|||
class MetadataGenerator {
|
||||
private wokwiElementsPath: string;
|
||||
private outputPath: string;
|
||||
private overridesPath: string;
|
||||
|
||||
constructor() {
|
||||
this.wokwiElementsPath = path.join(__dirname, '../wokwi-libs/wokwi-elements/src');
|
||||
this.outputPath = path.join(__dirname, '../frontend/public/components-metadata.json');
|
||||
this.overridesPath = path.join(__dirname, 'component-overrides.json');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -129,6 +131,9 @@ class MetadataGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply custom overrides from component-overrides.json
|
||||
this.applyOverrides(components);
|
||||
|
||||
// Sort by category and name
|
||||
components.sort((a, b) => {
|
||||
if (a.category !== b.category) {
|
||||
|
|
@ -154,6 +159,58 @@ class MetadataGenerator {
|
|||
console.log(`📄 Output: ${this.outputPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom overrides from component-overrides.json.
|
||||
*
|
||||
* Overrides can:
|
||||
* - Patch existing property fields (e.g. change control from "text" to "select")
|
||||
* - Add entirely new properties to a component
|
||||
* - Merge extra defaultValues
|
||||
*/
|
||||
private applyOverrides(components: ComponentMetadata[]): void {
|
||||
if (!fs.existsSync(this.overridesPath)) return;
|
||||
|
||||
let overrides: Record<string, any>;
|
||||
try {
|
||||
overrides = JSON.parse(fs.readFileSync(this.overridesPath, 'utf-8'));
|
||||
} catch (e) {
|
||||
console.warn(`⚠️ Could not parse ${this.overridesPath}:`, e);
|
||||
return;
|
||||
}
|
||||
|
||||
let applied = 0;
|
||||
for (const comp of components) {
|
||||
const ov = overrides[comp.id];
|
||||
if (!ov) continue;
|
||||
|
||||
// Merge property-level overrides
|
||||
if (ov.properties) {
|
||||
for (const [propName, patch] of Object.entries<any>(ov.properties)) {
|
||||
const existing = comp.properties.find((p: any) => p.name === propName);
|
||||
if (existing) {
|
||||
// Patch existing property (e.g. change control, add options)
|
||||
Object.assign(existing, patch);
|
||||
} else {
|
||||
// Add new property (e.g. SSD1306 "protocol")
|
||||
comp.properties.push(patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge defaultValues
|
||||
if (ov.defaultValues) {
|
||||
comp.defaultValues = { ...comp.defaultValues, ...ov.defaultValues };
|
||||
}
|
||||
|
||||
applied++;
|
||||
console.log(` 🔧 Applied overrides for ${comp.id}`);
|
||||
}
|
||||
|
||||
if (applied > 0) {
|
||||
console.log(`\n🔧 Applied overrides to ${applied} component(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all *-element.ts files (excluding .stories.ts)
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue