fix(js): 统一返回对象方法桥接(by AI)

This commit is contained in:
Star 2026-07-13 22:23:38 +08:00
parent 1fa63e7e3c
commit 0786841088
5 changed files with 113 additions and 14 deletions

View File

@ -1,5 +1,10 @@
# CHANGELOG - go/js
## v1.5.8 (2026-07-13)
- **桥接一致性修复**: Go 函数返回的 API 门面对象,其导出方法也统一经由桥接层调用。
JavaScript 的 lower-camel 对象字段会与顶层 API 一样自动转换为 Go struct 字段。
- **回归测试**: 覆盖 `table().Query({filter: [...]})` 形式的返回对象方法调用。
## v1.5.7 (2026-06-28)
- **低代码 TS 文档对齐**:
- `Doc()` 生成的模块入口改为顶层全局变量声明,如 `declare const api: Api_Module;`,不再使用 `declare const go: { ... }` 包裹。

View File

@ -36,11 +36,11 @@ import "apigo.cc/go/js"
func main() {
// Check if script needs update (e.g., from file mtime)
if !js.CheckVersion("myTask.js", mtime) {
js.Define(code, "myTask.js", mtime)
if !js.CheckVersion("myTask", mtime) {
js.Define("myTask", code, mtime)
}
res, err := js.Call(ctx, "myTask", "star")
res, err := js.Call("myTask", time.Second, nil, "star")
}
```
@ -79,9 +79,18 @@ interface Logger {
## Internal Bridge Details
The engine uses `goja`'s Host Object mechanism. When a Go struct/pointer is returned to JS, it remains a Go object. When passed back to a Go function, the original pointer is preserved, ensuring zero data loss and state consistency.
The engine preserves ordinary Go data values. API facade objects returned to JavaScript expose their exported methods through the same bridge as top-level module functions, ensuring identical context injection, safe-mode checking, error conversion, and struct conversion.
Types are automatically coerced:
- JS `string` -> Go `int` (via `go/cast`)
- JS `Object` -> Go `Struct`
- Go `error` -> JS `Exception`
Therefore both styles use normal JavaScript lower-camel fields:
```js
knowbase.Table("Account").Query({
filter: [{ field: "Name", operator: "=", value: "Admin" }],
limit: 1000
})
```

View File

@ -33,6 +33,8 @@ CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
--- PASS: TestBridgeOptionalParams (0.00s)
=== RUN TestBridgeVariadic
--- PASS: TestBridgeVariadic (0.00s)
=== RUN TestBridgeMapsLowerCamelCaseForReturnedObjectMethods
--- PASS: TestBridgeMapsLowerCamelCaseForReturnedObjectMethods (0.00s)
=== RUN TestDocGeneration
--- PASS: TestDocGeneration (0.00s)
=== RUN TestPoolVersioning
@ -55,6 +57,7 @@ ok apigo.cc/go/js 0.800s
## Features Verified
- [x] JS calling Go with automatic type conversion.
- [x] Returned Go API object methods use the same lower-camel struct conversion.
- [x] Go pointers preservation in JS.
- [x] Context and Logger injection.
- [x] Concurrent execution and script versioning.

View File

@ -164,17 +164,58 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
}
if len(results) == 1 {
return vm.ToValue(results[0].Interface())
return toJSValue(vm, results[0].Interface())
}
resSlice := make([]any, len(results))
for i, r := range results {
resSlice[i] = r.Interface()
resSlice[i] = toJSValue(vm, r.Interface())
}
return vm.ToValue(resSlice)
})
}
// toJSValue exposes Go API objects through bridge-wrapped methods rather than
// goja's direct reflection. Directly reflected methods bypass cast.Convert, so
// nested lower-camel JavaScript objects would not be converted to Go structs.
// Plain data structs retain goja's normal value export behavior.
func toJSValue(vm *goja.Runtime, value any) goja.Value {
reflectValue := reflect.ValueOf(value)
if !reflectValue.IsValid() {
return goja.Null()
}
reflectType := reflectValue.Type()
structType := reflectType
if structType.Kind() == reflect.Pointer {
if reflectValue.IsNil() {
return goja.Null()
}
structType = structType.Elem()
}
if structType.Kind() != reflect.Struct || reflectType.NumMethod() == 0 || hasExportedField(structType) {
return vm.ToValue(value)
}
object := vm.NewObject()
for index := 0; index < reflectType.NumMethod(); index++ {
method := reflectType.Method(index)
boundMethod := reflectValue.Method(index)
if boundMethod.IsValid() {
_ = object.Set(method.Name, wrapGoFunc(vm, boundMethod.Interface(), false))
}
}
return object
}
func hasExportedField(structType reflect.Type) bool {
for index := 0; index < structType.NumField(); index++ {
if structType.Field(index).IsExported() {
return true
}
}
return false
}
// trimGoPath shortens a Go source file path for display:
// keeps only the last two path components (package/file.go).
func trimGoPath(fullPath string) string {
@ -198,7 +239,7 @@ type goStackError struct {
func (e *goStackError) Error() string { return e.cause.Error() }
func (e *goStackError) Unwrap() error { return e.cause }
func (e *goStackError) Stack() string { return e.stack }
func (e *goStackError) Stack() string { return e.stack }
// formatGoStack filters a full goroutine stack trace, keeping only frames
// relevant to the user: dropping runtime, reflect, bridge, and goja internals.

View File

@ -13,6 +13,27 @@ import (
"github.com/dop251/goja"
)
type bridgeFacadeFilter struct {
Field string
Operator string
Value any
}
type bridgeFacadeRequest struct {
Filter []bridgeFacadeFilter
Limit int
}
// bridgeFacade intentionally follows the shape used by Go APIs exposed to JS:
// private state and exported methods.
type bridgeFacade struct {
received bridgeFacadeRequest
}
func (f *bridgeFacade) Query(request bridgeFacadeRequest) {
f.received = request
}
func TestBridgeSafeMode(t *testing.T) {
vm := goja.New()
@ -145,3 +166,23 @@ func TestBridgeVariadic(t *testing.T) {
t.Errorf("Variadic args content wrong, got %v", result)
}
}
func TestBridgeMapsLowerCamelCaseForReturnedObjectMethods(t *testing.T) {
vm := goja.New()
facade := &bridgeFacade{}
if err := vm.Set("table", wrapGoFunc(vm, func() *bridgeFacade { return facade }, false)); err != nil {
t.Fatal(err)
}
_, err := vm.RunString(`table().Query({filter: [{field: "Name", operator: "=", value: "Admin"}], limit: 1000})`)
if err != nil {
t.Fatalf("JS execution failed: %v", err)
}
if facade.received.Limit != 1000 || len(facade.received.Filter) != 1 {
t.Fatalf("lower-camel request was not converted: %#v", facade.received)
}
filter := facade.received.Filter[0]
if filter.Field != "Name" || filter.Operator != "=" || filter.Value != "Admin" {
t.Errorf("unexpected converted filter: %#v", filter)
}
}