5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 06:50:22 +08:00
wails/v2/internal/binding/generate.go
Jeremy Jay c4fdfd6415
Fix miscellaneous bindings and typescript export bugs (#3978)
* 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>
2025-01-13 20:14:54 +11:00

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
}