Skip to content

Commit e85d8f2

Browse files
committed
refactor(v3): generate the v2 compat bridge into migrated projects
The bridge is no longer a public package in the v3 module: its source lives at internal/migrate/v2compat/runtime (compiled, vetted and tested in-repo but not importable) and wails3 migrate copies it into the output project as <module>/v2compat/runtime with a generated-code header explaining it is temporary. This means only migrated projects carry the v2-style API - nobody can adopt it for new code - and there is no sunset obligation on the v3 module: each project deletes its own bridge functions as call sites are ported to the v3 API, and removes the package when nothing imports it. Import rewriting now targets the project-local path, and the bridge is only emitted when the project actually needs it (v2 runtime imports or lifecycle hooks).
1 parent 331781a commit e85d8f2

22 files changed

Lines changed: 91 additions & 14 deletions

File tree

docs/src/content/docs/migration/v2-to-v3.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ This parses your v2 project (wails.json and the `options.App` literal passed to
3434

3535
- `main.go` is rewritten around `application.New()` + `app.Window.NewWithOptions()`, keeping your own code and comments intact. Options are mapped to their v3 equivalents, including platform-specific window options.
3636
- Structs listed in `Bind` become v3 services; the `OnStartup`/`OnDomReady`/`OnShutdown`/`OnBeforeClose` callbacks are bridged automatically.
37-
- Go files calling the v2 `runtime` package are pointed at `github.com/wailsapp/wails/v3/pkg/v2compat/runtime`, a compatibility bridge with the v2 API (context-first functions) implemented on v3. Each bridge function documents its v3 replacement so you can migrate incrementally.
37+
- Go files calling the v2 `runtime` package are pointed at a `v2compat/runtime` package generated *into your project*: a temporary bridge with the v2 API (context-first functions) implemented on the v3 API. Each bridge function documents its v3 replacement, so you can port call sites incrementally, delete bridge functions as you go, and remove the package when nothing imports it any more.
3838
- The frontend is copied over and `wailsjs/` is regenerated as a thin layer over `@wailsio/runtime`, so existing imports like `../wailsjs/go/main/App` and `../wailsjs/runtime/runtime` keep working.
3939
- `wails.json` is replaced by the v3 project files: a Taskfile-based build system and `build/config.yml` populated from your v2 metadata (product info, file associations, protocols).
4040
- `go.mod` swaps `wails/v2` for `wails/v3`; everything else is preserved.

v3/internal/commands/migrate.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ func Migrate(options *flags.Migrate) error {
9090
return err
9191
}
9292

93+
// The compatibility bridge is generated into the project (not shipped as
94+
// part of the v3 module) so that only migrated projects carry it, and its
95+
// owners can delete it as they finish porting to the v3 API.
96+
if proj.UsesV2Runtime || v3opts.NeedsLifecycleService() {
97+
if err := migrate.WriteCompatBridge(proj, outDir); err != nil {
98+
return err
99+
}
100+
}
101+
93102
// go.mod: swap wails/v2 for wails/v3, keep everything else.
94103
// LatestStable is the released tag even in dev builds, so the generated
95104
// require is always resolvable.

v3/internal/commands/migrate_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ func TestMigrateEndToEnd(t *testing.T) {
121121
"frontend/wailsjs/runtime/runtime.js",
122122
"frontend/wailsjs/go/main/App.js",
123123
"frontend/dist/.gitkeep",
124+
"v2compat/runtime/window.go",
125+
"v2compat/runtime/lifecycle.go",
124126
}
125127
for _, rel := range mustExist {
126128
if _, err := os.Stat(filepath.Join(outDir, rel)); err != nil {

v3/internal/migrate/bridge.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package migrate
2+
3+
import (
4+
"embed"
5+
"io/fs"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// The v2 compatibility bridge lives in this repository as an internal package
11+
// so it is compiled, vetted and testable, but it is deliberately NOT part of
12+
// the public v3 API: `wails3 migrate` copies its source into the migrated
13+
// project instead. The user owns the copy and deletes functions (and finally
14+
// the whole package) as call sites are ported to the v3 API.
15+
//
16+
//go:embed v2compat/runtime/*.go
17+
var compatBridgeFS embed.FS
18+
19+
// compatBridgeHeader is prepended to every generated bridge file.
20+
const compatBridgeHeader = `// Code generated by wails3 migrate.
21+
//
22+
// This package is a TEMPORARY bridge that implements the Wails v2 runtime API
23+
// on top of Wails v3. It is part of your project: as you port call sites to
24+
// the v3 API (github.com/wailsapp/wails/v3/pkg/application), delete the
25+
// functions they used, and delete the whole package when nothing imports it
26+
// any more. Each function documents its v3 replacement.
27+
28+
`
29+
30+
// CompatRuntimeImport returns the project-local import path the bridge is
31+
// generated under.
32+
func (p *V2Project) CompatRuntimeImport() string {
33+
return p.ModulePath + "/v2compat/runtime"
34+
}
35+
36+
// WriteCompatBridge copies the v2 compatibility bridge sources into the
37+
// output project under v2compat/runtime.
38+
func WriteCompatBridge(proj *V2Project, outDir string) error {
39+
targetDir := filepath.Join(outDir, "v2compat", "runtime")
40+
if err := os.MkdirAll(targetDir, 0o755); err != nil {
41+
return err
42+
}
43+
entries, err := fs.ReadDir(compatBridgeFS, "v2compat/runtime")
44+
if err != nil {
45+
return err
46+
}
47+
for _, entry := range entries {
48+
data, err := fs.ReadFile(compatBridgeFS, "v2compat/runtime/"+entry.Name())
49+
if err != nil {
50+
return err
51+
}
52+
out := append([]byte(compatBridgeHeader), data...)
53+
if err := os.WriteFile(filepath.Join(targetDir, entry.Name()), out, 0o644); err != nil {
54+
return err
55+
}
56+
}
57+
proj.Report.Note("A v2 runtime compatibility bridge was generated at `v2compat/runtime` in your project. It is yours: delete its functions as you port call sites to the v3 API, and remove the package when nothing imports it any more.")
58+
return nil
59+
}

v3/internal/migrate/copy.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ func CopyProjectFiles(proj *V2Project, outDir string) error {
119119
func RewriteGoImports(proj *V2Project, rel string, src []byte) []byte {
120120
content := string(src)
121121
if strings.Contains(content, strconv.Quote(V2RuntimeImport)) {
122-
content = strings.ReplaceAll(content, strconv.Quote(V2RuntimeImport), strconv.Quote(V2CompatRuntimeImport))
123-
proj.Report.Mapped(rel+": "+V2RuntimeImport, V2CompatRuntimeImport)
122+
content = strings.ReplaceAll(content, strconv.Quote(V2RuntimeImport), strconv.Quote(proj.CompatRuntimeImport()))
123+
proj.Report.Mapped(rel+": "+V2RuntimeImport, proj.CompatRuntimeImport()+" (generated bridge)")
124124
}
125125

126126
// Report any other v2 imports that remain.

v3/internal/migrate/maingen.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func buildImports(proj *V2Project, opts *V3Options) string {
210210
}
211211
add("", "github.com/wailsapp/wails/v3/pkg/application")
212212
if opts.NeedsLifecycleService() {
213-
add(v2compatAlias, V2CompatRuntimeImport)
213+
add(v2compatAlias, proj.CompatRuntimeImport())
214214
}
215215
if opts.OnBeforeClose != "" {
216216
add("", "context")

v3/internal/migrate/migrate.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@ import (
1111
"go/token"
1212
)
1313

14-
// V2CompatRuntimeImport is the import path of the v3-hosted implementation of
15-
// the v2 runtime API. Migrated Go files have their v2 runtime import rewritten
16-
// to this path; the package is also named "runtime", so call sites compile
17-
// unchanged.
18-
const V2CompatRuntimeImport = "github.com/wailsapp/wails/v3/pkg/v2compat/runtime"
19-
2014
// V2RuntimeImport is the Wails v2 runtime package rewritten by the migrator.
15+
// It is replaced with the project-local compatibility bridge (see
16+
// CompatRuntimeImport and WriteCompatBridge); the generated package is also
17+
// named "runtime", so call sites compile unchanged.
2118
const V2RuntimeImport = "github.com/wailsapp/wails/v2/pkg/runtime"
2219

2320
// V2Project is everything the migrator learned about the source project.
@@ -38,6 +35,10 @@ type V2Project struct {
3835
// the file containing wails.Run (handled separately).
3936
GoFiles []string
4037

38+
// UsesV2Runtime is true when any project file imports the v2 runtime
39+
// package (the migrated project then needs the compatibility bridge).
40+
UsesV2Runtime bool
41+
4142
// Report accumulates human-readable notes about everything that needs
4243
// manual attention. It is written to MIGRATION.md in the output project.
4344
Report *Report

v3/internal/migrate/migrate_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func TestGenerateMain(t *testing.T) {
280280
"//go:embed all:frontend/dist",
281281
"// Create an instance of the app structure",
282282
"err := wailsApp.Run()",
283-
`v2runtime "github.com/wailsapp/wails/v3/pkg/v2compat/runtime"`,
283+
`v2runtime "myv2app/v2compat/runtime"`,
284284
} {
285285
if !strings.Contains(src, want) {
286286
t.Errorf("generated main.go missing %q\n---\n%s", want, src)
@@ -364,7 +364,7 @@ import (
364364
)
365365
`)
366366
out := RewriteGoImports(proj, "other.go", src)
367-
if !strings.Contains(string(out), V2CompatRuntimeImport) {
367+
if !strings.Contains(string(out), `"myv2app/v2compat/runtime"`) {
368368
t.Errorf("runtime import not rewritten:\n%s", out)
369369
}
370370
if !strings.Contains(string(out), "wails/v2/pkg/menu") {

v3/internal/migrate/parse.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ func ParseV2Project(dir string) (*V2Project, error) {
8080
if perr != nil {
8181
return fmt.Errorf("could not parse %s: %w", path, perr)
8282
}
83+
for _, imp := range file.Imports {
84+
if imp.Path.Value == strconv.Quote(V2RuntimeImport) {
85+
proj.UsesV2Runtime = true
86+
}
87+
}
8388
files[path] = file
8489
return nil
8590
})

v3/internal/migrate/report.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ func (r *Report) Markdown() string {
5454
sb.WriteString("1. Run `wails3 doctor` to check your environment.\n")
5555
sb.WriteString("2. Run `wails3 dev` to build and run the migrated app.\n")
5656
sb.WriteString("3. Work through the *Manual steps* below, if any.\n")
57-
sb.WriteString("4. Migrate incrementally from the `v2compat/runtime` bridge to the v3 API.\n")
58-
sb.WriteString(" Every function in `github.com/wailsapp/wails/v3/pkg/v2compat/runtime` documents its v3 replacement.\n")
57+
sb.WriteString("4. Migrate incrementally from the generated `v2compat/runtime` bridge in this\n")
58+
sb.WriteString(" project to the v3 API. Every bridge function documents its v3 replacement;\n")
59+
sb.WriteString(" delete functions as you go and remove the package when nothing imports it.\n")
5960
sb.WriteString(" See https://v3.wails.io/migration/v2-to-v3/ for the full guide.\n\n")
6061

6162
if len(r.manual) > 0 {

0 commit comments

Comments
 (0)