pluginsdk/blocks/powered.go
Alex Dunmow b6d40ed8ac feat: bootstrap block/pluginsdk — the proto-first plugin SDK (P1)
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>
2026-07-07 10:51:00 +08:00

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
}