375 lines
10 KiB
Go
375 lines
10 KiB
Go
package plugin_ui
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/goccy/go-json"
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
func (c *ComponentManager) renderComponents(renderFunc func(goja.FunctionCall) goja.Value) (interface{}, error) {
|
|
if renderFunc == nil {
|
|
return nil, errors.New("render function is not set")
|
|
}
|
|
|
|
// Get new components
|
|
newComponents := c.getComponentsData(renderFunc)
|
|
|
|
// If we have previous components, perform diffing
|
|
if c.lastRenderedComponents != nil {
|
|
newComponents = c.componentDiff(c.lastRenderedComponents, newComponents)
|
|
}
|
|
|
|
// Store the new components for next render
|
|
c.lastRenderedComponents = newComponents
|
|
|
|
return newComponents, nil
|
|
}
|
|
|
|
// getComponentsData calls the render function and returns the current state of the component tree
|
|
func (c *ComponentManager) getComponentsData(renderFunc func(goja.FunctionCall) goja.Value) interface{} {
|
|
// Call the render function
|
|
value := renderFunc(goja.FunctionCall{})
|
|
|
|
// Convert the value to a JSON string
|
|
v, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var ret interface{}
|
|
err = json.Unmarshal(v, &ret)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
////
|
|
|
|
type ComponentProp struct {
|
|
Name string // e.g. "label"
|
|
Type string // e.g. "string"
|
|
Default interface{} // Is set if the prop is not provided, if not set and required is false, the prop will not be included in the component
|
|
Required bool // If true an no default value is provided, the component will throw a type error
|
|
Validate func(value interface{}) error // Optional validation function
|
|
OptionalFirstArg bool // If true, it can be the first argument to declaring the component as a shorthand (e.g. tray.button("Click me") instead of tray.button({label: "Click me"}))
|
|
}
|
|
|
|
func defineComponent(vm *goja.Runtime, call goja.FunctionCall, t string, propDefs []ComponentProp) goja.Value {
|
|
component := Component{
|
|
ID: uuid.New().String(),
|
|
Type: t,
|
|
Props: make(map[string]interface{}),
|
|
}
|
|
|
|
propsList := make(map[string]interface{})
|
|
propDefsMap := make(map[string]*ComponentProp)
|
|
|
|
var shorthandProp *ComponentProp
|
|
for _, propDef := range propDefs {
|
|
|
|
propDefsMap[propDef.Name] = &propDef
|
|
|
|
if propDef.OptionalFirstArg {
|
|
shorthandProp = &propDef
|
|
}
|
|
}
|
|
|
|
if len(call.Arguments) > 0 {
|
|
// Check if the first argument is the type of the shorthand
|
|
hasShorthand := false
|
|
if shorthandProp != nil {
|
|
switch shorthandProp.Type {
|
|
case "string":
|
|
if _, ok := call.Argument(0).Export().(string); ok {
|
|
propsList[shorthandProp.Name] = call.Argument(0).Export().(string)
|
|
hasShorthand = true
|
|
}
|
|
case "boolean":
|
|
if _, ok := call.Argument(0).Export().(bool); ok {
|
|
propsList[shorthandProp.Name] = call.Argument(0).Export().(bool)
|
|
hasShorthand = true
|
|
}
|
|
case "array":
|
|
if _, ok := call.Argument(0).Export().([]interface{}); ok {
|
|
propsList[shorthandProp.Name] = call.Argument(0).Export().([]interface{})
|
|
hasShorthand = true
|
|
}
|
|
}
|
|
if hasShorthand {
|
|
// Get the rest of the props from the second argument
|
|
if len(call.Arguments) > 1 {
|
|
rest, ok := call.Argument(1).Export().(map[string]interface{})
|
|
if ok {
|
|
// Only add props that are defined in the propDefs
|
|
for k, v := range rest {
|
|
if _, ok := propDefsMap[k]; ok {
|
|
propsList[k] = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hasShorthand {
|
|
propsArg, ok := call.Argument(0).Export().(map[string]interface{})
|
|
if ok {
|
|
for k, v := range propsArg {
|
|
if _, ok := propDefsMap[k]; ok {
|
|
propsList[k] = v
|
|
} else {
|
|
// util.SpewMany(k, fmt.Sprintf("%T", v))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate props
|
|
for _, propDef := range propDefs {
|
|
// If a prop is required and no value is provided, panic
|
|
if propDef.Required && len(propsList) == 0 {
|
|
panic(vm.NewTypeError(fmt.Sprintf("%s is required", propDef.Name)))
|
|
}
|
|
|
|
// Validate the prop if the prop is defined
|
|
if propDef.Validate != nil {
|
|
if val, ok := propsList[propDef.Name]; ok {
|
|
err := propDef.Validate(val)
|
|
if err != nil {
|
|
log.Error().Msgf("Invalid prop value: %s", err.Error())
|
|
panic(vm.NewTypeError(err.Error()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set a default value if the prop is not provided
|
|
if _, ok := propsList[propDef.Name]; !ok && propDef.Default != nil {
|
|
propsList[propDef.Name] = propDef.Default
|
|
}
|
|
}
|
|
|
|
// Set the props
|
|
for k, v := range propsList {
|
|
component.Props[k] = v
|
|
}
|
|
|
|
return vm.ToValue(component)
|
|
}
|
|
|
|
// Helper function to create a validation function for a specific type
|
|
func validateType(expectedType string) func(interface{}) error {
|
|
return func(value interface{}) error {
|
|
if value == nil {
|
|
return fmt.Errorf("expected %s, got nil", expectedType)
|
|
}
|
|
switch expectedType {
|
|
case "string":
|
|
_, ok := value.(string)
|
|
if !ok {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected string, got %T", value)
|
|
}
|
|
return nil
|
|
case "number":
|
|
_, ok := value.(float64)
|
|
if !ok {
|
|
_, ok := value.(int64)
|
|
if !ok {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected number, got %T", value)
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
case "boolean":
|
|
_, ok := value.(bool)
|
|
if !ok {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected boolean, got %T", value)
|
|
}
|
|
return nil
|
|
case "array":
|
|
_, ok := value.([]interface{})
|
|
if !ok {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected array, got %T", value)
|
|
}
|
|
return nil
|
|
case "object":
|
|
_, ok := value.(map[string]interface{})
|
|
if !ok {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected object, got %T", value)
|
|
}
|
|
return nil
|
|
case "function":
|
|
_, ok := value.(func(goja.FunctionCall) goja.Value)
|
|
if !ok {
|
|
return fmt.Errorf("expected function, got %T", value)
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("invalid type: %s", expectedType)
|
|
}
|
|
}
|
|
}
|
|
|
|
// componentDiff compares two component trees and returns a new component tree that preserves the ID of old components that did not change.
|
|
// It also recursively handles props and items arrays.
|
|
//
|
|
// This is important to preserve state between renders in React.
|
|
func (c *ComponentManager) componentDiff(old, new interface{}) (ret interface{}) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// If a panic occurs, return the new component tree
|
|
ret = new
|
|
}
|
|
}()
|
|
|
|
if old == nil || new == nil {
|
|
return new
|
|
}
|
|
|
|
// Handle maps (components)
|
|
if oldMap, ok := old.(map[string]interface{}); ok {
|
|
if newMap, ok := new.(map[string]interface{}); ok {
|
|
// If types match and it's a component (has "type" field), preserve ID
|
|
if oldType, hasOldType := oldMap["type"]; hasOldType {
|
|
if newType, hasNewType := newMap["type"]; hasNewType && oldType == newType {
|
|
// Preserve the ID from the old component
|
|
if oldID, hasOldID := oldMap["id"]; hasOldID {
|
|
newMap["id"] = oldID
|
|
}
|
|
|
|
// Recursively handle props
|
|
if oldProps, hasOldProps := oldMap["props"].(map[string]interface{}); hasOldProps {
|
|
if newProps, hasNewProps := newMap["props"].(map[string]interface{}); hasNewProps {
|
|
// Special handling for items array in props
|
|
if oldItems, ok := oldProps["items"].([]interface{}); ok {
|
|
if newItems, ok := newProps["items"].([]interface{}); ok {
|
|
newProps["items"] = c.componentDiff(oldItems, newItems)
|
|
}
|
|
}
|
|
// Handle other props
|
|
for k, v := range newProps {
|
|
if k != "items" { // Skip items as we already handled it
|
|
if oldV, exists := oldProps[k]; exists {
|
|
newProps[k] = c.componentDiff(oldV, v)
|
|
}
|
|
}
|
|
}
|
|
newMap["props"] = newProps
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return newMap
|
|
}
|
|
}
|
|
|
|
// Handle arrays
|
|
if oldArr, ok := old.([]interface{}); ok {
|
|
if newArr, ok := new.([]interface{}); ok {
|
|
// Create a new array to store the diffed components
|
|
result := make([]interface{}, len(newArr))
|
|
|
|
// First, try to match components by key if available
|
|
oldKeyMap := make(map[string]interface{})
|
|
for _, oldComp := range oldArr {
|
|
if oldMap, ok := oldComp.(map[string]interface{}); ok {
|
|
if key, ok := oldMap["key"].(string); ok && key != "" {
|
|
oldKeyMap[key] = oldComp
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process each new component
|
|
for i, newComp := range newArr {
|
|
matched := false
|
|
|
|
// Try to match by key first
|
|
if newMap, ok := newComp.(map[string]interface{}); ok {
|
|
if key, ok := newMap["key"].(string); ok && key != "" {
|
|
if oldComp, exists := oldKeyMap[key]; exists {
|
|
// Found a match by key
|
|
result[i] = c.componentDiff(oldComp, newComp)
|
|
matched = true
|
|
// t.ctx.logger.Debug().
|
|
// Str("key", key).
|
|
// Str("type", fmt.Sprintf("%v", newMap["type"])).
|
|
// Msg("Component matched by key")
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no key match, try to match by position and type
|
|
if !matched && i < len(oldArr) {
|
|
oldComp := oldArr[i]
|
|
oldType, newType := "", ""
|
|
|
|
if oldMap, ok := oldComp.(map[string]interface{}); ok {
|
|
if t, ok := oldMap["type"].(string); ok {
|
|
oldType = t
|
|
}
|
|
}
|
|
if newMap, ok := newComp.(map[string]interface{}); ok {
|
|
if t, ok := newMap["type"].(string); ok {
|
|
newType = t
|
|
}
|
|
}
|
|
|
|
if oldType != "" && oldType == newType {
|
|
result[i] = c.componentDiff(oldComp, newComp)
|
|
matched = true
|
|
// t.ctx.logger.Debug().
|
|
// Str("type", oldType).
|
|
// Msg("Component matched by type and position")
|
|
}
|
|
}
|
|
|
|
// If no match found, use the new component as is
|
|
if !matched {
|
|
result[i] = newComp
|
|
// if newMap, ok := newComp.(map[string]interface{}); ok {
|
|
// t.ctx.logger.Debug().
|
|
// Str("type", fmt.Sprintf("%v", newMap["type"])).
|
|
// Msg("New component added")
|
|
// }
|
|
}
|
|
}
|
|
|
|
// Log removed components
|
|
// if len(oldArr) > len(newArr) {
|
|
// for i := len(newArr); i < len(oldArr); i++ {
|
|
// if oldMap, ok := oldArr[i].(map[string]interface{}); ok {
|
|
// t.ctx.logger.Debug().
|
|
// Str("type", fmt.Sprintf("%v", oldMap["type"])).
|
|
// Msg("Component removed")
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
return new
|
|
}
|