This commit is contained in:
Atterpac 2026-03-02 12:10:07 -07:00 committed by GitHub
commit 6f1ca75aaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 129 additions and 55 deletions

View file

@ -48,11 +48,12 @@ func generateModule(f *flags.GenerateModule) error {
}
_, err = bindings.GenerateBindings(bindings.Options{
Compiler: f.Compiler,
Tags: buildTags,
TsPrefix: projectConfig.Bindings.TsGeneration.Prefix,
TsSuffix: projectConfig.Bindings.TsGeneration.Suffix,
TsOutputType: projectConfig.Bindings.TsGeneration.OutputType,
Compiler: f.Compiler,
Tags: buildTags,
TsPrefix: projectConfig.Bindings.TsGeneration.Prefix,
TsSuffix: projectConfig.Bindings.TsGeneration.Suffix,
TsOutputType: projectConfig.Bindings.TsGeneration.OutputType,
UseNullableSlices: projectConfig.Bindings.TsGeneration.UseNullableSlices,
})
if err != nil {
return err

View file

@ -32,6 +32,7 @@ func (a *App) Run() error {
var tsPrefixFlag *string
var tsPostfixFlag *string
var tsOutputTypeFlag *string
var useNullableSlicesFlag *bool
tsPrefix := os.Getenv("tsprefix")
if tsPrefix == "" {
@ -48,6 +49,12 @@ func (a *App) Run() error {
tsOutputTypeFlag = bindingFlags.String("tsoutputtype", "", "Output type for generated typescript entities (classes|interfaces)")
}
useNullableSlicesEnv := os.Getenv("usenullableslices")
useNullableSlices := useNullableSlicesEnv == "true"
if useNullableSlicesEnv == "" {
useNullableSlicesFlag = bindingFlags.Bool("usenullableslices", false, "Generate nullable slice types (Type[] | null)")
}
_ = bindingFlags.Parse(os.Args[1:])
if tsPrefixFlag != nil {
tsPrefix = *tsPrefixFlag
@ -58,12 +65,16 @@ func (a *App) Run() error {
if tsOutputTypeFlag != nil {
tsOutputType = *tsOutputTypeFlag
}
if useNullableSlicesFlag != nil {
useNullableSlices = *useNullableSlicesFlag
}
appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated(), a.options.EnumBind)
appBindings.SetTsPrefix(tsPrefix)
appBindings.SetTsSuffix(tsSuffix)
appBindings.SetOutputType(tsOutputType)
appBindings.SetUseNullableSlices(useNullableSlices)
err := generateBindings(appBindings)
if err != nil {

View file

@ -25,10 +25,11 @@ type Bindings struct {
structsToGenerateTS map[string]map[string]interface{}
enumsToGenerateTS map[string]map[string]interface{}
tsPrefix string
tsSuffix string
tsInterface bool
obfuscate bool
tsPrefix string
tsSuffix string
tsInterface bool
obfuscate bool
useNullableSlices bool
}
// NewBindings returns a new Bindings object
@ -101,6 +102,7 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.WithUseNullableSlices(b.useNullableSlices)
w.Namespace = packageName
w.WithBackupDir("")
w.KnownStructs = allStructNames
@ -161,6 +163,7 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.WithUseNullableSlices(b.useNullableSlices)
w.Namespace = packageName
w.WithBackupDir("")
@ -328,6 +331,11 @@ func (b *Bindings) SetOutputType(outputType string) *Bindings {
return b
}
func (b *Bindings) SetUseNullableSlices(v bool) *Bindings {
b.useNullableSlices = v
return b
}
func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, structsToGenerate := range b.structsToGenerateTS {

View file

@ -92,7 +92,7 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error {
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))
args.Add(arg + ":" + b.goTypeToTypescriptType(entityName, &importNamespaces))
}
tsBody.WriteString(args.Join(",") + "):")
// now build Typescript return types
@ -108,11 +108,11 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error {
returnType = "Promise<void>"
} else {
outputTypeName := entityFullReturnType(methodDetails.Outputs[0].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
firstType := goTypeToTypescriptType(outputTypeName, &importNamespaces)
firstType := b.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)
secondType := b.goTypeToTypescriptType(outputTypeName, &importNamespaces)
returnType += "|" + secondType
}
returnType += ">"
@ -175,7 +175,7 @@ var (
jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`)
)
func arrayifyValue(valueArray string, valueType string) string {
func arrayifyValue(valueArray string, valueType string, useNullableSlices bool) string {
valueType = strings.ReplaceAll(valueType, "*", "")
gidx := strings.IndexRune(valueType, '[')
if gidx > 0 { // its a generic type
@ -187,23 +187,20 @@ func arrayifyValue(valueArray string, valueType string) string {
return valueType
}
return "Array<" + valueType + ">"
result := "Array<" + valueType + ">"
if useNullableSlices {
result += " | null"
}
return result
}
func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) string {
func (b *Bindings) 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" {
@ -222,20 +219,20 @@ func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) stri
key := fullyQualifiedName(keyPackage, keyType)
var value string
if strings.HasPrefix(valueType, "map") {
value = goTypeToJSDocType(valueType, importNamespaces)
value = b.goTypeToJSDocType(valueType, importNamespaces)
} else {
value = fullyQualifiedName(valuePackage, valueType)
}
if len(key) > 0 {
return fmt.Sprintf("Record<%s, %s>", key, arrayifyValue(valueArray, value))
return fmt.Sprintf("Record<%s, %s>", key, arrayifyValue(valueArray, value, b.useNullableSlices))
}
return arrayifyValue(valueArray, value)
return arrayifyValue(valueArray, value, b.useNullableSlices)
}
func goTypeToTypescriptType(input string, importNamespaces *slicer.StringSlicer) string {
return goTypeToJSDocType(input, importNamespaces)
func (b *Bindings) goTypeToTypescriptType(input string, importNamespaces *slicer.StringSlicer) string {
return b.goTypeToJSDocType(input, importNamespaces)
}
func entityFullReturnType(input, prefix, suffix string, importNamespaces *slicer.StringSlicer) string {

View file

@ -139,10 +139,44 @@ func Test_goTypeToJSDocType(t *testing.T) {
want: "main.ListData_net_http_Request_",
},
}
b := &Bindings{}
var importNamespaces slicer.StringSlicer
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := goTypeToJSDocType(tt.input, &importNamespaces); got != tt.want {
if got := b.goTypeToJSDocType(tt.input, &importNamespaces); got != tt.want {
t.Errorf("goTypeToJSDocType() = %v, want %v", got, tt.want)
}
})
}
}
func Test_goTypeToJSDocType_nullable(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "[]int nullable",
input: "[]int",
want: "Array<number> | null",
},
{
name: "[]bool nullable",
input: "[]bool",
want: "Array<boolean> | null",
},
{
name: "[]byte still string",
input: "[]byte",
want: "string",
},
}
b := &Bindings{useNullableSlices: true}
var importNamespaces slicer.StringSlicer
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := b.goTypeToJSDocType(tt.input, &importNamespaces); got != tt.want {
t.Errorf("goTypeToJSDocType() = %v, want %v", got, tt.want)
}
})

View file

@ -251,9 +251,10 @@ type Bindings struct {
}
type TsGeneration struct {
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
OutputType string `json:"outputType"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
OutputType string `json:"outputType"`
UseNullableSlices bool `json:"useNullableSlices"`
}
// Parse the given JSON data into a Project struct

View file

@ -116,9 +116,10 @@ type TypeScriptify struct {
// throwaway, used when converting
alreadyConverted map[string]bool
Namespace string
KnownStructs *slicer.StringSlicer
KnownEnums *slicer.StringSlicer
Namespace string
KnownStructs *slicer.StringSlicer
KnownEnums *slicer.StringSlicer
UseNullableSlices bool
}
func New() *TypeScriptify {
@ -253,6 +254,11 @@ func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify {
return t
}
func (t *TypeScriptify) WithUseNullableSlices(v bool) *TypeScriptify {
t.UseNullableSlices = v
return t
}
func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify {
switch ty := obj.(type) {
case StructType:
@ -658,11 +664,12 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m
result = "export " + result
}
builder := typeScriptClassBuilder{
types: t.kinds,
indent: t.Indent,
prefix: t.Prefix,
suffix: t.Suffix,
namespace: t.Namespace,
types: t.kinds,
indent: t.Indent,
prefix: t.Prefix,
suffix: t.Suffix,
namespace: t.Namespace,
useNullableSlices: t.UseNullableSlices,
}
for _, field := range fields {
@ -846,6 +853,7 @@ type typeScriptClassBuilder struct {
constructorBody []string
prefix, suffix string
namespace string
useNullableSlices bool
}
func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error {
@ -863,7 +871,11 @@ func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field ref
t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
return nil
} else if len(typeScriptType) > 0 {
t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth)), false)
nullableSuffix := ""
if t.useNullableSlices {
nullableSuffix = " | null"
}
t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth), nullableSuffix), false)
t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
return nil
}
@ -936,7 +948,11 @@ func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field
fieldType = field.Type.Elem().String()
}
strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth)), false)
nullableSuffix := ""
if t.useNullableSlices {
nullableSuffix = " | null"
}
t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth), nullableSuffix), false)
t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix))
}

View file

@ -15,14 +15,15 @@ import (
// Options for generating bindings
type Options struct {
Filename string
Tags []string
ProjectDirectory string
Compiler string
GoModTidy bool
TsPrefix string
TsSuffix string
TsOutputType string
Filename string
Tags []string
ProjectDirectory string
Compiler string
GoModTidy bool
TsPrefix string
TsSuffix string
TsOutputType string
UseNullableSlices bool
}
// GenerateBindings generates bindings for the Wails project in the given ProjectDirectory.
@ -83,6 +84,9 @@ func GenerateBindings(options Options) (string, error) {
env = shell.SetEnv(env, "tsprefix", options.TsPrefix)
env = shell.SetEnv(env, "tssuffix", options.TsSuffix)
env = shell.SetEnv(env, "tsoutputtype", options.TsOutputType)
if options.UseNullableSlices {
env = shell.SetEnv(env, "usenullableslices", "true")
}
stdout, stderr, err = shell.RunCommandWithEnv(env, workingDirectory, filename)
if err != nil {

View file

@ -231,12 +231,13 @@ func GenerateBindings(buildOptions *Options) error {
// Generate Bindings
output, err := bindings.GenerateBindings(bindings.Options{
Compiler: buildOptions.Compiler,
Tags: buildOptions.UserTags,
GoModTidy: !buildOptions.SkipModTidy,
TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix,
TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix,
TsOutputType: buildOptions.ProjectData.Bindings.TsGeneration.OutputType,
Compiler: buildOptions.Compiler,
Tags: buildOptions.UserTags,
GoModTidy: !buildOptions.SkipModTidy,
TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix,
TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix,
TsOutputType: buildOptions.ProjectData.Bindings.TsGeneration.OutputType,
UseNullableSlices: buildOptions.ProjectData.Bindings.TsGeneration.UseNullableSlices,
})
if err != nil {
return err

View file

@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added `nullableSlices` opt-in to allow nullable array type generation in [#4920](https://github.com/wailsapp/wails/pull/4920)
- Add origin verification for bindings by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/4480)
- Configure Vite timeout by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/4374)
- Added `ContentProtection` option to allow hiding the application window from screen sharing software [#4241](https://github.com/wailsapp/wails/pull/4241) by [@Taiterbase](https://github.com/Taiterbase)