Public Access
feat: scaffold API, client, or monorepo projects with Nuxt/Vue client support
- draft new now offers API, Client, or Full Stack (monorepo) project shapes - draft new client scaffolds a Nuxt or Vue app via the official nuxi/create-vue tooling, with a Draft-maintained auth/API overlay layered on top - draft new monorepo combines a scaffolded API and client under one root with go.work, pnpm-workspace.yaml, and docker-compose wiring - assets/embedded restructured into api/monorepo-root/client-nuxt/client-vue zones, synced via a generalized, job-based sync tool
This commit is contained in:
1 parent
fe48b7caee
commit
29f94539e0
7 files changed
+724
-99
No files matched your search
+58
-63
@@ -4,12 +4,10 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.fernandovideira.com/FernandoJVideira/draft/assets"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -21,67 +19,23 @@ var apiCmd = &cobra.Command{
|
||||
Use: "api",
|
||||
Short: "Create a new Draft API project",
|
||||
Long: `Create a new Draft API project with the necessary structure and files.`,
|
||||
Run: genProject,
|
||||
Run: genAPI,
|
||||
}
|
||||
|
||||
func genProject(cmd *cobra.Command, args []string) {
|
||||
|
||||
projectNamePrompt := promptui.Prompt{
|
||||
Label: "Project Name",
|
||||
}
|
||||
|
||||
rawInput, err := projectNamePrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
moduleName := strings.TrimSpace(rawInput)
|
||||
projectDir := path.Base(strings.TrimSuffix(moduleName, "/"))
|
||||
|
||||
apiType := promptui.Select{
|
||||
Label: "Select API Type",
|
||||
Items: []string{"HTTP (REST)", "gRPC"},
|
||||
}
|
||||
|
||||
_, result, err := apiType.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Logic to map the selection to a boolean
|
||||
useGRPC := strings.Contains(result, "gRPC")
|
||||
|
||||
fmt.Printf("🚀 Scaffolding '%s' as %s...\n", moduleName, result)
|
||||
if err := scaffoldProject(projectDir, moduleName, useGRPC); err != nil {
|
||||
fmt.Printf("error scaffolding project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("📦 Installing tools...")
|
||||
if err := installTools(useGRPC); err != nil {
|
||||
fmt.Printf("error installing tools: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("🧹 Tidying up Go modules...")
|
||||
if err := runCommand(projectDir, "go", "mod", "tidy"); err != nil {
|
||||
fmt.Printf("error running 'go mod tidy': %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Done!")
|
||||
func genAPI(cmd *cobra.Command, args []string) {
|
||||
runNewProject(scaffoldRecipe{label: "API"})
|
||||
}
|
||||
|
||||
func scaffoldProject(projectDir, moduleName string, useGRPC bool) error {
|
||||
func scaffoldAPI(projectDir, destSubdir, moduleName, serviceName string, useGRPC bool) error {
|
||||
|
||||
err := os.MkdirAll(projectDir, 0755)
|
||||
destRoot := filepath.Join(projectDir, destSubdir)
|
||||
|
||||
err := os.MkdirAll(destRoot, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating project directory: %v", err)
|
||||
return fmt.Errorf("error creating project directory: %w", err)
|
||||
}
|
||||
|
||||
root := "embedded"
|
||||
root := "embedded/api"
|
||||
err = fs.WalkDir(assets.EmbedFS, root, func(path string, d fs.DirEntry, err error) error {
|
||||
|
||||
relPath, _ := filepath.Rel(root, path)
|
||||
@@ -112,40 +66,81 @@ func scaffoldProject(projectDir, moduleName string, useGRPC bool) error {
|
||||
|
||||
contentBytes, err := assets.EmbedFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading embedded file %s: %v", path, err)
|
||||
return fmt.Errorf("error reading embedded file %s: %w", path, err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
targetPath := filepath.Join(projectDir, relPath)
|
||||
targetPath := filepath.Join(destRoot, relPath)
|
||||
|
||||
targetPath = strings.TrimSuffix(targetPath, ".tmpl")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return fmt.Errorf("error creating directory %s: %v", filepath.Dir(targetPath), err)
|
||||
return fmt.Errorf("error creating directory %s: %w", filepath.Dir(targetPath), err)
|
||||
}
|
||||
|
||||
newContent := strings.ReplaceAll(content, oldModuleName, moduleName)
|
||||
newContent = strings.ReplaceAll(newContent, "{{ .ModuleName }}", moduleName)
|
||||
newContent = strings.ReplaceAll(newContent, "AppTmpl", serviceName)
|
||||
|
||||
return os.WriteFile(targetPath, []byte(newContent), 0644)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error copying template files: %v\n", err)
|
||||
return fmt.Errorf("error copying template files: %w", err)
|
||||
}
|
||||
|
||||
if useGRPC {
|
||||
if err := regenerateProto(destRoot, serviceName); err != nil {
|
||||
return fmt.Errorf("error regenerating proto: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate main.go from template
|
||||
return genFile(
|
||||
"templates/main.go.tpl",
|
||||
"main",
|
||||
filepath.Join(projectDir, "cmd/api", "main.go"),
|
||||
filepath.Join(destRoot, "cmd/api", "main.go"),
|
||||
MainTemplateData{
|
||||
ModuleName: moduleName,
|
||||
UseGRPC: useGRPC,
|
||||
ModuleName: moduleName,
|
||||
ServiceName: serviceName,
|
||||
UseGRPC: useGRPC,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func regenerateProto(projectDir, serviceName string) error {
|
||||
protoPath := filepath.Join(projectDir, "proto", fmt.Sprintf("service_%s.proto", oldModuleName))
|
||||
content, err := os.ReadFile(protoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading service proto: %w", err)
|
||||
}
|
||||
newContent := strings.Replace(string(content), "service AppTmpl {", "service "+serviceName+" {", 1)
|
||||
if err := os.WriteFile(protoPath, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("error rewriting service proto: %w", err)
|
||||
}
|
||||
|
||||
protoFiles, err := filepath.Glob(filepath.Join(projectDir, "proto", "*.proto"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error globbing proto files: %w", err)
|
||||
}
|
||||
args := []string{
|
||||
"--proto_path=proto",
|
||||
"--go_out=pb", "--go_opt=paths=source_relative",
|
||||
"--go-grpc_out=pb", "--go-grpc_opt=paths=source_relative",
|
||||
"--grpc-gateway_out=pb", "--grpc-gateway_opt=paths=source_relative",
|
||||
"--openapiv2_out=docs/swagger", "--openapiv2_opt=allow_merge=true,merge_file_name=" + filepath.Base(projectDir),
|
||||
}
|
||||
for _, f := range protoFiles {
|
||||
rel, _ := filepath.Rel(projectDir, f)
|
||||
args = append(args, rel)
|
||||
}
|
||||
if err := runCommand(projectDir, "protoc", args...); err != nil {
|
||||
return fmt.Errorf("error running protoc: %w", err)
|
||||
}
|
||||
|
||||
return runCommand(projectDir, "statik", "-src=./docs/swagger", "-dest=./docs")
|
||||
}
|
||||
|
||||
func installTools(useGrpc bool) error {
|
||||
tools := []string{
|
||||
"github.com/air-verse/air@latest",
|
||||
@@ -170,7 +165,7 @@ func installTools(useGrpc bool) error {
|
||||
|
||||
for _, tool := range tools {
|
||||
if err := runCommand("", "go", "install", tool); err != nil {
|
||||
return fmt.Errorf("error installing tool %s: %v", tool, err)
|
||||
return fmt.Errorf("error installing tool %s: %w", tool, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user