5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-05 06:12:02 +08:00
wails/v3/internal/parser/bindings.go

465 lines
13 KiB
Go

package parser
import (
"fmt"
"sort"
"strings"
"github.com/samber/lo"
)
const header = `// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
`
const headerTypescript = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
`
const bindingTemplate = `
/**Comments
* @function {{methodName}}* @param names {string}
* @returns {Promise<string>}
**/
`
const bindingTemplateTypescript = `Comments`
const callByIDTypescript = `export async function {{methodName}}({{inputs}}) : {{ReturnType}} {
return Call.ByID({{ID}}{{params}});
}
`
const callByNameTypescript = `export async function {{methodName}}({{inputs}}) : {{ReturnType}} {
return Call.ByName("{{Name}}"{{params}});
}
`
const callByID = `export async function {{methodName}}({{inputs}}) {
return Call.ByID({{ID}}, ...Array.prototype.slice.call(arguments, 0));
}
`
const callByName = `export async function {{methodName}}({{inputs}}) {
return Call.ByName("{{Name}}", ...Array.prototype.slice.call(arguments, 0));
}
`
var reservedWords = []string{
"abstract",
"arguments",
"await",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"double",
"else",
"enum",
"eval",
"export",
"extends",
"false",
"final",
"finally",
"float",
"for",
"function",
"goto",
"if",
"implements",
"import",
"in",
"instanceof",
"int",
"interface",
"let",
"long",
"native",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"true",
"try",
"typeof",
"var",
"void",
"volatile",
"while",
"with",
"yield",
"object",
}
func sanitiseJSVarName(name string) string {
// if the name is a reserved word, prefix with an
// underscore
if lo.Contains(reservedWords, name) {
return "_" + name
}
return name
}
type ExternalStruct struct {
Package string
Name string
}
func (p *Project) GenerateBinding(thisStructName string, method *BoundMethod, useIDs bool) (string, []string, map[packagePath]map[string]*ExternalStruct) {
var externalStructs = make(map[packagePath]map[string]*ExternalStruct)
var models []string
template := bindingTemplate
if useIDs {
template += callByID
} else {
template += callByName
}
result := strings.ReplaceAll(template, "{{structName}}", thisStructName)
result = strings.ReplaceAll(result, "{{methodName}}", method.Name)
result = strings.ReplaceAll(result, "{{ID}}", fmt.Sprintf("%v", method.ID))
// get last part of method.Package path
parts := strings.Split(method.Package, "/")
packageName := parts[len(parts)-1]
result = strings.ReplaceAll(result, "{{Name}}", fmt.Sprintf("%v.%v.%v", packageName, thisStructName, method.Name))
comments := strings.TrimSpace(method.DocComment)
if comments != "" {
comments = "\n * " + comments
}
result = strings.ReplaceAll(result, "Comments", comments)
var params string
for _, input := range method.JSInputs() {
input.project = p
inputName := sanitiseJSVarName(input.Name)
pkgName := getPackageName(input)
if pkgName != "" {
models = append(models, pkgName)
}
if input.Type.IsStruct || input.Type.IsEnum {
if _, ok := externalStructs[input.Type.Package]; !ok {
externalStructs[input.Type.Package] = make(map[string]*ExternalStruct)
}
externalStructs[input.Type.Package][input.Type.Name] = &ExternalStruct{
Package: input.Type.Package,
Name: input.Type.Name,
}
}
inputType := input.JSType(packageName)
params += "\n * @param " + inputName + " {" + inputType + "}"
}
params = strings.TrimSuffix(params, "\n")
//if len(params) > 0 {
// params = "\n" + params
//}
result = strings.ReplaceAll(result, "* @param names {string}", params)
var inputs string
for _, input := range method.JSInputs() {
pkgName := getPackageName(input)
if pkgName != "" {
models = append(models, pkgName)
}
inputs += sanitiseJSVarName(input.Name) + ", "
}
inputs = strings.TrimSuffix(inputs, ", ")
args := inputs
if len(args) > 0 {
args = ", " + args
}
result = strings.ReplaceAll(result, "{{inputs}}", inputs)
result = strings.ReplaceAll(result, "{{args}}", args)
// outputs
var returns string
if len(method.Outputs) == 0 {
returns = " * @returns {Promise<void>}"
} else {
returns = " * @returns {Promise<"
for _, output := range method.Outputs {
output.project = p
pkgName := getPackageName(output)
if pkgName != "" {
models = append(models, pkgName)
}
jsType := output.JSType(pkgName)
if jsType == "error" {
jsType = "void"
}
if output.Type.IsStruct {
if _, ok := externalStructs[output.Type.Package]; !ok {
externalStructs[output.Type.Package] = make(map[string]*ExternalStruct)
}
externalStructs[output.Type.Package][output.Type.Name] = &ExternalStruct{
Package: output.Type.Package,
Name: output.Type.Name,
}
jsType = output.NamespacedStructVariable(output.Type.Package)
}
returns += jsType + ", "
}
returns = strings.TrimSuffix(returns, ", ")
returns += ">}"
}
result = strings.ReplaceAll(result, " * @returns {Promise<string>}", returns)
return result, lo.Uniq(models), externalStructs
}
func (p *Project) GenerateBindingTypescript(thisStructName string, method *BoundMethod, useIDs bool) (string, []string, map[packagePath]map[string]*ExternalStruct) {
var externalStructs = make(map[packagePath]map[string]*ExternalStruct)
var models []string
template := bindingTemplateTypescript
if useIDs {
template += callByIDTypescript
} else {
template += callByNameTypescript
}
result := strings.ReplaceAll(template, "{{structName}}", thisStructName)
result = strings.ReplaceAll(result, "{{methodName}}", method.Name)
result = strings.ReplaceAll(result, "{{ID}}", fmt.Sprintf("%v", method.ID))
// get last part of method.Package path
parts := strings.Split(method.Package, "/")
packageName := parts[len(parts)-1]
result = strings.ReplaceAll(result, "{{Name}}", fmt.Sprintf("%v.%v.%v", packageName, thisStructName, method.Name))
comments := strings.TrimSpace(method.DocComment)
if comments != "" {
comments = "// " + comments + "\n"
}
result = strings.ReplaceAll(result, "Comments", comments)
var params string
for _, input := range method.JSInputs() {
input.project = p
inputName := sanitiseJSVarName(input.Name)
pkgName := getPackageName(input)
if pkgName != "" {
models = append(models, pkgName)
}
if input.Type.IsStruct || input.Type.IsEnum {
if _, ok := externalStructs[input.Type.Package]; !ok {
externalStructs[input.Type.Package] = make(map[string]*ExternalStruct)
}
externalStructs[input.Type.Package][input.Type.Name] = &ExternalStruct{
Package: input.Type.Package,
Name: input.Type.Name,
}
}
params += ", " + inputName
}
result = strings.ReplaceAll(result, "{{params}}", params)
//if len(params) > 0 {
// params = "\n" + params
//}
var inputs string
for _, input := range method.JSInputs() {
pkgName := getPackageName(input)
if pkgName != "" {
models = append(models, pkgName)
}
inputs += sanitiseJSVarName(input.Name) + ": " + input.JSType(packageName) + ", "
}
inputs = strings.TrimSuffix(inputs, ", ")
args := inputs
if len(args) > 0 {
args = ", " + args
}
result = strings.ReplaceAll(result, "{{inputs}}", inputs)
result = strings.ReplaceAll(result, "{{args}}", args)
// outputs
var returns string
if len(method.Outputs) == 0 {
returns = "Promise<void>"
} else {
returns = "Promise<"
for _, output := range method.Outputs {
output.project = p
pkgName := getPackageName(output)
if pkgName != "" {
models = append(models, pkgName)
}
jsType := output.JSType(pkgName)
if jsType == "error" {
jsType = "void"
}
if output.Type.IsStruct {
if _, ok := externalStructs[output.Type.Package]; !ok {
externalStructs[output.Type.Package] = make(map[string]*ExternalStruct)
}
externalStructs[output.Type.Package][output.Type.Name] = &ExternalStruct{
Package: output.Type.Package,
Name: output.Type.Name,
}
jsType = output.NamespacedStructVariable(output.Type.Package)
}
returns += jsType + ", "
}
returns = strings.TrimSuffix(returns, ", ")
returns += ">"
}
result = strings.ReplaceAll(result, "{{ReturnType}}", returns)
return result, lo.Uniq(models), externalStructs
}
func getPackageName(input *Parameter) string {
if !input.Type.IsStruct {
return ""
}
result := input.Type.Package
if result == "" {
result = "main"
}
return result
}
func isContext(input *Parameter) bool {
return input.Type.Package == "context" && input.Type.Name == "Context"
}
func (p *Project) GenerateBindings(bindings map[string]map[string][]*BoundMethod, useIDs bool, useTypescript bool) map[string]map[string]string {
var result = make(map[string]map[string]string)
// sort the bindings keys
packageNames := lo.Keys(bindings)
sort.Strings(packageNames)
for _, packageName := range packageNames {
var allModels []string
packageBindings := bindings[packageName]
structNames := lo.Keys(packageBindings)
relativePackageDir := p.RelativePackageDir(packageName)
_ = relativePackageDir
sort.Strings(structNames)
for _, structName := range structNames {
if _, ok := result[relativePackageDir]; !ok {
result[relativePackageDir] = make(map[string]string)
}
methods := packageBindings[structName]
sort.Slice(methods, func(i, j int) bool {
return methods[i].Name < methods[j].Name
})
var allNamespacedStructs map[packagePath]map[string]*ExternalStruct
var namespacedStructs map[packagePath]map[string]*ExternalStruct
var thisBinding string
var models []string
var mainImports = ""
if len(methods) > 0 {
mainImports = "import {Call} from '@wailsio/runtime';\n"
}
for _, method := range methods {
if useTypescript {
thisBinding, models, namespacedStructs = p.GenerateBindingTypescript(structName, method, useIDs)
} else {
thisBinding, models, namespacedStructs = p.GenerateBinding(structName, method, useIDs)
}
// Merge the namespaced structs
allNamespacedStructs = mergeNamespacedStructs(allNamespacedStructs, namespacedStructs)
allModels = append(allModels, models...)
result[relativePackageDir][structName] += thisBinding
}
if len(allNamespacedStructs) > 0 {
thisPkg := p.packageCache[packageName]
if !useTypescript {
typedefs := "/**\n"
for externalPackageName, namespacedStruct := range allNamespacedStructs {
pkgInfo := p.packageCache[externalPackageName]
relativePackageDir := p.RelativeBindingsDir(thisPkg, pkgInfo)
namePrefix := ""
if pkgInfo.Name != "" && pkgInfo.Path != thisPkg.Path {
namePrefix = pkgInfo.Name
}
// Get keys from namespacedStruct and iterate over them in sorted order
namespacedStructNames := lo.Keys(namespacedStruct)
sort.Strings(namespacedStructNames)
for _, thisStructName := range namespacedStructNames {
structInfo := namespacedStruct[thisStructName]
typedefs += " * @typedef {import('" + relativePackageDir + "/models')." + thisStructName + "} " + namePrefix + structInfo.Name + "\n"
}
}
typedefs += " */\n"
result[relativePackageDir][structName] = typedefs + result[relativePackageDir][structName]
} else {
// Generate imports instead of typedefs
imports := ""
for externalPackageName, namespacedStruct := range allNamespacedStructs {
pkgInfo := p.packageCache[externalPackageName]
relativePackageDir := p.RelativeBindingsDir(thisPkg, pkgInfo)
namePrefix := ""
if pkgInfo.Name != "" && pkgInfo.Path != thisPkg.Path {
namePrefix = pkgInfo.Name
}
// Get keys from namespacedStruct and iterate over them in sorted order
namespacedStructNames := lo.Keys(namespacedStruct)
sort.Strings(namespacedStructNames)
for _, thisStructName := range namespacedStructNames {
structInfo := namespacedStruct[thisStructName]
if namePrefix != "" {
imports += "import {" + thisStructName + " as " + namePrefix + structInfo.Name + "} from '" + relativePackageDir + "/models';\n"
} else {
imports += "import {" + thisStructName + "} from '" + relativePackageDir + "/models';\n"
}
}
}
imports += "\n"
result[relativePackageDir][structName] = imports + result[relativePackageDir][structName]
}
}
if useTypescript {
result[relativePackageDir][structName] = headerTypescript + mainImports + result[relativePackageDir][structName]
} else {
result[relativePackageDir][structName] = header + mainImports + result[relativePackageDir][structName]
}
}
}
return result
}
func mergeNamespacedStructs(structs map[packagePath]map[string]*ExternalStruct, structs2 map[packagePath]map[string]*ExternalStruct) map[packagePath]map[string]*ExternalStruct {
if structs == nil {
structs = make(map[packagePath]map[string]*ExternalStruct)
}
for pkg, pkgStructs := range structs2 {
if _, ok := structs[pkg]; !ok {
structs[pkg] = make(map[string]*ExternalStruct)
}
for name, structInfo := range pkgStructs {
structs[pkg][name] = structInfo
}
}
return structs
}