node build fixed
This commit is contained in:
374
seanime-2.9.10/internal/plugin/ui/component_utils.go
Normal file
374
seanime-2.9.10/internal/plugin/ui/component_utils.go
Normal file
@@ -0,0 +1,374 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user