scaffold.go

121 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
package scaffold

import (
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"text/template"

	"congo.gg"
)

// Data holds template variables for scaffold rendering.
type Data struct {
	Name          string
	Module        string
	Dir           string // app subdirectory (default: "web")
	WithFrontend  bool
	WithAssistant bool
	WithPlatform  bool
	WithDatabase  bool
}

// RenderProject walks res/scaffold/ and writes files into targetDir.
//
// Routing rules:
//   - res/scaffold/web/ → targetDir/<Dir>/
//   - res/scaffold/<path> → targetDir/<path>
//
// Files ending in .tmpl are executed as text/template with <<>> delimiters
// and written with the .tmpl suffix stripped. All other files are copied verbatim.
func RenderProject(targetDir string, data Data) error {
	const root = "res/scaffold"
	return fs.WalkDir(congo.ScaffoldFS, root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		rel := strings.TrimPrefix(path, root+"/")
		if rel == path || rel == "" {
			return nil
		}

		// Map web/ subdirectory to the configured app dir.
		if strings.HasPrefix(rel, "web/") {
			rel = data.Dir + "/" + strings.TrimPrefix(rel, "web/")
		} else if rel == "web" && d.IsDir() {
			rel = data.Dir
		}

		outPath := filepath.Join(targetDir, rel)

		if d.IsDir() {
			return os.MkdirAll(outPath, 0755)
		}

		content, err := fs.ReadFile(congo.ScaffoldFS, path)
		if err != nil {
			return err
		}

		if strings.HasSuffix(path, ".tmpl") {
			outPath = strings.TrimSuffix(outPath, ".tmpl")
			os.MkdirAll(filepath.Dir(outPath), 0755)
			return renderTemplate(outPath, filepath.Base(path), content, data)
		}

		os.MkdirAll(filepath.Dir(outPath), 0755)
		return os.WriteFile(outPath, content, 0644)
	})
}

// RenderApp renders only the app subdirectory (controllers/, models/, views/, main.go)
// for adding a new app to an existing project.
func RenderApp(targetDir string, data Data) error {
	const root = "res/scaffold/web"
	return fs.WalkDir(congo.ScaffoldFS, root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		rel := strings.TrimPrefix(path, root+"/")
		if rel == path || rel == "" {
			return nil
		}

		outPath := filepath.Join(targetDir, rel)

		if d.IsDir() {
			return os.MkdirAll(outPath, 0755)
		}

		content, err := fs.ReadFile(congo.ScaffoldFS, path)
		if err != nil {
			return err
		}

		if strings.HasSuffix(path, ".tmpl") {
			outPath = strings.TrimSuffix(outPath, ".tmpl")
			os.MkdirAll(filepath.Dir(outPath), 0755)
			return renderTemplate(outPath, filepath.Base(path), content, data)
		}

		os.MkdirAll(filepath.Dir(outPath), 0755)
		return os.WriteFile(outPath, content, 0644)
	})
}

// renderTemplate executes a template to a buffer first, then writes to disk.
// Prevents partial files on template execution errors.
func renderTemplate(outPath, name string, content []byte, data Data) error {
	tmpl, err := template.New(name).Delims("<<", ">>").Parse(string(content))
	if err != nil {
		return err
	}
	var buf strings.Builder
	if err := tmpl.Execute(&buf, data); err != nil {
		return err
	}
	return os.WriteFile(outPath, []byte(buf.String()), 0644)
}