Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a mechanical block/core/X -> block/pluginsdk/X import rewrite. - abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms authoring source (cms/backend/abi/proto/v1) with go_package retargeted to git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept cms/core proto duplication (audit gap 4). - abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1 save the embedded go_package path. - plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim, caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the guest-facing type packages: blocks (+builtin/shared/tags), templates (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai, subscriptions, menus, datasources. Internal imports rewritten core -> pluginsdk; zero block/core references remain. - README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one binding; no replace directives; templates/bn is a synced copy authored in cms. Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 lines
2.6 KiB
Go
63 lines
2.6 KiB
Go
package blocks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
)
|
|
|
|
// poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL
|
|
// bytes so it can never collide with real block HTML: a BlockFunc returns a
|
|
// plain HTML string today, and this marker is a distinct, out-of-band signal
|
|
// that the block is "powered" (template + data, rendered host-side).
|
|
const poweredBlockSentinel = "\x00bn:powered\x00"
|
|
|
|
// PoweredResult is the decoded payload of a PoweredBlock marker: the template
|
|
// source and its data map. The wasm guest's RENDER_BLOCK handler decodes it
|
|
// and forwards it as abiv1.PoweredBlock so the HOST renders the template with
|
|
// pongo2 — after the block-invoke has returned, keeping the guest free.
|
|
type PoweredResult struct {
|
|
Template string `json:"template"`
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
|
|
// PoweredBlock marks a block's return value as "powered": instead of final
|
|
// HTML, the block hands back a template string plus a data map, and the host
|
|
// renders it (pongo2/ninjatpl) host-side. Return its result directly from a
|
|
// BlockFunc:
|
|
//
|
|
// func MyBlock(ctx context.Context, content map[string]any) string {
|
|
// posts := loadPosts(ctx) // build data via capabilities
|
|
// return blocks.PoweredBlock(tmpl, map[string]any{"posts": posts})
|
|
// }
|
|
//
|
|
// This is the guest-safe replacement for calling blocks.RenderTemplate inside
|
|
// a block: pongo2 never crosses the wasm boundary, so the guest cannot render
|
|
// itself — it defers rendering to the host. Because the host renders only
|
|
// after RENDER_BLOCK returns, any plugin-declared tag/filter the template
|
|
// hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke,
|
|
// never a re-entrant one.
|
|
func PoweredBlock(template string, data map[string]any) string {
|
|
payload, err := json.Marshal(PoweredResult{Template: template, Data: data})
|
|
if err != nil {
|
|
// A non-serializable data map is a programming error; fall back to an
|
|
// empty-data powered result so the template still renders.
|
|
payload, _ = json.Marshal(PoweredResult{Template: template})
|
|
}
|
|
return poweredBlockSentinel + string(payload)
|
|
}
|
|
|
|
// DecodePoweredBlock reports whether s is a PoweredBlock marker and, if so,
|
|
// returns the decoded template + data. The wasm guest uses it to distinguish a
|
|
// powered result from plain HTML. A non-marker (ordinary HTML) returns ok=false.
|
|
func DecodePoweredBlock(s string) (PoweredResult, bool) {
|
|
rest, ok := strings.CutPrefix(s, poweredBlockSentinel)
|
|
if !ok {
|
|
return PoweredResult{}, false
|
|
}
|
|
var pr PoweredResult
|
|
if err := json.Unmarshal([]byte(rest), &pr); err != nil {
|
|
return PoweredResult{}, false
|
|
}
|
|
return pr, true
|
|
}
|