manifest.yaml gains system_templates + page_templates declarations; each page template's .ninjatpl source lives at templates/<system>/<key>.ninjatpl and is validated at pack time. Verify no longer rejects declared system/page templates on codeless manifests (guest template_keys stay rejected) — the cms host registers them source-tracked and renders the files through the same host pongo pipeline powered blocks use. Blog/system/normal page layouts are now fully expressible with zero code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.8 KiB
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.--codelessasserts 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
.ninjatpltemplate + declared data providers (the CMS's own provider set —posts,site,menus,authors, … ADR 0018). The host fetches, the engine renders. Defined inblocks/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).
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 readercodelessHookViolation).
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 [...]
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)
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
.bnpreader accepts a missingplugin.wasmiffmanifest.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.
Seed schema (seed/seed.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.