153 lines
12 KiB
Markdown
153 lines
12 KiB
Markdown
---
|
|
title: Scaffolding
|
|
description: Create an application with summer new, build and watch it, and generate plugins, models, migrations, commands, jobs and admin controllers.
|
|
section: console
|
|
order: 30
|
|
---
|
|
# Scaffolding
|
|
|
|
The `summer` tool creates and builds applications and generates the files a plugin is made of, the way `create:plugin`, `create:model` and the other `create:` commands do in WinterCMS. Every generated file compiles as written, so you can build straight after running a command.
|
|
|
|
## Building and watching
|
|
|
|
| Command | Purpose |
|
|
|---------|---------|
|
|
| `summer build` | Reads `summer.yaml`, generates `plugins.gen.go` and `main.go`, and builds the binary into `bin/`, passing the manifest's `build.tags` to `go build` as `-tags`. On a terminal a spinner runs while `go build` works; it ends with `Built bin/<binary> in <time>`. Run it from the application directory or any directory below it. |
|
|
| `summer dev` | Builds the application as `summer build` does (build tags included), runs `bin/<binary> serve`, and rebuilds and restarts it whenever a Go or YAML source, `go.mod`, `go.work`, `.env` or `summer.yaml` changes. A failed build keeps the running server. |
|
|
|
|
```sh
|
|
summer build
|
|
summer dev
|
|
```
|
|
|
|
`summer dev` passes its global options on to `serve`: `summer dev -v` shows the per-plugin lines of the startup summary and every component's info logs, `summer dev -q` only warnings and errors. Ctrl+C stops `serve` and waits for its shutdown report: `serve` drains the requests in flight for up to 10 seconds, and `summer dev` kills it only if it is still running 15 seconds after the signal (a restart after a rebuild waits the same way). Then `summer dev` prints its own report (rebuilds, failed builds, restarts, uptime) and exits 0.
|
|
|
|
Do not edit `main.go` or `plugins.gen.go`: `summer build` rewrites them from the manifest.
|
|
|
|
## Creating an application
|
|
|
|
`summer new` creates a new application in the current directory, the way `composer create-project` starts a WinterCMS project. It takes the application name and writes `sm-<name>-app`:
|
|
|
|
```sh
|
|
summer new acme
|
|
```
|
|
|
|
The name must be lower-case letters and digits, starting with a letter. Go keywords such as `go` or `type`, and the names `main` and `internal`, are refused because the starter plugin package would not compile or could not be imported. `summer new` also refuses any existing `sm-<name>-app`, even an empty directory, and never merges into one. When a step fails after it created the directory, it removes the directory again, so you can rerun the same command.
|
|
|
|
| Flag | Effect |
|
|
|------|--------|
|
|
| `--framework <path>` | The framework checkout to build against. Without it, `summer new` looks for a sibling `summercms` or `summercms.go` checkout, then the source tree the `summer` binary was built from. [Go modules and workspaces](../architecture/go-modules-and-workspaces.md) describes the detection. |
|
|
| `--no-plugin` | Skip the starter plugin; `summer.yaml` then lists no plugins, and the application still builds and serves. |
|
|
| `--no-git` | Skip `git init` and the first commit. |
|
|
|
|
The application gets these files:
|
|
|
|
| File | Contents |
|
|
|------|----------|
|
|
| `go.mod` | Module `git.golem15.com/golem15/sm-<name>-app`, requiring the framework through a relative `replace`. |
|
|
| `go.work` | The workspace with the application and its plugin modules, as relative paths. It is committed. |
|
|
| `summer.yaml` | The manifest: module path, binary name, plugin list and optional `build.tags` (for example `summercms_s3` for an S3 uploads bucket). |
|
|
| `config/` | `app.yaml`, `database.yaml`, `http.yaml` and `storage.yaml`, enough for `serve` and `route:list`. |
|
|
| `.env`, `.env.example` | The database DSN, a freshly generated application key and a random admin token secret (`SUMMER_ADMIN__JWT__SECRET`). `.env` is readable by its owner only and never committed; `.env.example` lists the variables without the key and the secret. |
|
|
| `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/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`:
|
|
|
|
```sh
|
|
docker compose up -d --wait
|
|
summer build
|
|
summer migrate
|
|
summer serve
|
|
```
|
|
|
|
The next steps also explain the admin. It opens at `/backend` once a plugin registers an admin controller; until then the Admin line of `summer serve`'s startup summary reads `off`, followed by these steps:
|
|
|
|
```sh
|
|
summer make:admin-controller golem15.acme Posts
|
|
summer build
|
|
summer migrate
|
|
summer admin:create --email you@example.com --superuser
|
|
```
|
|
|
|
[Admin controllers](../backend/admin-controllers.md) explains the permission and model the generated controller needs.
|
|
|
|
Golem15 names repositories by what they hold: `sm-<name>-app` for an application such as the one `summer new` writes, `sm-<name>-plugin` for a standalone plugin module and `sm-<name>-theme` for a Nuxt or Vue frontend.
|
|
|
|
## Creating a plugin
|
|
|
|
`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/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:
|
|
|
|
```sh
|
|
summer make:model acme.blog Post
|
|
summer make:migration acme.blog AddPublishedAt
|
|
summer make:command acme.blog Publish
|
|
summer make:job acme.blog ImportPosts
|
|
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/acme/blog
|
|
summer make:model Comment
|
|
```
|
|
|
|
| Command | Writes | Notes |
|
|
|---------|--------|-------|
|
|
| `summer make:model` | `models/<name>.go` and `updates/<timestamp>_create_<table>.go` | The table name is the plugin ID and the plural name in snake case, such as `acme_blog_posts`. `--no-migration` skips the migration. |
|
|
| `summer make:migration` | `updates/<timestamp>_<name>.go` | An empty gormigrate migration with up and down steps to fill in. |
|
|
| `summer make:command` | `console/<name>.go` | A `bonfire.Command` named `<plugin>:<name>`, such as `blog:publish`, from a function that takes the `*backpack.App` the plugin kept from `Boot`. A scaffolded plugin's `Commands` returns `generatedCommands(p.app)`; when `plugin.go` still calls `generatedCommands()` without the app, the command notes that the function receives `nil`. |
|
|
| `summer make:job` | `jobs/<name>.go` | A typed job built with `conga.Job`; the plugin never imports the queue library. |
|
|
| `summer make:admin-controller` | `controllers/<name>.go`, `controllers/<name>/config_form.yaml`, `controllers/<name>/config_list.yaml`, `models/<model>/fields.yaml`, `models/<model>/columns.yaml` | A `pact.AdminController` with WinterCMS-shaped form and list configuration for the model `<model>`: the singular of the name (`Posts` edits `Post`, with its YAML in `models/post/`), or the model `--model` names. When `models/` already declares the model, `NewRecord` returns it; otherwise it returns `nil` and the command says to run `make:model`, so the screens stay closed until you finish it. The list offers `create` and `delete` (`showCheckboxes: true`). The controller requires the permission `<plugin>.access_<name>`, which you declare in the plugin's `Permissions()`. |
|
|
|
|
File names are the snake-case form of the name: `AddPublishedAt` becomes `add_published_at`. Migration file names start with a 14-digit UTC timestamp. Each new migration gets a timestamp after every file already in `updates/`: the current second, or one second past the newest existing migration when that is not earlier. File names therefore sort in creation order, and so does the list in `registry.gen.go`, even for migrations made within one second.
|
|
|
|
Every file these commands write for you to edit starts with the line `//summer:make <kind>`, such as `//summer:make model` or `//summer:make admin-controller`. Keep it: `registry.gen.go` lists only the files that carry it, or the older `// Code generated by summer make. DO NOT EDIT.` header, which earlier versions wrote and which is still recognised. `registry.gen.go` itself keeps that header, so it is the only file linters treat as generated.
|
|
|
|
After writing the files, every `make:` command regenerates the plugin's `registry.gen.go`, which lists the plugin's models, migrations, commands, jobs and admin controllers and embeds the admin YAML (`controllers/*/*.yaml` and `models/*/*.yaml`, once they exist), and runs `go mod tidy` in the plugin. The capability methods of a scaffolded `plugin.go` return those generated lists, and its `AdminFS` (`pact.AdminAssets`) returns `generatedAdminFS()`, an empty tree until the first `make:admin-controller`. If your `plugin.go` was written by hand and does not call them, the command prints a note naming the accessors to add.
|
|
|
|
Each command prints `Created <path>` for every file it writes. These are success lines, so `-q` hides them; run without `-q` when a script reads the created paths. A command refuses to overwrite an existing file or to declare a name the package already has.
|
|
|
|
When a `make:` command cannot do its work, the error says why and what to run instead. Outside an application (no `summer.yaml` in the directory or above it) it reads:
|
|
|
|
```text
|
|
ERROR build: no summer.yaml found from /home/jane
|
|
|
|
summer make:model runs inside an application: a directory with summer.yaml, or one below it.
|
|
|
|
Try:
|
|
cd <app>; summer make:model acme.blog Post
|
|
```
|
|
|
|
An invalid plugin ID (`Acme.Blog`), a name that is not an exported Go identifier (`post`), an artifact that already exists and a plugin that `summer.yaml` does not list get the same kind of explanation, with a valid command form to try.
|