2026-05-30 14:21:43 +08:00
|
|
|
package js
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2026-05-30 15:33:57 +08:00
|
|
|
"io"
|
|
|
|
|
"log"
|
2026-05-30 14:21:43 +08:00
|
|
|
"reflect"
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/cast"
|
|
|
|
|
"github.com/dop251/goja"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// wrapGoFunc converts a standard Go function into a goja.Callable.
|
2026-05-30 15:33:57 +08:00
|
|
|
// It handles context/logger injection, safeMode enforcement, and automatic type conversion.
|
|
|
|
|
func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
|
2026-05-30 14:21:43 +08:00
|
|
|
v := reflect.ValueOf(fn)
|
|
|
|
|
if v.Kind() != reflect.Func {
|
|
|
|
|
panic(fmt.Sprintf("js.bridge: expected func, got %T", fn))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t := v.Type()
|
|
|
|
|
|
|
|
|
|
return vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
2026-05-30 15:33:57 +08:00
|
|
|
// 1. Safety Check
|
|
|
|
|
if isUnsafe {
|
|
|
|
|
safeMode := true // Default to safe mode
|
|
|
|
|
smVal := vm.Get("__safeMode__")
|
|
|
|
|
if smVal != nil && !goja.IsUndefined(smVal) {
|
|
|
|
|
if sm, ok := smVal.Export().(bool); ok {
|
|
|
|
|
safeMode = sm
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if safeMode {
|
|
|
|
|
panic(vm.NewGoError(fmt.Errorf("unauthorized: unsafe operation blocked by safeMode")))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Prepare Arguments
|
2026-05-30 14:21:43 +08:00
|
|
|
numIn := t.NumIn()
|
|
|
|
|
goArgs := make([]reflect.Value, numIn)
|
|
|
|
|
jsArgs := call.Arguments
|
|
|
|
|
jsArgIdx := 0
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
for i := 0; i < numIn; i++ {
|
|
|
|
|
argType := t.In(i)
|
|
|
|
|
|
|
|
|
|
// Magic Injection: context.Context
|
|
|
|
|
if argType.Implements(reflect.TypeOf((*context.Context)(nil)).Elem()) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
ctxVal := vm.Get("__ctx__")
|
|
|
|
|
if ctxVal != nil && !goja.IsUndefined(ctxVal) {
|
|
|
|
|
if c, ok := ctxVal.Export().(context.Context); ok {
|
|
|
|
|
ctx = c
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
goArgs[i] = reflect.ValueOf(ctx)
|
|
|
|
|
continue
|
2026-05-30 14:21:43 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// Magic Injection: *log.Logger
|
|
|
|
|
if argType == reflect.TypeOf((*log.Logger)(nil)) {
|
|
|
|
|
var logger *log.Logger
|
|
|
|
|
logVal := vm.Get("__logger__")
|
|
|
|
|
if logVal != nil && !goja.IsUndefined(logVal) {
|
|
|
|
|
if l, ok := logVal.Export().(*log.Logger); ok {
|
|
|
|
|
logger = l
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if logger == nil {
|
|
|
|
|
// Fallback to a discard logger if none provided to avoid nil panic in Go side
|
|
|
|
|
logger = log.New(io.Discard, "", 0)
|
|
|
|
|
}
|
|
|
|
|
goArgs[i] = reflect.ValueOf(logger)
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-05-30 14:21:43 +08:00
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// Normal JS Argument with go/cast
|
|
|
|
|
goArgs[i] = reflect.New(argType).Elem()
|
2026-05-30 14:21:43 +08:00
|
|
|
if jsArgIdx < len(jsArgs) {
|
|
|
|
|
jsVal := jsArgs[jsArgIdx]
|
|
|
|
|
exported := jsVal.Export()
|
|
|
|
|
|
|
|
|
|
expV := reflect.ValueOf(exported)
|
|
|
|
|
if expV.IsValid() && expV.Type().AssignableTo(argType) {
|
|
|
|
|
goArgs[i].Set(expV)
|
|
|
|
|
} else {
|
|
|
|
|
cast.Convert(goArgs[i].Addr().Interface(), exported)
|
|
|
|
|
}
|
|
|
|
|
jsArgIdx++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// 3. Call the Go function
|
2026-05-30 14:21:43 +08:00
|
|
|
var results []reflect.Value
|
|
|
|
|
var recovered any
|
|
|
|
|
func() {
|
|
|
|
|
defer func() { recovered = recover() }()
|
|
|
|
|
results = v.Call(goArgs)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
if recovered != nil {
|
|
|
|
|
panic(vm.NewGoError(fmt.Errorf("go panic: %v", recovered)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// 4. Process Results
|
2026-05-30 14:21:43 +08:00
|
|
|
if len(results) == 0 {
|
|
|
|
|
return goja.Undefined()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// Check for error return
|
|
|
|
|
last := results[len(results)-1]
|
|
|
|
|
if last.Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
|
|
|
|
if !last.IsNil() {
|
|
|
|
|
err := last.Interface().(error)
|
|
|
|
|
panic(vm.NewGoError(err))
|
|
|
|
|
}
|
|
|
|
|
if len(results) == 1 {
|
|
|
|
|
return goja.Undefined()
|
2026-05-30 14:21:43 +08:00
|
|
|
}
|
2026-05-30 15:33:57 +08:00
|
|
|
results = results[:len(results)-1]
|
2026-05-30 14:21:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(results) == 1 {
|
|
|
|
|
return vm.ToValue(results[0].Interface())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resSlice := make([]any, len(results))
|
|
|
|
|
for i, r := range results {
|
|
|
|
|
resSlice[i] = r.Interface()
|
|
|
|
|
}
|
|
|
|
|
return vm.ToValue(resSlice)
|
|
|
|
|
})
|
|
|
|
|
}
|