feat(17-01): menu item type registry and dependent dropdown binding

- pact: HasMenuItemTypes, MenuItemType, MenuItemReference, MenuItemResolve,
  ResolvedMenuItem, MenuItemAlternate, DependentOptionsProvider,
  DependentOptionSet; Option gains Depth (omitempty)
- signpost (new module): boot-time registry with fail-loud key and pattern
  checks, layered URL patterns incl. PatternFrom for list types, host
  overrides at menu_items.patterns, Resolve and ReferenceOptions
- party: signpost.Activate after postcard, before Boot
- cabana: dependsOn on dropdowns (boot checks, controller-only), GET
  fields/{field}/dependent-options with swag, inventories, conformance case
- companions fixture, TestSignpostSmoke, TestCompanionsDependentSmoke, docs
This commit is contained in:
2026-10-12 15:09:14 +02:00
parent 9016e2a6fd
commit 0cdff4c258
29 changed files with 1752 additions and 14 deletions

View File

@@ -8,11 +8,11 @@ Compiled plugin registry that orders plugins by their dependencies and runs thei
## Overview
`party` is the WinterCMS PluginBase and PluginManager counterpart for plugins compiled into the binary. Each plugin package calls `party.Register` from its `init` function, and the application's generated `main` calls `party.Activate` with the plugin IDs listed in its manifest. Activation validates the selection, sorts it so every plugin comes after the plugins it requires, merges plugin config, runs every Register before any Boot, and wires translations and mail templates in between. Capabilities beyond the lifecycle are declared through the interfaces in [pact](../pact/README.md).
`party` is the WinterCMS PluginBase and PluginManager counterpart for plugins compiled into the binary. Each plugin package calls `party.Register` from its `init` function, and the application's generated `main` calls `party.Activate` with the plugin IDs listed in its manifest. Activation validates the selection, sorts it so every plugin comes after the plugins it requires, merges plugin config, runs every Register before any Boot, and wires translations, mail templates and menu item types in between. Capabilities beyond the lifecycle are declared through the interfaces in [pact](../pact/README.md).
## Role in the framework
The plugin lifecycle, on top of `backpack`, `pact`, `phrasebook` and `postcard`. `lagoon`, `conga`, `cabana` and `surf` ask it which plugins are active and in what order. It is the counterpart of WinterCMS's plugin manager, without autoloading: plugins are registered at build time.
The plugin lifecycle, on top of `backpack`, `pact`, `phrasebook`, `postcard` and `signpost`. `lagoon`, `conga`, `cabana` and `surf` ask it which plugins are active and in what order. It is the counterpart of WinterCMS's plugin manager, without autoloading: plugins are registered at build time.
Use cases:
@@ -26,7 +26,7 @@ Use cases:
- A process-wide, concurrency-safe registry filled by `party.Register` (nil plugins are ignored).
- `party.Activate` selects plugins by manifest ID and fails on an empty ID, a duplicate ID, an unregistered plugin, a missing requirement or a dependency cycle.
- Stable topological ordering: plugins without a dependency relation keep their manifest order.
- Activation sequence: records the ordered IDs on the `backpack.App`, publishes `party.Activated` (the ordered plugin list for commands that need plugin scopes at Run time), merges each `pact.HasConfig` tree into the app config under the plugin ID, runs every Register, publishes the translator ([phrasebook](../phrasebook/README.md)) and mailer ([postcard](../postcard/README.md)), then registers each plugin's mail templates and runs its Boot.
- Activation sequence: records the ordered IDs on the `backpack.App`, publishes `party.Activated` (the ordered plugin list for commands that need plugin scopes at Run time), merges each `pact.HasConfig` tree into the app config under the plugin ID, runs every Register, publishes the translator ([phrasebook](../phrasebook/README.md)), the mailer ([postcard](../postcard/README.md)) and the menu item type registry ([signpost](../signpost/README.md), collected from every `pact.HasMenuItemTypes` plugin; a duplicate type key fails the activation), then registers each plugin's mail templates and runs its Boot.
## Usage
@@ -72,12 +72,12 @@ if err != nil {
|------------|-------------|
| `party.Plugin` | Interface every compiled plugin implements: ID, required plugin IDs, Register and Boot. |
| `party.Register` | Adds a plugin to the process-wide registry; called from the plugin's `init`. |
| `party.Activate` | Selects registered plugins by ID, orders them by `party.Plugin.Requires` and runs the config, Register, translation, mail and Boot steps; returns the ordered plugins. |
| `party.Activate` | Selects registered plugins by ID, orders them by `party.Plugin.Requires` and runs the config, Register, translation, mail, menu item type and Boot steps; returns the ordered plugins. |
| `party.Activated` | The ordered `[]party.Plugin` published by `party.Activate`; commands look it up at Run time with `app.Lookup[party.Activated]()`. |
## Dependencies
- SummerCMS modules: [backpack](../backpack/README.md), [pact](../pact/README.md), [phrasebook](../phrasebook/README.md), [postcard](../postcard/README.md).
- SummerCMS modules: [backpack](../backpack/README.md), [pact](../pact/README.md), [phrasebook](../phrasebook/README.md), [postcard](../postcard/README.md), [signpost](../signpost/README.md).
- Third-party: none.
- Standard library: `fmt`, `strings`, `sync`.

View File

@@ -9,6 +9,7 @@ import (
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/phrasebook"
"git.golem15.com/golem15/summercms/modules/postcard"
"git.golem15.com/golem15/summercms/modules/signpost"
)
// Plugin is the required descriptor every compiled plugin implements.
@@ -131,6 +132,12 @@ func activate(registered []Plugin, app *backpack.App, ids []string) ([]Plugin, e
if err := postcard.Activate(app, ordered); err != nil {
return nil, fmt.Errorf("party: mail: %w", err)
}
// Menu item types (pact.HasMenuItemTypes) are collected after every
// Register and before the first Boot, so a plugin's Boot can read the
// registry.
if err := signpost.Activate(app, ordered); err != nil {
return nil, fmt.Errorf("party: menu item types: %w", err)
}
for _, p := range ordered {
if err := postcard.BootPlugin(app, p); err != nil {
return nil, fmt.Errorf("party: boot %s: %w", p.ID(), err)