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:
@@ -96,14 +96,14 @@ func makePluginCommand() bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: "make:plugin",
|
||||
Description: "Scaffold a compiling plugin module from a vendor.plugin id",
|
||||
Long: `Creates plugins/<plugin> in the current application: a Go module with plugin.go
|
||||
Long: `Creates plugins/<vendor>/<plugin> in the current application: a Go module with plugin.go
|
||||
(the plugin ID, Register and Boot) and go.mod, with a replace for a local
|
||||
framework checkout when the application uses one. Register it with plugin:add
|
||||
so the next summer build compiles it in. Run it inside an application; the
|
||||
directory must not exist yet.`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: "make:plugin acme.blog", Description: "Create plugins/blog with the ID acme.blog"},
|
||||
{Command: "plugin:add plugins/blog", Description: "Then register it in summer.yaml, go.mod and go.work"},
|
||||
{Command: "make:plugin acme.blog", Description: "Create plugins/acme/blog with the ID acme.blog"},
|
||||
{Command: "plugin:add plugins/acme/blog", Description: "Then register it in summer.yaml, go.mod and go.work"},
|
||||
},
|
||||
Args: []bonfire.Arg{{
|
||||
Name: "id",
|
||||
@@ -142,7 +142,7 @@ plugin picks them up on the next build. Existing files and names are never
|
||||
overwritten.`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: "make:model acme.blog Post", Description: "A Post model and its create-table migration"},
|
||||
{Command: "make:model Post --no-migration", Description: "Inside plugins/blog: only the model"},
|
||||
{Command: "make:model Post --no-migration", Description: "Inside plugins/acme/blog: only the model"},
|
||||
},
|
||||
Args: makeArtifactArgs(),
|
||||
Flags: []bonfire.Flag{{
|
||||
@@ -179,7 +179,7 @@ created them. registry.gen.go adds it to the plugin's migrations; run migrate
|
||||
to apply it.`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: "make:migration acme.blog AddSlugToPosts", Description: "An empty migration in acme.blog"},
|
||||
{Command: "make:migration AddSlugToPosts", Description: "The same from inside plugins/blog"},
|
||||
{Command: "make:migration AddSlugToPosts", Description: "The same from inside plugins/acme/blog"},
|
||||
},
|
||||
Args: makeArtifactArgs(),
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
@@ -212,7 +212,7 @@ takes a parameter instead and is returned from the plugin's Commands by hand
|
||||
(see the porting guide's blog:publish).`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: "make:command acme.blog Publish", Description: "A blog:publish command in acme.blog"},
|
||||
{Command: "make:command Publish", Description: "The same from inside plugins/blog"},
|
||||
{Command: "make:command Publish", Description: "The same from inside plugins/acme/blog"},
|
||||
},
|
||||
Args: makeArtifactArgs(),
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
@@ -243,7 +243,7 @@ generated registry adds it to the jobs the plugin returns. Dispatch it with
|
||||
conga.Manager.Dispatch; queue:work or serve runs it.`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: "make:job acme.blog SendDigest", Description: "A SendDigest job in acme.blog"},
|
||||
{Command: "make:job SendDigest", Description: "The same from inside plugins/blog"},
|
||||
{Command: "make:job SendDigest", Description: "The same from inside plugins/acme/blog"},
|
||||
},
|
||||
Args: makeArtifactArgs(),
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
@@ -276,7 +276,7 @@ Permissions(), and its NewRecord returns nil until you return your model, so
|
||||
the screens stay closed until you finish it.`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: "make:admin-controller acme.blog Posts", Description: "A Posts controller in acme.blog"},
|
||||
{Command: "make:admin-controller Posts", Description: "The same from inside plugins/blog"},
|
||||
{Command: "make:admin-controller Posts", Description: "The same from inside plugins/acme/blog"},
|
||||
},
|
||||
Args: makeArtifactArgs(),
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
|
||||
@@ -118,10 +118,10 @@ func TestMakeCommandsViaCLI(t *testing.T) {
|
||||
}
|
||||
|
||||
out := runSummer("make:plugin", "golem15.demo")
|
||||
if !strings.Contains(out, filepath.Join("plugins", "demo")) {
|
||||
if !strings.Contains(out, filepath.Join("plugins", "golem15", "demo")) {
|
||||
t.Fatalf("make:plugin output missing plugin dir:\n%s", out)
|
||||
}
|
||||
pluginDir := filepath.Join(dir, "plugins", "demo")
|
||||
pluginDir := filepath.Join(dir, "plugins", "golem15", "demo")
|
||||
if _, err := os.Stat(filepath.Join(pluginDir, "plugin.go")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -193,7 +193,7 @@ func TestNewCommandViaCLI(t *testing.T) {
|
||||
t.Fatalf("new demo --no-git: %v\n%s", err, out)
|
||||
}
|
||||
app := filepath.Join(parent, "sm-demo-app")
|
||||
for _, rel := range []string{"summer.yaml", "main.go", "plugins/demo/routes.go"} {
|
||||
for _, rel := range []string{"summer.yaml", "main.go", "plugins/golem15/demo/routes.go"} {
|
||||
if _, err := os.Stat(filepath.Join(app, rel)); err != nil {
|
||||
t.Fatalf("%s missing: %v", rel, err)
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ module: git.golem15.com/acme/acme
|
||||
binary: acme
|
||||
plugins:
|
||||
- id: acme.user
|
||||
module: git.golem15.com/acme/acme/plugins/user
|
||||
module: git.golem15.com/acme/acme/plugins/acme/user
|
||||
- id: acme.blog
|
||||
module: git.golem15.com/acme/acme/plugins/blog
|
||||
module: git.golem15.com/acme/acme/plugins/acme/blog
|
||||
build:
|
||||
tags:
|
||||
- summercms_s3
|
||||
@@ -90,17 +90,17 @@ build:
|
||||
|
||||
## Local plugins in a workspace
|
||||
|
||||
A plugin you develop inside the application lives in `plugins/<name>` as its own module. `summer make:plugin acme.blog` creates it there, and `summer plugin:add plugins/blog` registers it:
|
||||
A plugin you develop inside the application lives in `plugins/<vendor>/<name>` as its own module. `summer make:plugin acme.blog` creates it in `plugins/acme/blog`, and `summer plugin:add plugins/acme/blog` registers it:
|
||||
|
||||
- it adds the plugin to `summer.yaml`;
|
||||
- it adds a `require` and a `replace` pointing at the local directory to the application's `go.mod`;
|
||||
- it adds the plugin directory to the nearest `go.work` as a path relative to that file, such as `./plugins/blog`, creating a `go.work` in the application root when there is none.
|
||||
- it adds the plugin directory to the nearest `go.work` as a path relative to that file, such as `./plugins/acme/blog`, creating a `go.work` in the application root when there is none.
|
||||
|
||||
With a `go.work` in place, `go build`, `go test` and your editor see every local plugin module together. `summer build` uses the nearest `go.work` it finds; without one it builds in module mode.
|
||||
|
||||
```sh
|
||||
summer make:plugin acme.blog
|
||||
summer plugin:add plugins/blog
|
||||
summer plugin:add plugins/acme/blog
|
||||
summer build
|
||||
```
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ The application gets these files:
|
||||
| `compose.yaml` | PostgreSQL 16 on `127.0.0.1:54320`, with credentials matching `.env`. |
|
||||
| `.gitignore` | `bin/`, `tmp/`, `storage/`, `.env` and `go.work.sum`. |
|
||||
| `CLAUDE.md`, `AGENTS.md` | Instructions for AI coding agents: the commands, the layout, the admin, the links to the documentation for agents and the naming convention. `AGENTS.md` is a symlink to `CLAUDE.md`. |
|
||||
| `plugins/<name>` | The starter plugin `golem15.<name>`, created by the same code as `summer make:plugin` and registered by the same code as `summer plugin:add`. Its `routes.go` answers `GET /` with a JSON greeting. |
|
||||
| `plugins/golem15/<name>` | The starter plugin `golem15.<name>`, created by the same code as `summer make:plugin` and registered by the same code as `summer plugin:add`. Its `routes.go` answers `GET /` with a JSON greeting. |
|
||||
| `main.go`, `plugins.gen.go` | Generated from `summer.yaml`, as `summer build` does. |
|
||||
|
||||
Unless you pass `--no-git`, `summer new` initialises a git repository on `main` and commits everything except `.env`. When git has no user name or email configured, that first commit uses the author `SummerCMS <summer@localhost>` and prints a warning; your git configuration is not changed. Finally it prints the next steps to run inside `sm-acme-app`:
|
||||
@@ -79,15 +79,30 @@ Golem15 names repositories by what they hold: `sm-<name>-app` for an application
|
||||
|
||||
## Creating a plugin
|
||||
|
||||
`summer make:plugin` takes a plugin ID in `vendor.plugin` form and creates the plugin module in `plugins/<plugin>` of the current application, with the directory layout described in [Plugin registration](../plugins/registration.md):
|
||||
`summer make:plugin` takes a plugin ID in `vendor.plugin` form and creates the plugin module in `plugins/<vendor>/<plugin>` of the current application, with the directory layout described in [Plugin registration](../plugins/registration.md). `acme.blog` becomes `plugins/acme/blog` with the module path `<application module>/plugins/acme/blog` and `package blog`, so `acme.blog` and `pixelpixel.blog` can live in one application:
|
||||
|
||||
```sh
|
||||
summer make:plugin acme.blog
|
||||
summer plugin:add plugins/blog
|
||||
summer plugin:add plugins/acme/blog
|
||||
```
|
||||
|
||||
`summer plugin:add` then registers the local module: it adds the plugin to `summer.yaml`, adds a `require` and a local `replace` to the application's `go.mod`, and adds the directory to `go.work`. The next `summer build` compiles the plugin in.
|
||||
|
||||
### Upgrading: plugins without a vendor directory
|
||||
|
||||
Before this layout, `summer make:plugin` wrote `plugins/<plugin>` without the vendor, and `summer new` put its starter plugin there. Such a plugin keeps working: `summer build` follows `summer.yaml` and the `replace` lines of the application's `go.mod`, and the `make:` commands look for a plugin in `summer.yaml` first, then in `plugins/<vendor>/<plugin>`, then in `plugins/<plugin>`.
|
||||
|
||||
Moving it is optional. To move `acme.blog` from `plugins/blog` to `plugins/acme/blog`:
|
||||
|
||||
```sh
|
||||
mkdir -p plugins/acme
|
||||
mv plugins/blog plugins/acme/blog
|
||||
go work edit -dropuse ./plugins/blog
|
||||
summer plugin:add plugins/acme/blog
|
||||
```
|
||||
|
||||
`summer plugin:add` points the application's `replace` and `go.work` at the new directory; the module path and the `summer.yaml` entry stay as they were. If the plugin's own `go.mod` has a relative `replace` for the framework (or another local module), add one `../` to it, for example `../../../summercms` becomes `../../../../summercms`. Then run `summer build`.
|
||||
|
||||
## Generating plugin parts
|
||||
|
||||
The other `make:` commands add one artifact to an existing plugin. Each takes the plugin ID and an exported Go name:
|
||||
@@ -103,7 +118,7 @@ summer make:admin-controller acme.blog Posts
|
||||
When you run a command inside a plugin directory, leave the ID out and pass only the name. The tool finds the plugin from the nearest `plugin.go` above the current directory:
|
||||
|
||||
```sh
|
||||
cd plugins/blog
|
||||
cd plugins/acme/blog
|
||||
summer make:model Comment
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestScaffoldLayout(t *testing.T) {
|
||||
t.Fatalf("MakeCommand: %v", err)
|
||||
}
|
||||
|
||||
scaffolded := fileSet(t, filepath.Join(app, "plugins", "blog"), func(rel string) bool {
|
||||
scaffolded := fileSet(t, filepath.Join(app, "plugins", "acme", "blog"), func(rel string) bool {
|
||||
return rel != "go.mod" && rel != "go.sum"
|
||||
})
|
||||
walkthrough := fileSet(t, ".", func(rel string) bool {
|
||||
|
||||
@@ -12,7 +12,7 @@ Plugins are the foundation of every SummerCMS application. A plugin adds models,
|
||||
|
||||
Every plugin has an ID in `vendor.plugin` form: two lower-case parts, each starting with a letter and containing only letters and digits, such as `acme.blog`. The ID is how the manifest lists the plugin, how other plugins require it and how its configuration is namespaced (`acme.blog.posts_per_page`). WinterCMS writes the same identifier as `Acme.Blog`; in SummerCMS it is always lower case.
|
||||
|
||||
The scaffolder derives the package and directory name from the second part, so `summer make:plugin acme.blog` creates `plugins/blog` with `package blog`.
|
||||
The scaffolder puts the plugin in a directory per vendor and names the package after the second part, so `summer make:plugin acme.blog` creates `plugins/acme/blog` with `package blog`, and `acme.blog` and `pixelpixel.blog` can sit side by side.
|
||||
|
||||
## The plugin type
|
||||
|
||||
@@ -104,7 +104,7 @@ Configuration defaults, translations and mail templates ship inside the binary t
|
||||
`summer make:plugin acme.blog` writes a plugin that compiles and follows the WinterCMS directory layout, with each directory as a Go subpackage:
|
||||
|
||||
```text
|
||||
plugins/blog/
|
||||
plugins/acme/blog/
|
||||
├── go.mod the plugin module, requiring the framework
|
||||
├── plugin.go the Plugin type, its capabilities and init registration
|
||||
├── routes.go the Routes method
|
||||
|
||||
@@ -23,7 +23,7 @@ Tests that need Docker, such as database tests, skip themselves in short mode. U
|
||||
go test -short ./...
|
||||
```
|
||||
|
||||
In an application with local plugins in a `go.work` workspace, run the tests of one plugin by its directory, for example `go test ./plugins/blog/...`.
|
||||
In an application with local plugins in a `go.work` workspace, run the tests of one plugin by its directory, for example `go test ./plugins/acme/blog/...`.
|
||||
|
||||
## Unit tests without a database
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ module: git.golem15.com/acme/acme
|
||||
binary: acme
|
||||
plugins:
|
||||
- id: acme.blog
|
||||
module: git.golem15.com/acme/acme/plugins/blog
|
||||
module: git.golem15.com/acme/acme/plugins/acme/blog
|
||||
build:
|
||||
tags:
|
||||
- summercms_s3
|
||||
|
||||
@@ -20,18 +20,18 @@ summer make:model acme.blog Post
|
||||
summer make:migration acme.blog AddPublishedAt
|
||||
summer make:admin-controller acme.blog Posts
|
||||
summer make:command acme.blog Publish
|
||||
summer plugin:add plugins/blog
|
||||
summer plugin:add plugins/acme/blog
|
||||
summer build
|
||||
```
|
||||
|
||||
| Command | Writes |
|
||||
|---------|--------|
|
||||
| `summer make:plugin acme.blog` | `plugins/blog` as its own Go module: `plugin.go`, `routes.go`, `registry.gen.go`, `go.mod`, `config/config.yaml`, `lang/en/lang.yaml`, `views/mail/welcome.htm` and a `doc.go` in `classes`, `console`, `controllers`, `jobs`, `middleware`, `models` and `updates` |
|
||||
| `summer make:plugin acme.blog` | `plugins/acme/blog` as its own Go module: `plugin.go`, `routes.go`, `registry.gen.go`, `go.mod`, `config/config.yaml`, `lang/en/lang.yaml`, `views/mail/welcome.htm` and a `doc.go` in `classes`, `console`, `controllers`, `jobs`, `middleware`, `models` and `updates` |
|
||||
| `summer make:model acme.blog Post` | `models/post.go` and `updates/<timestamp>_create_acme_blog_posts.go` |
|
||||
| `summer make:migration acme.blog AddPublishedAt` | `updates/<timestamp>_add_published_at.go` |
|
||||
| `summer make:admin-controller acme.blog Posts` | `controllers/posts.go`, `controllers/posts/config_form.yaml`, `controllers/posts/config_list.yaml`, `models/posts/fields.yaml` and `models/posts/columns.yaml` |
|
||||
| `summer make:command acme.blog Publish` | `console/publish.go` |
|
||||
| `summer plugin:add plugins/blog` | The plugin in `summer.yaml`, a `require` and a local `replace` in the application's `go.mod`, and the directory in `go.work` |
|
||||
| `summer plugin:add plugins/acme/blog` | The plugin in `summer.yaml`, a `require` and a local `replace` in the application's `go.mod`, and the directory in `go.work` |
|
||||
| `summer build` | `bin/acme`, the application binary with the plugin compiled in |
|
||||
|
||||
Each `make:` command also rewrites `registry.gen.go` and runs `go mod tidy` in the plugin. The sections below fill in what each generated file leaves empty. See [Scaffolding](../console/scaffolding.md) for every option of these commands.
|
||||
@@ -39,7 +39,7 @@ Each `make:` command also rewrites `registry.gen.go` and runs `go mod tidy` in t
|
||||
The copy in the framework repository has no `go.mod`, because it is a package of the framework module so that the framework's own `go test ./...` covers it. A plugin you scaffold is a module of its own, and its `go.mod` starts like this for an application whose module is `example.com/acme` (indirect requirements left out):
|
||||
|
||||
```text
|
||||
module example.com/acme/plugins/blog
|
||||
module example.com/acme/plugins/acme/blog
|
||||
|
||||
go 1.27.0
|
||||
|
||||
|
||||
@@ -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