Version 0.1.0 #3

Merged
FernandoJVideira merged 12 commits from dev into main 2026-08-25 20:42:49 +00:00
7 changed files with 724 additions and 99 deletions
Showing only changes of commit 29f94539e0 - Show all commits

No files matched your search

+1 -1
View File
@@ -3,5 +3,5 @@ package assets
import "embed" import "embed"
//go:generate go run ../tools/sync_templates.go //go:generate go run ../tools/sync_templates.go
//go:embed templates/* embedded/* //go:embed all:templates all:embedded
var EmbedFS embed.FS var EmbedFS embed.FS
+58 -63
View File
@@ -4,12 +4,10 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"git.fernandovideira.com/FernandoJVideira/draft/assets" "git.fernandovideira.com/FernandoJVideira/draft/assets"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -21,67 +19,23 @@ var apiCmd = &cobra.Command{
Use: "api", Use: "api",
Short: "Create a new Draft API project", Short: "Create a new Draft API project",
Long: `Create a new Draft API project with the necessary structure and files.`, Long: `Create a new Draft API project with the necessary structure and files.`,
Run: genProject, Run: genAPI,
} }
func genProject(cmd *cobra.Command, args []string) { func genAPI(cmd *cobra.Command, args []string) {
runNewProject(scaffoldRecipe{label: "API"})
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 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 { 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 { err = fs.WalkDir(assets.EmbedFS, root, func(path string, d fs.DirEntry, err error) error {
relPath, _ := filepath.Rel(root, path) relPath, _ := filepath.Rel(root, path)
@@ -112,40 +66,81 @@ func scaffoldProject(projectDir, moduleName string, useGRPC bool) error {
contentBytes, err := assets.EmbedFS.ReadFile(path) contentBytes, err := assets.EmbedFS.ReadFile(path)
if err != nil { 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) content := string(contentBytes)
targetPath := filepath.Join(projectDir, relPath) targetPath := filepath.Join(destRoot, relPath)
targetPath = strings.TrimSuffix(targetPath, ".tmpl") targetPath = strings.TrimSuffix(targetPath, ".tmpl")
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { 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(content, oldModuleName, moduleName)
newContent = strings.ReplaceAll(newContent, "{{ .ModuleName }}", moduleName) newContent = strings.ReplaceAll(newContent, "{{ .ModuleName }}", moduleName)
newContent = strings.ReplaceAll(newContent, "AppTmpl", serviceName)
return os.WriteFile(targetPath, []byte(newContent), 0644) return os.WriteFile(targetPath, []byte(newContent), 0644)
}) })
if err != nil { 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 // Generate main.go from template
return genFile( return genFile(
"templates/main.go.tpl", "templates/main.go.tpl",
"main", "main",
filepath.Join(projectDir, "cmd/api", "main.go"), filepath.Join(destRoot, "cmd/api", "main.go"),
MainTemplateData{ MainTemplateData{
ModuleName: moduleName, ModuleName: moduleName,
UseGRPC: useGRPC, 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 { func installTools(useGrpc bool) error {
tools := []string{ tools := []string{
"github.com/air-verse/air@latest", "github.com/air-verse/air@latest",
@@ -170,7 +165,7 @@ func installTools(useGrpc bool) error {
for _, tool := range tools { for _, tool := range tools {
if err := runCommand("", "go", "install", tool); err != nil { 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)
} }
} }
+321
View File
@@ -0,0 +1,321 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
func init() {
newCmd.AddCommand(clientCmd)
}
var clientCmd = &cobra.Command{
Use: "client",
Short: "Create a new Draft Client project",
Long: `Create a new Draft Client project with the necessary structure and files.`,
Run: genClient,
}
func genClient(cmd *cobra.Command, args []string) {
_, projectDir, serviceName, err := promptProjectName()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
isVue, resultLabel, err := promptClientFramework()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
if err := requirePnpmAndNpx(); err != nil {
fmt.Printf("error checking for pnpm and npx: %v", err)
return
}
fmt.Printf("🚀 Scaffolding %s client '%s'...\n", resultLabel, projectDir)
if isVue {
err = scaffoldClientVue(projectDir, serviceName)
} else {
err = scaffoldClientNuxt(projectDir, serviceName)
}
if err != nil {
fmt.Printf("error scaffolding client: %v", err)
return
}
fmt.Println("✅ Done!")
}
func scaffoldClientNuxt(projectDir, serviceName string) error {
if err := runCommand("", "npx", "nuxi@latest", "init", projectDir,
"--no-install",
"--packageManager=pnpm",
"-t", "ui",
"--gitInit=false",
"-M", "@pinia/nuxt",
); err != nil {
return fmt.Errorf("error running nuxi init: %w", err)
}
replacements := map[string]string{
"App Template": serviceName,
}
if err := copyEmbeddedDir("embedded/client-nuxt", projectDir, replacements); err != nil {
return fmt.Errorf("error copying client overlay: %w", err)
}
if err := patchNuxtConfig(projectDir, serviceName); err != nil {
return err
}
fmt.Println("📦 Installing dependencies...")
return runCommand(projectDir, "pnpm", "add", "pinia", "@pinia/nuxt")
}
func patchNuxtConfig(projectDir, serviceName string) error {
configPath := filepath.Join(projectDir, "nuxt.config.ts")
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("error reading nuxt.config.ts: %w", err)
}
insertion := fmt.Sprintf(`
app: {
head: {
title: %q,
meta: [{ name: 'description', content: %q }],
},
},
runtimeConfig: {
apiInternalUrl: '',
public: {
baseUrl: '',
},
},
`, serviceName, serviceName)
newContent := strings.Replace(string(content), "defineNuxtConfig({", "defineNuxtConfig({"+insertion, 1)
if newContent == string(content) {
return fmt.Errorf("could not find defineNuxtConfig({ anchor in nuxt.config.ts")
}
return os.WriteFile(configPath, []byte(newContent), 0644)
}
func scaffoldClientVue(projectDir, serviceName string) error {
parentDir := filepath.Dir(projectDir)
leafName := filepath.Base(projectDir)
if err := os.MkdirAll(parentDir, 0755); err != nil {
return fmt.Errorf("error creating parent directory: %w", err)
}
if err := runCommand(parentDir, "pnpm", "create", "vue@latest", leafName,
"--ts", "--router", "--pinia", "--eslint", "--bare",
); err != nil {
return fmt.Errorf("error running create-vue: %w", err)
}
version, err := pnpmVersion()
if err != nil {
return err
}
if err := patchPackageManagerField(projectDir, version); err != nil {
return err
}
if err := os.WriteFile(
filepath.Join(projectDir, "pnpm-workspace.yaml"),
[]byte("allowBuilds:\n vue-demi: true\n"),
0644,
); err != nil {
return fmt.Errorf("error writing pnpm-workspace.yaml: %w", err)
}
fmt.Println("📦 Installing base dependencies...")
if err := runCommand(projectDir, "pnpm", "install"); err != nil {
return fmt.Errorf("error running pnpm install: %w", err)
}
if err := runCommand(projectDir, "pnpm", "add", "tailwindcss", "@tailwindcss/vite"); err != nil {
return fmt.Errorf("error adding tailwindcss: %w", err)
}
assetsDir := filepath.Join(projectDir, "src", "assets")
if err := os.MkdirAll(assetsDir, 0755); err != nil {
return fmt.Errorf("error creating src/assets: %w", err)
}
if err := os.WriteFile(filepath.Join(assetsDir, "main.css"), []byte("@import \"tailwindcss\";\n"), 0644); err != nil {
return fmt.Errorf("error writing main.css stub: %w", err)
}
if err := patchViteConfigForTailwind(projectDir); err != nil {
return err
}
if err := patchTsconfigAlias(projectDir); err != nil {
return err
}
fmt.Println("🎨 Setting up shadcn-vue...")
if err := runCommand(projectDir, "pnpm", "dlx", "shadcn-vue@latest", "init",
"-t", "vite", "-b", "neutral", "--icon-library", "lucide", "-y", "-d",
); err != nil {
return fmt.Errorf("error running shadcn-vue init: %w", err)
}
if err := runCommand(projectDir, "pnpm", "dlx", "shadcn-vue@latest", "add",
"button", "input", "label", "select", "-y",
); err != nil {
return fmt.Errorf("error adding shadcn-vue components: %w", err)
}
if err := patchMainCssTokens(projectDir); err != nil {
return err
}
if err := patchEslintConfig(projectDir); err != nil {
return err
}
replacements := map[string]string{
"App Template": serviceName,
}
if err := copyEmbeddedDir("embedded/client-vue", projectDir, replacements); err != nil {
return fmt.Errorf("error copying client overlay: %w", err)
}
fmt.Println("📦 Installing remaining dependencies...")
return runCommand(projectDir, "pnpm", "add", "ofetch", "vue-sonner")
}
func patchPackageManagerField(projectDir, version string) error {
pkgPath := filepath.Join(projectDir, "package.json")
content, err := os.ReadFile(pkgPath)
if err != nil {
return fmt.Errorf("error reading package.json: %w", err)
}
newContent := strings.Replace(string(content), "{",
fmt.Sprintf("{\n \"packageManager\": \"pnpm@%s\",", version), 1)
if newContent == string(content) {
return fmt.Errorf("could not find opening brace in package.json")
}
return os.WriteFile(pkgPath, []byte(newContent), 0644)
}
func patchViteConfigForTailwind(projectDir string) error {
configPath := filepath.Join(projectDir, "vite.config.ts")
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("error reading vite.config.ts: %w", err)
}
newContent := strings.Replace(string(content),
"import vueDevTools from 'vite-plugin-vue-devtools'",
"import vueDevTools from 'vite-plugin-vue-devtools'\nimport tailwindcss from '@tailwindcss/vite'",
1,
)
newContent = strings.Replace(newContent, "vueDevTools(),", "vueDevTools(),\n tailwindcss(),", 1)
if newContent == string(content) {
return fmt.Errorf("could not find expected anchors in vite.config.ts")
}
return os.WriteFile(configPath, []byte(newContent), 0644)
}
func patchTsconfigAlias(projectDir string) error {
configPath := filepath.Join(projectDir, "tsconfig.json")
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("error reading tsconfig.json: %w", err)
}
aliasBlock := ` ],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}`
newContent := strings.Replace(string(content), " ]\n}", aliasBlock, 1)
if newContent == string(content) {
return fmt.Errorf("could not find expected anchor in tsconfig.json")
}
return os.WriteFile(configPath, []byte(newContent), 0644)
}
func patchMainCssTokens(projectDir string) error {
cssPath := filepath.Join(projectDir, "src", "assets", "main.css")
content, err := os.ReadFile(cssPath)
if err != nil {
return fmt.Errorf("error reading main.css: %w", err)
}
s := string(content)
s2 := strings.Replace(s,
"--color-background: var(--background);",
"--color-background: var(--background);\n --color-success: var(--success);\n --color-success-foreground: var(--success-foreground);\n --color-warning: var(--warning);\n --color-warning-foreground: var(--warning-foreground);",
1,
)
if s2 == s {
return fmt.Errorf("could not find @theme anchor in main.css")
}
s3 := strings.Replace(s2,
"}\n\n.dark {",
" --success: oklch(0.6 0.15 145);\n --success-foreground: oklch(0.985 0 0);\n --warning: oklch(0.75 0.15 80);\n --warning-foreground: oklch(0.145 0 0);\n}\n\n.dark {",
1,
)
if s3 == s2 {
return fmt.Errorf("could not find :root close anchor in main.css")
}
s4 := strings.Replace(s3,
"}\n\n@layer base {",
" --success: oklch(0.65 0.15 145);\n --success-foreground: oklch(0.145 0 0);\n --warning: oklch(0.8 0.15 80);\n --warning-foreground: oklch(0.145 0 0);\n}\n\n@layer base {",
1,
)
if s4 == s3 {
return fmt.Errorf("could not find .dark close anchor in main.css")
}
return os.WriteFile(cssPath, []byte(s4), 0644)
}
func patchEslintConfig(projectDir string) error {
configPath := filepath.Join(projectDir, "eslint.config.ts")
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("error reading eslint.config.ts: %w", err)
}
override := ` {
name: 'app/rules-overrides',
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),`
newContent := strings.Replace(string(content),
" ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),",
override,
1,
)
if newContent == string(content) {
return fmt.Errorf("could not find expected anchor in eslint.config.ts")
}
return os.WriteFile(configPath, []byte(newContent), 0644)
}
+169 -1
View File
@@ -5,13 +5,18 @@ import (
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"math/big"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"text/template" "text/template"
"git.fernandovideira.com/FernandoJVideira/draft/assets" "git.fernandovideira.com/FernandoJVideira/draft/assets"
"github.com/manifoldco/promptui"
) )
const ( const (
@@ -22,7 +27,7 @@ func randomString(length int) string {
s, r := make([]rune, length), []rune(chars) s, r := make([]rune, length), []rune(chars)
for i := range s { for i := range s {
p, _ := rand.Prime(rand.Reader, len(r)) p, _ := rand.Int(rand.Reader, big.NewInt(int64(len(r))))
x, y := p.Uint64(), uint64(len(r)) x, y := p.Uint64(), uint64(len(r))
s[i] = r[x%y] s[i] = r[x%y]
} }
@@ -98,3 +103,166 @@ func runCommand(dir, name string, args ...string) error {
return cmd.Run() return cmd.Run()
} }
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
func requirePnpmAndNpx() error {
if !commandExists("pnpm") {
return fmt.Errorf("pnpm is required but not found on PATH")
}
if !commandExists("npx") {
return fmt.Errorf("npx is required but not found on PATH")
}
return nil
}
func promptClientFramework() (isVue bool, resultLabel string, err error) {
prompt := promptui.Select{
Label: "Select Client Framework",
Items: []string{"Nuxt", "Vue"},
}
_, result, err := prompt.Run()
if err != nil {
return false, "", err
}
return result == "Vue", result, nil
}
func pnpmVersion() (string, error) {
out, err := exec.Command("pnpm", "--version").Output()
if err != nil {
return "", fmt.Errorf("error getting pnpm version: %w", err)
}
return strings.TrimSpace(string(out)), nil
}
func toPascalCase(s string) string {
parts := regexp.MustCompile(`[^a-zA-Z0-9]+`).Split(s, -1)
var b strings.Builder
for _, p := range parts {
if p == "" {
continue
}
b.WriteString(strings.ToUpper(p[:1]) + p[1:])
}
return b.String()
}
func promptProjectName() (moduleName, projectDir, serviceName string, err error) {
prompt := promptui.Prompt{Label: "Project Name"}
rawInput, err := prompt.Run()
if err != nil {
return "", "", "", err
}
moduleName = strings.TrimSpace(rawInput)
projectDir = path.Base(strings.TrimSuffix(moduleName, "/"))
serviceName = toPascalCase(projectDir)
return moduleName, projectDir, serviceName, nil
}
func promptAPIType() (useGRPC bool, resultLabel string, err error) {
prompt := promptui.Select{
Label: "Select API Type",
Items: []string{"HTTP (REST)", "gRPC"},
}
_, result, err := prompt.Run()
if err != nil {
return false, "", err
}
return strings.Contains(result, "gRPC"), result, nil
}
func copyEmbeddedDir(root, destDir string, replacements map[string]string) error {
return fs.WalkDir(assets.EmbedFS, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
relPath, _ := filepath.Rel(root, path)
contentBytes, err := assets.EmbedFS.ReadFile(path)
if err != nil {
return fmt.Errorf("error reading embedded file %s: %w", path, err)
}
content := string(contentBytes)
for old, new := range replacements {
content = strings.ReplaceAll(content, old, new)
}
targetPath := filepath.Join(destDir, relPath)
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return fmt.Errorf("error creating directory %s: %w", filepath.Dir(targetPath), err)
}
return os.WriteFile(targetPath, []byte(content), 0644)
})
}
type scaffoldRecipe struct {
label string
destSubdir string
preScaffold func(projectDir string) error
postScaffold func(projectDir string) error
}
func runNewProject(recipe scaffoldRecipe) {
moduleName, projectDir, serviceName, err := promptProjectName()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
useGRPC, resultLabel, err := promptAPIType()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
if useGRPC && !commandExists("protoc") {
fmt.Println("❌ protoc is required for gRPC projects but wasn't found on PATH.")
fmt.Println(" Install it (e.g. `brew install protobuf`) and try again.")
return
}
fmt.Printf("🚀 Scaffolding %s '%s' as %s...\n", recipe.label, moduleName, resultLabel)
if recipe.preScaffold != nil {
if err := recipe.preScaffold(projectDir); err != nil {
fmt.Printf("error preparing project: %v", err)
return
}
}
if err := scaffoldAPI(projectDir, recipe.destSubdir, moduleName, serviceName, 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...")
destRoot := filepath.Join(projectDir, recipe.destSubdir)
if err := runCommand(destRoot, "go", "mod", "tidy"); err != nil {
fmt.Printf("error running 'go mod tidy': %v", err)
return
}
if recipe.postScaffold != nil {
if err := recipe.postScaffold(projectDir); err != nil {
fmt.Printf("error finishing project: %v", err)
return
}
}
fmt.Println("✅ Done!")
}
+52
View File
@@ -0,0 +1,52 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
func init() {
newCmd.AddCommand(monorepoCmd)
}
var monorepoCmd = &cobra.Command{
Use: "monorepo",
Short: "Create a new Draft Monorepo project",
Long: `Create a new Draft Monorepo project with the necessary structure and files.`,
Run: genMonorepo,
}
func genMonorepo(cmd *cobra.Command, args []string) {
runNewProject(scaffoldRecipe{
label: "monorepo",
destSubdir: "apps/api",
preScaffold: func(projectDir string) error {
if err := os.MkdirAll(projectDir, 0755); err != nil {
return fmt.Errorf("error creating project directory: %w", err)
}
return copyEmbeddedDir("embedded/monorepo-root", projectDir, nil)
},
postScaffold: func(projectDir string) error {
if err := requirePnpmAndNpx(); err != nil {
return fmt.Errorf("error checking for pnpm and npx: %w", err)
}
isVue, resultLabel, err := promptClientFramework()
if err != nil {
return fmt.Errorf("prompt failed: %w", err)
}
clientDir := filepath.Join(projectDir, "apps/client")
serviceName := toPascalCase(filepath.Base(projectDir))
fmt.Printf("🚀 Scaffolding %s client app...\n", resultLabel)
if isVue {
return scaffoldClientVue(clientDir, serviceName)
}
return scaffoldClientNuxt(clientDir, serviceName)
},
})
}
+31 -4
View File
@@ -1,15 +1,18 @@
package cmd package cmd
import ( import (
"fmt"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const oldModuleName = "github.com/FernandoJVideira/api_tmpl" const oldModuleName = "app_tmpl"
const srcDir = "embedded/api_tmpl"
type MainTemplateData struct { type MainTemplateData struct {
ModuleName string ModuleName string
UseGRPC bool ServiceName string
UseGRPC bool
} }
func init() { func init() {
@@ -22,3 +25,27 @@ var newCmd = &cobra.Command{
Long: `Create a new Draft project with the necessary structure and files.`, Long: `Create a new Draft project with the necessary structure and files.`,
Run: genProject, Run: genProject,
} }
func genProject(cmd *cobra.Command, args []string) {
projType := promptui.Select{
Label: "Select Project Type",
Items: []string{"API", "Client", "Full Stack"},
}
_, result, err := projType.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
switch result {
case "API":
genAPI(cmd, args)
case "Client":
genClient(cmd, args)
case "Full Stack":
genMonorepo(cmd, args)
default:
fmt.Println("Invalid selection")
}
}
+92 -30
View File
@@ -3,67 +3,129 @@
package main package main
import ( import (
"io/fs"
"log" "log"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
) )
const ( const (
sourceDir = "assets/testdata/api_tmpl" oldModuleName = "app_tmpl"
targetDir = "assets/embedded"
oldModuleName = "github.com/FernandoJVideira/api_tmpl"
moduleTemplate = "{{ .ModuleName }}" moduleTemplate = "{{ .ModuleName }}"
) )
func main() { type syncJob struct {
log.Println("Syncing templates...") templateName string // optional, for logging purposes
syncTemplates() sourceDir string
targetDir string
excludePrefix string // relative-path prefix to skip, e.g. "apps/"; empty = sync everything
includePrefixes []string // if set, only paths with one of these prefixes sync; empty = sync everything
} }
func syncTemplates() { var jobs = []syncJob{
{
templateName: "api",
sourceDir: "assets/testdata/template/apps/api",
targetDir: "assets/embedded/api",
},
{
templateName: "monorepo-root",
sourceDir: "assets/testdata/template",
targetDir: "assets/embedded/monorepo-root",
excludePrefix: "apps/",
},
{
templateName: "client-nuxt",
sourceDir: "assets/testdata/template/apps/client",
targetDir: "assets/embedded/client-nuxt",
includePrefixes: []string{"app/", "public/"},
},
{
templateName: "client-vue",
sourceDir: "assets/testdata/template/apps/vue-client",
targetDir: "assets/embedded/client-vue",
includePrefixes: []string{
"src/App.vue",
"src/main.ts",
"src/router/",
"src/stores/",
"src/composables/",
"src/lib/api-client.ts",
"src/components/AppBrand.vue",
"src/components/AppLayout.vue",
"src/views/",
"src/shared/",
".env.example",
},
},
}
err := filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, err error) error { func main() {
if err != nil { log.Println("Syncing templates...")
return err for _, job := range jobs {
} syncTemplates(job)
}
}
relPath, _ := filepath.Rel(sourceDir, path) func syncTemplates(job syncJob) {
destPath := filepath.Join(targetDir, relPath)
if strings.HasPrefix(d.Name(), ".") && d.Name() != "." { err := os.RemoveAll(job.targetDir)
if d.IsDir() { if err != nil {
return filepath.SkipDir log.Fatalf("Error removing target directory: %v", err)
}
cmd := exec.Command("git", "-C", job.sourceDir, "ls-files")
output, err := cmd.Output()
if err != nil {
log.Fatalf("Error listing files in source directory: %v", err)
}
paths := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, relPath := range paths {
if len(job.includePrefixes) > 0 {
included := false
for _, prefix := range job.includePrefixes {
if strings.HasPrefix(relPath, prefix) {
included = true
break
}
} }
return nil if !included {
continue
}
} else if job.excludePrefix != "" && strings.HasPrefix(relPath, job.excludePrefix) {
continue
} }
if d.IsDir() { srcPath := filepath.Join(job.sourceDir, relPath)
return os.MkdirAll(destPath, 0755) destPath := filepath.Join(job.targetDir, relPath)
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
log.Fatalf("Error creating destination directory: %v", err)
} }
content, err := os.ReadFile(path) content, err := os.ReadFile(srcPath)
if err != nil { if err != nil {
return err log.Fatalf("Error reading source file: %v", err)
} }
if d.Name() == "go.mod" || d.Name() == "go.sum" { if filepath.Base(relPath) == "go.mod" || filepath.Base(relPath) == "go.sum" {
destPath += ".tmpl" destPath += ".tmpl"
} }
if d.Name() == "go.mod" { if filepath.Base(relPath) == "go.mod" {
strContent := string(content) strContent := string(content)
strContent = strings.Replace(strContent, oldModuleName, moduleTemplate, 1) strContent = strings.Replace(strContent, oldModuleName, moduleTemplate, 1)
content = []byte(strContent) content = []byte(strContent)
} }
return os.WriteFile(destPath, content, 0644) if err := os.WriteFile(destPath, content, 0644); err != nil {
}) log.Fatalf("Error writing to destination file: %v", err)
}
if err != nil {
log.Fatalf("Error walking source: %v", err)
} }
log.Println("Templates synced successfully!") log.Printf("Template %s synced successfully!", job.templateName)
} }