Files
FernandoJVideira 29f94539e0 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
2026-08-25 21:33:00 +01:00

174 lines
4.8 KiB
Go

package cmd
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"git.fernandovideira.com/FernandoJVideira/draft/assets"
"github.com/spf13/cobra"
)
func init() {
newCmd.AddCommand(apiCmd)
}
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: genAPI,
}
func genAPI(cmd *cobra.Command, args []string) {
runNewProject(scaffoldRecipe{label: "API"})
}
func scaffoldAPI(projectDir, destSubdir, moduleName, serviceName string, useGRPC bool) error {
destRoot := filepath.Join(projectDir, destSubdir)
err := os.MkdirAll(destRoot, 0755)
if err != nil {
return fmt.Errorf("error creating project directory: %w", err)
}
root := "embedded/api"
err = fs.WalkDir(assets.EmbedFS, root, func(path string, d fs.DirEntry, err error) error {
relPath, _ := filepath.Rel(root, path)
if !useGRPC {
// If User chose HTTP: Remove gRPC stuff
if strings.HasPrefix(relPath, "pb") ||
strings.HasPrefix(relPath, "proto") ||
strings.Contains(relPath, "handlers/grpc") ||
strings.HasPrefix(relPath, "docs/statik") ||
strings.HasPrefix(relPath, "docs/swagger") {
return nil
}
} else {
// If User chose gRPC: Remove HTTP stuff
if strings.Contains(relPath, "handlers/http") {
return nil
}
}
if strings.HasSuffix(relPath, "cmd/api/main.go") {
return nil
}
if d.IsDir() {
return nil
}
contentBytes, err := assets.EmbedFS.ReadFile(path)
if err != nil {
return fmt.Errorf("error reading embedded file %s: %w", path, err)
}
content := string(contentBytes)
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: %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: %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(destRoot, "cmd/api", "main.go"),
MainTemplateData{
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",
"github.com/golangci/golangci-lint/cmd/golangci-lint@latest",
"go.uber.org/mock/mockgen@latest",
"github.com/sqlc-dev/sqlc/cmd/sqlc@latest",
"github.com/golang-migrate/migrate/v4/cmd/migrate@latest",
}
if useGrpc {
tools = append(tools,
"google.golang.org/protobuf/cmd/protoc-gen-go@latest",
"google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest",
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest",
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest",
)
} else {
tools = append(tools,
"github.com/swaggo/swag/cmd/swag@latest",
)
}
for _, tool := range tools {
if err := runCommand("", "go", "install", tool); err != nil {
return fmt.Errorf("error installing tool %s: %w", tool, err)
}
}
return nil
}