fix(js): 统一返回对象方法桥接(by AI)
This commit is contained in:
parent
1fa63e7e3c
commit
0786841088
@ -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: { ... }` 包裹。
|
||||
|
||||
17
README.md
17
README.md
@ -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
|
||||
})
|
||||
```
|
||||
|
||||
3
TEST.md
3
TEST.md
@ -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.
|
||||
|
||||
47
bridge.go
47
bridge.go
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user