github.com/AlpineAIO/wails/v2@v2.0.0-beta.32.0.20240505041856-1047a8fa5fef/internal/binding/generate.go (about)

     1  package binding
     2  
     3  import (
     4  	"bytes"
     5  	_ "embed"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/AlpineAIO/wails/v2/internal/fs"
    14  
    15  	"github.com/leaanthony/slicer"
    16  )
    17  
    18  var (
    19  	mapRegex          *regexp.Regexp
    20  	keyPackageIndex   int
    21  	keyTypeIndex      int
    22  	valueArrayIndex   int
    23  	valuePackageIndex int
    24  	valueTypeIndex    int
    25  )
    26  
    27  func init() {
    28  	mapRegex = regexp.MustCompile(`(?:map\[(?:(?P<keyPackage>\w+)\.)?(?P<keyType>\w+)])?(?P<valueArray>\[])?(?:\*?(?P<valuePackage>\w+)\.)?(?P<valueType>.+)`)
    29  	keyPackageIndex = mapRegex.SubexpIndex("keyPackage")
    30  	keyTypeIndex = mapRegex.SubexpIndex("keyType")
    31  	valueArrayIndex = mapRegex.SubexpIndex("valueArray")
    32  	valuePackageIndex = mapRegex.SubexpIndex("valuePackage")
    33  	valueTypeIndex = mapRegex.SubexpIndex("valueType")
    34  }
    35  
    36  func (b *Bindings) GenerateGoBindings(baseDir string) error {
    37  	store := b.db.store
    38  	var obfuscatedBindings map[string]int
    39  	if b.obfuscate {
    40  		obfuscatedBindings = b.db.UpdateObfuscatedCallMap()
    41  	}
    42  	for packageName, structs := range store {
    43  		packageDir := filepath.Join(baseDir, packageName)
    44  		err := fs.Mkdir(packageDir)
    45  		if err != nil {
    46  			return err
    47  		}
    48  		for structName, methods := range structs {
    49  			var jsoutput bytes.Buffer
    50  			jsoutput.WriteString(`// @ts-check
    51  // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
    52  // This file is automatically generated. DO NOT EDIT
    53  `)
    54  			var tsBody bytes.Buffer
    55  			var tsContent bytes.Buffer
    56  			tsContent.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
    57  // This file is automatically generated. DO NOT EDIT
    58  `)
    59  			// Sort the method names alphabetically
    60  			methodNames := make([]string, 0, len(methods))
    61  			for methodName := range methods {
    62  				methodNames = append(methodNames, methodName)
    63  			}
    64  			sort.Strings(methodNames)
    65  
    66  			var importNamespaces slicer.StringSlicer
    67  			for _, methodName := range methodNames {
    68  				// Get the method details
    69  				methodDetails := methods[methodName]
    70  
    71  				// Generate JS
    72  				var args slicer.StringSlicer
    73  				for count := range methodDetails.Inputs {
    74  					arg := fmt.Sprintf("arg%d", count+1)
    75  					args.Add(arg)
    76  				}
    77  				argsString := args.Join(", ")
    78  				jsoutput.WriteString(fmt.Sprintf("\nexport function %s(%s) {", methodName, argsString))
    79  				jsoutput.WriteString("\n")
    80  				if b.obfuscate {
    81  					id := obfuscatedBindings[strings.Join([]string{packageName, structName, methodName}, ".")]
    82  					jsoutput.WriteString(fmt.Sprintf("  return ObfuscatedCall(%d, [%s]);", id, argsString))
    83  				} else {
    84  					jsoutput.WriteString(fmt.Sprintf("  return window['go']['%s']['%s']['%s'](%s);", packageName, structName, methodName, argsString))
    85  				}
    86  				jsoutput.WriteString("\n}\n")
    87  
    88  				// Generate TS
    89  				tsBody.WriteString(fmt.Sprintf("\nexport function %s(", methodName))
    90  
    91  				args.Clear()
    92  				for count, input := range methodDetails.Inputs {
    93  					arg := fmt.Sprintf("arg%d", count+1)
    94  					entityName := entityFullReturnType(input.TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
    95  					args.Add(arg + ":" + goTypeToTypescriptType(entityName, &importNamespaces))
    96  				}
    97  				tsBody.WriteString(args.Join(",") + "):")
    98  				// now build Typescript return types
    99  				// If there is no return value or only returning error, TS returns Promise<void>
   100  				// If returning single value, TS returns Promise<type>
   101  				// If returning single value or error, TS returns Promise<type>
   102  				// If returning two values, TS returns Promise<type1|type2>
   103  				// Otherwise, TS returns Promise<type1> (instead of throwing Go error?)
   104  				var returnType string
   105  				if methodDetails.OutputCount() == 0 {
   106  					returnType = "Promise<void>"
   107  				} else if methodDetails.OutputCount() == 1 && methodDetails.Outputs[0].TypeName == "error" {
   108  					returnType = "Promise<void>"
   109  				} else {
   110  					outputTypeName := entityFullReturnType(methodDetails.Outputs[0].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
   111  					firstType := goTypeToTypescriptType(outputTypeName, &importNamespaces)
   112  					returnType = "Promise<" + firstType
   113  					if methodDetails.OutputCount() == 2 && methodDetails.Outputs[1].TypeName != "error" {
   114  						outputTypeName = entityFullReturnType(methodDetails.Outputs[1].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
   115  						secondType := goTypeToTypescriptType(outputTypeName, &importNamespaces)
   116  						returnType += "|" + secondType
   117  					}
   118  					returnType += ">"
   119  				}
   120  				tsBody.WriteString(returnType + ";\n")
   121  			}
   122  
   123  			importNamespaces.Deduplicate()
   124  			importNamespaces.Each(func(namespace string) {
   125  				tsContent.WriteString("import {" + namespace + "} from '../models';\n")
   126  			})
   127  			tsContent.WriteString(tsBody.String())
   128  
   129  			jsfilename := filepath.Join(packageDir, structName+".js")
   130  			err = os.WriteFile(jsfilename, jsoutput.Bytes(), 0o755)
   131  			if err != nil {
   132  				return err
   133  			}
   134  			tsfilename := filepath.Join(packageDir, structName+".d.ts")
   135  			err = os.WriteFile(tsfilename, tsContent.Bytes(), 0o755)
   136  			if err != nil {
   137  				return err
   138  			}
   139  		}
   140  	}
   141  	err := b.WriteModels(baseDir)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	return nil
   146  }
   147  
   148  func fullyQualifiedName(packageName string, typeName string) string {
   149  	if len(packageName) > 0 {
   150  		return packageName + "." + typeName
   151  	}
   152  
   153  	switch true {
   154  	case len(typeName) == 0:
   155  		return ""
   156  	case typeName == "interface{}" || typeName == "interface {}":
   157  		return "any"
   158  	case typeName == "string":
   159  		return "string"
   160  	case typeName == "error":
   161  		return "Error"
   162  	case
   163  		strings.HasPrefix(typeName, "int"),
   164  		strings.HasPrefix(typeName, "uint"),
   165  		strings.HasPrefix(typeName, "float"):
   166  		return "number"
   167  	case typeName == "bool":
   168  		return "boolean"
   169  	default:
   170  		return "any"
   171  	}
   172  }
   173  
   174  func arrayifyValue(valueArray string, valueType string) string {
   175  	if len(valueArray) == 0 {
   176  		return valueType
   177  	}
   178  
   179  	return "Array<" + valueType + ">"
   180  }
   181  
   182  func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) string {
   183  	matches := mapRegex.FindStringSubmatch(input)
   184  	keyPackage := matches[keyPackageIndex]
   185  	keyType := matches[keyTypeIndex]
   186  	valueArray := matches[valueArrayIndex]
   187  	valuePackage := matches[valuePackageIndex]
   188  	valueType := matches[valueTypeIndex]
   189  	// fmt.Printf("input=%s, keyPackage=%s, keyType=%s, valueArray=%s, valuePackage=%s, valueType=%s\n",
   190  	//	input,
   191  	//	keyPackage,
   192  	//	keyType,
   193  	//	valueArray,
   194  	//	valuePackage,
   195  	//	valueType)
   196  
   197  	// byte array is special case
   198  	if valueArray == "[]" && valueType == "byte" {
   199  		return "string"
   200  	}
   201  
   202  	// if any packages, make sure they're saved
   203  	if len(keyPackage) > 0 {
   204  		importNamespaces.Add(keyPackage)
   205  	}
   206  
   207  	if len(valuePackage) > 0 {
   208  		importNamespaces.Add(valuePackage)
   209  	}
   210  
   211  	key := fullyQualifiedName(keyPackage, keyType)
   212  	var value string
   213  	if strings.HasPrefix(valueType, "map") {
   214  		value = goTypeToJSDocType(valueType, importNamespaces)
   215  	} else {
   216  		value = fullyQualifiedName(valuePackage, valueType)
   217  	}
   218  
   219  	if len(key) > 0 {
   220  		return fmt.Sprintf("{[key: %s]: %s}", key, arrayifyValue(valueArray, value))
   221  	}
   222  
   223  	return arrayifyValue(valueArray, value)
   224  }
   225  
   226  func goTypeToTypescriptType(input string, importNamespaces *slicer.StringSlicer) string {
   227  	return goTypeToJSDocType(input, importNamespaces)
   228  }
   229  
   230  func entityFullReturnType(input, prefix, suffix string, importNamespaces *slicer.StringSlicer) string {
   231  	if strings.ContainsRune(input, '.') {
   232  		nameSpace, returnType := getSplitReturn(input)
   233  		return nameSpace + "." + prefix + returnType + suffix
   234  	}
   235  
   236  	return input
   237  }