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 }