A wasm plugin.build now folds an optional root manifest.yaml into the DESCRIBE-derived manifest.pb exactly as BuildCodeless does, so a reduced "mixed" plugin can keep a minimal guest for genuine logic while shipping the full declarative surface set host-rendered: theme_presets, bundled_fonts, master_pages, system/page templates, template_overrides, email_wrappers, css, required_icon_packs (and the referenced root JSON is embedded into manifest.pb). applyManifestYAML now only overwrites a scalar/bytes key when manifest.yaml actually declares it, so folding onto a guest-populated manifest supplements and overrides but never WIPES a guest DESCRIBE field. A repo with no manifest.yaml is a no-op — existing pure-wasm plugins build byte-for-byte as before. Codeless builds are unaffected (empty manifest → identical result). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
155 lines
8.4 KiB
Markdown
155 lines
8.4 KiB
Markdown
# Codeless `.bnp` — Declarative Plugins (WO-WZ-020)
|
|
|
|
A **codeless `.bnp`** is a plugin artifact with **no `plugin.wasm`**: pure
|
|
declaration the host runs. No wazero compile, no instance pool, no capability
|
|
binding — zero guest code ever executes. This is the end-state of "injection,
|
|
not reliance" for themes, content sites, and template-only block packs: the
|
|
plugin is *data the host runs*, not a program that links a library. No Go, no
|
|
`block/core` dependency, no toolchain beyond the `ninja` CLI.
|
|
|
|
## The classifier — exactly one rule
|
|
|
|
`ninja plugin build` classifies by repo shape:
|
|
|
|
- **No Go source at the repo root → codeless.** The manifest is synthesized
|
|
from `plugin.mod` + the declarative files below; the artifact packs without
|
|
a wasm. `--codeless` asserts this and fails if Go is present.
|
|
- **Go source → wasm**, exactly as before. A converted repo DELETES its Go —
|
|
partial conversions keep a smaller wasm and still ship `blocks/` +
|
|
`seed/` alongside it (both artifact kinds carry the declarative dirs).
|
|
|
|
## The declarative / logic split
|
|
|
|
**Codeless-expressible** (declaration or host-rendered template):
|
|
- Blocks: a `.ninjatpl` template + declared **data providers** (the CMS's own
|
|
provider set — `posts`, `site`, `menus`, `authors`, … ADR 0018). The host
|
|
fetches, the engine renders. Defined in `blocks/blocks.yaml` — the SAME
|
|
manifest-FS layout core builtin block definitions use.
|
|
- Master pages, theme presets, fonts, CSS manifest, icon packs, settings
|
|
schema (`manifest.yaml`), assets (host-served), migrations (host-run Goose),
|
|
seed data (`seed/seed.json`, applied via the WO-WZ-019 provisioner).
|
|
- **Template overrides** (WO-WZ-021): theme-scoped re-renders of builtin
|
|
blocks. Declared in `manifest.yaml` as
|
|
`template_overrides: [{template: <system-key>, block: <block-key>}]`; the
|
|
source lives at `templates/overrides/<template>/<block>.ninjatpl` and is
|
|
rendered host-side with the block's content map as the template context.
|
|
- **Email wrappers** (WO-WZ-021): `email_wrappers: [<system-key>, …]` in
|
|
`manifest.yaml`, source at `templates/email/<system>.ninjatpl`. The context
|
|
carries `body` (pre-rendered HTML — emit it with `{{ body|safe }}`),
|
|
`colors.*` (snake_case hex tokens: `primary`, `primary_foreground`,
|
|
`secondary`, `secondary_foreground`, `background`, `foreground`, `muted`,
|
|
`muted_foreground`, `border`, `card`, `card_foreground`), `site.*` (`name`,
|
|
`logo_url`, `url`, `support_email`), `unsubscribe_url`, `preview_text`.
|
|
Render failure falls back to the unwrapped body.
|
|
|
|
**Requires code (a wasm plugin):**
|
|
- A block whose data no declared provider can produce; a *computing* tag or
|
|
filter; HTTP handlers; background jobs; Load/Unload logic; RAG fetchers;
|
|
media hooks; bridge services; AI tools; `data_dir`. A codeless manifest
|
|
declaring any of these is rejected at build (`ninja plugin verify` /
|
|
`CodelessHookViolation`) AND at load (cms reader `codelessHookViolation`).
|
|
|
|
## Repo layout
|
|
|
|
```
|
|
my-theme/
|
|
plugin.mod # name/version/kind (data_dir forbidden)
|
|
manifest.yaml # optional: theme_presets/bundled_fonts/settings_schema
|
|
# (JSON file refs), master_pages (JSON file),
|
|
# required_icon_packs, css {...}, dependencies [...],
|
|
# system_templates/page_templates,
|
|
# template_overrides [{template, block}],
|
|
# email_wrappers [system keys]
|
|
blocks/
|
|
blocks.yaml # key/title/category/schema/template/providers per block
|
|
hero.schema.json
|
|
hero.ninjatpl
|
|
templates/ # <system>/<key>.ninjatpl layout sources for the
|
|
# system/page templates declared in manifest.yaml,
|
|
# rendered host-side (blog/system/normal page layouts)
|
|
overrides/<system>/<block>.ninjatpl # template_overrides sources
|
|
email/<system>.ninjatpl # email_wrappers sources
|
|
seed/
|
|
seed.json # settings {merge/override/ensure}, media, pages, menu_items
|
|
hero.jpg # media bytes referenced by seed.json entries
|
|
assets/ # host-served statics
|
|
migrations/ # Goose SQL, host-run under the plugin schema
|
|
```
|
|
|
|
`ninja plugin build` → `<name>-<version>.bnp` (tar.zst, no `plugin.wasm`,
|
|
`manifest.pb` with `codeless: true`). `ninja plugin verify` re-runs the
|
|
loader's checks standalone.
|
|
|
|
## How the host runs it (cms `plugin/codeless_loader.go`)
|
|
|
|
- The `.bnp` reader accepts a missing `plugin.wasm` iff `manifest.codeless`,
|
|
and rejects codeless manifests with computing-hook declarations.
|
|
- Load: `blocks/` → `blocks.LoadManifest` → `Registry.RegisterDefinition` —
|
|
rendering flows through the existing definition engine (providers +
|
|
ninjatpl + layer fallback, WO-089..096). `seed/seed.json` → provisioner
|
|
(`EnsureMedia`/`MergeSiteSettings`/`EnsureSetting`/`EnsurePage`/
|
|
`EnsureMenuItem`), idempotent by construction. Presets/fonts/CSS/master
|
|
pages/settings schema come from the manifest exactly as for wasm plugins.
|
|
- Unload/disable: definitions unregister; seeded content stays (it is site
|
|
data, not runtime state). Uninstall: normal teardown (schema/role drop,
|
|
artifact removal).
|
|
- Hot-swap: codeless→codeless swaps run migrations first, then re-register
|
|
definitions and re-apply seed. Wasm↔codeless transitions require
|
|
uninstall + reinstall.
|
|
|
|
## Mixed-form artifacts (WO-WZ-021)
|
|
|
|
A **mixed** `.bnp` keeps a `plugin.wasm` (it is NOT codeless) for genuine logic
|
|
— a contact route, a Stripe handler, a background job — while everything the
|
|
codeless surface can express stays declarative and **host-rendered**. This is
|
|
how the B2 site conversions (coterieos / coteriehealth / bcms-public /
|
|
judgefest) keep a minimal guest yet ship their page templates, overrides, email
|
|
wrappers, master pages, presets, fonts, and CSS as data the host runs.
|
|
|
|
The classifier is unchanged: a repo with Go source at the root builds wasm. What
|
|
changed is that a wasm build now **folds an optional root `manifest.yaml`** into
|
|
the DESCRIBE-derived `manifest.pb`, exactly as `BuildCodeless` does — the SAME
|
|
declarative key set: `theme_presets` / `bundled_fonts` / `settings_schema`
|
|
(JSON-file refs, embedded into `manifest.pb`), `master_pages` (JSON file),
|
|
`system_templates` / `page_templates`, `template_overrides`, `email_wrappers`,
|
|
`css`, `required_icon_packs`, `dependencies`. The `.ninjatpl` sources ride the
|
|
already-packed `templates/` dir; `blocks/` + `seed/` ride along as before.
|
|
|
|
- **Supplement / override, never wipe.** A declarative key supplements the
|
|
guest's DESCRIBE output (slice keys append; scalar/bytes keys overwrite) — but
|
|
a key the `manifest.yaml` does NOT declare leaves the guest's value untouched.
|
|
A repo with no `manifest.yaml` builds byte-for-byte as before.
|
|
- **Host-render precedence at load** (cms `plugin/wasm_loader.go`,
|
|
`registerWasmStatics`). A page template / block override / email wrapper is
|
|
host-rendered through the cms `pongoengine` (with the full synthesized page
|
|
context — `head_html` / `body_end_html` / `admin_banner_html`) **iff the
|
|
artifact ships its convention-path source** (`templates/<system>/<key>.ninjatpl`,
|
|
`templates/overrides/<template>/<block>.ninjatpl`,
|
|
`templates/email/<system>.ninjatpl`). Otherwise the guest forwarder stands. A
|
|
reduced plugin's DESCRIBE may still list a template it no longer renders
|
|
in-guest; the declarative source wins. Master pages / presets / fonts / CSS /
|
|
icon packs flow from `manifest.pb` through the existing wasm registration path
|
|
unchanged.
|
|
- **Lifecycle** rides the standard source-scoped `Register` /
|
|
`UnregisterBySource` pipeline, so install / disable / uninstall / hot-swap /
|
|
cross-form swap tear the declarative surfaces down with no extra wiring —
|
|
identical to codeless.
|
|
|
|
## Seed schema (`seed/seed.json`)
|
|
|
|
```json
|
|
{
|
|
"settings": {"merge": {"k": "v"}, "override": {"k": "v"}, "ensure": {"k": "v"}},
|
|
"media": [{"id": "<uuid>", "file": "hero.jpg", "alt": "", "folder": ""}],
|
|
"pages": [{"slug": "/", "title": "Home", "template_key": "landing",
|
|
"parent_slug": "", "reconcile_blocks": false,
|
|
"blocks": [{"block_key": "hero", "title": "Hero",
|
|
"content": {}, "slot": "main", "sort_order": 1}]}],
|
|
"menu_items": [{"menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1}]
|
|
}
|
|
```
|
|
|
|
Media `id` is the deterministic, template-referable key (`media:<uuid>`,
|
|
`{% img %}`); bytes live in `seed/` next to the JSON. Apply order:
|
|
media → settings → pages → menu items.
|