Compare commits

..

2 Commits
v1.5.8 ... main

Author SHA1 Message Date
Star
3787accfda fix(js): 支持可变参数中的嵌套流式回调(by AI) 2026-08-17 12:36:32 +08:00
Star
7d597422ad chore(deps): 修复模块 checksum(by AI) 2026-07-18 19:00:27 +08:00
7 changed files with 119 additions and 29 deletions

View File

@ -1,5 +1,11 @@
# 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`
## v1.5.8 (2026-07-13)
- **桥接一致性修复**: Go 函数返回的 API 门面对象,其导出方法也统一经由桥接层调用。
JavaScript 的 lower-camel 对象字段会与顶层 API 一样自动转换为 Go struct 字段。

View File

@ -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

View File

@ -1,5 +1,9 @@
# Test Report - go/js
## v1.5.10 验证
- 独立模块解析、全量测试与基准测试均通过。
- `TestBridgeVariadicNestedCallback` 验证可变 options 中 lower-camel 数据与嵌套 JavaScript 回调同时正确转换。
## Performance (Benchmark)
Date: 2026-06-28
OS: darwin

View File

@ -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.

View File

@ -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{}

8
go.mod
View File

@ -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
)
@ -20,8 +20,8 @@ require (
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

52
go.sum
View File

@ -1,23 +1,27 @@
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
apigo.cc/go/config v1.5.0 h1:Yuz9QEb11XXG4XkhDi/ueT2M1T3Q9PElE5tiakvjehs=
apigo.cc/go/config v1.5.0/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM=
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA=
apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw=
apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0=
apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8=
apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA=
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
apigo.cc/go/log v1.5.5 h1:AFU7d7AQxkpgDHl7SnlEwd6yzGSFAlnrrjbrNDQnQHI=
apigo.cc/go/log v1.5.5/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=
apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM=
apigo.cc/go/shell v1.5.0 h1:WLDMMqUU0INeaBDmQsTPr0h/NfB2RknAtiJ5NL467+Q=
apigo.cc/go/shell v1.5.0/go.mod h1:rYHA77d5hEsQHcJrbAWf1pHy0sxayeJ0gU55LA/JWQk=
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=
apigo.cc/go/encoding v1.5.5/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8=
apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs=
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
apigo.cc/go/id v1.5.6 h1:Z3PPp8H8FgNHMwHerxtT2GgstueDv4aCDnUUCoQZoFQ=
apigo.cc/go/id v1.5.6/go.mod h1:HcJK691qfBPzvQ/lt8bo/incKUFtG5vENX/rDEgzJh0=
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=
apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc=
apigo.cc/go/shell v1.5.4/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
@ -34,10 +38,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=