import type { SduiActionWire, SduiBlockWire, SduiHeaderWire, SduiScreenMetaWire, SduiScreenWire, StyleWire, } from '@proulr/sdui-contract'; import type { BdcElement } from './jsx-runtime'; function camelToSnake(key: string): string { return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } function mapPropsToWire(props: Record): Record { const wire: Record = {}; for (const [key, value] of Object.entries(props)) { if (key === 'children') continue; if (key === 'actionId') { wire.action_id = value; continue; } if (key === 'style' && value && typeof value === 'object') { const styleWire: Record = {}; for (const [sk, sv] of Object.entries(value as StyleWire)) { styleWire[camelToSnake(sk)] = sv; } wire.style = styleWire; continue; } wire[camelToSnake(key)] = value; } return wire; } function textFromChildren(children: BdcElement[]): string { return children .map((child) => { if (child.type === '_text_node') return String(child.props.value ?? ''); if (child.type === 'text' && typeof child.props.text === 'string') return child.props.text; return ''; }) .join('') .trim(); } function mapHeaderToWire(header: Record): SduiHeaderWire { const wire: Record = {}; for (const [key, value] of Object.entries(header)) { if (key === 'slotBlocks' || key === 'slot_blocks' || key === 'headerSlot') continue; if (key === 'headerSearch') { wire.header_search = value; continue; } wire[camelToSnake(key)] = value; } return wire as SduiHeaderWire; } export function compileBdcBlock(element: BdcElement, path = '0'): SduiBlockWire { return mapBlock(element, path); } function flattenBlockChildren(children: BdcElement[]): BdcElement[] { const out: BdcElement[] = []; for (const child of children) { if (child.type === '_text_node') continue; if (child.type === '_fragment') { out.push(...flattenBlockChildren(child.children)); continue; } out.push(child); } return out; } function mapBlock(element: BdcElement, path: string): SduiBlockWire { if (element.type === '_text_node') { return { id: path, type: 'text', props: { text: String(element.props.value ?? '') } }; } const props = mapPropsToWire(element.props); if (element.type === 'button' && !props.label) { props.label = textFromChildren(element.children); } if (element.type === 'text' && !props.text) { props.text = textFromChildren(element.children); } const blockChildren = flattenBlockChildren(element.children); return { id: typeof element.props.id === 'string' ? element.props.id : path, type: element.type, props, children: blockChildren.map((child, index) => mapBlock(child, `${path}.${index}`)), }; } export function compileBdcToWire(root: BdcElement): SduiScreenWire { if (root.type !== 'screen') { throw new Error('bdc_root_must_be_screen'); } const { id, header, headerSlot, actions, meta, data, children: _ignored, ...rest } = root.props as { id: string; header?: SduiHeaderWire; headerSlot?: BdcElement | BdcElement[]; actions?: Record; meta?: SduiScreenMetaWire; data?: Record; children?: unknown; }; void rest; const blocks = flattenBlockChildren(root.children).map((child, index) => mapBlock(child, String(index)), ); const compiledHeader = header ? mapHeaderToWire(header as Record) : undefined; if (compiledHeader && headerSlot) { const slotRoots = Array.isArray(headerSlot) ? headerSlot : [headerSlot]; compiledHeader.slot_blocks = slotRoots.map((slot, index) => mapBlock(slot, `header.${index}`)); } return { schema_version: 1, screen_id: id, header: compiledHeader, blocks, data: data ?? {}, actions: actions ?? {}, meta: meta ?? { bff: undefined, cache_ttl_ms: 30_000, offline_policy: 'cache_only' }, }; }