Files
metabuilder/old/DECLARATIVE_COMPONENTS.md
2026-01-08 22:59:15 +00:00

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 executor
  • src/lib/package-loader.ts - Package initialization system
  • src/lib/package-catalog.ts - Package definitions including IRC
  • src/components/RenderComponent.tsx - Updated to support declarative components

IRC Implementation

  • src/components/IRCWebchatDeclarative.tsx - Declarative IRC component
  • src/components/IRCWebchat.tsx - REMOVED (replaced by declarative version)

Integration Points

  • src/App.tsx - Calls initializePackageSystem() on startup
  • src/lib/component-catalog.ts - IRC added to component catalog
  • src/components/Level2.tsx - Updated to use IRCWebchatDeclarative

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

  1. Modularity: Components are self-contained in packages
  2. Maintainability: Logic (Lua), UI (JSON), rendering (React) are separated
  3. Extensibility: New components added without code changes
  4. Testing: Lua scripts can be tested independently
  5. Distribution: Packages can be shared as ZIP files
  6. 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:

  1. Define component config in package catalog with props, layout, and children structure
  2. Extract business logic into Lua scripts (functions, event handlers)
  3. Create declarative wrapper component that uses getDeclarativeRenderer()
  4. Register component in component catalog
  5. Update references to use new declarative component
  6. Test thoroughly to ensure parity with original
  7. 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.