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:
Jakub Zych
2026-10-28 18:55:46 +01:00
parent a941ab2394
commit a95a014b52
26 changed files with 247 additions and 86 deletions

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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/

View File

@@ -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.

View File

@@ -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):

View File

@@ -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.

View File

@@ -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

View File

@@ -4,5 +4,5 @@ toolchain go1.27.0
use (
.
./plugins/demo
./plugins/golem15/demo
)

View File

@@ -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.

View File

@@ -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