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:
@@ -337,17 +337,83 @@ func TestParseManifestRejectsUppercaseID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScaffoldPluginSmoke(t *testing.T) {
|
||||||
|
dir := copyHelloApp(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assertScaffoldFiles(t, pluginDir)
|
||||||
|
|
||||||
|
pluginSrc, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
regSrc, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"generatedModels()",
|
||||||
|
"generatedMigrations()",
|
||||||
|
"generatedCommands()",
|
||||||
|
"generatedJobs()",
|
||||||
|
"generatedAdminControllers()",
|
||||||
|
"//go:embed config",
|
||||||
|
"//go:embed lang",
|
||||||
|
"//go:embed views/mail",
|
||||||
|
} {
|
||||||
|
if !bytes.Contains(pluginSrc, []byte(want)) {
|
||||||
|
t.Fatalf("plugin.go missing %s:\n%s", want, pluginSrc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"func generatedModels()",
|
||||||
|
"func generatedMigrations()",
|
||||||
|
"func generatedCommands()",
|
||||||
|
"func generatedJobs()",
|
||||||
|
"func generatedAdminControllers()",
|
||||||
|
} {
|
||||||
|
if !bytes.Contains(regSrc, []byte(want)) {
|
||||||
|
t.Fatalf("registry.gen.go missing %s:\n%s", want, regSrc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := App(ctx, dir, &buf); err != nil {
|
||||||
|
t.Fatalf("app build: %v\n%s", err, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
vet := execGo(t, pluginDir, "vet", "./...")
|
||||||
|
if out, err := vet.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("plugin go vet: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginAfter, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(pluginSrc, pluginAfter) {
|
||||||
|
t.Fatal("hand-written plugin.go was rewritten")
|
||||||
|
}
|
||||||
|
regAfter, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(regSrc, regAfter) {
|
||||||
|
t.Fatal("registry.gen.go is not byte-stable after add/build")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if _, err := os.Stat(filepath.Join(pluginDir, "go.mod")); err != nil {
|
assertWinterPluginShape(t, pluginDir)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(filepath.Join(pluginDir, "plugin.go")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(filepath.Join(pluginDir, "config")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
body, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
body, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -375,6 +441,36 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
|||||||
t.Fatalf("plugin go.mod missing module path:\n%s", mod)
|
t.Fatalf("plugin go.mod missing module path:\n%s", mod)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd := execGo(t, pluginDir, "build", "./...")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("plugin does not compile: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertWinterPluginShape(t *testing.T, pluginDir string) {
|
||||||
|
t.Helper()
|
||||||
|
required := []string{
|
||||||
|
"go.mod",
|
||||||
|
"plugin.go",
|
||||||
|
"routes.go",
|
||||||
|
"registry.gen.go",
|
||||||
|
filepath.Join("config", "config.yaml"),
|
||||||
|
filepath.Join("lang", "en", "lang.yaml"),
|
||||||
|
filepath.Join("views", "mail", "welcome.htm"),
|
||||||
|
}
|
||||||
|
for _, leaf := range pluginLeaves {
|
||||||
|
required = append(required, filepath.Join(leaf, "doc.go"))
|
||||||
|
}
|
||||||
|
allowed := map[string]struct{}{
|
||||||
|
"go.sum": {},
|
||||||
|
}
|
||||||
|
for _, rel := range required {
|
||||||
|
allowed[rel] = struct{}{}
|
||||||
|
if _, err := os.Stat(filepath.Join(pluginDir, rel)); err != nil {
|
||||||
|
t.Fatalf("missing %s: %v", rel, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var extra []string
|
var extra []string
|
||||||
if err := filepath.WalkDir(pluginDir, func(path string, d os.DirEntry, err error) error {
|
if err := filepath.WalkDir(pluginDir, func(path string, d os.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -387,13 +483,15 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch rel {
|
if _, ok := allowed[rel]; ok {
|
||||||
case "go.mod", "go.sum", "plugin.go", filepath.Join("config", ".gitkeep"):
|
|
||||||
return nil
|
return nil
|
||||||
default:
|
}
|
||||||
|
if strings.HasPrefix(filepath.Base(rel), "registry.gen.go.") {
|
||||||
extra = append(extra, rel)
|
extra = append(extra, rel)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
extra = append(extra, rel)
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -401,9 +499,14 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
|||||||
t.Fatalf("unexpected scaffold files: %v", extra)
|
t.Fatalf("unexpected scaffold files: %v", extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := execGo(t, pluginDir, "build", "-o", filepath.Join(pluginDir, "demo.bin"), ".")
|
for _, asset := range []string{
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
filepath.Join("config", "config.yaml"),
|
||||||
t.Fatalf("plugin does not compile: %v\n%s", err, out)
|
filepath.Join("lang", "en", "lang.yaml"),
|
||||||
|
filepath.Join("views", "mail", "welcome.htm"),
|
||||||
|
} {
|
||||||
|
if strings.HasPrefix(filepath.Base(asset), ".") || strings.HasPrefix(filepath.Base(asset), "_") {
|
||||||
|
t.Fatalf("embed-incompatible hidden asset %s", asset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
95
internal/build/registry.go
Normal file
95
internal/build/registry.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
const registryFileName = "registry.gen.go"
|
||||||
|
|
||||||
|
type registryRef struct {
|
||||||
|
Package string
|
||||||
|
Ident string
|
||||||
|
}
|
||||||
|
|
||||||
|
type registryData struct {
|
||||||
|
Package string
|
||||||
|
Module string
|
||||||
|
Imports []string
|
||||||
|
Models []registryRef
|
||||||
|
Migrations []registryRef
|
||||||
|
Commands []registryRef
|
||||||
|
Jobs []registryRef
|
||||||
|
AdminControllers []registryRef
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptyRegistry(pkg, module string) registryData {
|
||||||
|
data := registryData{Package: pkg, Module: module}
|
||||||
|
data.prepare()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *registryData) prepare() {
|
||||||
|
d.Imports = uniqueLeafImports(d.Module, d.Models, d.Migrations, d.Commands, d.Jobs, d.AdminControllers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueLeafImports(module string, groups ...[]registryRef) []string {
|
||||||
|
if module == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
var imports []string
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, ref := range group {
|
||||||
|
if ref.Package == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := module + "/" + ref.Package
|
||||||
|
if _, ok := seen[path]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[path] = struct{}{}
|
||||||
|
imports = append(imports, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(imports)
|
||||||
|
return imports
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRegistry(pluginDir string, data registryData) error {
|
||||||
|
data.prepare()
|
||||||
|
src, err := renderStub("registry.go", data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := filepath.Join(pluginDir, registryFileName)
|
||||||
|
existing, err := os.ReadFile(path)
|
||||||
|
if err == nil && bytes.Equal(existing, src) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("build: read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
tmp, err := os.CreateTemp(pluginDir, registryFileName+".*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("build: create %s temp: %w", registryFileName, err)
|
||||||
|
}
|
||||||
|
tmpName := tmp.Name()
|
||||||
|
if _, err := tmp.Write(src); err != nil {
|
||||||
|
tmp.Close()
|
||||||
|
os.Remove(tmpName)
|
||||||
|
return fmt.Errorf("build: write %s temp: %w", registryFileName, err)
|
||||||
|
}
|
||||||
|
if err := tmp.Close(); err != nil {
|
||||||
|
os.Remove(tmpName)
|
||||||
|
return fmt.Errorf("build: close %s temp: %w", registryFileName, err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmpName, path); err != nil {
|
||||||
|
os.Remove(tmpName)
|
||||||
|
return fmt.Errorf("build: rename %s: %w", registryFileName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package build
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/format"
|
"go/format"
|
||||||
@@ -13,8 +14,34 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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 (
|
const (
|
||||||
frameworkModule = "git.golem15.com/golem15/summercms"
|
frameworkModule = "git.golem15.com/golem15/summercms"
|
||||||
goVersionLine = "go 1.27.0"
|
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)
|
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)
|
replace, err := frameworkReplaceFor(pluginDir, appDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(pluginDir, "go.mod"), pluginGoMod(modPath, replace), 0o644); err != nil {
|
data := stubData{
|
||||||
return "", fmt.Errorf("build: write go.mod: %w", err)
|
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 {
|
if err := runGo(ctx, pluginDir, "mod", "tidy"); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -156,39 +177,83 @@ func pluginName(id string) string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
func pluginGoSource(pkg, id string) ([]byte, error) {
|
func writePluginScaffold(pluginDir string, data stubData) error {
|
||||||
var b strings.Builder
|
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
|
||||||
b.WriteString("package " + pkg + "\n\n")
|
return fmt.Errorf("build: create plugin dir: %w", err)
|
||||||
b.WriteString("import (\n")
|
}
|
||||||
b.WriteString("\t\"git.golem15.com/golem15/summercms/backpack\"\n")
|
pluginSrc, err := renderStub("plugin.go", data)
|
||||||
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()))
|
|
||||||
if err != nil {
|
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
|
return formatted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pluginGoMod(modPath, replace string) []byte {
|
func writeFile(path string, data []byte) error {
|
||||||
var b strings.Builder
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
fmt.Fprintf(&b, "module %s\n\n", modPath)
|
return fmt.Errorf("build: mkdir %s: %w", filepath.Dir(path), err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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) {
|
func frameworkReplaceFor(pluginDir, appDir string) (string, error) {
|
||||||
|
|||||||
109
internal/build/stubs/plugin.tmpl
Normal file
109
internal/build/stubs/plugin.tmpl
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{{define "plugin.go"}}package {{.Package}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/bonfire"
|
||||||
|
"git.golem15.com/golem15/summercms/pact"
|
||||||
|
"git.golem15.com/golem15/summercms/party"
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ pact.HasConfig = (*Plugin)(nil)
|
||||||
|
_ pact.HasLang = (*Plugin)(nil)
|
||||||
|
_ pact.HasMailTemplates = (*Plugin)(nil)
|
||||||
|
_ pact.HasModels = (*Plugin)(nil)
|
||||||
|
_ pact.HasMigrations = (*Plugin)(nil)
|
||||||
|
_ pact.HasCommands = (*Plugin)(nil)
|
||||||
|
_ pact.HasJobs = (*Plugin)(nil)
|
||||||
|
_ pact.HasAdminControllers = (*Plugin)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed config
|
||||||
|
var configFS embed.FS
|
||||||
|
|
||||||
|
//go:embed lang
|
||||||
|
var langFS embed.FS
|
||||||
|
|
||||||
|
//go:embed views/mail
|
||||||
|
var mailFS embed.FS
|
||||||
|
|
||||||
|
// Plugin is the {{.ID}} plugin.
|
||||||
|
type Plugin struct{}
|
||||||
|
|
||||||
|
func (p *Plugin) ID() string { return {{printf "%q" .ID}} }
|
||||||
|
func (p *Plugin) Requires() []string { return nil }
|
||||||
|
|
||||||
|
func (p *Plugin) Register(*backpack.App) error { return nil }
|
||||||
|
func (p *Plugin) Boot(*backpack.App) error { return nil }
|
||||||
|
|
||||||
|
func (p *Plugin) ConfigFS() fs.FS { return configFS }
|
||||||
|
func (p *Plugin) LangFS() fs.FS { return langFS }
|
||||||
|
|
||||||
|
func (p *Plugin) MailTemplatesFS() fs.FS { return mailFS }
|
||||||
|
func (p *Plugin) MailTemplates() []string { return nil }
|
||||||
|
func (p *Plugin) MailLayouts() map[string]string { return nil }
|
||||||
|
|
||||||
|
func (p *Plugin) Models() []any { return generatedModels() }
|
||||||
|
func (p *Plugin) Migrations() []*gormigrate.Migration { return generatedMigrations() }
|
||||||
|
func (p *Plugin) Commands() []bonfire.Command { return generatedCommands() }
|
||||||
|
func (p *Plugin) Jobs() []pact.Job { return generatedJobs() }
|
||||||
|
func (p *Plugin) AdminControllers() []pact.AdminController {
|
||||||
|
return generatedAdminControllers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
party.Register(&Plugin{})
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "routes.go"}}package {{.Package}}
|
||||||
|
|
||||||
|
import "git.golem15.com/golem15/summercms/pact"
|
||||||
|
|
||||||
|
var _ pact.HasRoutes = (*Plugin)(nil)
|
||||||
|
|
||||||
|
func (p *Plugin) Routes(pact.Router) error { return nil }
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "go.mod"}}module {{.Module}}
|
||||||
|
|
||||||
|
{{.GoVersion}}
|
||||||
|
|
||||||
|
{{.Toolchain}}
|
||||||
|
|
||||||
|
require {{.Framework}} v0.0.0
|
||||||
|
{{if .Replace}}
|
||||||
|
replace {{.Framework}} => {{.Replace}}
|
||||||
|
{{end}}{{end}}
|
||||||
|
|
||||||
|
{{define "models/doc.go"}}// Package models holds GORM models for the {{.ID}} plugin.
|
||||||
|
package models
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "classes/doc.go"}}// Package classes holds services and hooks for the {{.ID}} plugin.
|
||||||
|
package classes
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "controllers/doc.go"}}// Package controllers holds HTTP handlers and admin controllers for the {{.ID}} plugin.
|
||||||
|
package controllers
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "console/doc.go"}}// Package console holds bonfire commands for the {{.ID}} plugin.
|
||||||
|
package console
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "jobs/doc.go"}}// Package jobs holds background jobs for the {{.ID}} plugin.
|
||||||
|
package jobs
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "middleware/doc.go"}}// Package middleware holds named HTTP middleware for the {{.ID}} plugin.
|
||||||
|
package middleware
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "updates/doc.go"}}// Package updates holds the gormigrate set for the {{.ID}} plugin.
|
||||||
|
package updates
|
||||||
|
{{end}}
|
||||||
73
internal/build/stubs/registry.tmpl
Normal file
73
internal/build/stubs/registry.tmpl
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{{define "registry.go"}}// Code generated by summer make. DO NOT EDIT.
|
||||||
|
|
||||||
|
package {{.Package}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.golem15.com/golem15/summercms/bonfire"
|
||||||
|
"git.golem15.com/golem15/summercms/pact"
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
{{- range .Imports}}
|
||||||
|
{{printf "%q" .}}
|
||||||
|
{{- end}}
|
||||||
|
)
|
||||||
|
|
||||||
|
func generatedModels() []any {
|
||||||
|
{{- if .Models}}
|
||||||
|
return []any{
|
||||||
|
{{- range .Models}}
|
||||||
|
&{{.Package}}.{{.Ident}}{},
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{- else}}
|
||||||
|
return nil
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatedMigrations() []*gormigrate.Migration {
|
||||||
|
{{- if .Migrations}}
|
||||||
|
return []*gormigrate.Migration{
|
||||||
|
{{- range .Migrations}}
|
||||||
|
{{.Package}}.{{.Ident}}(),
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{- else}}
|
||||||
|
return nil
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatedCommands() []bonfire.Command {
|
||||||
|
{{- if .Commands}}
|
||||||
|
return []bonfire.Command{
|
||||||
|
{{- range .Commands}}
|
||||||
|
{{.Package}}.{{.Ident}}(),
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{- else}}
|
||||||
|
return nil
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatedJobs() []pact.Job {
|
||||||
|
{{- if .Jobs}}
|
||||||
|
return []pact.Job{
|
||||||
|
{{- range .Jobs}}
|
||||||
|
{{.Package}}.{{.Ident}}(),
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{- else}}
|
||||||
|
return nil
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatedAdminControllers() []pact.AdminController {
|
||||||
|
{{- if .AdminControllers}}
|
||||||
|
return []pact.AdminController{
|
||||||
|
{{- range .AdminControllers}}
|
||||||
|
{{.Package}}.{{.Ident}}(),
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{- else}}
|
||||||
|
return nil
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package pact
|
package pact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -53,6 +54,53 @@ type HasModels interface {
|
|||||||
Models() []any
|
Models() []any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobArgs is the typed payload a job worker receives. Kind identifies the
|
||||||
|
// job so a worker can reject unexpected argument types.
|
||||||
|
type JobArgs interface {
|
||||||
|
Kind() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job is a unit of background work. Phase 11 adapts this onto River; the
|
||||||
|
// interface itself must not import River.
|
||||||
|
type Job interface {
|
||||||
|
Work(ctx context.Context, args JobArgs) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasJobs is implemented by plugins that register background jobs.
|
||||||
|
type HasJobs interface {
|
||||||
|
Jobs() []Job
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminController is the compile-time admin controller contract. Phase 9
|
||||||
|
// grows the schema pipeline; ID, model name and YAML config directory are
|
||||||
|
// enough for generated stubs to compile.
|
||||||
|
type AdminController interface {
|
||||||
|
ID() string
|
||||||
|
ModelName() string
|
||||||
|
ConfigDir() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasAdminControllers is implemented by plugins that register admin controllers.
|
||||||
|
type HasAdminControllers interface {
|
||||||
|
AdminControllers() []AdminController
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasLang is implemented by plugins that ship embedded translation YAML
|
||||||
|
// under lang/<locale>/<group>.yaml.
|
||||||
|
type HasLang interface {
|
||||||
|
LangFS() fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMailTemplates is implemented by plugins that ship Winter-shaped mail
|
||||||
|
// templates and layout aliases. MailTemplatesFS contains views/mail assets;
|
||||||
|
// MailTemplates lists dotted template names; MailLayouts maps a short layout
|
||||||
|
// name to the full dotted layout name.
|
||||||
|
type HasMailTemplates interface {
|
||||||
|
MailTemplatesFS() fs.FS
|
||||||
|
MailTemplates() []string
|
||||||
|
MailLayouts() map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
// OptionalMessage is a service an optional plugin may publish so other
|
// OptionalMessage is a service an optional plugin may publish so other
|
||||||
// plugins can integrate without importing that plugin's package.
|
// plugins can integrate without importing that plugin's package.
|
||||||
type OptionalMessage interface {
|
type OptionalMessage interface {
|
||||||
@@ -62,14 +110,10 @@ type OptionalMessage interface {
|
|||||||
// Future capability families are type-asserted when their first consumer
|
// Future capability families are type-asserted when their first consumer
|
||||||
// packages exist:
|
// packages exist:
|
||||||
//
|
//
|
||||||
// HasJobs
|
|
||||||
// HasListeners
|
// HasListeners
|
||||||
// HasAdminControllers
|
|
||||||
// HasNavigation
|
// HasNavigation
|
||||||
// HasPermissions
|
// HasPermissions
|
||||||
// HasSchedule
|
// HasSchedule
|
||||||
// HasMailTemplates
|
|
||||||
// HasLang
|
|
||||||
//
|
//
|
||||||
// The kernel type-asserts HasConfig (party, before Register), HasCommands
|
// The kernel type-asserts HasConfig (party, before Register), HasCommands
|
||||||
// (generated app main, after Boot), HasMigrations (lagoon migrate), and
|
// (generated app main, after Boot), HasMigrations (lagoon migrate), and
|
||||||
|
|||||||
Reference in New Issue
Block a user