feat(04-01): scaffold Winter-shaped plugins with generated registry
Render plugin.go, routes.go, leaf packages and go.mod from embedded templates, emit empty registry accessors, and define job, admin, lang and mail capability contracts so a new plugin compiles before artifacts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package build
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
@@ -13,8 +14,34 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed stubs/*.tmpl
|
||||
var stubFS embed.FS
|
||||
|
||||
var stubTmpl = template.Must(template.ParseFS(stubFS, "stubs/*.tmpl"))
|
||||
|
||||
var pluginLeaves = []string{
|
||||
"models",
|
||||
"classes",
|
||||
"controllers",
|
||||
"console",
|
||||
"jobs",
|
||||
"middleware",
|
||||
"updates",
|
||||
}
|
||||
|
||||
type stubData struct {
|
||||
Package string
|
||||
ID string
|
||||
Module string
|
||||
Framework string
|
||||
GoVersion string
|
||||
Toolchain string
|
||||
Replace string
|
||||
}
|
||||
|
||||
const (
|
||||
frameworkModule = "git.golem15.com/golem15/summercms"
|
||||
goVersionLine = "go 1.27.0"
|
||||
@@ -50,27 +77,21 @@ func MakePlugin(ctx context.Context, startDir, id string) (string, error) {
|
||||
return "", fmt.Errorf("build: plugin module: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(pluginDir, "config"), 0o755); err != nil {
|
||||
return "", fmt.Errorf("build: create plugin dir: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pluginDir, "config", ".gitkeep"), []byte{}, 0o644); err != nil {
|
||||
return "", fmt.Errorf("build: write config placeholder: %w", err)
|
||||
}
|
||||
|
||||
src, err := pluginGoSource(name, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.go"), src, 0o644); err != nil {
|
||||
return "", fmt.Errorf("build: write plugin.go: %w", err)
|
||||
}
|
||||
|
||||
replace, err := frameworkReplaceFor(pluginDir, appDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pluginDir, "go.mod"), pluginGoMod(modPath, replace), 0o644); err != nil {
|
||||
return "", fmt.Errorf("build: write go.mod: %w", err)
|
||||
data := stubData{
|
||||
Package: name,
|
||||
ID: id,
|
||||
Module: modPath,
|
||||
Framework: frameworkModule,
|
||||
GoVersion: goVersionLine,
|
||||
Toolchain: toolchainLine,
|
||||
Replace: replace,
|
||||
}
|
||||
if err := writePluginScaffold(pluginDir, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := runGo(ctx, pluginDir, "mod", "tidy"); err != nil {
|
||||
return "", err
|
||||
@@ -156,39 +177,83 @@ func pluginName(id string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func pluginGoSource(pkg, id string) ([]byte, error) {
|
||||
var b strings.Builder
|
||||
b.WriteString("package " + pkg + "\n\n")
|
||||
b.WriteString("import (\n")
|
||||
b.WriteString("\t\"git.golem15.com/golem15/summercms/backpack\"\n")
|
||||
b.WriteString("\t\"git.golem15.com/golem15/summercms/party\"\n")
|
||||
b.WriteString(")\n\n")
|
||||
fmt.Fprintf(&b, "// Plugin is the %s plugin.\n", id)
|
||||
b.WriteString("type Plugin struct{}\n\n")
|
||||
fmt.Fprintf(&b, "func (p *Plugin) ID() string { return %s }\n", strconv.Quote(id))
|
||||
b.WriteString("func (p *Plugin) Requires() []string { return nil }\n\n")
|
||||
b.WriteString("func (p *Plugin) Register(*backpack.App) error { return nil }\n")
|
||||
b.WriteString("func (p *Plugin) Boot(*backpack.App) error { return nil }\n\n")
|
||||
b.WriteString("func init() {\n")
|
||||
b.WriteString("\tparty.Register(&Plugin{})\n")
|
||||
b.WriteString("}\n")
|
||||
formatted, err := format.Source([]byte(b.String()))
|
||||
func writePluginScaffold(pluginDir string, data stubData) error {
|
||||
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
|
||||
return fmt.Errorf("build: create plugin dir: %w", err)
|
||||
}
|
||||
pluginSrc, err := renderStub("plugin.go", data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build: format plugin.go: %w", err)
|
||||
return err
|
||||
}
|
||||
if err := writeFile(filepath.Join(pluginDir, "plugin.go"), pluginSrc); err != nil {
|
||||
return err
|
||||
}
|
||||
routesSrc, err := renderStub("routes.go", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeFile(filepath.Join(pluginDir, "routes.go"), routesSrc); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, leaf := range pluginLeaves {
|
||||
src, err := renderStub(leaf+"/doc.go", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeFile(filepath.Join(pluginDir, leaf, "doc.go"), src); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, asset := range pluginAssets {
|
||||
if err := writeFile(filepath.Join(pluginDir, filepath.FromSlash(asset.rel)), []byte(asset.body)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := writeRegistry(pluginDir, emptyRegistry(data.Package, data.Module)); err != nil {
|
||||
return err
|
||||
}
|
||||
modSrc, err := renderStub("go.mod", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeFile(filepath.Join(pluginDir, "go.mod"), modSrc)
|
||||
}
|
||||
|
||||
type pluginAsset struct {
|
||||
rel string
|
||||
body string
|
||||
}
|
||||
|
||||
var pluginAssets = []pluginAsset{
|
||||
{rel: "config/config.yaml", body: "{}\n"},
|
||||
{rel: "lang/en/lang.yaml", body: "{}\n"},
|
||||
{rel: "views/mail/welcome.htm", body: "subject = \"Welcome\"\ndescription = \"Placeholder mail template\"\n==\nHello.\n"},
|
||||
}
|
||||
|
||||
func renderStub(name string, data any) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := stubTmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
||||
return nil, fmt.Errorf("build: render stub %s: %w", name, err)
|
||||
}
|
||||
raw := buf.Bytes()
|
||||
if !strings.HasSuffix(name, ".go") {
|
||||
return raw, nil
|
||||
}
|
||||
formatted, err := format.Source(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build: format stub %s: %w\n%s", name, err, raw)
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
func pluginGoMod(modPath, replace string) []byte {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "module %s\n\n", modPath)
|
||||
b.WriteString(goVersionLine + "\n\n")
|
||||
b.WriteString(toolchainLine + "\n\n")
|
||||
fmt.Fprintf(&b, "require %s v0.0.0\n", frameworkModule)
|
||||
if replace != "" {
|
||||
fmt.Fprintf(&b, "\nreplace %s => %s\n", frameworkModule, replace)
|
||||
func writeFile(path string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return fmt.Errorf("build: mkdir %s: %w", filepath.Dir(path), err)
|
||||
}
|
||||
return []byte(b.String())
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return fmt.Errorf("build: write %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func frameworkReplaceFor(pluginDir, appDir string) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user