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) {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(filepath.Join(pluginDir, "go.mod")); err != nil {
|
||||
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)
|
||||
}
|
||||
assertWinterPluginShape(t, pluginDir)
|
||||
|
||||
body, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
if err := filepath.WalkDir(pluginDir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
@@ -387,13 +483,15 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch rel {
|
||||
case "go.mod", "go.sum", "plugin.go", filepath.Join("config", ".gitkeep"):
|
||||
if _, ok := allowed[rel]; ok {
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
if strings.HasPrefix(filepath.Base(rel), "registry.gen.go.") {
|
||||
extra = append(extra, rel)
|
||||
return nil
|
||||
}
|
||||
extra = append(extra, rel)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -401,9 +499,14 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
||||
t.Fatalf("unexpected scaffold files: %v", extra)
|
||||
}
|
||||
|
||||
cmd := execGo(t, pluginDir, "build", "-o", filepath.Join(pluginDir, "demo.bin"), ".")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("plugin does not compile: %v\n%s", err, out)
|
||||
for _, asset := range []string{
|
||||
filepath.Join("config", "config.yaml"),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user