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" } 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 }