js/doc.go

362 lines
9.1 KiB
Go
Raw Normal View History

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
}
isVariadic := t.IsVariadic()
for i := startIdx; i < numIn; i++ {
argType := t.In(i)
typeName := argType.String()
if typeName == "context.Context" || strings.Contains(typeName, "log.Logger") {
continue
}
isLast := i == numIn-1
paramName := fmt.Sprintf("arg%d", jsArgIdx)
if isVariadic && isLast {
// Variadic parameters are optional in TS
params = append(params, fmt.Sprintf("...%s: %s", paramName, goTypeToTS(argType.Elem(), ctx)))
} else if argType.Kind() == reflect.Ptr {
// Pointer parameters at the end are optional
params = append(params, fmt.Sprintf("%s?: %s", paramName, goTypeToTS(argType, ctx)))
} else {
params = append(params, fmt.Sprintf("%s: %s", paramName, 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
}