Skip to content

feat(v3): add experimental migrate command to CLI - #5895

Open
leaanthony wants to merge 7 commits into
masterfrom
agent/experimental-v3-migrate
Open

feat(v3): add experimental migrate command to CLI#5895
leaanthony wants to merge 7 commits into
masterfrom
agent/experimental-v3-migrate

Conversation

@leaanthony

@leaanthony leaanthony commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • add wails3 migrate to the released V3 CLI
  • keep the command explicitly experimental in CLI output and generated MIGRATION.md
  • migrate deterministic project structure, build assets, frontend setup, and configuration
  • leave V2 API call sites listed for deliberate manual porting
  • document the actual V3 build/config.yml and Taskfile.yml layout
  • document reproducible bug reports and Wails Enhancement Proposal feedback

Validation

  • go test ./internal/commands ./internal/migrate
  • go build ./cmd/wails3
  • wails3 migrate --help
  • migrated the V2 panic-recovery-test fixture into a fresh output directory
  • verified the experimental warning, MIGRATION.md, Taskfile.yml, and build/config.yml

The generated project remains intentionally non-zero-touch: users must review the report, port remaining V2 API calls, regenerate bindings, and test their application.

Summary by CodeRabbit

  • New Features

    • Added an experimental wails3 migrate command to help convert Wails v2 projects to v3.
    • Generates a separate v3 project, updates configuration and dependencies, migrates frontend assets, and preserves supported project settings.
    • Creates a MIGRATION.md checklist identifying API calls and steps requiring manual review.
  • Documentation

    • Added CLI reference, usage guidance, migration instructions, troubleshooting steps, and beta release notes for the migration assistant.

… migration

wails3 migrate -d ./myv2project -o ./myv3project converts a Wails v2
project into a v3 project:

- Parses wails.json and the declarative options.App literal passed to
  wails.Run (syntax-only, no module downloads needed) and generates an
  equivalent programmatic main.go via application.New() +
  app.Window.NewWithOptions(), preserving user code and comments through
  textual surgery on just the wails.Run statement and imports.
- Maps v2 options to v3 (window geometry, start state, background
  colour, platform-specific options, single instance, asset server),
  converts Bind entries into v3 services and bridges the
  OnStartup/OnDomReady/OnShutdown/OnBeforeClose lifecycle callbacks.
- Adds pkg/v2compat/runtime: the v2 runtime API (context-first
  functions) implemented on the v3 application API, so migrated code
  only needs its import path rewritten. Covers events, window control,
  dialogs, clipboard, browser, screens, logging and app control; every
  function documents its v3 replacement for incremental migration.
- Migrates the frontend: regenerates wailsjs/ as a compatibility layer
  over @wailsio/runtime (runtime shim + Call.ByName binding shims from
  the parsed bound-struct methods) and adds the npm dependency.
- Scaffolds the v3 build system via the init machinery: Taskfile,
  build/ assets and build/config.yml populated from the v2 metadata
  (product info, file associations, protocols, bundle id kept as
  com.wails.<name> so the app keeps its identity).
- Transforms go.mod (wails/v2 -> wails/v3, go directive raised),
  preserving all other requires.
- Writes MIGRATION.md documenting every mapped option and any manual
  steps (menus, custom loggers, EnumBind, ...).

Docs: the v2-to-v3 migration guide now leads with the automated path.
The migrate command now announces its experimental status in the CLI
output, the generated MIGRATION.md and the migration guide, with a
pointer to the issue tracker for reports and contributions.
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).
…at layers

Half-migrated code helps nobody, and compatibility shims invite new code
onto the old API. The migrate command now draws a hard line:

Migrated fully (deterministic):
- project scaffold, Taskfile, build assets, config.yml from wails.json
- main.go rewritten around application.New()/NewWithOptions with the
  options mapped; lifecycle hooks wired natively (ApplicationStarted
  event, WindowRuntimeReady event, Options.OnShutdown, ShouldQuit)
- go.mod v2 -> v3; frontend copied with @wailsio/runtime added

Documented instead of migrated:
- every call into the v2 runtime package, listed in MIGRATION.md by
  file:line with its concrete v3 replacement; the sources are copied
  untouched, so the compiler points at exactly the listed locations
  until they are ported
- every frontend wailsjs import, with the @wailsio/runtime equivalent
  and the generate-bindings workflow; the generated wailsjs directory
  is not carried over (it is v2 build output and cannot work with v3)

Removed: the v2compat runtime bridge and the generated wailsjs shims.
go mod tidy is skipped (with a warning) while v2 call sites remain,
since tidying would re-add the v2 dependency and let old calls compile
only to fail at runtime; MIGRATION.md spells this out.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds the experimental wails3 migrate command. It parses Wails v2 projects, generates v3 sources and configuration, migrates Go and frontend metadata, writes MIGRATION.md, validates conversion behavior, and documents the workflow.

Changes

Wails v2-to-v3 migration assistant

Layer / File(s) Summary
Migration models, configuration, and parsing
v3/internal/migrate/migrate.go, v3/internal/migrate/wailsjson.go, v3/internal/migrate/parse.go
Defines migration models, loads v2 configuration, parses Go projects, resolves bindings, and derives parameter metadata.
Option mapping and v3 source generation
v3/internal/migrate/mapping.go, v3/internal/migrate/maingen.go
Maps v2 application options and generates formatted v3 main.go source.
Migration advice and reporting
v3/internal/migrate/advisor.go, v3/internal/migrate/report.go
Finds v2 Go and frontend usage and renders migration notes, call sites, and manual steps.
Project, module, and frontend conversion
v3/internal/migrate/gomod.go, v3/internal/migrate/copy.go, v3/internal/migrate/frontend.go
Transforms go.mod, copies project files, migrates frontend sources, and adds @wailsio/runtime.
Command orchestration and CLI wiring
v3/internal/commands/migrate.go, v3/internal/flags/migrate.go, v3/cmd/wails3/main.go
Adds command options and orchestrates validation, scaffolding, configuration generation, conversion, and reporting.
Validation and documentation
v3/internal/commands/migrate_test.go, v3/internal/migrate/migrate_test.go, docs/src/content/docs/..., IMPLEMENTATION.md
Adds migration tests and documents command usage, output, configuration changes, manual review, and Go version handling.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as wails3 migrate
  participant Parser as ParseV2Project
  participant Mapper as MapOptions
  participant Generator as GenerateMain
  participant Report as Report
  CLI->>Parser: parse v2 project
  Parser->>Mapper: provide V2Project
  Mapper->>Generator: provide V3Options
  Generator->>Report: record migration results
  Report->>CLI: write MIGRATION.md
Loading

Possibly related issues

Possibly related PRs

Poem

A rabbit maps each v2 trail,
Builds v3 paths without a fail.
A checklist marks what hands must do,
While sources hop to something new.
🐇✨ wails3 migrate leads the way.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description summarizes the feature and validation, but it omits the issue reference, change type, platform details, wails doctor output, and checklist status. Add the issue or accepted WEP link, select the change type and tested platforms, provide wails doctor output or environment details, and complete the checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the experimental migrate command added to the V3 CLI, which is the main change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/experimental-v3-migrate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added Documentation Improvements or additions to documentation cli v3-alpha labels Aug 4, 2026
@leaanthony
leaanthony marked this pull request as ready for review August 4, 2026 12:13
Copilot AI lite review requested due to automatic review settings August 4, 2026 12:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an experimental wails3 migrate command to the v3 CLI that parses a Wails v2 project (wails.json, go.mod, and the wails.Run(&options.App{...}) site), scaffolds an equivalent v3 project into a separate output directory, and generates a MIGRATION.md checklist for manual follow-up work (notably remaining v2 runtime/wailsjs call sites).

Changes:

  • Introduces a new v3/internal/migrate engine (parse + map + generate + report) with unit and end-to-end tests.
  • Wires the new migrate command into the v3 CLI, adds flags, and implements project scaffolding/output behavior.
  • Updates docs (CLI reference, migration guide, blog post) to document the experimental migration workflow and reporting guidance.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
v3/internal/migrate/wailsjson.go Parses v2 wails.json into a minimal config model with defaults used by the migrator.
v3/internal/migrate/report.go Builds MIGRATION.md summarizing automatic mappings and manual porting steps.
v3/internal/migrate/parse.go Syntax-parses a v2 module, locates wails.Run, discovers bound services, and enumerates call sites.
v3/internal/migrate/migrate.go Defines core migrator data structures (V2Project, MainInfo, bound types).
v3/internal/migrate/migrate_test.go Unit tests covering parsing, mapping, main generation, go.mod rewriting, and frontend migration behavior.
v3/internal/migrate/mapping.go Maps v2 options.App fields to v3 application.Options / WebviewWindowOptions equivalents and records manual steps.
v3/internal/migrate/maingen.go Rewrites main.go by replacing imports and the wails.Run(...) statement with v3 equivalents, then formats/prunes imports.
v3/internal/migrate/gomod.go Rewrites go.mod from v2 to v3, bumps the Go directive minimum, and removes v2-specific replaces.
v3/internal/migrate/frontend.go Copies frontend sources while excluding v2-generated wailsjs, ensures dist/ exists, and injects @wailsio/runtime.
v3/internal/migrate/copy.go Copies remaining project files (excluding frontend/build and regenerated files) into the output directory.
v3/internal/migrate/advisor.go Collects v2 runtime and wailsjs import call sites with concrete v3 migration advice.
v3/internal/flags/migrate.go Adds CLI flags for wails3 migrate.
v3/internal/commands/migrate.go Implements the CLI command orchestration (parsing, scaffolding, writing output, report generation, optional tidy).
v3/internal/commands/migrate_test.go End-to-end test ensuring expected migrated outputs are generated and v2 artifacts are excluded.
v3/cmd/wails3/main.go Registers the migrate subcommand in the CLI.
docs/src/content/docs/reference/cli.mdx Documents the new wails3 migrate command in the CLI reference table.
docs/src/content/docs/migration/v2-to-v3.mdx Adds an “Automated Migration (Experimental)” section and related guidance.
docs/src/content/docs/guides/cli.mdx Adds a CLI guide section documenting the migrate workflow and reporting expectations.
docs/src/content/docs/blog/2026-08-02-wails-v3-beta.md Updates the beta announcement to mention the experimental migration assistant.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +280 to +285
name := ""
if imp.Name != nil {
name = imp.Name.Name
} else {
name = path[strings.LastIndex(path, "/")+1:]
}
Comment thread v3/internal/migrate/parse.go Outdated
Comment on lines +365 to +379
ast.Inspect(mainFile, func(n ast.Node) bool {
if typeName != "" {
return false
}
assign, ok := n.(*ast.AssignStmt)
if !ok || len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
return true
}
lhs, ok := assign.Lhs[0].(*ast.Ident)
if !ok || lhs.Name != e.Name {
return true
}
typeName = resolveElementType(fset, files, mainFile, assign.Rhs[0])
return false
})
Comment thread v3/internal/migrate/frontend.go Outdated
Comment on lines +89 to +96
if dependenciesRe.MatchString(content) {
content = dependenciesRe.ReplaceAllString(content, "$1\n \"@wailsio/runtime\": \"latest\",")
} else if idx := strings.Index(content, "{"); idx >= 0 {
content = content[:idx+1] + "\n \"dependencies\": {\n \"@wailsio/runtime\": \"latest\"\n }," + content[idx+1:]
} else {
proj.Report.Manual("frontend/package.json", "Could not add the `@wailsio/runtime` dependency automatically; add it and run your package manager's install.")
return nil
}
Comment thread v3/internal/commands/migrate.go Outdated
Comment on lines +106 to +107
// Frontend: copy, regenerate wailsjs as a compatibility layer, add the
// @wailsio/runtime dependency.
Comment thread v3/internal/migrate/report.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🧹 Nitpick comments (10)
v3/internal/migrate/mapping.go (1)

336-341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record a manual step when Debug is not a literal.

If Debug is not a composite literal, or the literal has no OpenInspectorOnStartup key, the option is dropped silently. The user gets no entry in MIGRATION.md. Add an m.manual("Debug", ...) call in the non-literal case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/mapping.go` around lines 336 - 341, Update the “Debug”
handling around compositeLit and litFields so a manual migration step is
recorded when Debug is not a composite literal or lacks OpenInspectorOnStartup.
Preserve the existing m.carry call when that key is present, and add
m.manual("Debug", ...) only for the unsupported or missing-key path.
v3/internal/migrate/maingen.go (1)

271-293: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import pruning drops the spec but keeps its comment.

The removal span covers only imp.Pos() to imp.End(). A line comment or doc comment attached to that import stays in the file. The result is a dangling comment inside the import block. Extend the span to the spec's Doc and Comment positions, or rebuild the import block from the AST with printer instead of cutting byte ranges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/maingen.go` around lines 271 - 293, Update the
import-pruning logic in the loop over gen.Specs to include comments attached to
each ast.ImportSpec when calculating the removal span. Extend the start and end
positions using imp.Doc and imp.Comment so associated comments are removed with
the unused import, while preserving the existing handling for used and
side-effect imports.
docs/src/content/docs/blog/2026-08-02-wails-v3-beta.md (1)

137-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Match the version casing used in this file.

Lines 124-135 use v3 and v2. The new paragraph uses V3 and V2. Use the lowercase form for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/content/docs/blog/2026-08-02-wails-v3-beta.md` around lines 137 -
141, Update the migration assistant paragraph to use lowercase v3 and v2
consistently, including the project description, checklist reference, and API
calls.
v3/internal/commands/migrate.go (3)

106-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the stale comment.

The comment says the frontend step regenerates wailsjs as a compatibility layer. The final behavior drops wailsjs and only copies the frontend plus adds the @wailsio/runtime dependency. TestMigrateFrontend asserts that frontend/wailsjs does not exist.

♻️ Proposed fix
-	// Frontend: copy, regenerate wailsjs as a compatibility layer, add the
-	// `@wailsio/runtime` dependency.
+	// Frontend: copy the sources (the v2 wailsjs output is dropped) and add
+	// the `@wailsio/runtime` dependency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/commands/migrate.go` around lines 106 - 107, Update the frontend
migration comment near TestMigrateFrontend to remove the claim that wailsjs is
regenerated or retained as a compatibility layer; describe only copying the
frontend and adding the `@wailsio/runtime` dependency, matching the asserted
absence of frontend/wailsjs.

309-315: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Report the icon copy only when the write succeeds.

Line 312 discards the os.WriteFile error. The note at Line 313 then tells the user that build/appicon.png was copied even when the write failed. Check the error and warn instead.

♻️ Proposed fix
 	if _, err := os.Stat(srcIcon); err == nil {
 		data, err := os.ReadFile(srcIcon)
 		if err == nil {
-			_ = os.WriteFile(filepath.Join(outDir, "build", "appicon.png"), data, 0o644)
-			proj.Report.Note("Copied build/appicon.png from the v2 project. Run `wails3 task common:generate:icons` to regenerate the platform icon files (icons.icns / icon.ico) from it.")
+			if werr := os.WriteFile(filepath.Join(outDir, "build", "appicon.png"), data, 0o644); werr != nil {
+				term.Warningf("Could not copy build/appicon.png (%v) - copy it manually.\n", werr)
+			} else {
+				proj.Report.Note("Copied build/appicon.png from the v2 project. Run `wails3 task common:generate:icons` to regenerate the platform icon files (icons.icns / icon.ico) from it.")
+			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/commands/migrate.go` around lines 309 - 315, Update the icon-copy
block in the migration flow to check the error returned by os.WriteFile before
reporting success. Only call proj.Report.Note after writing build/appicon.png
succeeds; when the write fails, issue a warning through the existing reporting
mechanism instead.

190-203: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Restore stdout and the working directory with defer.

templates.Install mutates process-global state. The restore of os.Stdout at Line 197 and the os.Chdir(wd) at Line 201 only run on the normal path. If templates.Install panics, the process keeps writing to /dev/null and stays in the deleted scratch directory. defer makes the restore unconditional.

♻️ Proposed fix
 	term.DisableOutput()
-	stdout := os.Stdout
-	if devnull, dnErr := os.OpenFile(os.DevNull, os.O_WRONLY, 0); dnErr == nil {
-		os.Stdout = devnull
-		defer devnull.Close()
-	}
-	err = templates.Install(initFlags)
-	os.Stdout = stdout
-	if !quiet {
-		term.EnableOutput()
-	}
-	if chdirErr := os.Chdir(wd); chdirErr != nil {
-		return chdirErr
-	}
+	stdout := os.Stdout
+	if devnull, dnErr := os.OpenFile(os.DevNull, os.O_WRONLY, 0); dnErr == nil {
+		os.Stdout = devnull
+		defer devnull.Close()
+	}
+	defer func() {
+		os.Stdout = stdout
+		if !quiet {
+			term.EnableOutput()
+		}
+		_ = os.Chdir(wd)
+	}()
+	err = templates.Install(initFlags)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/commands/migrate.go` around lines 190 - 203, In the migration
flow surrounding templates.Install, defer restoration of os.Stdout immediately
after redirecting it and defer returning to wd before invoking the installer, so
both process-global state and the working directory are restored during panics
or early returns. Remove the corresponding normal-path restoration calls while
preserving quiet-mode output handling and existing error propagation.
v3/internal/migrate/migrate_test.go (1)

370-383: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the dead sentinel in the assertion loop.

Line 375 adds "`src/main.js` should not appear" to the want list. Lines 377-379 then skip that exact entry. The loop asserts nothing for it. Either drop the entry and the guard, or write the intended negative assertion explicitly.

♻️ Proposed fix
 	for _, want := range []string{
 		"## Port these to the v3 API",
 		"`app.go:", // Go call site location
 		"`runtime.WindowSetTitle`",
 		"window.SetTitle(title)",
-		"`src/main.js` should not appear",
 	} {
-		if want == "`src/main.js` should not appear" {
-			continue
-		}
 		if !strings.Contains(md, want) {
 			t.Errorf("report missing %q\n---\n%s", want, md)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/migrate_test.go` around lines 370 - 383, Remove the
unused "`src/main.js` should not appear" entry and its special-case guard from
the assertion loop in the migration test, or replace them with an explicit
negative assertion that verifies `md` does not contain that string.
docs/src/content/docs/guides/cli.mdx (1)

56-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a #### Flags table for consistency.

Every other command on this page documents its flags in a table (init at Line 23, dev at Line 87, build at Line 107). The migrate section points at --help instead. The flag set is small and stable in v3/internal/flags/migrate.go.

♻️ Proposed addition
 ```bash
 wails3 migrate -d /path/to/v2-project -o /path/to/v3-project

+#### Flags
+| Flag | Description | Default |
+|-------------------|------------------------------------------------|---------|
+| -d | Path to the Wails v2 project to migrate | . |
+| -o | Directory to write the migrated v3 project to | |
+| -f | Write into a non-empty output directory | false |
+| -q | Suppress output | false |
+| -skipgomodtidy | Skip go mod tidy | false |

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/src/content/docs/guides/cli.mdx around lines 56 - 77, Add a “####
Flags” table to the migrate documentation section, listing -d, -o, -f,
-q, and -skipgomodtidy with their descriptions and defaults consistent with
v3/internal/flags/migrate.go; retain the existing migration guidance and
--help reference.


</details>

<!-- cr-comment:v1:ccbb6802bc24f525259c197d -->

</blockquote></details>
<details>
<summary>v3/internal/commands/migrate_test.go (1)</summary><blockquote>

`223-235`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _⚡ Quick win_

**Add the reverse containment case.**

`TestMigrateRefusesNestedOutput` covers only `outputDir` inside `v2Dir`. Add a case where `outputDir` is the parent of `v2Dir`. That case currently passes the path check and can overwrite the v2 sources. See the related comment on `v3/internal/commands/migrate.go` Line 54.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @v3/internal/commands/migrate_test.go around lines 223 - 235, Add a
reverse-containment test alongside TestMigrateRefusesNestedOutput that sets
OutputDir to a parent of v2Dir, invokes Migrate with the same options, and
asserts the same “must not be inside” error. Ensure the test verifies Migrate
rejects output paths that contain V2Dir and protects the existing v2 sources.


</details>

<!-- cr-comment:v1:70ed96ad24765bbf30bcaad5 -->

</blockquote></details>
<details>
<summary>v3/internal/migrate/report.go (1)</summary><blockquote>

`72-82`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _⚡ Quick win_

**Render call sites in a stable order.**

`callSites` is appended during `adviseGoRuntimeCalls` from `range files`, and `Markdown()` renders them in that order. Copy and sort the slice before rendering to keep `MIGRATION.md` deterministic for equivalent inputs.

<details>
<summary>Proposed change</summary>

```diff
-		for _, cs := range r.callSites {
+		callSites := append([]string(nil), r.callSites...)
+		sort.Strings(callSites)
+		for _, cs := range callSites {
 			sb.WriteString(cs + "\n")
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/report.go` around lines 72 - 82, Update Markdown() to
copy r.callSites into a separate slice, sort the copy before iterating, and
render the sorted values. Preserve r.callSites itself unchanged while ensuring
equivalent inputs produce deterministic MIGRATION.md output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/src/content/docs/guides/cli.mdx`:
- Around line 75-76: Update the Wails Enhancement Proposal link in the migration
guidance near generated MIGRATION.md so it points to the existing
/tree/master/v3/wep path, preserving the surrounding text and link purpose.

In `@v3/internal/commands/migrate.go`:
- Around line 54-59: The output containment validation only rejects outputs
nested inside the source; make it reject both directions so the source project
remains unchanged. In v3/internal/commands/migrate.go lines 54-59, update the
guards around isSubPath to also reject isSubPath(outDir, proj.Dir) with an
appropriate error. In v3/internal/commands/migrate_test.go lines 223-235, extend
TestMigrateRefusesNestedOutput with a case using the parent of v2Dir as
OutputDir and assert the new error.
- Around line 358-387: Update the YAML generation in the file-associations and
protocols migration blocks to serialize every emitted scalar safely, including
Ext, Name, Description, IconName, Role, and Scheme; use the project’s YAML
marshaller or quote each value so characters such as :, #, leading -, and quotes
remain parseable. Omit optional empty descriptions instead of emitting
null-valued fields, and preserve the existing migration notes and structure.

In `@v3/internal/migrate/advisor.go`:
- Line 155: Update wailsjsImportRe and the associated FindStringSubmatch
scanning loop to recognize side-effect static imports and dynamic import(...)
expressions in addition to existing from/require forms. Iterate through each
line’s matches so every wailsjs import, including multiple imports on one line,
is reported in MIGRATION.md before continuing to the next line.
- Around line 193-209: Update the scanner setup in adviseFrontendImports to
configure an explicit buffer limit appropriate for supported frontend files,
allowing valid long source lines beyond bufio.MaxScanTokenSize while retaining a
bounded maximum. Add a regression fixture containing an overlong frontend line
and verify migration advice completes without scanner.Err().
- Around line 111-151: Extend adviseGoRuntimeCalls to detect dot-imported
V2RuntimeImport entries, retaining an *ast.Ident reference for the v2 runtime
package and recording runtime.Updates when such an import is present. During AST
inspection, also match unqualified ast.CallExpr calls against the known runtime
advice names and report them using the v2 runtime reference, while preserving
existing SelectorExpr handling for aliased or named imports.

In `@v3/internal/migrate/frontend.go`:
- Around line 69-92: The addRuntimeDependency function must update package.json
as structured JSON instead of using regex or global text checks, which can
create trailing commas or skip required changes. Unmarshal the document, inspect
only the root dependencies object for `@wailsio/runtime`, add the latest entry
when absent, and marshal it back while preserving the existing missing-file
behavior; add tests covering empty root and empty dependencies objects.

In `@v3/internal/migrate/gomod.go`:
- Around line 10-12: Update the minGoVersion constant to 1.25 so generated Wails
v3 projects declare the supported minimum Go version, and update the
corresponding expectation in the migration test covering the Go directive to
assert 1.25.

In `@v3/internal/migrate/maingen.go`:
- Around line 89-93: Deduplicate field names in MapOptions before they reach
generation, ensuring repeated keys such as BackgroundType and Assets are emitted
only once. Also update writeFields to track already-written f.Name values and
skip duplicates, while preserving the existing field order and formatting for
the first occurrence.

In `@v3/internal/migrate/mapping.go`:
- Around line 296-304: Update the Assets mapping cases and mapAssetServer to use
set semantics like BackgroundType, ensuring only one application.Options.Assets
field remains when multiple v2 asset fields are configured. When a later asset
value replaces an earlier one, record the corresponding overwrite in
m.proj.Report.
- Around line 460-469: Update v3/internal/migrate/mapping.go lines 460-469 to
replace existing BackgroundType fields when mapping WebviewIsTransparent and
WindowIsTranslucent, and apply the same replacement behavior to
Mac.WebviewIsTransparent at line 547; update v3/internal/migrate/mapping.go
lines 296-304 so Assets, AssetsHandler, and mapAssetServer at line 429 replace
the Assets field and record a report note when overwriting; update
v3/internal/migrate/maingen.go lines 89-93 in writeFields to skip field names
already written as a defensive duplicate-key guard.
- Around line 280-286: Update the Fullscreen handling branch in the migration
mapping logic to distinguish the literal false identifier from other non-true
values. Skip m.manual for Fullscreen: false, while preserving the existing
fullscreen mapping for true and manual guidance for genuinely non-constant
values.

In `@v3/internal/migrate/migrate_test.go`:
- Around line 359-361: Align the v3 migration minimum Go version with the
toolchain by changing minGoVersion in gomod.go to 1.25, updating the
migrate_test.go expectation from “go 1.24” to “go 1.25”, and synchronizing
IMPLEMENTATION.md with the same v3 Go version decision.

In `@v3/internal/migrate/parse.go`:
- Around line 148-239: Update findRunCall to collect every wails.Run candidate
instead of stopping at the first match, preserving each call’s enclosing
statement context; reject multiple candidates and unsupported contexts before
GenerateMain writes output. Detect the documented if err := wails.Run(...); err
!= nil form and set RunStmt to the complete *ast.IfStmt only when its error
behavior can be preserved, otherwise reject it, so GenerateMain replaces a valid
rewrite range. Add fixtures covering multiple calls and both assignment/if
forms.
- Around line 309-448: Update resolveElementType, constructorReturnType, and
collectMethods to resolve each Bind entry from the wails.Run file’s lexical
scope and retain its package identity. Handle package-level and local var
declarations while selecting the nearest valid declaration, and restrict
constructor and receiver-method lookup to the resolved package instead of
matching names across all parsed files. Add fixtures covering package-level
vars, local shadowing, and duplicate symbols in a subpackage.
- Around line 58-90: Update the source-file selection in the filepath.WalkDir
callback to honor Go build constraints using a defined build context, excluding
files and directories such as testdata that are not part of the selected project
build. If migration supports multiple platforms, process each applicable
constrained entrypoint; otherwise reject or omit non-selected variants before
populating files and writing output. Add fixtures covering mutually exclusive
platform files and testdata.

In `@v3/internal/migrate/report.go`:
- Around line 60-70: Update the next-steps rendering in the report builder
around r.callSites so the call-site migration step and its continuation lines
are emitted only when callSites is non-empty. Renumber the subsequent go mod
tidy, dev, and manual-steps items based on whether that conditional step was
included, while preserving the existing guidance text.

---

Nitpick comments:
In `@docs/src/content/docs/blog/2026-08-02-wails-v3-beta.md`:
- Around line 137-141: Update the migration assistant paragraph to use lowercase
v3 and v2 consistently, including the project description, checklist reference,
and API calls.

In `@docs/src/content/docs/guides/cli.mdx`:
- Around line 56-77: Add a “#### Flags” table to the `migrate` documentation
section, listing `-d`, `-o`, `-f`, `-q`, and `-skipgomodtidy` with their
descriptions and defaults consistent with `v3/internal/flags/migrate.go`; retain
the existing migration guidance and `--help` reference.

In `@v3/internal/commands/migrate_test.go`:
- Around line 223-235: Add a reverse-containment test alongside
TestMigrateRefusesNestedOutput that sets OutputDir to a parent of v2Dir, invokes
Migrate with the same options, and asserts the same “must not be inside” error.
Ensure the test verifies Migrate rejects output paths that contain V2Dir and
protects the existing v2 sources.

In `@v3/internal/commands/migrate.go`:
- Around line 106-107: Update the frontend migration comment near
TestMigrateFrontend to remove the claim that wailsjs is regenerated or retained
as a compatibility layer; describe only copying the frontend and adding the
`@wailsio/runtime` dependency, matching the asserted absence of frontend/wailsjs.
- Around line 309-315: Update the icon-copy block in the migration flow to check
the error returned by os.WriteFile before reporting success. Only call
proj.Report.Note after writing build/appicon.png succeeds; when the write fails,
issue a warning through the existing reporting mechanism instead.
- Around line 190-203: In the migration flow surrounding templates.Install,
defer restoration of os.Stdout immediately after redirecting it and defer
returning to wd before invoking the installer, so both process-global state and
the working directory are restored during panics or early returns. Remove the
corresponding normal-path restoration calls while preserving quiet-mode output
handling and existing error propagation.

In `@v3/internal/migrate/maingen.go`:
- Around line 271-293: Update the import-pruning logic in the loop over
gen.Specs to include comments attached to each ast.ImportSpec when calculating
the removal span. Extend the start and end positions using imp.Doc and
imp.Comment so associated comments are removed with the unused import, while
preserving the existing handling for used and side-effect imports.

In `@v3/internal/migrate/mapping.go`:
- Around line 336-341: Update the “Debug” handling around compositeLit and
litFields so a manual migration step is recorded when Debug is not a composite
literal or lacks OpenInspectorOnStartup. Preserve the existing m.carry call when
that key is present, and add m.manual("Debug", ...) only for the unsupported or
missing-key path.

In `@v3/internal/migrate/migrate_test.go`:
- Around line 370-383: Remove the unused "`src/main.js` should not appear" entry
and its special-case guard from the assertion loop in the migration test, or
replace them with an explicit negative assertion that verifies `md` does not
contain that string.

In `@v3/internal/migrate/report.go`:
- Around line 72-82: Update Markdown() to copy r.callSites into a separate
slice, sort the copy before iterating, and render the sorted values. Preserve
r.callSites itself unchanged while ensuring equivalent inputs produce
deterministic MIGRATION.md output.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a7a7af04-289f-4fb2-981b-2693cce22560

📥 Commits

Reviewing files that changed from the base of the PR and between 2014eb6 and 3495d3a.

📒 Files selected for processing (19)
  • docs/src/content/docs/blog/2026-08-02-wails-v3-beta.md
  • docs/src/content/docs/guides/cli.mdx
  • docs/src/content/docs/migration/v2-to-v3.mdx
  • docs/src/content/docs/reference/cli.mdx
  • v3/cmd/wails3/main.go
  • v3/internal/commands/migrate.go
  • v3/internal/commands/migrate_test.go
  • v3/internal/flags/migrate.go
  • v3/internal/migrate/advisor.go
  • v3/internal/migrate/copy.go
  • v3/internal/migrate/frontend.go
  • v3/internal/migrate/gomod.go
  • v3/internal/migrate/maingen.go
  • v3/internal/migrate/mapping.go
  • v3/internal/migrate/migrate.go
  • v3/internal/migrate/migrate_test.go
  • v3/internal/migrate/parse.go
  • v3/internal/migrate/report.go
  • v3/internal/migrate/wailsjson.go

Comment thread docs/src/content/docs/guides/cli.mdx Outdated
Comment thread v3/internal/commands/migrate.go
Comment thread v3/internal/commands/migrate.go
Comment thread v3/internal/migrate/advisor.go
Comment thread v3/internal/migrate/advisor.go Outdated
Comment thread v3/internal/migrate/migrate_test.go Outdated
Comment thread v3/internal/migrate/parse.go
Comment thread v3/internal/migrate/parse.go Outdated
Comment thread v3/internal/migrate/parse.go Outdated
Comment thread v3/internal/migrate/report.go
@taliesin-ai taliesin-ai added v3 and removed v3-alpha labels Aug 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
v3/internal/migrate/mapping.go (1)

297-310: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use setField for the remaining fields that two v2 options can set.

Line 299 and Line 306 both append StartState to out.Win. A v2 project that sets Fullscreen: true and WindowStartState produces the key twice. The same applies to ContentProtectionEnabled, which Windows.ContentProtection (Line 532) and Mac.ContentProtection (Line 585) both carry into out.Win.

writeFields in v3/internal/migrate/maingen.go now drops the duplicate, so the generated file still compiles. The earlier value wins silently, and no report note is produced. Route these appends through setField so the later value wins and the overwrite is reported.

♻️ Proposed change
 	case "Fullscreen":
 		if isTrue(value) {
-			out.Win = append(out.Win, GenField{"StartState", "application.WindowStateFullscreen"})
+			m.setField(&out.Win, GenField{"StartState", "application.WindowStateFullscreen"})
 			m.proj.Report.Mapped("options.App.Fullscreen", "WebviewWindowOptions.StartState")
 		} else if ident, ok := value.(*ast.Ident); !ok || ident.Name != "false" {
 			m.manual("Fullscreen", "Non-constant Fullscreen value; set `WebviewWindowOptions.StartState` to `application.WindowStateFullscreen` as needed.")
 		}
 	case "WindowStartState":
 		if v3 := selectorConst(value, windowStartStates); v3 != "" {
-			out.Win = append(out.Win, GenField{"StartState", v3})
+			m.setField(&out.Win, GenField{"StartState", v3})

carry must apply the same set semantics for ContentProtectionEnabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/mapping.go` around lines 297 - 310, Replace the direct
StartState appends in the Fullscreen and WindowStartState cases with setField
calls so later values overwrite earlier ones and the overwrite is reported.
Apply the same set semantics to ContentProtectionEnabled handling in carry,
covering both Windows.ContentProtection and Mac.ContentProtection mappings.
v3/internal/migrate/parse.go (2)

62-87: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Record files excluded by build constraints in the report.

build.Default uses the host GOOS/GOARCH. On Linux, files such as app_windows.go do not match, so their bound types and exported methods are dropped silently. The generated project then misses services or method shims without any warning.

Add a report note for each skipped file so users can port the platform-specific code manually.

♻️ Proposed change
 		if !matched {
+			proj.Report.Note("Skipped " + path + ": build constraints exclude it for " + buildContext.GOOS + "/" + buildContext.GOARCH + ". Review it manually if it declares bound types or methods.")
 			return nil
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/parse.go` around lines 62 - 87, Update the file-walking
logic around buildContext.MatchFile to record a report note whenever a Go source
file is excluded by build constraints (matched is false). Include the skipped
file path and indicate that its platform-specific code may need manual porting,
while preserving the existing behavior of ignoring the file.

299-307: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove findRunCall.

findRunCall has no callers outside its definition, so keep findRunCalls direct and remove the unused wrapper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/parse.go` around lines 299 - 307, Remove the unused
findRunCall compatibility wrapper and retain findRunCalls as the direct
candidate-discovery function. Ensure no references or wrapper-specific handling
remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/internal/commands/migrate_test.go`:
- Around line 260-287: Extend the test after YAML unmarshalling in the
appendAssociations flow to inspect the emitted file-association entry and assert
that its Ext, Name, Description, IconName, and Role values match the configured
V2FileAssociation, including special scalar characters. Keep the existing
protocol assertion unchanged.

In `@v3/internal/migrate/maingen.go`:
- Around line 100-112: Update defaultImportName to safely handle paths without a
slash, avoiding parent slicing when the separator is absent. Normalize both
trailing /vN path elements and .vN suffixes such as gopkg.in/yaml.v3, and return
a conservative import name when derivation is uncertain so pruneAndFormat does
not remove potentially used imports.
- Around line 202-208: Update the RunIf generation branch to emit the assignment
token recorded in main.RunIf.AssignTok instead of always using :=, preserving =
for previously declared error variables and := for declarations. Keep the
existing condition and body generation unchanged.

In `@v3/internal/migrate/migrate_test.go`:
- Line 409: Update the generated disabled_windows.go fixture in the
ParseV2Project test to use an unsatisfiable build constraint such as windows &&
linux, ensuring it is excluded on every host while preserving the fixture’s
purpose.

In `@v3/internal/migrate/parse.go`:
- Around line 533-543: Update enclosingFunc to check that fn.Body is non-nil
before accessing its position bounds, while preserving the existing target
containment logic for declarations with bodies.

---

Nitpick comments:
In `@v3/internal/migrate/mapping.go`:
- Around line 297-310: Replace the direct StartState appends in the Fullscreen
and WindowStartState cases with setField calls so later values overwrite earlier
ones and the overwrite is reported. Apply the same set semantics to
ContentProtectionEnabled handling in carry, covering both
Windows.ContentProtection and Mac.ContentProtection mappings.

In `@v3/internal/migrate/parse.go`:
- Around line 62-87: Update the file-walking logic around buildContext.MatchFile
to record a report note whenever a Go source file is excluded by build
constraints (matched is false). Include the skipped file path and indicate that
its platform-specific code may need manual porting, while preserving the
existing behavior of ignoring the file.
- Around line 299-307: Remove the unused findRunCall compatibility wrapper and
retain findRunCalls as the direct candidate-discovery function. Ensure no
references or wrapper-specific handling remain.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e356003b-394d-4e1e-9d15-230830c91e0a

📥 Commits

Reviewing files that changed from the base of the PR and between 3495d3a and 22ef7f8.

📒 Files selected for processing (13)
  • IMPLEMENTATION.md
  • docs/src/content/docs/guides/cli.mdx
  • v3/internal/commands/migrate.go
  • v3/internal/commands/migrate_test.go
  • v3/internal/migrate/advisor.go
  • v3/internal/migrate/frontend.go
  • v3/internal/migrate/gomod.go
  • v3/internal/migrate/maingen.go
  • v3/internal/migrate/mapping.go
  • v3/internal/migrate/migrate.go
  • v3/internal/migrate/migrate_test.go
  • v3/internal/migrate/parse.go
  • v3/internal/migrate/report.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • docs/src/content/docs/guides/cli.mdx
  • v3/internal/migrate/frontend.go
  • v3/internal/migrate/report.go
  • v3/internal/migrate/advisor.go
  • v3/internal/migrate/migrate.go
  • v3/internal/commands/migrate.go

Comment on lines +260 to +287
proj := &V2ProjectAlias{Config: migrate.V2Config{}, Report: migrate.NewReport()}
proj.Config.Info.FileAssociations = []migrate.V2FileAssociation{{
Ext: `a:b#c`,
Name: `- app`,
Description: `say "hello"`,
IconName: `icon`,
Role: `Editor`,
}}
proj.Config.Info.Protocols = []migrate.V2Protocol{{Scheme: `my:scheme#1`}}
if err := appendAssociations(proj, outDir); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
var config map[string]any
if err := yaml.Unmarshal(data, &config); err != nil {
t.Fatalf("generated config.yml is invalid: %v\n%s", err, data)
}
protocols, ok := config["protocols"].([]any)
if !ok || len(protocols) != 1 {
t.Fatalf("protocols were not emitted: %#v", config["protocols"])
}
protocol, ok := protocols[0].(map[string]any)
if !ok || protocol["scheme"] != "my:scheme#1" {
t.Fatalf("protocol scalar changed during YAML round-trip: %#v", protocols[0])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert file-association values after YAML parsing.

This test only checks that YAML parses and that protocols[0].scheme is unchanged. It does not verify the file-association values that contain the special YAML scalar characters. A change that alters Ext, Name, or Description can pass this test.

Proposed test expansion
 	if err := yaml.Unmarshal(data, &config); err != nil {
 		t.Fatalf("generated config.yml is invalid: %v\n%s", err, data)
 	}
+	associations, ok := config["fileAssociations"].([]any)
+	if !ok || len(associations) != 1 {
+		t.Fatalf("file associations were not emitted: %#v", config["fileAssociations"])
+	}
+	association, ok := associations[0].(map[string]any)
+	if !ok ||
+		association["ext"] != "a:b#c" ||
+		association["name"] != "- app" ||
+		association["description"] != `say "hello"` {
+		t.Fatalf("file-association scalar changed during YAML round-trip: %#v", associations[0])
+	}
 	protocols, ok := config["protocols"].([]any)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
proj := &V2ProjectAlias{Config: migrate.V2Config{}, Report: migrate.NewReport()}
proj.Config.Info.FileAssociations = []migrate.V2FileAssociation{{
Ext: `a:b#c`,
Name: `- app`,
Description: `say "hello"`,
IconName: `icon`,
Role: `Editor`,
}}
proj.Config.Info.Protocols = []migrate.V2Protocol{{Scheme: `my:scheme#1`}}
if err := appendAssociations(proj, outDir); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
var config map[string]any
if err := yaml.Unmarshal(data, &config); err != nil {
t.Fatalf("generated config.yml is invalid: %v\n%s", err, data)
}
protocols, ok := config["protocols"].([]any)
if !ok || len(protocols) != 1 {
t.Fatalf("protocols were not emitted: %#v", config["protocols"])
}
protocol, ok := protocols[0].(map[string]any)
if !ok || protocol["scheme"] != "my:scheme#1" {
t.Fatalf("protocol scalar changed during YAML round-trip: %#v", protocols[0])
}
proj := &V2ProjectAlias{Config: migrate.V2Config{}, Report: migrate.NewReport()}
proj.Config.Info.FileAssociations = []migrate.V2FileAssociation{{
Ext: `a:b#c`,
Name: `- app`,
Description: `say "hello"`,
IconName: `icon`,
Role: `Editor`,
}}
proj.Config.Info.Protocols = []migrate.V2Protocol{{Scheme: `my:scheme#1`}}
if err := appendAssociations(proj, outDir); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
var config map[string]any
if err := yaml.Unmarshal(data, &config); err != nil {
t.Fatalf("generated config.yml is invalid: %v\n%s", err, data)
}
associations, ok := config["fileAssociations"].([]any)
if !ok || len(associations) != 1 {
t.Fatalf("file associations were not emitted: %#v", config["fileAssociations"])
}
association, ok := associations[0].(map[string]any)
if !ok ||
association["ext"] != "a:b#c" ||
association["name"] != "- app" ||
association["description"] != `say "hello"` {
t.Fatalf("file-association scalar changed during YAML round-trip: %#v", associations[0])
}
protocols, ok := config["protocols"].([]any)
if !ok || len(protocols) != 1 {
t.Fatalf("protocols were not emitted: %#v", config["protocols"])
}
protocol, ok := protocols[0].(map[string]any)
if !ok || protocol["scheme"] != "my:scheme#1" {
t.Fatalf("protocol scalar changed during YAML round-trip: %#v", protocols[0])
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/commands/migrate_test.go` around lines 260 - 287, Extend the test
after YAML unmarshalling in the appendAssociations flow to inspect the emitted
file-association entry and assert that its Ext, Name, Description, IconName, and
Role values match the configured V2FileAssociation, including special scalar
characters. Keep the existing protocol assertion unchanged.

Comment on lines +100 to +112
// defaultImportName returns the conventional local name for an import path.
// Go module major-version suffixes are part of the module path, not normally
// part of the package name (for example, /foo/v2 is usually imported as foo).
func defaultImportName(path string) string {
name := path[strings.LastIndex(path, "/")+1:]
if len(name) > 1 && name[0] == 'v' {
if _, err := strconv.Atoi(name[1:]); err == nil {
parent := path[:strings.LastIndex(path, "/")]
name = parent[strings.LastIndex(parent, "/")+1:]
}
}
return name
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

defaultImportName can panic and can mis-name gopkg.in imports.

Two problems exist:

  1. If path contains no /, strings.LastIndex(path, "/") returns -1. For a path such as "v2", the code reaches Line 107 and evaluates path[:-1], which panics with a slice bounds error.
  2. The heuristic only strips a trailing /vN element. For gopkg.in/yaml.v3 it returns yaml.v3, but the package name is yaml. pruneAndFormat at Line 310 compares that value against the selector names collected at Line 284, so it treats the used import as unused and deletes it. The generated main.go then fails to compile.

Guard the missing separator. Also strip a .vN suffix, and prefer keeping an import when the name cannot be derived with confidence.

🛡️ Proposed fix
 func defaultImportName(path string) string {
-	name := path[strings.LastIndex(path, "/")+1:]
-	if len(name) > 1 && name[0] == 'v' {
-		if _, err := strconv.Atoi(name[1:]); err == nil {
-			parent := path[:strings.LastIndex(path, "/")]
-			name = parent[strings.LastIndex(parent, "/")+1:]
-		}
-	}
-	return name
+	slash := strings.LastIndex(path, "/")
+	name := path[slash+1:]
+	if slash >= 0 && len(name) > 1 && name[0] == 'v' {
+		if _, err := strconv.Atoi(name[1:]); err == nil {
+			parent := path[:slash]
+			name = parent[strings.LastIndex(parent, "/")+1:]
+		}
+	}
+	// gopkg.in/yaml.v3 -> yaml
+	if dot := strings.LastIndex(name, "."); dot > 0 {
+		if _, err := strconv.Atoi(strings.TrimPrefix(name[dot+1:], "v")); err == nil {
+			name = name[:dot]
+		}
+	}
+	return name
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/maingen.go` around lines 100 - 112, Update
defaultImportName to safely handle paths without a slash, avoiding parent
slicing when the separator is absent. Normalize both trailing /vN path elements
and .vN suffixes such as gopkg.in/yaml.v3, and return a conservative import name
when derivation is uncertain so pruneAndFormat does not remove potentially used
imports.

Comment on lines +202 to +208
case main.RunIf != nil:
sb.WriteString("if " + main.ErrIdent + " := " + appVar + ".Run(); " + main.ErrIdent + " != nil ")
sb.WriteString(exprText(main.Fset, main.Source, main.RunIf.Body))
if main.RunIf.Else != nil {
sb.WriteString(" else ")
sb.WriteString(exprText(main.Fset, main.Source, main.RunIf.Else))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the assignment token in the if form.

parse.go records AssignTok for the if form as well. This branch always emits :=. If the original code used if err = wails.Run(...); err != nil with err declared earlier, the generated := shadows that variable, and the outer declaration can become unused. The migrated project then fails to compile.

♻️ Proposed change
 	case main.RunIf != nil:
-		sb.WriteString("if " + main.ErrIdent + " := " + appVar + ".Run(); " + main.ErrIdent + " != nil ")
+		assign := ":="
+		if main.AssignTok == token.ASSIGN {
+			assign = "="
+		}
+		sb.WriteString("if " + main.ErrIdent + " " + assign + " " + appVar + ".Run(); " + main.ErrIdent + " != nil ")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case main.RunIf != nil:
sb.WriteString("if " + main.ErrIdent + " := " + appVar + ".Run(); " + main.ErrIdent + " != nil ")
sb.WriteString(exprText(main.Fset, main.Source, main.RunIf.Body))
if main.RunIf.Else != nil {
sb.WriteString(" else ")
sb.WriteString(exprText(main.Fset, main.Source, main.RunIf.Else))
}
case main.RunIf != nil:
assign := ":="
if main.AssignTok == token.ASSIGN {
assign = "="
}
sb.WriteString("if " + main.ErrIdent + " " + assign + " " + appVar + ".Run(); " + main.ErrIdent + " != nil ")
sb.WriteString(exprText(main.Fset, main.Source, main.RunIf.Body))
if main.RunIf.Else != nil {
sb.WriteString(" else ")
sb.WriteString(exprText(main.Fset, main.Source, main.RunIf.Else))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/maingen.go` around lines 202 - 208, Update the RunIf
generation branch to emit the assignment token recorded in main.RunIf.AssignTok
instead of always using :=, preserving = for previously declared error variables
and := for declarations. Keep the existing condition and body generation
unchanged.


func TestParseHonoursBuildConstraintsAndTestdata(t *testing.T) {
dir := writeFixture(t)
if err := os.WriteFile(filepath.Join(dir, "disabled_windows.go"), []byte("//go:build windows\n\npackage main\n\nimport \"github.com/wailsapp/wails/v2\"\n\nfunc disabled() { wails.Run(nil) }\n"), 0o644); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm this test still uses a host-satisfiable Windows-only constraint.
sed -n '407,422p' v3/internal/migrate/migrate_test.go

# Validate this test on a Windows CI runner after changing the constraint.

Repository: wailsapp/wails

Length of output: 822


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant implementation references:"
rg -n "ParseV2Project|BuildConstraints|build.Default|Parse.*Project|multiple-run|WAILS_RUN_ONCE|wails\.Run" v3 v2 -g '*.go' | head -200

echo
echo "Build constraint package availability:"
go env GOROOT GOMODCACHE
find "$(go env GOROOT)/src" -path '*compile/build.go' -o -path '*importer/import.go' -o -path '*go/parser/parser.go' | head -50

Repository: wailsapp/wails

Length of output: 5927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate relevant implementation references:"
rg -n "func ParseV2Project|BuildConstraints|build.Default|Parse.*Project|multiple-run|WAILS_RUN_ONCE|wails\.Run" v3 v2 -g '*.go' | head -200

echo
echo "Check buildtag behavior in available toolchain, if present:"
go env GOROOT GOMODCACHE GOROOTBOOTSTRAP 2>/dev/null || true

Repository: wailsapp/wails

Length of output: 5918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "ParseV2Project implementation:"
sed -n '15,125p' v3/internal/migrate/parse.go

echo
echo "Build tag matcher behavior probe:"
python3 - <<'PY'
lines = open("/usr/local/go/src/go/build/build.go",{r}).read()
print("// build.Default host-OS assignment:" , 'GOOS: goos,\n' in lines)
print("// build.Default host-arch assignment:" , 'GOARCH: goarch,\n' in lines)
print("// build.Default GOEXPERIMENT assignment (if present):", 'GOEXPERIMENT: os.Getenv("GOEXPERIMENT"),\n' in lines)

# Use the build package when it is installed, without running repo code.
PY

GOOS=windows GOARCH=amd64 go tool compile -S -e -p test -I "$PWD" /dev/stdin 2>&1 | head -20 || true
GOOS=linux GOARCH=amd64 go env -w GOEXPERIMENT=xyz GOEXPERIMENT=abc
GOOS=windows GOARCH=amd64 /usr/local/go/bin/go env -w GOEXPERIMENT=xyz GOEXPERIMENT=abc
go run -gcflags=all=-d=go/build/constraints=1 -- <(printf 'package main\nvar _ = "check"\n') 2>&1 || true

echo
echo "Check Go version/toolchain packages:"
/usr/local/go/bin/go version
ls /usr/local/go/pkg/tool/linux_amd64/go 2>/dev/null || ls /usr/local/go/pkg/tool/linux_amd64 -S | head -5

Repository: wailsapp/wails

Length of output: 3509


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "MatchFile and build tag constraint evaluation:"
rg -n "func .*MatchFile|MatchFile|checkConstraints|go:build|tags" "$GOROOT/src/go/build*" "$GOROOT/src/go/parser/parser.go" 2>/dev/null | head -120

echo
echo "Cross-host match probes with env-specific go/build metadata if available:"
# Run the Go command itself so the probe uses vendor/constraint handling without repo code.
cat > /tmp/build_constraint_probe.go <<'GO'
package main

import (
	"go/build"
	"os"
	"path/filepath"
)

func main() {
	dir := os.TempDir()
	if filepath.Base(dir) == "proc" || filepath.Base(dir) == "dev" {
		dir = "/tmp"
	}
	path := filepath.Join(dir, "disabled_windows.go")
	file := []byte("//go:build windows\n\npackage main\n\nfunc main() {}\n")
	_ = os.WriteFile(path, file, 0o644)
	defer os.Remove(path)

	for _, e := range []struct{ GOOS, GOARCH string }{
		{"", ""},
		{"windows", "amd64"},
		{"linux", "amd64"},
		{"darwin", "arm64"},
	} {
		ctx := build.Default
		if e.GOOS != "" {
			ctx.GOOS = e.GOOS
			ctx.GOARCH = e.GOARCH
		}
		var env string
		if e.GOOS != "" {
			env = "GOOS=" + e.GOOS + " GOARCH=" + e.GOARCH
		} else {
			env = "current host"
		}
		match, err := ctx.MatchFile(dir, "disabled_windows.go")
		var fail string
		if err != nil {
			fail = "ERR=" + err.Error()
		}
		println(env, "MatchFile=", strconv.FormatBool(match), fail)
	}
}
import "strconv"
GO

echo "Probe source has syntax error intentionally? No; check local tooling can build standalone source:"
go tool compile -o /tmp/parsevc /tmp/build_constraint_probe.go 2>&1 || true

echo
echo "Inspect build package source snippets:"
sed -n '1,120p' "$GOROOT/src/go/build/build.go"
sed -n '120,240p' "$GOROOT/src/go/build/build.go"
sed -n '1120,1240p' "$GOROOT/src/go/build/build.go" 2>/dev/null || true

Repository: wailsapp/wails

Length of output: 419


Make the excluded fixture unsatisfiable on every host.

//go:build windows still matches the test harness on Windows, so ParseV2Project parses disabled_windows.go, discovers the second wails.Run, and fails with the multiple-run error. Use an unsatisfiable constraint, such as windows && linux.

Proposed fix
-//go:build windows
+//go:build windows && linux
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/migrate_test.go` at line 409, Update the generated
disabled_windows.go fixture in the ParseV2Project test to use an unsatisfiable
build constraint such as windows && linux, ensuring it is excluded on every host
while preserving the fixture’s purpose.

Comment on lines +533 to +543
func enclosingFunc(file *ast.File, target token.Pos) *ast.FuncDecl {
var result *ast.FuncDecl
ast.Inspect(file, func(node ast.Node) bool {
fn, ok := node.(*ast.FuncDecl)
if ok && fn.Body.Pos() <= target && target <= fn.Body.End() {
result = fn
}
return true
})
return result
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Guard against a nil fn.Body in enclosingFunc.

A *ast.FuncDecl has a nil Body when the function is declared without one, for example an assembly-implemented or //go:linkname stub. Line 537 dereferences fn.Body for every FuncDecl in the file, so parsing such a project panics.

🛡️ Proposed fix
 		fn, ok := node.(*ast.FuncDecl)
-		if ok && fn.Body.Pos() <= target && target <= fn.Body.End() {
+		if ok && fn.Body != nil && fn.Body.Pos() <= target && target <= fn.Body.End() {
 			result = fn
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func enclosingFunc(file *ast.File, target token.Pos) *ast.FuncDecl {
var result *ast.FuncDecl
ast.Inspect(file, func(node ast.Node) bool {
fn, ok := node.(*ast.FuncDecl)
if ok && fn.Body.Pos() <= target && target <= fn.Body.End() {
result = fn
}
return true
})
return result
}
func enclosingFunc(file *ast.File, target token.Pos) *ast.FuncDecl {
var result *ast.FuncDecl
ast.Inspect(file, func(node ast.Node) bool {
fn, ok := node.(*ast.FuncDecl)
if ok && fn.Body != nil && fn.Body.Pos() <= target && target <= fn.Body.End() {
result = fn
}
return true
})
return result
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/migrate/parse.go` around lines 533 - 543, Update enclosingFunc to
check that fn.Body is non-nil before accessing its position bounds, while
preserving the existing target containment logic for declarations with bodies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Documentation Improvements or additions to documentation v3

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants