mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 06:50:22 +08:00

* Do not attempt to export fields that cannot be json-encoded * update changelog w/ PR * also skip UnsafePointers * WIP to allow conversion from Go generic types to typescript * support for non-primitive generics also :) * fix generic types in parameters / return args * fixes a namespacing bug when mapping to pointer to struct * fixing invalid knownstructs * found a place it mattered, pushing the star replacement to the generate side * descend as much as necessary to find structs caught these examples in http.Request.TLS: PeerCertificates []*x509.Certificate VerifiedChains [][]*x509.Certificate * accidently reverted other fix * switch syntax for typescript record outputs prior syntax is primarily useful for naming keys so not useful here, and this syntax avoids square brackets in output which greatly simplifies generation for Go generics * better handle edge cases for nested arrays and slices * lots o tests * update changelog --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
249 lines
7.5 KiB
Go
249 lines
7.5 KiB
Go
package binding
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/wailsapp/wails/v2/internal/fs"
|
|
|
|
"github.com/leaanthony/slicer"
|
|
)
|
|
|
|
var (
|
|
mapRegex *regexp.Regexp
|
|
keyPackageIndex int
|
|
keyTypeIndex int
|
|
valueArrayIndex int
|
|
valuePackageIndex int
|
|
valueTypeIndex int
|
|
)
|
|
|
|
func init() {
|
|
mapRegex = regexp.MustCompile(`(?:map\[(?:(?P<keyPackage>\w+)\.)?(?P<keyType>\w+)])?(?P<valueArray>\[])?(?:\*?(?P<valuePackage>\w+)\.)?(?P<valueType>.+)`)
|
|
keyPackageIndex = mapRegex.SubexpIndex("keyPackage")
|
|
keyTypeIndex = mapRegex.SubexpIndex("keyType")
|
|
valueArrayIndex = mapRegex.SubexpIndex("valueArray")
|
|
valuePackageIndex = mapRegex.SubexpIndex("valuePackage")
|
|
valueTypeIndex = mapRegex.SubexpIndex("valueType")
|
|
}
|
|
|
|
func (b *Bindings) GenerateGoBindings(baseDir string) error {
|
|
store := b.db.store
|
|
var obfuscatedBindings map[string]int
|
|
if b.obfuscate {
|
|
obfuscatedBindings = b.db.UpdateObfuscatedCallMap()
|
|
}
|
|
for packageName, structs := range store {
|
|
packageDir := filepath.Join(baseDir, packageName)
|
|
err := fs.Mkdir(packageDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for structName, methods := range structs {
|
|
var jsoutput bytes.Buffer
|
|
jsoutput.WriteString(`// @ts-check
|
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
|
// This file is automatically generated. DO NOT EDIT
|
|
`)
|
|
var tsBody bytes.Buffer
|
|
var tsContent bytes.Buffer
|
|
tsContent.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
|
// This file is automatically generated. DO NOT EDIT
|
|
`)
|
|
// Sort the method names alphabetically
|
|
methodNames := make([]string, 0, len(methods))
|
|
for methodName := range methods {
|
|
methodNames = append(methodNames, methodName)
|
|
}
|
|
sort.Strings(methodNames)
|
|
|
|
var importNamespaces slicer.StringSlicer
|
|
for _, methodName := range methodNames {
|
|
// Get the method details
|
|
methodDetails := methods[methodName]
|
|
|
|
// Generate JS
|
|
var args slicer.StringSlicer
|
|
for count := range methodDetails.Inputs {
|
|
arg := fmt.Sprintf("arg%d", count+1)
|
|
args.Add(arg)
|
|
}
|
|
argsString := args.Join(", ")
|
|
jsoutput.WriteString(fmt.Sprintf("\nexport function %s(%s) {", methodName, argsString))
|
|
jsoutput.WriteString("\n")
|
|
if b.obfuscate {
|
|
id := obfuscatedBindings[strings.Join([]string{packageName, structName, methodName}, ".")]
|
|
jsoutput.WriteString(fmt.Sprintf(" return ObfuscatedCall(%d, [%s]);", id, argsString))
|
|
} else {
|
|
jsoutput.WriteString(fmt.Sprintf(" return window['go']['%s']['%s']['%s'](%s);", packageName, structName, methodName, argsString))
|
|
}
|
|
jsoutput.WriteString("\n}\n")
|
|
|
|
// Generate TS
|
|
tsBody.WriteString(fmt.Sprintf("\nexport function %s(", methodName))
|
|
|
|
args.Clear()
|
|
for count, input := range methodDetails.Inputs {
|
|
arg := fmt.Sprintf("arg%d", count+1)
|
|
entityName := entityFullReturnType(input.TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
|
|
args.Add(arg + ":" + goTypeToTypescriptType(entityName, &importNamespaces))
|
|
}
|
|
tsBody.WriteString(args.Join(",") + "):")
|
|
// now build Typescript return types
|
|
// If there is no return value or only returning error, TS returns Promise<void>
|
|
// If returning single value, TS returns Promise<type>
|
|
// If returning single value or error, TS returns Promise<type>
|
|
// If returning two values, TS returns Promise<type1|type2>
|
|
// Otherwise, TS returns Promise<type1> (instead of throwing Go error?)
|
|
var returnType string
|
|
if methodDetails.OutputCount() == 0 {
|
|
returnType = "Promise<void>"
|
|
} else if methodDetails.OutputCount() == 1 && methodDetails.Outputs[0].TypeName == "error" {
|
|
returnType = "Promise<void>"
|
|
} else {
|
|
outputTypeName := entityFullReturnType(methodDetails.Outputs[0].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
|
|
firstType := goTypeToTypescriptType(outputTypeName, &importNamespaces)
|
|
returnType = "Promise<" + firstType
|
|
if methodDetails.OutputCount() == 2 && methodDetails.Outputs[1].TypeName != "error" {
|
|
outputTypeName = entityFullReturnType(methodDetails.Outputs[1].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
|
|
secondType := goTypeToTypescriptType(outputTypeName, &importNamespaces)
|
|
returnType += "|" + secondType
|
|
}
|
|
returnType += ">"
|
|
}
|
|
tsBody.WriteString(returnType + ";\n")
|
|
}
|
|
|
|
importNamespaces.Deduplicate()
|
|
importNamespaces.Each(func(namespace string) {
|
|
tsContent.WriteString("import {" + namespace + "} from '../models';\n")
|
|
})
|
|
tsContent.WriteString(tsBody.String())
|
|
|
|
jsfilename := filepath.Join(packageDir, structName+".js")
|
|
err = os.WriteFile(jsfilename, jsoutput.Bytes(), 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tsfilename := filepath.Join(packageDir, structName+".d.ts")
|
|
err = os.WriteFile(tsfilename, tsContent.Bytes(), 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
err := b.WriteModels(baseDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fullyQualifiedName(packageName string, typeName string) string {
|
|
if len(packageName) > 0 {
|
|
return packageName + "." + typeName
|
|
}
|
|
|
|
switch true {
|
|
case len(typeName) == 0:
|
|
return ""
|
|
case typeName == "interface{}" || typeName == "interface {}":
|
|
return "any"
|
|
case typeName == "string":
|
|
return "string"
|
|
case typeName == "error":
|
|
return "Error"
|
|
case
|
|
strings.HasPrefix(typeName, "int"),
|
|
strings.HasPrefix(typeName, "uint"),
|
|
strings.HasPrefix(typeName, "float"):
|
|
return "number"
|
|
case typeName == "bool":
|
|
return "boolean"
|
|
default:
|
|
return "any"
|
|
}
|
|
}
|
|
|
|
var (
|
|
jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`)
|
|
)
|
|
|
|
func arrayifyValue(valueArray string, valueType string) string {
|
|
valueType = strings.ReplaceAll(valueType, "*", "")
|
|
gidx := strings.IndexRune(valueType, '[')
|
|
if gidx > 0 { // its a generic type
|
|
rem := strings.SplitN(valueType, "[", 2)
|
|
valueType = rem[0] + "_" + jsVariableUnsafeChars.ReplaceAllLiteralString(rem[1], "_")
|
|
}
|
|
|
|
if len(valueArray) == 0 {
|
|
return valueType
|
|
}
|
|
|
|
return "Array<" + valueType + ">"
|
|
}
|
|
|
|
func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) string {
|
|
matches := mapRegex.FindStringSubmatch(input)
|
|
keyPackage := matches[keyPackageIndex]
|
|
keyType := matches[keyTypeIndex]
|
|
valueArray := matches[valueArrayIndex]
|
|
valuePackage := matches[valuePackageIndex]
|
|
valueType := matches[valueTypeIndex]
|
|
// fmt.Printf("input=%s, keyPackage=%s, keyType=%s, valueArray=%s, valuePackage=%s, valueType=%s\n",
|
|
// input,
|
|
// keyPackage,
|
|
// keyType,
|
|
// valueArray,
|
|
// valuePackage,
|
|
// valueType)
|
|
|
|
// byte array is special case
|
|
if valueArray == "[]" && valueType == "byte" {
|
|
return "string"
|
|
}
|
|
|
|
// if any packages, make sure they're saved
|
|
if len(keyPackage) > 0 {
|
|
importNamespaces.Add(keyPackage)
|
|
}
|
|
|
|
if len(valuePackage) > 0 {
|
|
importNamespaces.Add(valuePackage)
|
|
}
|
|
|
|
key := fullyQualifiedName(keyPackage, keyType)
|
|
var value string
|
|
if strings.HasPrefix(valueType, "map") {
|
|
value = goTypeToJSDocType(valueType, importNamespaces)
|
|
} else {
|
|
value = fullyQualifiedName(valuePackage, valueType)
|
|
}
|
|
|
|
if len(key) > 0 {
|
|
return fmt.Sprintf("Record<%s, %s>", key, arrayifyValue(valueArray, value))
|
|
}
|
|
|
|
return arrayifyValue(valueArray, value)
|
|
}
|
|
|
|
func goTypeToTypescriptType(input string, importNamespaces *slicer.StringSlicer) string {
|
|
return goTypeToJSDocType(input, importNamespaces)
|
|
}
|
|
|
|
func entityFullReturnType(input, prefix, suffix string, importNamespaces *slicer.StringSlicer) string {
|
|
if strings.ContainsRune(input, '.') {
|
|
nameSpace, returnType := getSplitReturn(input)
|
|
return nameSpace + "." + prefix + returnType + suffix
|
|
}
|
|
|
|
return input
|
|
}
|