fix(build): scaffold plugins under plugins/<vendor>/<name> (quick-261028-pq8)
- make:plugin writes plugins/<vendor>/<name> with module <app>/plugins/<vendor>/<name>; the package name stays <name> - make:* and the models-leaf check look in summer.yaml, then plugins/<vendor>/<name>, then the older plugins/<name> - summer new puts its starter plugin at plugins/golem15/<name> - refuse a vendor directory that is a legacy plugin module and a second copy of a plugin that already sits in plugins/<name> - help text and docs, with an upgrading note: moving is optional
This commit is contained in:
@@ -451,21 +451,14 @@ func locatePluginDir(appDir, id string) (string, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
candidate := filepath.Join(appDir, "plugins", pluginName(id))
|
||||
if !underRoot(appDir, candidate) {
|
||||
return "", fmt.Errorf("build: plugin path %q escapes the app root", candidate)
|
||||
}
|
||||
st, err := os.Stat(candidate)
|
||||
dir, err := defaultPluginDir(appDir, id)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", pluginMissingHint(fmt.Errorf("build: plugin %s not found", id), id)
|
||||
}
|
||||
return "", fmt.Errorf("build: stat %s: %w", candidate, err)
|
||||
return "", err
|
||||
}
|
||||
if !st.IsDir() {
|
||||
if dir == "" {
|
||||
return "", pluginMissingHint(fmt.Errorf("build: plugin %s not found", id), id)
|
||||
}
|
||||
return candidate, nil
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func finishArtifact(ctx context.Context, plugin pluginRef) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
@@ -248,16 +250,16 @@ func TestMakeAndAddPluginOnHelloCopy(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantDir := filepath.Join(dir, "plugins", "demo")
|
||||
wantDir := filepath.Join(dir, "plugins", "golem15", "demo")
|
||||
if pluginDir != wantDir {
|
||||
t.Fatalf("plugin dir = %s, want %s", pluginDir, wantDir)
|
||||
}
|
||||
assertScaffoldFiles(t, pluginDir)
|
||||
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "golem15", "demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "golem15", "demo")); err != nil {
|
||||
t.Fatalf("repeat plugin:add: %v", err)
|
||||
}
|
||||
|
||||
@@ -272,7 +274,7 @@ func TestMakeAndAddPluginOnHelloCopy(t *testing.T) {
|
||||
if last.ID != "golem15.demo" {
|
||||
t.Fatalf("last plugin id = %q", last.ID)
|
||||
}
|
||||
if last.Module != m.Module+"/plugins/demo" {
|
||||
if last.Module != m.Module+"/plugins/golem15/demo" {
|
||||
t.Fatalf("last plugin module = %q", last.Module)
|
||||
}
|
||||
demoCount := 0
|
||||
@@ -289,8 +291,8 @@ func TestMakeAndAddPluginOnHelloCopy(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(work, []byte("./plugins/demo")) {
|
||||
t.Fatalf("go.work missing ./plugins/demo:\n%s", work)
|
||||
if !bytes.Contains(work, []byte("./plugins/golem15/demo")) {
|
||||
t.Fatalf("go.work missing ./plugins/golem15/demo:\n%s", work)
|
||||
}
|
||||
// plugin:add writes go.work use paths relative to the go.work directory,
|
||||
// so a committed go.work carries no machine-specific path.
|
||||
@@ -376,15 +378,15 @@ func TestAddPluginRejectsConflictingID(t *testing.T) {
|
||||
if _, err := MakePlugin(ctx, dir, "golem15.demo"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src, err := os.ReadFile(filepath.Join(dir, "plugins", "demo", "plugin.go"))
|
||||
src, err := os.ReadFile(filepath.Join(dir, "plugins", "golem15", "demo", "plugin.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
patched := bytes.ReplaceAll(src, []byte(`return "golem15.demo"`), []byte(`return "golem15.hello"`))
|
||||
if err := os.WriteFile(filepath.Join(dir, "plugins", "demo", "plugin.go"), patched, 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, "plugins", "golem15", "demo", "plugin.go"), patched, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = AddPlugin(ctx, dir, filepath.Join(dir, "plugins", "demo"))
|
||||
err = AddPlugin(ctx, dir, filepath.Join(dir, "plugins", "golem15", "demo"))
|
||||
if err == nil || !strings.Contains(err.Error(), "golem15.hello") {
|
||||
t.Fatalf("want conflicting id error, got %v", err)
|
||||
}
|
||||
@@ -510,7 +512,7 @@ func TestScaffoldPluginSmoke(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "golem15", "demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
@@ -691,7 +693,7 @@ func TestScaffoldCoreArtifacts(t *testing.T) {
|
||||
t.Fatal("handwritten greeter plugin.go was rewritten")
|
||||
}
|
||||
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "golem15", "demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
@@ -913,7 +915,7 @@ func TestScaffoldAllArtifacts(t *testing.T) {
|
||||
t.Fatal("handwritten plugin.go was rewritten")
|
||||
}
|
||||
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "golem15", "demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
@@ -938,7 +940,7 @@ func TestModelsLeaf(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "golem15", "demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
modPath, err := readModulePath(filepath.Join(pluginDir, "go.mod"))
|
||||
@@ -1183,7 +1185,7 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
||||
if !bytes.Contains(mod, []byte("toolchain go1.27.0")) {
|
||||
t.Fatalf("plugin go.mod missing toolchain:\n%s", mod)
|
||||
}
|
||||
if !bytes.Contains(mod, []byte("/plugins/demo")) {
|
||||
if !bytes.Contains(mod, []byte("/plugins/golem15/demo")) {
|
||||
t.Fatalf("plugin go.mod missing module path:\n%s", mod)
|
||||
}
|
||||
|
||||
@@ -1424,3 +1426,95 @@ func TestNextMigrationFileSortsAfterExisting(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMakePluginVendorDirs: make:plugin writes plugins/<vendor>/<name>, so two
|
||||
// vendors' plugins with the same name live side by side and both build.
|
||||
func TestMakePluginVendorDirs(t *testing.T) {
|
||||
dir := copyHelloApp(t)
|
||||
ctx := t.Context()
|
||||
m, err := LoadManifest(filepath.Join(dir, "summer.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct{ id, vendor string }{
|
||||
{"acme.blog", "acme"},
|
||||
{"pixelpixel.blog", "pixelpixel"},
|
||||
} {
|
||||
pluginDir, err := MakePlugin(ctx, dir, tc.id)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", tc.id, err)
|
||||
}
|
||||
if want := filepath.Join(dir, "plugins", tc.vendor, "blog"); pluginDir != want {
|
||||
t.Fatalf("%s dir = %s, want %s", tc.id, pluginDir, want)
|
||||
}
|
||||
mod, err := readModulePath(filepath.Join(pluginDir, "go.mod"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := m.Module + "/plugins/" + tc.vendor + "/blog"; mod != want {
|
||||
t.Fatalf("%s module = %s, want %s", tc.id, mod, want)
|
||||
}
|
||||
if pkg, err := packageNameOf(filepath.Join(pluginDir, "plugin.go")); err != nil || pkg != "blog" {
|
||||
t.Fatalf("%s package = %q, %v", tc.id, pkg, err)
|
||||
}
|
||||
if out, err := execGo(t, pluginDir, "build", "./...").CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s does not build: %v\n%s", tc.id, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = MakePlugin(ctx, dir, "acme.blog")
|
||||
if err == nil {
|
||||
t.Fatal("second make:plugin acme.blog want error")
|
||||
}
|
||||
var hint *bonfire.HintError
|
||||
if !errors.As(err, &hint) || !strings.Contains(hint.Try, "plugin:add plugins/acme/blog") {
|
||||
t.Fatalf("existing-directory hint does not name plugins/acme/blog: %#v", hint)
|
||||
}
|
||||
|
||||
// examples/hello keeps plugins/greeter in the older layout: a vendor
|
||||
// named greeter would nest a module inside it.
|
||||
if _, err := MakePlugin(ctx, dir, "greeter.extra"); err == nil || !strings.Contains(err.Error(), "not a vendor directory") {
|
||||
t.Fatalf("make:plugin inside a legacy plugin module: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocatePluginLegacyLayout: a plugin in the older plugins/<name> layout,
|
||||
// not listed in summer.yaml, is still found by the make: commands.
|
||||
func TestLocatePluginLegacyLayout(t *testing.T) {
|
||||
dir := copyHelloApp(t)
|
||||
ctx := t.Context()
|
||||
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
legacy := filepath.Join(dir, "plugins", "demo")
|
||||
if err := os.Rename(pluginDir, legacy); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := locatePluginDir(dir, "golem15.demo")
|
||||
if err != nil || got != legacy {
|
||||
t.Fatalf("locatePluginDir = %q, %v; want %s", got, err, legacy)
|
||||
}
|
||||
res, err := MakeModel(ctx, dir, "golem15.demo", "Album", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.PluginDir != legacy {
|
||||
t.Fatalf("make:model plugin dir = %s, want %s", res.PluginDir, legacy)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(legacy, "models", "album.go")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := MakePlugin(ctx, dir, "golem15.demo"); err == nil || !strings.Contains(err.Error(), "already exists") {
|
||||
t.Fatalf("make:plugin over a legacy copy: %v", err)
|
||||
}
|
||||
|
||||
// With both layouts present, plugins/<vendor>/<name> wins.
|
||||
vendorDir := filepath.Join(dir, "plugins", "golem15", "demo")
|
||||
if err := os.MkdirAll(vendorDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := locatePluginDir(dir, "golem15.demo"); err != nil || got != vendorDir {
|
||||
t.Fatalf("locatePluginDir with both layouts = %q, %v; want %s", got, err, vendorDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ func checkModelsLeaf(appDir string, m Manifest) error {
|
||||
for _, p := range m.Plugins {
|
||||
pluginDir := localModuleDir(appDir, p.Module)
|
||||
if pluginDir == "" {
|
||||
pluginDir = filepath.Join(appDir, "plugins", pluginName(p.ID))
|
||||
dir, err := defaultPluginDir(appDir, p.ID)
|
||||
if err != nil || dir == "" {
|
||||
continue
|
||||
}
|
||||
pluginDir = dir
|
||||
}
|
||||
if !underRoot(appDir, pluginDir) {
|
||||
continue
|
||||
|
||||
@@ -104,9 +104,9 @@ func ValidateAppName(name string) error {
|
||||
}
|
||||
switch name {
|
||||
case "main":
|
||||
return fmt.Errorf("build: app name %q is reserved: plugins/main would be a program, not an importable package", name)
|
||||
return fmt.Errorf("build: app name %q is reserved: plugins/golem15/main would be a program, not an importable package", name)
|
||||
case "internal":
|
||||
return fmt.Errorf("build: app name %q is reserved: Go forbids importing plugins/internal from the app", name)
|
||||
return fmt.Errorf("build: app name %q is reserved: Go forbids importing plugins/golem15/internal from the app", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -459,5 +459,5 @@ func addStarterPlugin(ctx context.Context, name, target string) error {
|
||||
if _, err := makePlugin(ctx, target, "golem15."+name, pluginOptions{StarterRoute: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
return AddPlugin(ctx, target, filepath.Join("plugins", name))
|
||||
return AddPlugin(ctx, target, filepath.Join("plugins", "golem15", name))
|
||||
}
|
||||
|
||||
@@ -135,15 +135,15 @@ func TestNewGolden(t *testing.T) {
|
||||
"go.mod", "go.work", "summer.yaml", "main.go", "plugins.gen.go",
|
||||
"config/app.yaml", "config/database.yaml", "config/http.yaml", "config/storage.yaml", "config/log.yaml",
|
||||
".env", ".env.example", ".gitignore", "compose.yaml", "CLAUDE.md",
|
||||
"plugins/demo/plugin.go", "plugins/demo/routes.go",
|
||||
"plugins/golem15/demo/plugin.go", "plugins/golem15/demo/routes.go",
|
||||
} {
|
||||
assertGolden(t, "new", rel, readAppFile(t, dir, rel))
|
||||
}
|
||||
assertGolden(t, "new", "next-steps.txt", out)
|
||||
|
||||
pluginMod := readAppFile(t, dir, "plugins/demo/go.mod")
|
||||
if !bytes.Contains(pluginMod, []byte("replace git.golem15.com/golem15/summercms => ../../../summercms")) {
|
||||
t.Errorf("plugins/demo/go.mod lacks the relative framework replace:\n%s", pluginMod)
|
||||
pluginMod := readAppFile(t, dir, "plugins/golem15/demo/go.mod")
|
||||
if !bytes.Contains(pluginMod, []byte("replace git.golem15.com/golem15/summercms => ../../../../summercms")) {
|
||||
t.Errorf("plugins/golem15/demo/go.mod lacks the relative framework replace:\n%s", pluginMod)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.sum")); !os.IsNotExist(err) {
|
||||
t.Errorf("app root go.sum exists or stat failed: %v", err)
|
||||
|
||||
@@ -141,8 +141,8 @@ func TestNewAppSmoke(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(work, []byte("./plugins/demo")) {
|
||||
t.Fatalf("go.work lacks ./plugins/demo:\n%s", work)
|
||||
if !bytes.Contains(work, []byte("./plugins/golem15/demo")) {
|
||||
t.Fatalf("go.work lacks ./plugins/golem15/demo:\n%s", work)
|
||||
}
|
||||
for _, line := range strings.Split(string(work), "\n") {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "/") {
|
||||
@@ -555,7 +555,7 @@ func TestNewGitFirstCommit(t *testing.T) {
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, want := range []string{".env.example", ".gitignore", "go.work", "main.go", "plugins.gen.go", "AGENTS.md", "CLAUDE.md", "summer.yaml", "plugins/demo/plugin.go"} {
|
||||
for _, want := range []string{".env.example", ".gitignore", "go.work", "main.go", "plugins.gen.go", "AGENTS.md", "CLAUDE.md", "summer.yaml", "plugins/golem15/demo/plugin.go"} {
|
||||
if !has(want) {
|
||||
t.Errorf("%s is not tracked: %v", want, tracked)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ const (
|
||||
toolchainLine = "toolchain go1.27.0"
|
||||
)
|
||||
|
||||
// MakePlugin scaffolds a compiling plugin module under plugins/<name>.
|
||||
// MakePlugin scaffolds a compiling plugin module under plugins/<vendor>/<name>
|
||||
// for the ID <vendor>.<name>; its module path is the application module
|
||||
// followed by /plugins/<vendor>/<name>, and its Go package is <name>.
|
||||
func MakePlugin(ctx context.Context, startDir, id string) (string, error) {
|
||||
return makePlugin(ctx, startDir, id, pluginOptions{})
|
||||
}
|
||||
@@ -73,24 +75,39 @@ func makePlugin(ctx context.Context, startDir, id string, opts pluginOptions) (s
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := pluginName(id)
|
||||
pluginDir := filepath.Join(appDir, "plugins", name)
|
||||
vendor, name := pluginVendor(id), pluginName(id)
|
||||
pluginDir := filepath.Join(appDir, "plugins", vendor, name)
|
||||
if !underRoot(appDir, pluginDir) {
|
||||
return "", fmt.Errorf("build: plugin path %q escapes the app root", pluginDir)
|
||||
}
|
||||
addArg := "plugins/" + vendor + "/" + name
|
||||
if _, err := os.Stat(pluginDir); err == nil {
|
||||
return "", bonfire.WithHint(fmt.Errorf("build: plugin directory %s already exists", pluginDir),
|
||||
"summer make:plugin creates a new directory under plugins/ and never overwrites one; register an existing plugin with plugin:add.",
|
||||
"{bin} plugin:add "+filepath.Join("plugins", name))
|
||||
"{bin} plugin:add "+addArg)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("build: stat %s: %w", pluginDir, err)
|
||||
}
|
||||
// A legacy plugins/<vendor> module would swallow the new directory, and a
|
||||
// legacy plugins/<name> with this ID is the same plugin.
|
||||
vendorDir := filepath.Join(appDir, "plugins", vendor)
|
||||
if _, err := os.Stat(filepath.Join(vendorDir, "go.mod")); err == nil {
|
||||
return "", bonfire.WithHint(fmt.Errorf("build: %s is a plugin module, not a vendor directory", vendorDir),
|
||||
"summer make:plugin writes plugins/<vendor>/<name>; plugins/"+vendor+" already holds a plugin in the older plugins/<name> layout. Move that plugin first, or choose another vendor.",
|
||||
"{bin} make:plugin <vendor>."+name)
|
||||
}
|
||||
legacyDir := filepath.Join(appDir, "plugins", name)
|
||||
if got, err := pluginIDFromGo(filepath.Join(legacyDir, "plugin.go")); err == nil && got == id {
|
||||
return "", bonfire.WithHint(fmt.Errorf("build: plugin %s already exists in %s", id, legacyDir),
|
||||
"summer make:plugin never creates a second copy of a plugin; register the existing one with plugin:add.",
|
||||
"{bin} plugin:add plugins/"+name)
|
||||
}
|
||||
|
||||
m, err := LoadManifest(filepath.Join(appDir, manifestName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
modPath := m.Module + "/plugins/" + name
|
||||
modPath := m.Module + "/" + addArg
|
||||
if err := validateModulePath(modPath); err != nil {
|
||||
return "", fmt.Errorf("build: plugin module: %w", err)
|
||||
}
|
||||
@@ -197,6 +214,44 @@ func pluginName(id string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
// pluginVendor returns the vendor part of a plugin ID, before the dot.
|
||||
func pluginVendor(id string) string {
|
||||
vendor, _, _ := strings.Cut(id, ".")
|
||||
return vendor
|
||||
}
|
||||
|
||||
// pluginDirCandidates lists the default directories of a plugin that
|
||||
// summer.yaml does not place, in lookup order: plugins/<vendor>/<name>, the
|
||||
// layout make:plugin writes, then the older plugins/<name>.
|
||||
func pluginDirCandidates(appDir, id string) []string {
|
||||
return []string{
|
||||
filepath.Join(appDir, "plugins", pluginVendor(id), pluginName(id)),
|
||||
filepath.Join(appDir, "plugins", pluginName(id)),
|
||||
}
|
||||
}
|
||||
|
||||
// defaultPluginDir returns the first of pluginDirCandidates that is an
|
||||
// existing directory, or "" when none is. A candidate outside the app root is
|
||||
// an error.
|
||||
func defaultPluginDir(appDir, id string) (string, error) {
|
||||
for _, dir := range pluginDirCandidates(appDir, id) {
|
||||
if !underRoot(appDir, dir) {
|
||||
return "", fmt.Errorf("build: plugin path %q escapes the app root", dir)
|
||||
}
|
||||
st, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("build: stat %s: %w", dir, err)
|
||||
}
|
||||
if st.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func writePluginScaffold(pluginDir string, data stubData) error {
|
||||
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
|
||||
return fmt.Errorf("build: create plugin dir: %w", err)
|
||||
|
||||
@@ -141,8 +141,8 @@ summer build # generate main.go and plugins.gen.go, build
|
||||
summer dev # watch sources and rebuild
|
||||
summer migrate # run plugin migrations
|
||||
summer serve # start the HTTP server
|
||||
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/<name>
|
||||
summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.work
|
||||
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/golem15/<name>
|
||||
summer plugin:add plugins/<vendor>/<name> # register it in summer.yaml, go.mod and go.work
|
||||
summer make:admin-controller golem15.<plugin> <Name> # scaffold an admin controller with its YAML
|
||||
summer admin:create --email you@example.com --superuser # create a backend administrator (password prompted)
|
||||
```
|
||||
@@ -150,7 +150,7 @@ summer admin:create --email you@example.com --superuser # create a backend admi
|
||||
## Layout
|
||||
|
||||
- `summer.yaml`: the application manifest (module path, binary name, ordered plugin list).
|
||||
- `plugins/`: local plugin modules.{{if not .NoPlugin}} `plugins/{{.Name}}` is the starter plugin `golem15.{{.Name}}`; its `routes.go` answers `GET /` with JSON.{{end}}
|
||||
- `plugins/`: local plugin modules, one directory per vendor (`plugins/<vendor>/<name>`).{{if not .NoPlugin}} `plugins/golem15/{{.Name}}` is the starter plugin `golem15.{{.Name}}`; its `routes.go` answers `GET /` with JSON.{{end}}
|
||||
- `config/`: YAML configuration (`app.yaml`, `database.yaml`, `http.yaml`, `log.yaml`, `storage.yaml`). Any key can be overridden with a `SUMMER_` environment variable, a double underscore separating path segments (`SUMMER_DATABASE__DSN` sets `database.dsn`).
|
||||
- `.env`: local secrets (database DSN, application key, admin token secret). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
|
||||
- `compose.yaml`: the local PostgreSQL 16.
|
||||
@@ -184,7 +184,7 @@ Next steps, inside sm-{{.Name}}-app:
|
||||
summer serve
|
||||
{{if .NoPlugin}}
|
||||
The app has no plugins yet. Add one with summer make:plugin golem15.<name>
|
||||
and summer plugin:add plugins/<name>, then run summer build again.
|
||||
and summer plugin:add plugins/golem15/<name>, then run summer build again.
|
||||
{{else}}
|
||||
Then, from another terminal:
|
||||
curl http://127.0.0.1:8080/
|
||||
|
||||
@@ -21,8 +21,8 @@ summer build # generate main.go and plugins.gen.go, build
|
||||
summer dev # watch sources and rebuild
|
||||
summer migrate # run plugin migrations
|
||||
summer serve # start the HTTP server
|
||||
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/<name>
|
||||
summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.work
|
||||
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/golem15/<name>
|
||||
summer plugin:add plugins/<vendor>/<name> # register it in summer.yaml, go.mod and go.work
|
||||
summer make:admin-controller golem15.<plugin> <Name> # scaffold an admin controller with its YAML
|
||||
summer admin:create --email you@example.com --superuser # create a backend administrator (password prompted)
|
||||
```
|
||||
@@ -30,7 +30,7 @@ summer admin:create --email you@example.com --superuser # create a backend admi
|
||||
## Layout
|
||||
|
||||
- `summer.yaml`: the application manifest (module path, binary name, ordered plugin list).
|
||||
- `plugins/`: local plugin modules.
|
||||
- `plugins/`: local plugin modules, one directory per vendor (`plugins/<vendor>/<name>`).
|
||||
- `config/`: YAML configuration (`app.yaml`, `database.yaml`, `http.yaml`, `log.yaml`, `storage.yaml`). Any key can be overridden with a `SUMMER_` environment variable, a double underscore separating path segments (`SUMMER_DATABASE__DSN` sets `database.dsn`).
|
||||
- `.env`: local secrets (database DSN, application key, admin token secret). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
|
||||
- `compose.yaml`: the local PostgreSQL 16.
|
||||
|
||||
@@ -7,7 +7,7 @@ Next steps, inside sm-demo-app:
|
||||
summer serve
|
||||
|
||||
The app has no plugins yet. Add one with summer make:plugin golem15.<name>
|
||||
and summer plugin:add plugins/<name>, then run summer build again.
|
||||
and summer plugin:add plugins/golem15/<name>, then run summer build again.
|
||||
|
||||
Admin, at http://127.0.0.1:8080/backend once a plugin registers an admin
|
||||
controller (.env already holds SUMMER_ADMIN__JWT__SECRET):
|
||||
|
||||
6
internal/build/testdata/new/CLAUDE.md.golden
vendored
6
internal/build/testdata/new/CLAUDE.md.golden
vendored
@@ -21,8 +21,8 @@ summer build # generate main.go and plugins.gen.go, build
|
||||
summer dev # watch sources and rebuild
|
||||
summer migrate # run plugin migrations
|
||||
summer serve # start the HTTP server
|
||||
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/<name>
|
||||
summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.work
|
||||
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/golem15/<name>
|
||||
summer plugin:add plugins/<vendor>/<name> # register it in summer.yaml, go.mod and go.work
|
||||
summer make:admin-controller golem15.<plugin> <Name> # scaffold an admin controller with its YAML
|
||||
summer admin:create --email you@example.com --superuser # create a backend administrator (password prompted)
|
||||
```
|
||||
@@ -30,7 +30,7 @@ summer admin:create --email you@example.com --superuser # create a backend admi
|
||||
## Layout
|
||||
|
||||
- `summer.yaml`: the application manifest (module path, binary name, ordered plugin list).
|
||||
- `plugins/`: local plugin modules. `plugins/demo` is the starter plugin `golem15.demo`; its `routes.go` answers `GET /` with JSON.
|
||||
- `plugins/`: local plugin modules, one directory per vendor (`plugins/<vendor>/<name>`). `plugins/golem15/demo` is the starter plugin `golem15.demo`; its `routes.go` answers `GET /` with JSON.
|
||||
- `config/`: YAML configuration (`app.yaml`, `database.yaml`, `http.yaml`, `log.yaml`, `storage.yaml`). Any key can be overridden with a `SUMMER_` environment variable, a double underscore separating path segments (`SUMMER_DATABASE__DSN` sets `database.dsn`).
|
||||
- `.env`: local secrets (database DSN, application key, admin token secret). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
|
||||
- `compose.yaml`: the local PostgreSQL 16.
|
||||
|
||||
4
internal/build/testdata/new/go.mod.golden
vendored
4
internal/build/testdata/new/go.mod.golden
vendored
@@ -5,10 +5,10 @@ go 1.27.0
|
||||
toolchain go1.27.0
|
||||
|
||||
require (
|
||||
git.golem15.com/golem15/sm-demo-app/plugins/demo v0.0.0
|
||||
git.golem15.com/golem15/sm-demo-app/plugins/golem15/demo v0.0.0
|
||||
git.golem15.com/golem15/summercms v0.0.0
|
||||
)
|
||||
|
||||
replace git.golem15.com/golem15/summercms => ../summercms
|
||||
|
||||
replace git.golem15.com/golem15/sm-demo-app/plugins/demo => ./plugins/demo
|
||||
replace git.golem15.com/golem15/sm-demo-app/plugins/golem15/demo => ./plugins/golem15/demo
|
||||
|
||||
2
internal/build/testdata/new/go.work.golden
vendored
2
internal/build/testdata/new/go.work.golden
vendored
@@ -4,5 +4,5 @@ toolchain go1.27.0
|
||||
|
||||
use (
|
||||
.
|
||||
./plugins/demo
|
||||
./plugins/golem15/demo
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "git.golem15.com/golem15/sm-demo-app/plugins/demo"
|
||||
_ "git.golem15.com/golem15/sm-demo-app/plugins/golem15/demo"
|
||||
)
|
||||
|
||||
// PluginIDs is the ordered plugin list from summer.yaml.
|
||||
|
||||
@@ -2,4 +2,4 @@ module: git.golem15.com/golem15/sm-demo-app
|
||||
binary: demo
|
||||
plugins:
|
||||
- id: golem15.demo
|
||||
module: git.golem15.com/golem15/sm-demo-app/plugins/demo
|
||||
module: git.golem15.com/golem15/sm-demo-app/plugins/golem15/demo
|
||||
|
||||
Reference in New Issue
Block a user