package main import ( "bufio" "fmt" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/Masterminds/semver" "github.com/leaanthony/spinner" "github.com/wailsapp/wails/cmd" ) // Constants var checkSpinner = spinner.NewSpinner() var migrateProjectOptions = &cmd.ProjectOptions{} var migrateFS = cmd.NewFSHelper() var migrateGithub = cmd.NewGitHubHelper() var programHelper = cmd.NewProgramHelper() var lessThanV1 *semver.Constraints // The user's go.mod var goMod string var goModFile string // The user's main.js var mainJSFile string var mainJSContents string // Frontend directory var frontEndDir string func init() { var dryrun bool var err error lessThanV1, err = semver.NewConstraint("< v1.0.0") if err != nil { log.Fatal(err) } // var forceRebuild = false checkSpinner.SetSpinSpeed(50) commandDescription := `EXPERIMENTAL - This command attempts to migrate projects to the latest Wails version.` updateCmd := app.Command("migrate", "Migrate projects to latest Wails release"). LongDescription(commandDescription). BoolFlag("dryrun", "Only display what would be done", &dryrun) updateCmd.Action(func() error { message := "Migrate Project" logger.PrintSmallBanner(message) logger.Red("WARNING: This is an experimental command. Ensure you have backups of your project!") logger.Red("It currently only supports npm based projects.") fmt.Println() // Check project directory err := checkProjectDirectory() if err != nil { return err } // Find Wails version from go.mod wailsVersion, err := getWailsVersion() if err != nil { return err } // Get latest stable version var latestVersion *semver.Version latestVersion, err = getLatestWailsVersion() if err != nil { return err } var canMigrate bool canMigrate, err = canMigrateVersion(wailsVersion, latestVersion) if err != nil { return err } if !canMigrate { return nil } // Check for wailsbridge wailsBridge, err := checkWailsBridge() if err != nil { return err } // Is main.js using bridge.Init() canUpdateMainJS, err := checkMainJS() if err != nil { return err } // TODO: Check if we are using legacy js runtime // Operations logger.Yellow("Operations to perform:") logger.Yellowf(" - Update to Wails v%s\n", latestVersion) if len(wailsBridge) > 0 { logger.Yellow(" - Delete wailsbridge.js") } if canUpdateMainJS { logger.Yellow(" - Patch main.js") } logger.Yellow(" - Ensure '@wailsapp/runtime` module is installed") if dryrun { logger.White("Exiting: Dry Run") return nil } logger.Red("*WARNING* About to modify your project!") logger.Red("Type 'YES' to continue: ") scanner := bufio.NewScanner(os.Stdin) scanner.Scan() input := scanner.Text() if input != "YES" { logger.Red("ABORTED!") return nil } logger.Yellow("Let's do this!") err = updateWailsVersion(wailsVersion, latestVersion) if err != nil { return err } if len(wailsBridge) > 0 { err = deleteWailsBridge(wailsBridge) if err != nil { return err } } if canUpdateMainJS { err = patchMainJS() if err != nil { return err } } // Install runtime err = installWailsRuntime() if err != nil { return err } fmt.Println() logger.Yellow("Migration complete! Check project by running `wails build`.") return nil }) } func checkProjectDirectory() error { // Get versions checkSpinner.Start("Check Project Directory") // Check we are in project directory err := migrateProjectOptions.LoadConfig(migrateFS.Cwd()) if err != nil { checkSpinner.Error() return fmt.Errorf("Unable to find 'project.json'. Please check you are in a Wails project directory") } checkSpinner.Success() return nil } func getWailsVersion() (*semver.Version, error) { checkSpinner.Start("Get Wails Version") result, err := cmd.GetWailsVersion() if err != nil { checkSpinner.Error(err.Error()) return nil, err } return result, nil } func canMigrateVersion(wailsVersion *semver.Version, latestVersion *semver.Version) (bool, error) { checkSpinner.Start("Checking ability to Migrate") // Check if we are at the latest version!!!! if wailsVersion.Equal(latestVersion) || wailsVersion.GreaterThan(latestVersion) { checkSpinner.Errorf("Checking ability to Migrate: No! (v%s >= v%s)", wailsVersion, latestVersion) return false, nil } // Check for < v1.0.0 if lessThanV1.Check(wailsVersion) { checkSpinner.Successf("Checking ability to Migrate: Yes! (v%s < v1.0.0)", wailsVersion) return true, nil } checkSpinner.Error("Unable to migrate") return false, fmt.Errorf("No migration rules for version %s", wailsVersion) } func checkWailsBridge() (string, error) { checkSpinner.Start("Checking if legacy Wails Bridge present") // Check frontend dir is available if migrateProjectOptions.FrontEnd == nil || len(migrateProjectOptions.FrontEnd.Dir) == 0 || !migrateFS.DirExists(migrateProjectOptions.FrontEnd.Dir) { checkSpinner.Error("Unable to determine frontend directory") return "", fmt.Errorf("Unable to determine frontend directory") } frontEndDir = migrateProjectOptions.FrontEnd.Dir wailsBridgePath, err := filepath.Abs(filepath.Join(".", frontEndDir, "src", "wailsbridge.js")) if err != nil { checkSpinner.Error(err.Error()) return "", err } // If it doesn't exist, return blank string if !migrateFS.FileExists(wailsBridgePath) { checkSpinner.Success("Checking if legacy Wails Bridge present: No") return "", nil } checkSpinner.Success("Checking if legacy Wails Bridge present: Yes") return wailsBridgePath, nil } // This function determines if the main.js file using wailsbridge can be auto-updated func checkMainJS() (bool, error) { checkSpinner.Start("Checking if main.js can be migrated") var err error // Check main.js is there if migrateProjectOptions.FrontEnd == nil || len(migrateProjectOptions.FrontEnd.Dir) == 0 || !migrateFS.DirExists(migrateProjectOptions.FrontEnd.Dir) { checkSpinner.Error("Unable to determine frontend directory") return false, fmt.Errorf("Unable to determine frontend directory") } frontEndDir = migrateProjectOptions.FrontEnd.Dir mainJSFile, err = filepath.Abs(filepath.Join(".", frontEndDir, "src", "main.js")) if err != nil { checkSpinner.Error("Unable to find main.js") return false, err } mainJSContents, err = migrateFS.LoadAsString(mainJSFile) if err != nil { checkSpinner.Error("Unable to load main.js") return false, err } // Check we have a line like: import Bridge from "./wailsbridge"; if strings.Index(mainJSContents, `import Bridge from "./wailsbridge";`) == -1 { checkSpinner.Success("Checking if main.js can be migrated: No - Cannot find `import Bridge`") return false, nil } // Check we have a line like: Bridge.Start(() => { if strings.Index(mainJSContents, `Bridge.Start(`) == -1 { checkSpinner.Success("Checking if main.js can be migrated: No - Cannot find `Bridge.Start`") return false, nil } checkSpinner.Success("Checking if main.js can be migrated: Yes") return true, nil } func getLatestWailsVersion() (*semver.Version, error) { checkSpinner.Start("Checking GitHub for latest Wails version") version, err := migrateGithub.GetLatestStableRelease() if err != nil { checkSpinner.Error("Checking GitHub for latest Wails version: Failed") return nil, err } checkSpinner.Successf("Checking GitHub for latest Wails version: v%s", version) return version.Version, nil } func updateWailsVersion(currentVersion, latestVersion *semver.Version) error { // Patch go.mod checkSpinner.Start("Patching go.mod") wailsModule := "github.com/wailsapp/wails" old := fmt.Sprintf("%s v%s", wailsModule, currentVersion) new := fmt.Sprintf("%s v%s", wailsModule, latestVersion) goMod = strings.Replace(goMod, old, new, -1) err := ioutil.WriteFile(goModFile, []byte(goMod), 0600) if err != nil { checkSpinner.Error() return err } checkSpinner.Success() return nil } func deleteWailsBridge(bridgeFilename string) error { // Patch go.mod checkSpinner.Start("Delete legacy wailsbridge.js") err := migrateFS.RemoveFile(bridgeFilename) if err != nil { checkSpinner.Error() return err } checkSpinner.Success() return nil } func patchMainJS() error { // Patch main.js checkSpinner.Start("Patching main.js") // Patch import line oldImportLine := `import Bridge from "./wailsbridge";` newImportLine := `import * as Wails from "@wailsapp/runtime";` mainJSContents = strings.Replace(mainJSContents, oldImportLine, newImportLine, -1) // Patch Start line oldStartLine := `Bridge.Start` newStartLine := `Wails.Init` mainJSContents = strings.Replace(mainJSContents, oldStartLine, newStartLine, -1) err := ioutil.WriteFile(mainJSFile, []byte(mainJSContents), 0600) if err != nil { checkSpinner.Error() return err } checkSpinner.Success() return nil } func installWailsRuntime() error { checkSpinner.Start("Installing @wailsapp/runtime module") // Change to the frontend directory err := os.Chdir(frontEndDir) if err != nil { checkSpinner.Error() return nil } // Determine package manager packageManager, err := migrateProjectOptions.GetNPMBinaryName() if err != nil { checkSpinner.Error() return nil } switch packageManager { case cmd.NPM: // npm install --save @wailsapp/runtime programHelper.InstallNPMPackage("@wailsapp/runtime", true) default: checkSpinner.Error() return fmt.Errorf("Unknown package manager") } checkSpinner.Success() return nil }