mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 14:25:02 +00:00
7.9 KiB
7.9 KiB
Declarative Component System
Overview
MetaBuilder now supports declarative components - components defined via JSON configuration and Lua scripts instead of traditional TSX files. This enables:
- Package-based components: Components can be distributed as part of packages
- Dynamic loading: Components are loaded at runtime from the package catalog
- Better separation: Logic (Lua), UI (JSON), and rendering (React) are cleanly separated
- No code deployment: New components can be added without code changes
Architecture
1. Component Definition (JSON)
Components are defined in package catalogs with:
{
type: 'IRCWebchat', // Component type identifier
category: 'social', // Category for organization
label: 'IRC Webchat', // Display name
description: '...', // Description
icon: '💬', // Icon
props: [...], // Prop schema
config: { // UI structure
layout: 'Card',
styling: { className: '...' },
children: [...] // Nested component tree
}
}
2. Business Logic (Lua)
Lua scripts handle component logic:
-- Send IRC Message
function sendMessage(channelId, username, userId, message)
local msgId = "msg_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999)
local msg = {
id = msgId,
channelId = channelId,
username = username,
userId = userId,
message = message,
type = "message",
timestamp = os.time() * 1000
}
log("Sending message: " .. message)
return msg
end
return sendMessage
3. Component Renderer (React)
The renderer bridges JSON config, Lua scripts, and React:
import { IRCWebchatDeclarative } from './IRCWebchatDeclarative'
import { getDeclarativeRenderer } from '@/lib/declarative-component-renderer'
// Check if component is declarative
const renderer = getDeclarativeRenderer()
if (renderer.hasComponentConfig('IRCWebchat')) {
return <IRCWebchatDeclarative {...props} />
}
IRC Webchat Example
The IRC Webchat has been fully rewritten as a declarative component:
Package Definition
Located in src/lib/package-catalog.ts:
'irc-webchat': {
manifest: {
id: 'irc-webchat',
name: 'IRC-Style Webchat',
// ... metadata
},
content: {
schemas: [
// ChatChannel, ChatMessage, ChatUser schemas
],
pages: [
// Page configuration
],
luaScripts: [
{
id: 'lua_irc_send_message',
name: 'Send IRC Message',
code: '...',
parameters: [...],
returnType: 'table'
},
{
id: 'lua_irc_handle_command',
name: 'Handle IRC Command',
// Processes /help, /users, /clear, /me
},
{
id: 'lua_irc_format_time',
// Formats timestamps
},
{
id: 'lua_irc_user_join',
// Handles user joining
},
{
id: 'lua_irc_user_leave',
// Handles user leaving
}
],
componentConfigs: {
IRCWebchat: {
// Full component configuration
}
}
}
}
Component Implementation
src/components/IRCWebchatDeclarative.tsx:
export function IRCWebchatDeclarative({ user, channelName = 'general', onClose }) {
const renderer = getDeclarativeRenderer()
// Execute Lua script for sending messages
const handleSendMessage = async () => {
const newMessage = await renderer.executeLuaScript('lua_irc_send_message', [
`chat_${channelName}`,
user.username,
user.id,
trimmed,
])
if (newMessage) {
setMessages((current) => [...(current || []), newMessage])
}
}
// Execute Lua script for commands
const handleCommand = async (command: string) => {
const response = await renderer.executeLuaScript('lua_irc_handle_command', [
command,
`chat_${channelName}`,
user.username,
onlineUsers || [],
])
if (response.message === 'CLEAR_MESSAGES') {
setMessages([])
} else {
setMessages((current) => [...(current || []), response])
}
}
// UI rendering with shadcn components
return <Card>...</Card>
}
Key Files
Core System
src/lib/declarative-component-renderer.ts- Component renderer and Lua script executorsrc/lib/package-loader.ts- Package initialization systemsrc/lib/package-catalog.ts- Package definitions including IRCsrc/components/RenderComponent.tsx- Updated to support declarative components
IRC Implementation
src/components/IRCWebchatDeclarative.tsx- Declarative IRC component- REMOVED (replaced by declarative version)src/components/IRCWebchat.tsx
Integration Points
src/App.tsx- CallsinitializePackageSystem()on startupsrc/lib/component-catalog.ts- IRC added to component catalogsrc/components/Level2.tsx- Updated to useIRCWebchatDeclarative
Lua Script Execution
Scripts are executed through the declarative renderer:
const renderer = getDeclarativeRenderer()
// Parameters are passed as array
const result = await renderer.executeLuaScript('lua_irc_send_message', [
'channel_id',
'username',
'user_id',
'message text'
])
// Result contains the Lua function's return value
console.log(result) // { id: "msg_...", message: "...", ... }
Parameter Mapping
The renderer wraps Lua code to map parameters:
const wrappedCode = `
${script.code}
local fn = ...
if fn then
local args = {}
table.insert(args, context.params.channelId)
table.insert(args, context.params.username)
// ... more parameters
return fn(table.unpack(args))
end
`
Benefits
- Modularity: Components are self-contained in packages
- Maintainability: Logic (Lua), UI (JSON), rendering (React) are separated
- Extensibility: New components added without code changes
- Testing: Lua scripts can be tested independently
- Distribution: Packages can be shared as ZIP files
- Security: Lua sandbox prevents malicious code execution
Future Enhancements
- Visual component builder for declarative components
- Hot-reload for component definitions
- Component marketplace
- Version management for component definitions
- Automated testing for Lua scripts
- TypeScript type generation from JSON schemas
- More built-in declarative components (Forum, Guestbook, etc.)
Migration Guide
To convert an existing TSX component to declarative:
- Define component config in package catalog with props, layout, and children structure
- Extract business logic into Lua scripts (functions, event handlers)
- Create declarative wrapper component that uses
getDeclarativeRenderer() - Register component in component catalog
- Update references to use new declarative component
- Test thoroughly to ensure parity with original
- Remove old TSX file once verified
Example: Converting a Simple Component
Before (TSX):
export function SimpleCounter() {
const [count, setCount] = useState(0)
const increment = () => setCount(c => c + 1)
return (
<Card>
<p>Count: {count}</p>
<Button onClick={increment}>Increment</Button>
</Card>
)
}
After (Declarative):
Package Definition:
{
componentConfigs: {
SimpleCounter: {
type: 'SimpleCounter',
props: [],
config: { layout: 'Card', children: [...] }
}
},
luaScripts: [{
id: 'lua_counter_increment',
code: 'function increment(count) return count + 1 end; return increment'
}]
}
Component:
export function SimpleCounterDeclarative() {
const [count, setCount] = useState(0)
const renderer = getDeclarativeRenderer()
const increment = async () => {
const newCount = await renderer.executeLuaScript('lua_counter_increment', [count])
setCount(newCount)
}
return <Card>...</Card>
}
For questions or contributions, see the main project documentation.