github.com/pojntfx/hydrapp/hydrapp@v0.0.0-20240516002902-d08759d6ca9f/cmd/new.go (about) 1 package cmd 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "net/http" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/manifoldco/promptui" 17 "github.com/pojntfx/hydrapp/hydrapp/pkg/config" 18 "github.com/pojntfx/hydrapp/hydrapp/pkg/generators" 19 "github.com/pojntfx/hydrapp/hydrapp/pkg/renderers" 20 "github.com/pojntfx/hydrapp/hydrapp/pkg/renderers/rpm" 21 "github.com/pojntfx/hydrapp/hydrapp/pkg/utils" 22 "github.com/spf13/cobra" 23 "github.com/spf13/viper" 24 "gopkg.in/yaml.v2" 25 ) 26 27 const ( 28 noNetworkFlag = "no-network" 29 30 vanillaJSRESTKey = "vanillajs-rest" 31 vanillaJSFormsKey = "vanillajs-forms" 32 reactPanrpcKey = "react-panrpc" 33 ) 34 35 var ( 36 errUnknownProjectType = errors.New("unknown project type") 37 38 projectTypeItems = []generators.ProjectTypeOption{ 39 { 40 Name: vanillaJSRESTKey, 41 Description: "Simple starter project with a REST API to connect the Vanilla JS frontend and backend", 42 }, 43 { 44 Name: vanillaJSFormsKey, 45 Description: "Traditional starter project with Web 1.0-style forms to connect the Vanilla JS frontend and backend", 46 }, 47 { 48 Name: reactPanrpcKey, 49 Description: "Complete starter project with panrpc RPCs to connect the React frontend and backend", 50 }, 51 } 52 ) 53 54 var newCmd = &cobra.Command{ 55 Use: "new", 56 Aliases: []string{"n"}, 57 Short: "Generate a new hydrapp project", 58 RunE: func(cmd *cobra.Command, args []string) error { 59 if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 60 return err 61 } 62 63 projectTypeIndex, _, err := (&promptui.Select{ 64 Templates: &promptui.SelectTemplates{ 65 Label: fmt.Sprintf("%s {{.}}: ", promptui.IconInitial), 66 Active: fmt.Sprintf("%s {{ .Name | underline }}: {{ .Description | faint }}", promptui.IconSelect), 67 Inactive: " {{ .Name }}: {{ .Description | faint }}", 68 Selected: fmt.Sprintf(`{{ "%s" | green }} {{ .Name | faint }}: {{ .Description | faint }}`, promptui.IconGood), 69 }, 70 Label: "Which project type do you want to generate?", 71 Items: projectTypeItems, 72 }).Run() 73 if err != nil { 74 return err 75 } 76 77 appID, err := (&promptui.Prompt{ 78 Label: "App ID in reverse domain notation", 79 Default: "com.github.example.myapp", 80 }).Run() 81 if err != nil { 82 return err 83 } 84 85 appName, err := (&promptui.Prompt{ 86 Label: "App name", 87 Default: "My App", 88 }).Run() 89 if err != nil { 90 return err 91 } 92 93 appSummary, err := (&promptui.Prompt{ 94 Label: "App summary", 95 Default: "My first app", 96 }).Run() 97 if err != nil { 98 return err 99 } 100 101 appDescription, err := (&promptui.Prompt{ 102 Label: "App description", 103 Default: "My first application, built with hydrapp.", 104 }).Run() 105 if err != nil { 106 return err 107 } 108 109 appHomepage, err := (&promptui.Prompt{ 110 Label: "App homepage", 111 Default: "https://github.com/example/myapp", 112 }).Run() 113 if err != nil { 114 return err 115 } 116 117 appGit, err := (&promptui.Prompt{ 118 Label: "App git repo", 119 Default: appHomepage + ".git", 120 }).Run() 121 if err != nil { 122 return err 123 } 124 125 appBaseurl, err := (&promptui.Prompt{ 126 Label: "App base URL to expect the built assets to be published to", 127 Default: "https://example.github.io/myapp/", 128 }).Run() 129 if err != nil { 130 return err 131 } 132 133 goMod, err := (&promptui.Prompt{ 134 Label: "Go module name", 135 Default: "github.com/example/myapp", 136 }).Run() 137 if err != nil { 138 return err 139 } 140 141 licenseSPDX, err := (&promptui.Prompt{ 142 Label: "License SPDX identifier (see https://spdx.org/licenses/)", 143 Default: "Apache-2.0", 144 }).Run() 145 if err != nil { 146 return err 147 } 148 149 releaseAuthor, err := (&promptui.Prompt{ 150 Label: "Release author name", 151 Default: "Jean Doe", 152 }).Run() 153 if err != nil { 154 return err 155 } 156 157 releaseEmail, err := (&promptui.Prompt{ 158 Label: "Release author email", 159 Default: "jean.doe@example.com", 160 }).Run() 161 if err != nil { 162 return err 163 } 164 165 dir, err := (&promptui.Prompt{ 166 Label: "Directory to write the app to", 167 Default: "myapp", 168 }).Run() 169 if err != nil { 170 return err 171 } 172 173 _, advancedConfiguration, err := (&promptui.Select{ 174 Label: "Do you want to do any advanced configuration?", 175 Items: []string{"no", "yes"}, 176 }).Run() 177 if err != nil { 178 return err 179 } 180 181 goMain := "." 182 goFlags := "" 183 goGenerate := "go generate ./..." 184 goTests := "go test ./..." 185 goImg := "ghcr.io/pojntfx/hydrapp-build-tests:main" 186 187 debArchitectures := "amd64" 188 flatpakArchitectures := "amd64" 189 msiArchitectures := "amd64" 190 rpmArchitectures := "amd64" 191 192 binariesExclude := "(android/*|ios/*|plan9/*|aix/*|linux/loong64|freebsd/riscv64|wasip1/wasm|js/wasm|openbsd/mips64)" 193 194 if advancedConfiguration == "yes" { 195 goMain, err = (&promptui.Prompt{ 196 Label: "Go main package path", 197 Default: goMain, 198 }).Run() 199 if err != nil { 200 return err 201 } 202 203 goFlags, err = (&promptui.Prompt{ 204 Label: "Go flags to pass to the compiler", 205 Default: goFlags, 206 }).Run() 207 if err != nil { 208 return err 209 } 210 211 goGenerate, err = (&promptui.Prompt{ 212 Label: "Go generate command to run", 213 Default: goGenerate, 214 }).Run() 215 if err != nil { 216 return err 217 } 218 219 goTests, err = (&promptui.Prompt{ 220 Label: "Go test command to run", 221 Default: goTests, 222 }).Run() 223 if err != nil { 224 return err 225 } 226 227 goImg, err = (&promptui.Prompt{ 228 Label: "Go test OCI image to use", 229 Default: goImg, 230 }).Run() 231 if err != nil { 232 return err 233 } 234 235 debArchitectures, err = (&promptui.Prompt{ 236 Label: "DEB architectures to build for (comma-seperated list of GOARCH values)", 237 Default: debArchitectures, 238 }).Run() 239 if err != nil { 240 return err 241 } 242 243 flatpakArchitectures, err = (&promptui.Prompt{ 244 Label: "Flatpak architectures to build for (comma-seperated list of GOARCH values)", 245 Default: flatpakArchitectures, 246 }).Run() 247 if err != nil { 248 return err 249 } 250 251 msiArchitectures, err = (&promptui.Prompt{ 252 Label: "MSI architectures to build for (comma-seperated list of GOARCH values)", 253 Default: msiArchitectures, 254 }).Run() 255 if err != nil { 256 return err 257 } 258 259 rpmArchitectures, err = (&promptui.Prompt{ 260 Label: "RPM architectures to build for (comma-seperated list of GOARCH values)", 261 Default: rpmArchitectures, 262 }).Run() 263 if err != nil { 264 return err 265 } 266 267 binariesExclude, err = (&promptui.Prompt{ 268 Label: "Regex of binaries to exclude from compilation", 269 Default: binariesExclude, 270 }).Run() 271 if err != nil { 272 return err 273 } 274 } 275 276 licenseText := "" 277 if !viper.GetBool(noNetworkFlag) { 278 log.Println("Fetching full license text from SPDX ...") 279 280 res, err := http.Get("https://raw.githubusercontent.com/spdx/license-list-data/main/text/" + licenseSPDX + ".txt") 281 if err != nil { 282 return err 283 } 284 if res.Body != nil { 285 defer res.Body.Close() 286 } 287 if res.StatusCode != http.StatusOK { 288 panic(errors.New(res.Status)) 289 } 290 291 b, err := ioutil.ReadAll(res.Body) 292 if err != nil { 293 return err 294 } 295 296 licenseText = string(b) 297 298 log.Println("Success!") 299 } 300 301 { 302 cfg := config.Root{} 303 cfg.App = config.App{ 304 ID: appID, 305 Name: appName, 306 Summary: appSummary, 307 Description: appDescription, 308 License: licenseSPDX, 309 Homepage: appHomepage, 310 Git: appGit, 311 BaseURL: appBaseurl, 312 } 313 cfg.Go = config.Go{ 314 Main: goMain, 315 Flags: goFlags, 316 Generate: goGenerate, 317 Tests: goTests, 318 Image: goImg, 319 } 320 cfg.Releases = []renderers.Release{ 321 { 322 Version: "0.0.1", 323 Date: time.Now(), 324 Description: "Initial release", 325 Author: releaseAuthor, 326 Email: releaseEmail, 327 }, 328 } 329 330 cfg.APK = config.APK{ 331 Path: "apk", 332 } 333 334 debs := []config.DEB{} 335 for _, arch := range strings.Split(debArchitectures, ",") { 336 debs = append(debs, config.DEB{ 337 Path: path.Join("deb", "debian", "sid", utils.GetArchIdentifier(arch)), 338 OS: "debian", 339 Distro: "sid", 340 Mirrorsite: "http://http.us.debian.org/debian", 341 Components: []string{"main", "contrib"}, 342 Debootstrapopts: "", 343 Architecture: arch, 344 Packages: []rpm.Package{}, 345 }) 346 } 347 cfg.DEB = debs 348 349 cfg.DMG = config.DMG{ 350 Path: "dmg", 351 Packages: []string{}, 352 } 353 354 flatpaks := []config.Flatpak{} 355 for _, arch := range strings.Split(flatpakArchitectures, ",") { 356 flatpaks = append(flatpaks, config.Flatpak{ 357 Path: path.Join("flatpak", utils.GetArchIdentifier(arch)), 358 Architecture: arch, 359 }) 360 } 361 cfg.Flatpak = flatpaks 362 363 msis := []config.MSI{} 364 for _, arch := range strings.Split(msiArchitectures, ",") { 365 msis = append(msis, config.MSI{ 366 Path: path.Join("msi", utils.GetArchIdentifier(arch)), 367 Architecture: arch, 368 Include: `^\\b$`, 369 Packages: []string{}, 370 }) 371 } 372 cfg.MSI = msis 373 374 rpms := []config.RPM{} 375 for _, arch := range strings.Split(rpmArchitectures, ",") { 376 rpms = append(rpms, config.RPM{ 377 Path: path.Join("rpm", "fedora", "40", utils.GetArchIdentifier(arch)), 378 Trailer: "fc40", 379 Distro: "fedora-40", 380 Architecture: arch, 381 Packages: []rpm.Package{}, 382 }) 383 } 384 cfg.RPM = rpms 385 386 cfg.Binaries = config.Binaries{ 387 Path: "binaries", 388 Exclude: binariesExclude, 389 Packages: []string{}, 390 } 391 cfg.Docs = config.Docs{ 392 Path: "docs", 393 } 394 395 b, err := yaml.Marshal(cfg) 396 if err != nil { 397 return err 398 } 399 400 if err := os.MkdirAll(dir, 0755); err != nil { 401 return err 402 } 403 404 if err := ioutil.WriteFile(filepath.Join(dir, "hydrapp.yaml"), b, 0664); err != nil { 405 return err 406 } 407 } 408 409 if err := ioutil.WriteFile(filepath.Join(dir, "icon.png"), generators.IconTpl, 0664); err != nil { 410 return err 411 } 412 413 if err := generators.RenderTemplate( 414 filepath.Join(dir, "go.mod"), 415 generators.GoModTpl, 416 generators.GoModData{ 417 GoMod: goMod, 418 }, 419 ); err != nil { 420 return err 421 } 422 423 switch projectTypeItems[projectTypeIndex].Name { 424 case vanillaJSRESTKey: 425 if err := generators.RenderTemplate( 426 filepath.Join(dir, "main.go"), 427 generators.GoMainVanillaJSRESTTpl, 428 generators.GoMainData{ 429 GoMod: goMod, 430 }, 431 ); err != nil { 432 return err 433 } 434 435 if err := generators.RenderTemplate( 436 filepath.Join(dir, "android.go"), 437 generators.AndroidVanillaJSRESTTpl, 438 generators.AndroidData{ 439 GoMod: goMod, 440 JNIExport: strings.Replace(appID, ".", "_", -1), 441 }, 442 ); err != nil { 443 return err 444 } 445 446 if err := generators.RenderTemplate( 447 filepath.Join(dir, ".gitignore"), 448 generators.GitignoreVanillaJSRESTTpl, 449 nil, 450 ); err != nil { 451 return err 452 } 453 454 if err := generators.RenderTemplate( 455 filepath.Join(dir, "pkg", "backend", "server.go"), 456 generators.BackendVanillaJSRESTTpl, 457 nil, 458 ); err != nil { 459 return err 460 } 461 462 if err := generators.RenderTemplate( 463 filepath.Join(dir, "pkg", "frontend", "server.go"), 464 generators.FrontendVanillaJSRESTTpl, 465 nil, 466 ); err != nil { 467 return err 468 } 469 470 if err := generators.RenderTemplate( 471 filepath.Join(dir, "pkg", "frontend", "index.html"), 472 generators.IndexHTMLVanillaJSRESTTpl, 473 generators.IndexHTMLData{ 474 AppName: appName, 475 }, 476 ); err != nil { 477 return err 478 } 479 480 if err := generators.RenderTemplate( 481 filepath.Join(dir, "README.md"), 482 generators.ReadmeMDVanillaJSRESTTpl, 483 generators.ReadmeMDData{ 484 AppName: appName, 485 AppSummary: appSummary, 486 AppGitWeb: strings.TrimSuffix(appGit, ".git"), 487 AppDescription: appDescription, 488 AppBaseURL: appBaseurl, 489 AppGit: appGit, 490 CurrentYear: time.Now().Format("2006"), 491 ReleaseAuthor: releaseAuthor, 492 LicenseSPDX: licenseSPDX, 493 Dir: dir, 494 }, 495 ); err != nil { 496 return err 497 } 498 case vanillaJSFormsKey: 499 if err := generators.RenderTemplate( 500 filepath.Join(dir, "main.go"), 501 generators.GoMainVanillaJSFormsTpl, 502 generators.GoMainData{ 503 GoMod: goMod, 504 }, 505 ); err != nil { 506 return err 507 } 508 509 if err := generators.RenderTemplate( 510 filepath.Join(dir, "android.go"), 511 generators.AndroidVanillaJSFormsTpl, 512 generators.AndroidData{ 513 GoMod: goMod, 514 JNIExport: strings.Replace(appID, ".", "_", -1), 515 }, 516 ); err != nil { 517 return err 518 } 519 520 if err := generators.RenderTemplate( 521 filepath.Join(dir, "pkg", "frontend", "server.go"), 522 generators.FrontendVanillaJSFormsTpl, 523 nil, 524 ); err != nil { 525 return err 526 } 527 528 if err := generators.RenderTemplate( 529 filepath.Join(dir, "pkg", "frontend", "index.html"), 530 generators.IndexHTMLVanillaJSFormsTpl, 531 generators.IndexHTMLData{ 532 AppName: appName, 533 }, 534 ); err != nil { 535 return err 536 } 537 538 if err := generators.RenderTemplate( 539 filepath.Join(dir, "README.md"), 540 generators.ReadmeMDVanillaJSRESTTpl, 541 generators.ReadmeMDData{ 542 AppName: appName, 543 AppSummary: appSummary, 544 AppGitWeb: strings.TrimSuffix(appGit, ".git"), 545 AppDescription: appDescription, 546 AppBaseURL: appBaseurl, 547 AppGit: appGit, 548 CurrentYear: time.Now().Format("2006"), 549 ReleaseAuthor: releaseAuthor, 550 LicenseSPDX: licenseSPDX, 551 Dir: dir, 552 }, 553 ); err != nil { 554 return err 555 } 556 case reactPanrpcKey: 557 if err := generators.RenderTemplate( 558 filepath.Join(dir, "main.go"), 559 generators.GoMainReactPanrpcTpl, 560 generators.GoMainData{ 561 GoMod: goMod, 562 }, 563 ); err != nil { 564 return err 565 } 566 567 if err := generators.RenderTemplate( 568 filepath.Join(dir, "android.go"), 569 generators.AndroidReactPanrpcTpl, 570 generators.AndroidData{ 571 GoMod: goMod, 572 JNIExport: strings.Replace(appID, ".", "_", -1), 573 }, 574 ); err != nil { 575 return err 576 } 577 578 if err := generators.RenderTemplate( 579 filepath.Join(dir, ".gitignore"), 580 generators.GitignoreReactPanrpcTpl, 581 nil, 582 ); err != nil { 583 return err 584 } 585 586 if err := generators.RenderTemplate( 587 filepath.Join(dir, "pkg", "backend", "server.go"), 588 generators.BackendReactPanrpcTpl, 589 nil, 590 ); err != nil { 591 return err 592 } 593 594 if err := generators.RenderTemplate( 595 filepath.Join(dir, "pkg", "frontend", "server.go"), 596 generators.FrontendReactPanrpcTpl, 597 nil, 598 ); err != nil { 599 return err 600 } 601 602 if err := generators.RenderTemplate( 603 filepath.Join(dir, "pkg", "frontend", "src", "App.tsx"), 604 generators.AppTSXTpl, 605 generators.AppTSXData{ 606 AppName: appName, 607 }, 608 ); err != nil { 609 return err 610 } 611 612 if err := generators.RenderTemplate( 613 filepath.Join(dir, "pkg", "frontend", "src", "main.tsx"), 614 generators.MainTSXTpl, 615 nil, 616 ); err != nil { 617 return err 618 } 619 620 if err := generators.RenderTemplate( 621 filepath.Join(dir, "pkg", "frontend", "index.html"), 622 generators.IndexHTMLReactPanrpcTpl, 623 generators.IndexHTMLData{ 624 AppName: appName, 625 }, 626 ); err != nil { 627 return err 628 } 629 630 if err := generators.RenderTemplate( 631 filepath.Join(dir, "pkg", "frontend", "package.json"), 632 generators.PackageJSONReactPanrpcTpl, 633 generators.PackageJSONData{ 634 AppID: appID, 635 AppDescription: appDescription, 636 ReleaseAuthor: releaseAuthor, 637 ReleaseEmail: releaseEmail, 638 LicenseSPDX: licenseSPDX, 639 }, 640 ); err != nil { 641 return err 642 } 643 644 if err := generators.RenderTemplate( 645 filepath.Join(dir, "pkg", "frontend", "tsconfig.json"), 646 generators.TsconfigJSONTpl, 647 nil, 648 ); err != nil { 649 return err 650 } 651 652 if err := generators.RenderTemplate( 653 filepath.Join(dir, "README.md"), 654 generators.ReadmeMDReactPanrpcTpl, 655 generators.ReadmeMDData{ 656 AppName: appName, 657 AppSummary: appSummary, 658 AppGitWeb: strings.TrimSuffix(appGit, ".git"), 659 AppDescription: appDescription, 660 AppBaseURL: appBaseurl, 661 AppGit: appGit, 662 CurrentYear: time.Now().Format("2006"), 663 ReleaseAuthor: releaseAuthor, 664 LicenseSPDX: licenseSPDX, 665 Dir: dir, 666 }, 667 ); err != nil { 668 return err 669 } 670 default: 671 panic(errUnknownProjectType) 672 } 673 674 if err := generators.RenderTemplate( 675 filepath.Join(dir, "LICENSE"), 676 licenseText, 677 nil, 678 ); err != nil { 679 return err 680 } 681 682 if err := generators.RenderTemplate( 683 filepath.Join(dir, "CODE_OF_CONDUCT.md"), 684 generators.CodeOfConductMDTpl, 685 generators.CodeOfConductMDData{ 686 ReleaseEmail: releaseEmail, 687 }, 688 ); err != nil { 689 return err 690 } 691 692 if err := generators.RenderTemplate( 693 filepath.Join(dir, ".github", "workflows", "hydrapp.yaml"), 694 generators.HydrappYAMLTpl, 695 generators.HydrappYAMLData{ 696 AppID: appID, 697 }, 698 ); err != nil { 699 return err 700 } 701 702 if !viper.GetBool(noNetworkFlag) { 703 { 704 log.Println("Downloading Go dependencies ...") 705 706 cmd := exec.Command("go", "get", "-x", "./...") 707 cmd.Dir = dir 708 cmd.Stdin = os.Stdin 709 cmd.Stdout = os.Stdout 710 cmd.Stderr = os.Stderr 711 712 if err := cmd.Run(); err != nil { 713 return err 714 } 715 716 log.Println("Success!") 717 } 718 719 { 720 log.Println("Generating Go dependencies ...") 721 722 cmd := exec.Command("go", "generate", "-x", "./...") 723 cmd.Dir = dir 724 cmd.Stdin = os.Stdin 725 cmd.Stdout = os.Stdout 726 cmd.Stderr = os.Stderr 727 728 if err := cmd.Run(); err != nil { 729 return err 730 } 731 732 log.Println("Success!") 733 } 734 } 735 736 { 737 fmt.Printf(`Succesfully generated application. To start it, run the following: 738 739 cd %v 740 go run %v 741 742 You can find more information in the generated README. 743 `, dir, goMain) 744 } 745 746 return nil 747 }, 748 } 749 750 func init() { 751 newCmd.PersistentFlags().Bool(noNetworkFlag, false, "Disable all network interaction") 752 753 viper.AutomaticEnv() 754 755 rootCmd.AddCommand(newCmd) 756 }