From 3787accfdabd15a033103dfc2409d08cb7b1d3f8 Mon Sep 17 00:00:00 2001 From: Star Date: Mon, 17 Aug 2026 12:36:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(js):=20=E6=94=AF=E6=8C=81=E5=8F=AF=E5=8F=98?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E4=B8=AD=E7=9A=84=E5=B5=8C=E5=A5=97=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=9B=9E=E8=B0=83=EF=BC=88by=20AI=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +++ README.md | 1 + TEST.md | 3 ++- bridge.go | 48 +++++++++++++++++++++++++++++++++++++++++++++++- bridge_test.go | 29 +++++++++++++++++++++++++++++ go.mod | 4 ++-- go.sum | 4 ++++ 7 files changed, 88 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd7209..8ad8029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG - go/js +## v1.5.10 (2026-08-17) +- **嵌套回调桥接**: 可变参数中的 lower-camel 配置对象现可包含 JavaScript 回调;普通字段继续使用 `cast` 转换,函数字段由 goja 原生导出,支持 API 流式回调。 + ## v1.5.9 (2026-07-18) - **依赖校验修复**: 重新整理模块 checksum,并对齐 `x/crypto v0.52.0`、`x/sys v0.45.0`。 diff --git a/README.md b/README.md index 01fc702..93aea29 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A lightweight, frictionless, and AI-friendly JavaScript engine for Go applicatio - **Versioned Pool**: Thread-safe VM pool with incremental code synchronization and version checking (`CheckVersion`). - **Function Discovery**: List all defined functions via `FuncList()`. - **Context Interruption**: Safe execution with `context.Context` cancellation support. +- **Nested Callback Bridging**: JavaScript callbacks inside lower-camel option objects are preserved when converted to Go structs, including variadic streaming options. - **AI-Ready**: Generates TypeScript definitions (`.d.ts`) for AI to understand available capabilities. ## Usage diff --git a/TEST.md b/TEST.md index b9e8144..a72928f 100644 --- a/TEST.md +++ b/TEST.md @@ -1,7 +1,8 @@ # Test Report - go/js -## v1.5.9 验证 +## v1.5.10 验证 - 独立模块解析、全量测试与基准测试均通过。 +- `TestBridgeVariadicNestedCallback` 验证可变 options 中 lower-camel 数据与嵌套 JavaScript 回调同时正确转换。 ## Performance (Benchmark) Date: 2026-06-28 diff --git a/bridge.go b/bridge.go index 9878629..6af5ffc 100644 --- a/bridge.go +++ b/bridge.go @@ -89,11 +89,13 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value { if isVariadic && i == numIn-1 { elemType := argType.Elem() for jsArgIdx < len(jsArgs) { - exported := jsArgs[jsArgIdx].Export() + jsValue := jsArgs[jsArgIdx] + exported := jsValue.Export() expV := reflect.ValueOf(exported) if !expV.IsValid() || !expV.Type().AssignableTo(elemType) { elem := reflect.New(elemType).Elem() cast.Convert(elem.Addr().Interface(), exported) + exportJSCallbacks(vm, jsValue, elem) expV = elem } goArgs = append(goArgs, expV) @@ -175,6 +177,50 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value { }) } +// exportJSCallbacks overlays callable fields after cast.Convert has applied the +// project's lower-camel struct mapping. goja's native ExportTo preserves JS +// functions, while cast remains authoritative for ordinary data conversion. +func exportJSCallbacks(vm *goja.Runtime, source goja.Value, target reflect.Value) { + if source == nil { + return + } + for target.Kind() == reflect.Ptr { + if target.IsNil() { + if goja.IsUndefined(source) || goja.IsNull(source) { + return + } + target.Set(reflect.New(target.Type().Elem())) + } + target = target.Elem() + } + if target.Kind() != reflect.Struct || goja.IsUndefined(source) || goja.IsNull(source) { + return + } + object := source.ToObject(vm) + targetType := target.Type() + for i := 0; i < target.NumField(); i++ { + field := target.Field(i) + if !field.CanSet() { + continue + } + name := cast.GetLowerName(targetType.Field(i).Name) + value := object.Get(name) + if goja.IsUndefined(value) { + value = object.Get(targetType.Field(i).Name) + } + if value == nil || goja.IsUndefined(value) || goja.IsNull(value) { + continue + } + if field.Kind() == reflect.Func { + _ = vm.ExportTo(value, field.Addr().Interface()) + continue + } + if field.Kind() == reflect.Struct || field.Kind() == reflect.Ptr { + exportJSCallbacks(vm, value, field) + } + } +} + // 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. diff --git a/bridge_test.go b/bridge_test.go index 1f7e98b..b2195ed 100644 --- a/bridge_test.go +++ b/bridge_test.go @@ -24,6 +24,15 @@ type bridgeFacadeRequest struct { Limit int } +type bridgeCallbackGroup struct { + OnData func([]byte) error +} + +type bridgeCallbackOptions struct { + Token string + Stream *bridgeCallbackGroup +} + // bridgeFacade intentionally follows the shape used by Go APIs exposed to JS: // private state and exported methods. type bridgeFacade struct { @@ -167,6 +176,26 @@ func TestBridgeVariadic(t *testing.T) { } } +func TestBridgeVariadicNestedCallback(t *testing.T) { + vm := goja.New() + call := func(options ...*bridgeCallbackOptions) error { + if len(options) != 1 || options[0] == nil || options[0].Token != "internal" || options[0].Stream == nil || options[0].Stream.OnData == nil { + return fmt.Errorf("options were not converted: %#v", options) + } + return options[0].Stream.OnData([]byte("chunk")) + } + if err := vm.Set("call", wrapGoFunc(vm, call, false)); err != nil { + t.Fatal(err) + } + value, err := vm.RunString(`let received = ''; call({token: 'internal', stream: {onData(chunk) { received = String.fromCharCode(...chunk); return null }}}); received`) + if err != nil { + t.Fatal(err) + } + if value.String() != "chunk" { + t.Fatalf("callback received %q", value.String()) + } +} + func TestBridgeMapsLowerCamelCaseForReturnedObjectMethods(t *testing.T) { vm := goja.New() facade := &bridgeFacade{} diff --git a/go.mod b/go.mod index f3d7819..5c7c7b3 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module apigo.cc/go/js go 1.25.0 require ( - apigo.cc/go/cast v1.5.3 + apigo.cc/go/cast v1.5.5 apigo.cc/go/jsmod v1.5.3 - apigo.cc/go/log v1.5.8 + apigo.cc/go/log v1.5.9 github.com/dop251/goja v0.0.0-20260311135729-065cd970411c ) diff --git a/go.sum b/go.sum index 7a7297e..9eaa316 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw= apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= +apigo.cc/go/cast v1.5.5 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw= +apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= apigo.cc/go/config v1.5.3 h1:peq1FM2xO+vzPHJf8Dwg3DXm8PtFQMfTFKQj6fpoG7A= apigo.cc/go/config v1.5.3/go.mod h1:ZiOAjWa1mQIzszaJZN+kO6YU4GXreng+NxkcK/TAkqQ= apigo.cc/go/encoding v1.5.5 h1:kduNLWQgtcQqHYobOuu1djbgg8LedkGOe8f18ZMfqzs= @@ -12,6 +14,8 @@ apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU= apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0= apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE= +apigo.cc/go/log v1.5.9 h1:g8JehZrpVJyesesv+JFig5szEELsO4UjShsjlEihCfc= +apigo.cc/go/log v1.5.9/go.mod h1:b4f/UB5Kk7oiFtkXvG4GVilmHGygWgxELRAdEZrIyfs= apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius= apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4= apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=