Compare commits

...

1 Commits
v1.5.6 ... main

Author SHA1 Message Date
AI Engineer
1fa63e7e3c fix(js): 修正低代码 TS 声明生成(by AI) 2026-06-28 01:39:41 +08:00
6 changed files with 112 additions and 25 deletions

View File

@ -1,5 +1,13 @@
# CHANGELOG - go/js # CHANGELOG - go/js
## v1.5.7 (2026-06-28)
- **低代码 TS 文档对齐**:
- `Doc()` 生成的模块入口改为顶层全局变量声明,如 `declare const api: Api_Module;`,不再使用 `declare const go: { ... }` 包裹。
- 为运行时注入的日志对象补充 `Logger` 接口,仅暴露 `Debug``Info``Warning``Error` 四个 JS 侧需要的方法。
- 统一非项目类型的不透明命名风格,默认声明改为 `GoContext``GoHTTPRequest``GoHTTPResponse``GoURL`,标准库结构体采用 `GoTime` 这类更稳定的命名。
- **测试增强**:
- `TestDocGeneration` 新增对顶层全局模块声明、`Logger` 暴露面和 `GoTime` 命名的覆盖。
## v1.5.6 (2026-06-21) ## v1.5.6 (2026-06-21)
- **可变参数桥接修复**: `wrapGoFunc` 修复对 Go 可变参数(`...any`)的桥接处理。之前将 JS 剩余参数错误打包为单一切片元素,导致 `Call` 二次嵌套;现在改为逐个追加到 `goArgs` 尾部,由 `reflect.Call` 自动构建可变切片,确保 JS 调用 `redis.Do('HSET', a, b, c)` 正确展开。 - **可变参数桥接修复**: `wrapGoFunc` 修复对 Go 可变参数(`...any`)的桥接处理。之前将 JS 剩余参数错误打包为单一切片元素,导致 `Call` 二次嵌套;现在改为逐个追加到 `goArgs` 尾部,由 `reflect.Call` 自动构建可变切片,确保 JS 调用 `redis.Do('HSET', a, b, c)` 正确展开。
- **新增测试**: `TestBridgeVariadic` 覆盖可变参数 0/1/多参数场景。 - **新增测试**: `TestBridgeVariadic` 覆盖可变参数 0/1/多参数场景。

View File

@ -58,6 +58,25 @@ dts := js.Doc()
// Feed d.ts to LLM to provide coding context // Feed d.ts to LLM to provide coding context
``` ```
The generated declarations are optimized for low-code editors:
- bridged modules are exposed as top-level globals such as `api`, `cast`, `db`, `service`
- injected logger values are typed as `Logger`
- opaque runtime handles use stable names such as `GoContext`, `GoHTTPRequest`, `GoHTTPResponse`, `GoURL`
- exposed standard-library structs use stable names such as `GoTime`
Example:
```ts
declare const service: Service_Module
interface Logger {
Debug(message: string, ...extra: any[]): void
Info(message: string, ...extra: any[]): void
Warning(message: string, ...extra: any[]): void
Error(message: string, ...extra: any[]): void
}
```
## Internal Bridge Details ## 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 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.

13
TEST.md
View File

@ -1,15 +1,15 @@
# Test Report - go/js # Test Report - go/js
## Performance (Benchmark) ## Performance (Benchmark)
Date: 2026-06-21 Date: 2026-06-28
OS: darwin OS: darwin
Arch: amd64 Arch: amd64
CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
| Benchmark | Iterations | Time/op | | Benchmark | Iterations | Time/op |
|-----------|------------|---------| |-----------|------------|---------|
| BenchmarkCall | 661954 | 1566 ns/op | | BenchmarkCall | 766462 | 1331 ns/op |
| BenchmarkSync | 51656 | 50748 ns/op | | BenchmarkSync | 31066 | 52789 ns/op |
*Note: BenchmarkCall covers the hot path of executing a JS function from the pool. BenchmarkSync covers the cost of defining new code (including VM sync).* *Note: BenchmarkCall covers the hot path of executing a JS function from the pool. BenchmarkSync covers the cost of defining new code (including VM sync).*
@ -31,6 +31,8 @@ CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
--- PASS: TestBridgeMixedInjection (0.00s) --- PASS: TestBridgeMixedInjection (0.00s)
=== RUN TestBridgeOptionalParams === RUN TestBridgeOptionalParams
--- PASS: TestBridgeOptionalParams (0.00s) --- PASS: TestBridgeOptionalParams (0.00s)
=== RUN TestBridgeVariadic
--- PASS: TestBridgeVariadic (0.00s)
=== RUN TestDocGeneration === RUN TestDocGeneration
--- PASS: TestDocGeneration (0.00s) --- PASS: TestDocGeneration (0.00s)
=== RUN TestPoolVersioning === RUN TestPoolVersioning
@ -48,7 +50,7 @@ CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
=== RUN TestDefineRedefine === RUN TestDefineRedefine
--- PASS: TestDefineRedefine (0.00s) --- PASS: TestDefineRedefine (0.00s)
PASS PASS
ok apigo.cc/go/js 0.572s ok apigo.cc/go/js 0.800s
``` ```
## Features Verified ## Features Verified
@ -61,4 +63,7 @@ ok apigo.cc/go/js 0.572s
- [x] Context cancellation interruption. - [x] Context cancellation interruption.
- [x] Graceful shutdown. - [x] Graceful shutdown.
- [x] TypeScript definition generation. - [x] TypeScript definition generation.
- [x] Top-level global module declarations for low-code hints.
- [x] Opaque type naming stability (`GoContext`, `GoTime`, `GoURL`).
- [x] Runtime-injected `Logger` type hints with JS-safe methods only.
- [x] JS VM call stack parsing & Go dynamic call stack restoration with `jsmod.MakeError`. - [x] JS VM call stack parsing & Go dynamic call stack restoration with `jsmod.MakeError`.

68
doc.go
View File

@ -16,17 +16,23 @@ func Doc() string {
sb.WriteString("/**\n * Go/JS Low-Code Environment Type Definitions\n") sb.WriteString("/**\n * Go/JS Low-Code Environment Type Definitions\n")
sb.WriteString(" * Generated by js.Doc(). DO NOT EDIT.\n */\n\n") sb.WriteString(" * Generated by js.Doc(). DO NOT EDIT.\n */\n\n")
// 1. Basic Opaque types (Manual fallback for critical non-project types) // 1. Basic opaque types for critical non-project types
sb.WriteString("/** Opaque handle to Go context.Context */\n") sb.WriteString("/** Opaque handle to Go context.Context */\n")
sb.WriteString("interface GoContext { _isGoContext: true; }\n") sb.WriteString("interface GoContext { _isGoContext: true; }\n")
sb.WriteString("/** Opaque handle to Go log.Logger */\n") sb.WriteString("/** Logger injected by the Go runtime */\n")
sb.WriteString("interface GoLogger { _isGoLogger: true; }\n") sb.WriteString("interface Logger {\n")
sb.WriteString(" _isLogger: true;\n")
sb.WriteString(" Debug(message: string, ...extra: any[]): void;\n")
sb.WriteString(" Info(message: string, ...extra: any[]): void;\n")
sb.WriteString(" Warning(message: string, ...extra: any[]): void;\n")
sb.WriteString(" Error(message: string, ...extra: any[]): void;\n")
sb.WriteString("}\n")
sb.WriteString("/** Opaque handle to Go net/http.Request */\n") sb.WriteString("/** Opaque handle to Go net/http.Request */\n")
sb.WriteString("interface GoHttp_Request { _isGoHttpReq: true; }\n") sb.WriteString("interface GoHTTPRequest { _isGoHTTPRequest: true; }\n")
sb.WriteString("/** Opaque handle to Go net/http.Response */\n") sb.WriteString("/** Opaque handle to Go net/http.Response */\n")
sb.WriteString("interface GoHttp_Response { _isGoHttpRes: true; }\n") sb.WriteString("interface GoHTTPResponse { _isGoHTTPResponse: true; }\n")
sb.WriteString("/** Opaque handle to Go net/url.URL */\n") sb.WriteString("/** Opaque handle to Go net/url.URL */\n")
sb.WriteString("interface GoNet_URL { _isGoNetURL: true; }\n\n") sb.WriteString("interface GoURL { _isGoURL: true; }\n\n")
modules := jsmod.GetModules() modules := jsmod.GetModules()
modNames := make([]string, 0, len(modules)) modNames := make([]string, 0, len(modules))
@ -106,13 +112,11 @@ func Doc() string {
sb.WriteString("\n\n") sb.WriteString("\n\n")
} }
// 5. Global 'go' declaration // 5. Global module declarations
sb.WriteString("/** Global entry point for Go bridged modules */\n") sb.WriteString("/** Global Go bridged modules */\n")
sb.WriteString("declare const go: {\n")
for _, modName := range modNames { for _, modName := range modNames {
sb.WriteString(fmt.Sprintf(" readonly %s: %s_Module;\n", modName, strings.Title(modName))) sb.WriteString(fmt.Sprintf("declare const %s: %s_Module;\n", modName, strings.Title(modName)))
} }
sb.WriteString("};\n")
return sb.String() return sb.String()
} }
@ -232,6 +236,21 @@ func goTypeToTS(t reflect.Type, ctx *docCtx) string {
} }
// 3. Special Mappings for remaining named types (mostly Structs/Interfaces) // 3. Special Mappings for remaining named types (mostly Structs/Interfaces)
if pkgPath == "context" && rawName == "Context" {
return "GoContext"
}
if pkgPath == "apigo.cc/go/log" && rawName == "Logger" {
return "Logger"
}
if pkgPath == "net/http" && rawName == "Request" {
return "GoHTTPRequest"
}
if pkgPath == "net/http" && rawName == "Response" {
return "GoHTTPResponse"
}
if pkgPath == "net/url" && rawName == "URL" {
return "GoURL"
}
if pkgPath == "time" && rawName == "Time" { if pkgPath == "time" && rawName == "Time" {
return registerInterface(t, ctx) return registerInterface(t, ctx)
} }
@ -242,7 +261,7 @@ func goTypeToTS(t reflect.Type, ctx *docCtx) string {
if pkgPath != "" && !isProject { if pkgPath != "" && !isProject {
base := filepath.Base(pkgPath) base := filepath.Base(pkgPath)
opaqueName := "Go" + strings.Title(base) + "_" + rawName opaqueName := strings.Title(base) + "_" + rawName
ctx.opaqueList[opaqueName] = true ctx.opaqueList[opaqueName] = true
return opaqueName return opaqueName
} }
@ -274,9 +293,7 @@ func registerInterface(t reflect.Type, ctx *docCtx) string {
parts := strings.Split(pkgPath, "/") parts := strings.Split(pkgPath, "/")
name = fmt.Sprintf("%s_%s", strings.Title(parts[len(parts)-1]), rawName) name = fmt.Sprintf("%s_%s", strings.Title(parts[len(parts)-1]), rawName)
} else if pkgPath != "" { } else if pkgPath != "" {
// Standard lib types like time.Time name = externalTypeName(pkgPath, rawName)
base := filepath.Base(pkgPath)
name = fmt.Sprintf("Go%s_%s", strings.Title(base), rawName)
} else { } else {
name = fmt.Sprintf("%s_%s", strings.Title(ctx.currentMod), rawName) name = fmt.Sprintf("%s_%s", strings.Title(ctx.currentMod), rawName)
} }
@ -330,6 +347,27 @@ func registerInterface(t reflect.Type, ctx *docCtx) string {
return name return name
} }
func externalTypeName(pkgPath, rawName string) string {
base := filepath.Base(pkgPath)
baseName := externalSegmentName(base)
typeName := externalSegmentName(rawName)
if baseName == typeName {
return "Go" + typeName
}
return "Go" + baseName + typeName
}
func externalSegmentName(name string) string {
switch strings.ToLower(name) {
case "http":
return "HTTP"
case "url":
return "URL"
default:
return strings.Title(name)
}
}
func isMethodUnusable(t reflect.Type) bool { func isMethodUnusable(t reflect.Type) bool {
for i := 0; i < t.NumIn(); i++ { for i := 0; i < t.NumIn(); i++ {
if isTypeUnusable(t.In(i)) { if isTypeUnusable(t.In(i)) {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"strings" "strings"
"testing" "testing"
"time"
"apigo.cc/go/jsmod" "apigo.cc/go/jsmod"
) )
@ -13,19 +14,35 @@ func TestDocGeneration(t *testing.T) {
"query": func(ctx context.Context, sql string, args []any) ([]map[string]any, error) { "query": func(ctx context.Context, sql string, args []any) ([]map[string]any, error) {
return nil, nil return nil, nil
}, },
"now": func() time.Time { return time.Time{} },
"version": "1.0.0", "version": "1.0.0",
"__exportInternal": func() *struct{ Name string } { return nil }, "__exportInternal": func() *struct{ Name string } { return nil },
}) })
doc := Doc() doc := Doc()
if !strings.Contains(doc, "interface GoTime") { if !strings.Contains(doc, "interface GoContext {") {
t.Error("doc should contain GoContext interface")
}
if !strings.Contains(doc, "interface Logger {") {
t.Error("doc should contain Logger interface")
}
if !strings.Contains(doc, "Info(message: string, ...extra: any[]): void;") {
t.Error("doc should contain Logger methods")
}
if strings.Contains(doc, "GetTraceId(): string;") || strings.Contains(doc, "New(traceId: string): Logger;") || strings.Contains(doc, "As<T = any>(value: T, err: any): T;") {
t.Error("doc should not contain Logger internal methods")
}
if !strings.Contains(doc, "interface GoTime {") {
t.Error("doc should contain GoTime interface") t.Error("doc should contain GoTime interface")
} }
if !strings.Contains(doc, "interface Db_Module") { if !strings.Contains(doc, "interface Db_Module") {
t.Error("doc should contain Db_Module interface") t.Error("doc should contain Db_Module interface")
} }
if !strings.Contains(doc, "declare const go:") { if !strings.Contains(doc, "declare const db: Db_Module;") {
t.Error("doc should contain global go declaration") t.Error("doc should contain top-level module declaration")
}
if strings.Contains(doc, "declare const go:") {
t.Error("doc should not contain legacy go namespace declaration")
} }
if strings.Contains(doc, "__exportInternal") { if strings.Contains(doc, "__exportInternal") {
t.Error("doc should NOT contain __exportInternal") t.Error("doc should NOT contain __exportInternal")

6
go.mod
View File

@ -11,12 +11,12 @@ require (
require ( require (
apigo.cc/go/config v1.5.3 // indirect apigo.cc/go/config v1.5.3 // indirect
apigo.cc/go/encoding v1.5.4 // indirect apigo.cc/go/encoding v1.5.5 // indirect
apigo.cc/go/file v1.5.5 // indirect apigo.cc/go/file v1.5.5 // indirect
apigo.cc/go/id v1.5.4 // indirect apigo.cc/go/id v1.5.6 // indirect
apigo.cc/go/rand v1.5.3 // indirect apigo.cc/go/rand v1.5.3 // indirect
apigo.cc/go/safe v1.5.2 // indirect apigo.cc/go/safe v1.5.2 // indirect
apigo.cc/go/shell v1.5.3 // indirect apigo.cc/go/shell v1.5.4 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect