Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions v2/cmd/wails/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions v2/internal/app/app_bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand All @@ -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
Expand All @@ -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 {
Expand Down
16 changes: 12 additions & 4 deletions v2/internal/binding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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("")

Expand Down Expand Up @@ -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 {
Expand Down
33 changes: 15 additions & 18 deletions v2/internal/binding/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 += ">"
Expand Down Expand Up @@ -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
Expand All @@ -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" {
Expand All @@ -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 {
Expand Down
36 changes: 35 additions & 1 deletion v2/internal/binding/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
Expand Down
7 changes: 4 additions & 3 deletions v2/internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 26 additions & 10 deletions v2/internal/typescriptify/typescriptify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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))
}

Expand Down
20 changes: 12 additions & 8 deletions v2/pkg/commands/bindings/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 7 additions & 6 deletions v2/pkg/commands/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions website/src/pages/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move this entry to “Unreleased → Added” unless it’s a backport.

This PR was opened on January 28, 2026, while v2.11.0 is dated November 8, 2025, so the changelog entry likely belongs under the Unreleased “Added” section. Please confirm if this is intentionally backported to v2.11.0; otherwise, relocate it.

📌 Suggested edit (if not a backport)
 ## [Unreleased]
 
+### Added
+
+- Added `nullableSlices` opt-in to allow nullable array type generation in [`#4920`](https://github.com/wailsapp/wails/pull/4920)
+
 ### Fixed
@@
-### Added
-
-- Added `nullableSlices` opt-in to allow nullable array type generation in [`#4920`](https://github.com/wailsapp/wails/pull/4920)
🤖 Prompt for AI Agents
In `@website/src/pages/changelog.mdx` at line 36, The changelog line mentioning
the `nullableSlices` opt-in currently placed under v2.11.0 should be moved to
the "Unreleased → Added" section unless this was intentionally backported;
verify whether this PR (the `nullableSlices` entry referencing `#4920`) is a
backport, and if not, cut the line "Added `nullableSlices` opt-in to allow
nullable array type generation in
[`#4920`](https://github.com/wailsapp/wails/pull/4920)" from its current v2.11.0
location in website/src/pages/changelog.mdx and paste it under the Unreleased →
Added subsection so the entry appears in the correct release section.

- 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)
Expand Down
Loading