349 lines
8.7 KiB
Go
349 lines
8.7 KiB
Go
package js
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"apigo.cc/go/jsmod"
|
|
)
|
|
|
|
// Doc generates a comprehensive TypeScript definition (.d.ts) for the Go/JS environment.
|
|
func Doc() string {
|
|
var sb strings.Builder
|
|
sb.WriteString("/**\n * Go/JS Low-Code Environment Type Definitions\n")
|
|
sb.WriteString(" * Generated by js.Doc(). DO NOT EDIT.\n */\n\n")
|
|
|
|
// 1. Basic Opaque types (Manual fallback for critical non-project types)
|
|
sb.WriteString("/** Opaque handle to Go context.Context */\n")
|
|
sb.WriteString("interface GoContext { _isGoContext: true; }\n")
|
|
sb.WriteString("/** Opaque handle to Go log.Logger */\n")
|
|
sb.WriteString("interface GoLogger { _isGoLogger: true; }\n")
|
|
sb.WriteString("/** Opaque handle to Go net/http.Request */\n")
|
|
sb.WriteString("interface GoHttp_Request { _isGoHttpReq: true; }\n")
|
|
sb.WriteString("/** Opaque handle to Go net/http.Response */\n")
|
|
sb.WriteString("interface GoHttp_Response { _isGoHttpRes: true; }\n")
|
|
sb.WriteString("/** Opaque handle to Go net/url.URL */\n")
|
|
sb.WriteString("interface GoNet_URL { _isGoNetURL: true; }\n\n")
|
|
|
|
modules := jsmod.GetModules()
|
|
modNames := make([]string, 0, len(modules))
|
|
for k := range modules {
|
|
modNames = append(modNames, k)
|
|
}
|
|
sort.Strings(modNames)
|
|
|
|
ctx := &docCtx{
|
|
seenTypes: make(map[reflect.Type]string),
|
|
interfaces: make(map[string]string),
|
|
opaqueList: make(map[string]bool),
|
|
}
|
|
|
|
// 2. Build Module Interfaces
|
|
moduleDefs := make(map[string]string)
|
|
for _, modName := range modNames {
|
|
mod := modules[modName]
|
|
ctx.currentMod = modName
|
|
|
|
var msb strings.Builder
|
|
msb.WriteString(fmt.Sprintf("interface %s_Module {\n", strings.Title(modName)))
|
|
|
|
expKeys := make([]string, 0, len(mod.Exports))
|
|
for k := range mod.Exports {
|
|
expKeys = append(expKeys, k)
|
|
}
|
|
sort.Strings(expKeys)
|
|
|
|
for _, name := range expKeys {
|
|
isHidden := strings.HasPrefix(name, "__export")
|
|
val := mod.Exports[name]
|
|
|
|
memberDef := formatExport(name, val, ctx, false)
|
|
|
|
if !isHidden {
|
|
isUnsafe := mod.UnsafeList[name]
|
|
if isUnsafe {
|
|
msb.WriteString(" /** @unsafe */\n")
|
|
}
|
|
msb.WriteString(fmt.Sprintf(" %s\n", memberDef))
|
|
}
|
|
}
|
|
msb.WriteString("}")
|
|
moduleDefs[modName] = msb.String()
|
|
}
|
|
|
|
// 2. Output Opaque Interfaces (Dynamic)
|
|
opaqueNames := make([]string, 0, len(ctx.opaqueList))
|
|
for name := range ctx.opaqueList {
|
|
opaqueNames = append(opaqueNames, name)
|
|
}
|
|
sort.Strings(opaqueNames)
|
|
for _, name := range opaqueNames {
|
|
sb.WriteString(fmt.Sprintf("interface %s { _is%s: true; }\n", name, name))
|
|
}
|
|
sb.WriteString("\n")
|
|
|
|
// 3. Supporting Interfaces
|
|
if len(ctx.interfaces) > 0 {
|
|
sb.WriteString("// --- Supporting Interfaces ---\n\n")
|
|
ifaceNames := make([]string, 0, len(ctx.interfaces))
|
|
for name := range ctx.interfaces {
|
|
ifaceNames = append(ifaceNames, name)
|
|
}
|
|
sort.Strings(ifaceNames)
|
|
for _, name := range ifaceNames {
|
|
sb.WriteString(ctx.interfaces[name])
|
|
sb.WriteString("\n\n")
|
|
}
|
|
}
|
|
|
|
// 4. Module Interfaces
|
|
sb.WriteString("// --- Module Definitions ---\n\n")
|
|
for _, modName := range modNames {
|
|
sb.WriteString(moduleDefs[modName])
|
|
sb.WriteString("\n\n")
|
|
}
|
|
|
|
// 5. Global 'go' declaration
|
|
sb.WriteString("/** Global entry point for Go bridged modules */\n")
|
|
sb.WriteString("declare const go: {\n")
|
|
for _, modName := range modNames {
|
|
sb.WriteString(fmt.Sprintf(" readonly %s: %s_Module;\n", modName, strings.Title(modName)))
|
|
}
|
|
sb.WriteString("};\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
type docCtx struct {
|
|
currentMod string
|
|
seenTypes map[reflect.Type]string
|
|
interfaces map[string]string
|
|
opaqueList map[string]bool
|
|
}
|
|
|
|
func formatExport(name string, val any, ctx *docCtx, isMethod bool) string {
|
|
t := reflect.TypeOf(val)
|
|
if t == nil {
|
|
return fmt.Sprintf("%s: any;", name)
|
|
}
|
|
|
|
if t.Kind() == reflect.Func {
|
|
return fmt.Sprintf("%s%s;", name, formatFunc(t, ctx, isMethod))
|
|
}
|
|
|
|
return fmt.Sprintf("%s: %s;", name, goTypeToTS(t, ctx))
|
|
}
|
|
|
|
func formatFunc(t reflect.Type, ctx *docCtx, isMethod bool) string {
|
|
var params []string
|
|
numIn := t.NumIn()
|
|
jsArgIdx := 0
|
|
|
|
startIdx := 0
|
|
if isMethod {
|
|
startIdx = 1 // Skip receiver
|
|
}
|
|
|
|
for i := startIdx; i < numIn; i++ {
|
|
argType := t.In(i)
|
|
typeName := argType.String()
|
|
if typeName == "context.Context" || strings.Contains(typeName, "log.Logger") {
|
|
continue
|
|
}
|
|
|
|
params = append(params, fmt.Sprintf("arg%d: %s", jsArgIdx, goTypeToTS(argType, ctx)))
|
|
jsArgIdx++
|
|
}
|
|
|
|
numOut := t.NumOut()
|
|
var retType string
|
|
if numOut == 0 {
|
|
retType = "void"
|
|
} else {
|
|
realOut := numOut
|
|
if numOut > 0 && t.Out(numOut-1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
|
realOut--
|
|
}
|
|
|
|
if realOut <= 0 {
|
|
retType = "void"
|
|
} else if realOut == 1 {
|
|
retType = goTypeToTS(t.Out(0), ctx)
|
|
} else {
|
|
retType = "any[]"
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("(%s): %s", strings.Join(params, ", "), retType)
|
|
}
|
|
|
|
func goTypeToTS(t reflect.Type, ctx *docCtx) string {
|
|
if t == nil {
|
|
return "any"
|
|
}
|
|
|
|
for t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
pkgPath := t.PkgPath()
|
|
rawName := t.Name()
|
|
|
|
// 1. Blacklist / Filter
|
|
if isTypeUnusable(t) {
|
|
return "any"
|
|
}
|
|
|
|
// 2. Map primitive-like types (even with PkgPath) to TS primitives
|
|
switch t.Kind() {
|
|
case reflect.String:
|
|
return "string"
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
|
reflect.Float32, reflect.Float64:
|
|
// time.Duration is an Int64, but we treat it as number (ms)
|
|
return "number"
|
|
case reflect.Bool:
|
|
return "boolean"
|
|
case reflect.Slice, reflect.Array:
|
|
return goTypeToTS(t.Elem(), ctx) + "[]"
|
|
case reflect.Map:
|
|
// http.Header as a special Record mapping
|
|
if pkgPath == "net/http" && rawName == "Header" {
|
|
return "Record<string, string[]>"
|
|
}
|
|
return fmt.Sprintf("Record<%s, %s>", goTypeToTS(t.Key(), ctx), goTypeToTS(t.Elem(), ctx))
|
|
}
|
|
|
|
// 3. Special Mappings for remaining named types (mostly Structs/Interfaces)
|
|
if pkgPath == "time" && rawName == "Time" {
|
|
return registerInterface(t, ctx)
|
|
}
|
|
|
|
// 4. Strict Penetration Blocking for named non-project types
|
|
isProject := strings.HasPrefix(pkgPath, "apigo.cc/go/")
|
|
isAnonymous := rawName == ""
|
|
|
|
if pkgPath != "" && !isProject {
|
|
base := filepath.Base(pkgPath)
|
|
opaqueName := "Go" + strings.Title(base) + "_" + rawName
|
|
ctx.opaqueList[opaqueName] = true
|
|
return opaqueName
|
|
}
|
|
|
|
// 5. Recursive Struct/Interface Parsing
|
|
switch t.Kind() {
|
|
case reflect.Struct:
|
|
if isAnonymous {
|
|
return "{ [key: string]: any }"
|
|
}
|
|
return registerInterface(t, ctx)
|
|
case reflect.Interface:
|
|
return "any"
|
|
default:
|
|
return "any"
|
|
}
|
|
}
|
|
|
|
func registerInterface(t reflect.Type, ctx *docCtx) string {
|
|
if name, ok := ctx.seenTypes[t]; ok {
|
|
return name
|
|
}
|
|
|
|
rawName := t.Name()
|
|
pkgPath := t.PkgPath()
|
|
|
|
var name string
|
|
if strings.HasPrefix(pkgPath, "apigo.cc/go/") {
|
|
parts := strings.Split(pkgPath, "/")
|
|
name = fmt.Sprintf("%s_%s", strings.Title(parts[len(parts)-1]), rawName)
|
|
} else if pkgPath != "" {
|
|
// Standard lib types like time.Time
|
|
base := filepath.Base(pkgPath)
|
|
name = fmt.Sprintf("Go%s_%s", strings.Title(base), rawName)
|
|
} else {
|
|
name = fmt.Sprintf("%s_%s", strings.Title(ctx.currentMod), rawName)
|
|
}
|
|
|
|
ctx.seenTypes[t] = name
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("interface %s {\n", name))
|
|
|
|
// Fields with Tag check
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
if field.PkgPath != "" || field.Tag.Get("js") == "-" {
|
|
continue
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %s: %s;\n", field.Name, goTypeToTS(field.Type, ctx)))
|
|
}
|
|
|
|
// Methods check
|
|
ptrType := reflect.PtrTo(t)
|
|
methods := make(map[string]reflect.Method)
|
|
for i := 0; i < t.NumMethod(); i++ {
|
|
m := t.Method(i)
|
|
methods[m.Name] = m
|
|
}
|
|
for i := 0; i < ptrType.NumMethod(); i++ {
|
|
m := ptrType.Method(i)
|
|
methods[m.Name] = m
|
|
}
|
|
|
|
mNames := make([]string, 0, len(methods))
|
|
for n := range methods {
|
|
mNames = append(mNames, n)
|
|
}
|
|
sort.Strings(mNames)
|
|
|
|
for _, n := range mNames {
|
|
m := methods[n]
|
|
if m.PkgPath != "" {
|
|
continue
|
|
}
|
|
if isMethodUnusable(m.Type) {
|
|
continue
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %s%s;\n", n, formatFunc(m.Type, ctx, true)))
|
|
}
|
|
|
|
sb.WriteString("}")
|
|
ctx.interfaces[name] = sb.String()
|
|
|
|
return name
|
|
}
|
|
|
|
func isMethodUnusable(t reflect.Type) bool {
|
|
for i := 0; i < t.NumIn(); i++ {
|
|
if isTypeUnusable(t.In(i)) {
|
|
return true
|
|
}
|
|
}
|
|
for i := 0; i < t.NumOut(); i++ {
|
|
if isTypeUnusable(t.Out(i)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isTypeUnusable(t reflect.Type) bool {
|
|
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
|
t = t.Elem()
|
|
}
|
|
pkgPath := t.PkgPath()
|
|
rawName := t.Name()
|
|
|
|
if pkgPath == "reflect" || pkgPath == "runtime" || pkgPath == "unsafe" || pkgPath == "sync" {
|
|
return true
|
|
}
|
|
if pkgPath == "io" && (rawName == "Reader" || rawName == "Writer" || rawName == "Closer") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|