Switch remote deploy to vendored source builds

Move remote deployment to a vendored source bundle built on the target host via Docker so redeploys no longer require local cross-compilation or host Go installation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub Actions
2026-05-08 12:19:18 +08:00
parent bb27566e38
commit c1a0fe2949
1320 changed files with 497125 additions and 11 deletions

View File

@@ -0,0 +1,384 @@
package binding
import (
"bufio"
"bytes"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"github.com/wailsapp/wails/v2/internal/typescriptify"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/logger"
)
type Bindings struct {
db *DB
logger logger.CustomLogger
exemptions slicer.StringSlicer
structsToGenerateTS map[string]map[string]interface{}
enumsToGenerateTS map[string]map[string]interface{}
tsPrefix string
tsSuffix string
tsInterface bool
obfuscate bool
}
// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings {
result := &Bindings{
db: newDB(),
logger: logger.CustomLogger("Bindings"),
structsToGenerateTS: make(map[string]map[string]interface{}),
enumsToGenerateTS: make(map[string]map[string]interface{}),
obfuscate: obfuscate,
}
for _, exemption := range exemptions {
if exemption == nil {
continue
}
name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name()
// Yuk yuk yuk! Is there a better way?
name = strings.TrimSuffix(name, "-fm")
result.exemptions.Add(name)
}
for _, enum := range enumsToBind {
result.AddEnumToGenerateTS(enum)
}
// Add the structs to bind
for _, ptr := range structPointersToBind {
err := result.Add(ptr)
if err != nil {
logger.Fatal("Error during binding: " + err.Error())
}
}
return result
}
// Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error {
methods, err := b.getMethods(structPtr)
if err != nil {
return fmt.Errorf("cannot bind value to app: %s", err.Error())
}
for _, method := range methods {
b.db.AddMethod(method.Path.Package, method.Path.Struct, method.Path.Name, method)
}
return nil
}
func (b *Bindings) DB() *DB {
return b.db
}
func (b *Bindings) ToJSON() (string, error) {
return b.db.ToJSON()
}
func (b *Bindings) GenerateModels() ([]byte, error) {
models := map[string]string{}
var seen slicer.StringSlicer
var seenEnumsPackages slicer.StringSlicer
allStructNames := b.getAllStructNames()
allStructNames.Sort()
allEnumNames := b.getAllEnumNames()
allEnumNames.Sort()
for packageName, structsToGenerate := range b.structsToGenerateTS {
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
w.KnownStructs = allStructNames
w.KnownEnums = allEnumNames
// sort the structs
var structNames []string
for structName := range structsToGenerate {
structNames = append(structNames, structName)
}
sort.Strings(structNames)
for _, structName := range structNames {
fqstructname := packageName + "." + structName
if seen.Contains(fqstructname) {
continue
}
structInterface := structsToGenerate[structName]
w.Add(structInterface)
}
// if we have enums for this package, add them as well
var enums, enumsExist = b.enumsToGenerateTS[packageName]
if enumsExist {
// Sort the enum names first to make the output deterministic
sortedEnumNames := make([]string, 0, len(enums))
for enumName := range enums {
sortedEnumNames = append(sortedEnumNames, enumName)
}
sort.Strings(sortedEnumNames)
for _, enumName := range sortedEnumNames {
enum := enums[enumName]
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
seenEnumsPackages.Add(packageName)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
seen.AddSlice(w.GetGeneratedStructs())
models[packageName] = thisPackageCode
}
// Add outstanding enums to the models that were not in packages with structs
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
if seenEnumsPackages.Contains(packageName) {
continue
}
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
for enumName, enum := range enumsToGenerate {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
models[packageName] = thisPackageCode
}
// Sort the package names first to make the output deterministic
sortedPackageNames := make([]string, 0, len(models))
for packageName := range models {
sortedPackageNames = append(sortedPackageNames, packageName)
}
sort.Strings(sortedPackageNames)
var modelsData bytes.Buffer
for _, packageName := range sortedPackageNames {
modelData := models[packageName]
if strings.TrimSpace(modelData) == "" {
continue
}
modelsData.WriteString("export namespace " + packageName + " {\n")
sc := bufio.NewScanner(strings.NewReader(modelData))
for sc.Scan() {
modelsData.WriteString("\t" + sc.Text() + "\n")
}
modelsData.WriteString("\n}\n\n")
}
return modelsData.Bytes(), nil
}
func (b *Bindings) WriteModels(modelsDir string) error {
modelsData, err := b.GenerateModels()
if err != nil {
return err
}
// Don't write if we don't have anything
if len(modelsData) == 0 {
return nil
}
filename := filepath.Join(modelsDir, "models.ts")
err = os.WriteFile(filename, modelsData, 0o755)
if err != nil {
return err
}
return nil
}
func (b *Bindings) AddEnumToGenerateTS(e interface{}) {
enumType := reflect.TypeOf(e)
var packageName string
var enumName string
// enums should be represented as array of all possible values
if hasElements(enumType) {
enum := enumType.Elem()
// simple enum represented by struct with Value/TSName fields
if enum.Kind() == reflect.Struct {
_, tsNamePresented := enum.FieldByName("TSName")
enumT, valuePresented := enum.FieldByName("Value")
if tsNamePresented && valuePresented {
packageName = getPackageName(enumT.Type.String())
enumName = enumT.Type.Name()
} else {
return
}
// otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname
} else {
packageName = getPackageName(enumType.Elem().String())
enumName = enumType.Elem().Name()
}
if b.enumsToGenerateTS[packageName] == nil {
b.enumsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.enumsToGenerateTS[packageName][enumName] != nil {
return
}
b.enumsToGenerateTS[packageName][enumName] = e
}
}
func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) {
if b.structsToGenerateTS[packageName] == nil {
b.structsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.structsToGenerateTS[packageName][structName] != nil {
return
}
b.structsToGenerateTS[packageName][structName] = s
// Iterate this struct and add any struct field references
structType := reflect.TypeOf(s)
for hasElements(structType) {
structType = structType.Elem()
}
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
if field.Anonymous || !field.IsExported() {
continue
}
kind := field.Type.Kind()
if kind == reflect.Struct {
fqname := field.Type.String()
sNameSplit := strings.SplitN(fqname, ".", 2)
if len(sNameSplit) < 2 {
continue
}
sName := sNameSplit[1]
pName := getPackageName(fqname)
a := reflect.New(field.Type)
if b.hasExportedJSONFields(field.Type) {
s := reflect.Indirect(a).Interface()
b.AddStructToGenerateTS(pName, sName, s)
}
} else {
fType := field.Type
for hasElements(fType) {
fType = fType.Elem()
}
if fType.Kind() == reflect.Struct {
fqname := fType.String()
sNameSplit := strings.SplitN(fqname, ".", 2)
if len(sNameSplit) < 2 {
continue
}
sName := sNameSplit[1]
pName := getPackageName(fqname)
a := reflect.New(fType)
if b.hasExportedJSONFields(fType) {
s := reflect.Indirect(a).Interface()
b.AddStructToGenerateTS(pName, sName, s)
}
}
}
}
}
func (b *Bindings) SetTsPrefix(prefix string) *Bindings {
b.tsPrefix = prefix
return b
}
func (b *Bindings) SetTsSuffix(postfix string) *Bindings {
b.tsSuffix = postfix
return b
}
func (b *Bindings) SetOutputType(outputType string) *Bindings {
if outputType == "interfaces" {
b.tsInterface = true
}
return b
}
func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, structsToGenerate := range b.structsToGenerateTS {
for structName := range structsToGenerate {
result.Add(packageName + "." + structName)
}
}
return &result
}
func (b *Bindings) getAllEnumNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
for enumName := range enumsToGenerate {
result.Add(packageName + "." + enumName)
}
}
return &result
}
func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool {
for i := 0; i < typeOf.NumField(); i++ {
jsonFieldName := ""
f := typeOf.Field(i)
// function, complex, and channel types cannot be json-encoded
if f.Type.Kind() == reflect.Chan ||
f.Type.Kind() == reflect.Func ||
f.Type.Kind() == reflect.UnsafePointer ||
f.Type.Kind() == reflect.Complex128 ||
f.Type.Kind() == reflect.Complex64 {
continue
}
jsonTag, hasTag := f.Tag.Lookup("json")
if !hasTag && f.IsExported() {
return true
}
if len(jsonTag) == 0 {
continue
}
jsonTagParts := strings.Split(jsonTag, ",")
if len(jsonTagParts) > 0 {
jsonFieldName = jsonTagParts[0]
}
for _, t := range jsonTagParts {
if t == "-" {
continue
}
}
if jsonFieldName != "" {
return true
}
}
return false
}

View File

@@ -0,0 +1,109 @@
package binding
import (
"encoding/json"
"fmt"
"reflect"
)
type BoundedMethodPath struct {
Package string
Struct string
Name string
}
func (p *BoundedMethodPath) FullName() string {
return fmt.Sprintf("%s.%s.%s", p.Package, p.Struct, p.Name)
}
// BoundMethod defines all the data related to a Go method that is
// bound to the Wails application
type BoundMethod struct {
Path *BoundedMethodPath `json:"path"`
Inputs []*Parameter `json:"inputs,omitempty"`
Outputs []*Parameter `json:"outputs,omitempty"`
Comments string `json:"comments,omitempty"`
Method reflect.Value `json:"-"`
}
// InputCount returns the number of inputs this bound method has
func (b *BoundMethod) InputCount() int {
return len(b.Inputs)
}
// OutputCount returns the number of outputs this bound method has
func (b *BoundMethod) OutputCount() int {
return len(b.Outputs)
}
// ParseArgs method converts the input json into the types expected by the method
func (b *BoundMethod) ParseArgs(args []json.RawMessage) ([]interface{}, error) {
result := make([]interface{}, b.InputCount())
if len(args) != b.InputCount() {
return nil, fmt.Errorf("received %d arguments to method '%s', expected %d", len(args), b.Path.FullName(), b.InputCount())
}
for index, arg := range args {
typ := b.Inputs[index].reflectType
inputValue := reflect.New(typ).Interface()
err := json.Unmarshal(arg, inputValue)
if err != nil {
return nil, err
}
if inputValue == nil {
result[index] = reflect.Zero(typ).Interface()
} else {
result[index] = reflect.ValueOf(inputValue).Elem().Interface()
}
}
return result, nil
}
// Call will attempt to call this bound method with the given args
func (b *BoundMethod) Call(args []interface{}) (interface{}, error) {
// Check inputs
expectedInputLength := len(b.Inputs)
actualInputLength := len(args)
if expectedInputLength != actualInputLength {
return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Path.FullName(), expectedInputLength, actualInputLength)
}
/** Convert inputs to reflect values **/
// Create slice for the input arguments to the method call
callArgs := make([]reflect.Value, expectedInputLength)
// Iterate over given arguments
for index, arg := range args {
// Save the converted argument
callArgs[index] = reflect.ValueOf(arg)
}
// Do the call
callResults := b.Method.Call(callArgs)
//** Check results **//
var returnValue interface{}
var err error
switch b.OutputCount() {
case 1:
// Loop over results and determine if the result
// is an error or not
for _, result := range callResults {
interfac := result.Interface()
temp, ok := interfac.(error)
if ok {
err = temp
} else {
returnValue = interfac
}
}
case 2:
returnValue = callResults[0].Interface()
if temp, ok := callResults[1].Interface().(error); ok {
err = temp
}
}
return returnValue, err
}

View File

@@ -0,0 +1,134 @@
package binding
import (
"encoding/json"
"sync"
"unsafe"
)
// DB is our database of method bindings
type DB struct {
// map[packagename] -> map[structname] -> map[methodname]*method
store map[string]map[string]map[string]*BoundMethod
// This uses fully qualified method names as a shortcut for store traversal.
// It used for performance gains at runtime
methodMap map[string]*BoundMethod
// This uses ids to reference bound methods at runtime
obfuscatedMethodArray []*ObfuscatedMethod
// Lock to ensure sync access to the data
lock sync.RWMutex
}
type ObfuscatedMethod struct {
method *BoundMethod
methodName string
}
func newDB() *DB {
return &DB{
store: make(map[string]map[string]map[string]*BoundMethod),
methodMap: make(map[string]*BoundMethod),
obfuscatedMethodArray: []*ObfuscatedMethod{},
}
}
// GetMethodFromStore returns the method for the given package/struct/method names
// nil is returned if any one of those does not exist
func (d *DB) GetMethodFromStore(packageName string, structName string, methodName string) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
structMap, exists := d.store[packageName]
if !exists {
return nil
}
methodMap, exists := structMap[structName]
if !exists {
return nil
}
return methodMap[methodName]
}
// GetMethod returns the method for the given qualified method name
// qualifiedMethodName is "packagename.structname.methodname"
func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
return d.methodMap[qualifiedMethodName]
}
// GetObfuscatedMethod returns the method for the given ID
func (d *DB) GetObfuscatedMethod(id int) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
if len(d.obfuscatedMethodArray) <= id {
return nil
}
return d.obfuscatedMethodArray[id].method
}
// AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName
func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) {
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
// Get the map associated with the package name
structMap, exists := d.store[packageName]
if !exists {
// Create a new map for this packagename
d.store[packageName] = make(map[string]map[string]*BoundMethod)
structMap = d.store[packageName]
}
// Get the map associated with the struct name
methodMap, exists := structMap[structName]
if !exists {
// Create a new map for this packagename
structMap[structName] = make(map[string]*BoundMethod)
methodMap = structMap[structName]
}
// Store the method definition
methodMap[methodName] = methodDefinition
// Store in the methodMap
key := packageName + "." + structName + "." + methodName
d.methodMap[key] = methodDefinition
d.obfuscatedMethodArray = append(d.obfuscatedMethodArray, &ObfuscatedMethod{method: methodDefinition, methodName: key})
}
// ToJSON converts the method map to JSON
func (d *DB) ToJSON() (string, error) {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
d.UpdateObfuscatedCallMap()
bytes, err := json.Marshal(&d.store)
// Return zero copy string as this string will be read only
result := *(*string)(unsafe.Pointer(&bytes))
return result, err
}
// UpdateObfuscatedCallMap sets up the secure call mappings
func (d *DB) UpdateObfuscatedCallMap() map[string]int {
mappings := make(map[string]int)
for id, k := range d.obfuscatedMethodArray {
mappings[k.methodName] = id
}
return mappings
}

View File

@@ -0,0 +1,248 @@
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
}

View File

@@ -0,0 +1,28 @@
package binding
import "reflect"
// Parameter defines a Go method parameter
type Parameter struct {
Name string `json:"name,omitempty"`
TypeName string `json:"type"`
reflectType reflect.Type
}
func newParameter(Name string, Type reflect.Type) *Parameter {
return &Parameter{
Name: Name,
TypeName: Type.String(),
reflectType: Type,
}
}
// IsType returns true if the given
func (p *Parameter) IsType(typename string) bool {
return p.TypeName == typename
}
// IsError returns true if the parameter type is an error
func (p *Parameter) IsError() bool {
return p.IsType("error")
}

View File

@@ -0,0 +1,200 @@
package binding
import (
"fmt"
"reflect"
"runtime"
"strings"
)
// isStructPtr returns true if the value given is a
// pointer to a struct
func isStructPtr(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Ptr &&
reflect.ValueOf(value).Elem().Kind() == reflect.Struct
}
// isFunction returns true if the given value is a function
func isFunction(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Func
}
// isStruct returns true if the value given is a struct
func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct
}
func normalizeStructName(name string) string {
return strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
name,
",",
"-",
),
"*",
"",
),
"]",
"__",
),
"[",
"__",
)
}
func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
// Create result placeholder
var result []*BoundMethod
// Check type
if !isStructPtr(value) {
if isStruct(value) {
name := reflect.ValueOf(value).Type().Name()
return nil, fmt.Errorf("%s is a struct, not a pointer to a struct", name)
}
if isFunction(value) {
name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name()
return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct.", name)
}
return nil, fmt.Errorf("not a pointer to a struct.")
}
// Process Struct
structType := reflect.TypeOf(value)
structValue := reflect.ValueOf(value)
structName := structType.Elem().Name()
structNameNormalized := normalizeStructName(structName)
pkgPath := strings.TrimSuffix(structType.Elem().String(), fmt.Sprintf(".%s", structName))
// Process Methods
for i := 0; i < structType.NumMethod(); i++ {
methodDef := structType.Method(i)
methodName := methodDef.Name
method := structValue.MethodByName(methodName)
methodReflectName := runtime.FuncForPC(methodDef.Func.Pointer()).Name()
if b.exemptions.Contains(methodReflectName) {
continue
}
// Create new method
boundMethod := &BoundMethod{
Path: &BoundedMethodPath{
Package: pkgPath,
Struct: structNameNormalized,
Name: methodName,
},
Inputs: nil,
Outputs: nil,
Comments: "",
Method: method,
}
// Iterate inputs
methodType := method.Type()
inputParamCount := methodType.NumIn()
var inputs []*Parameter
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
input := methodType.In(inputIndex)
thisParam := newParameter("", input)
thisInput := input
if thisInput.Kind() == reflect.Slice {
thisInput = thisInput.Elem()
}
// Process struct pointer params
if thisInput.Kind() == reflect.Ptr {
if thisInput.Elem().Kind() == reflect.Struct {
typ := thisInput.Elem()
a := reflect.New(typ)
s := reflect.Indirect(a).Interface()
name := typ.Name()
packageName := getPackageName(thisInput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
}
// Process struct params
if thisInput.Kind() == reflect.Struct {
a := reflect.New(thisInput)
s := reflect.Indirect(a).Interface()
name := thisInput.Name()
packageName := getPackageName(thisInput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
inputs = append(inputs, thisParam)
}
boundMethod.Inputs = inputs
// Iterate outputs
// TODO: Determine what to do about limiting return types
// especially around errors.
outputParamCount := methodType.NumOut()
var outputs []*Parameter
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
output := methodType.Out(outputIndex)
thisParam := newParameter("", output)
thisOutput := output
if thisOutput.Kind() == reflect.Slice {
thisOutput = thisOutput.Elem()
}
// Process struct pointer params
if thisOutput.Kind() == reflect.Ptr {
if thisOutput.Elem().Kind() == reflect.Struct {
typ := thisOutput.Elem()
a := reflect.New(typ)
s := reflect.Indirect(a).Interface()
name := typ.Name()
packageName := getPackageName(thisOutput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
}
// Process struct params
if thisOutput.Kind() == reflect.Struct {
a := reflect.New(thisOutput)
s := reflect.Indirect(a).Interface()
name := thisOutput.Name()
packageName := getPackageName(thisOutput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
outputs = append(outputs, thisParam)
}
boundMethod.Outputs = outputs
// Save method in result
result = append(result, boundMethod)
}
return result, nil
}
func getPackageName(in string) string {
result := strings.Split(in, ".")[0]
result = strings.ReplaceAll(result, "[]", "")
result = strings.ReplaceAll(result, "*", "")
return result
}
func getSplitReturn(in string) (string, string) {
result := strings.SplitN(in, ".", 2)
return result[0], result[1]
}
func hasElements(typ reflect.Type) bool {
kind := typ.Kind()
return kind == reflect.Ptr || kind == reflect.Array || kind == reflect.Slice || kind == reflect.Map
}