Compiling templates
Plume has two back-ends over one language and one front-end:
- The interpreting renderer powers static-site generation. It is dynamic and
- The compiling back-end lowers a template to Swift source that compiles under
feature-complete (scoped styles, client scripts, the asset pipeline, host functions, @state, and the full filter library).
Embedded Swift: a typed render function that writes HTML bytes. This is what a request-time view layer (for example, a Cloudflare Workers Wasm isolate) uses to render views with no interpreter in the loop.
The same .plume file is the source for both. The compiling back-end accepts a dynamically-renderable subset of the language (below) and rejects the rest with clear, source-located errors.
Compiling
$plumekit compile Views/ # print generated Swift to stdout$plumekit compile Views/ -o Generated/ # write one .swift per templateEach template is checked against the renderable subset first; any out-of-subset feature is reported as path:line:col: message and compilation stops with a non-zero exit. Valid templates emit Swift that imports PlumeRuntime.
Typed props
For the compiled target, component parameters name a Swift type. The interpreting renderer ignores the annotation, so a typed component still renders on the static-site path unchanged.
@component PostPage(post: Post, related: [Post] = [], currentUser: User?) {
<h1>{post.title}</h1>
@for item in related { ... }
}lowers to:
func postPage(post: Post, related: [Post] = [], currentUser: User?,
into out: inout HTML) { ... }- Defaults map to Swift default arguments; optionals stay optionals.
{post.title}→ escapingout.text(post.title)(auto-escaping is the default).{value | raw}→out.raw(value)(the explicit unescaped opt-out).@if/@for→ Swiftif/for;forloopis available inside loops.@Child(post: p)→ a typed callchild(post: p, into: &out).@slot/@slot(name)→ optional render-closure parameters; an unfilled slot
renders its fallback.
Member-level type checking is deferred to swiftc. Because generated code carries #sourceLocation directives, a type error (for example, interpolating a value that is not a String) is reported against the .plume line, not generated Swift.
The dynamically-renderable subset
Generated render functions are pure and synchronous: already-materialised data in, bytes out. No await, no I/O, no concurrency. Supported:
- Text, escaped interpolation (
{x}), and the raw opt-out ({x | raw}). @if/@else if/@else,@for ... in ...(withforloop),@let,- Components, component calls, default and named slots, and defaults.
- Embedded-safe filters:
plus,minus,times,dividedBy,modulo,abs, - Embedded-safe methods:
hasPrefix/startsWith,hasSuffix/endsWith,
@if let optional binding. Conditions must be Bool (swiftc enforces this), so write @if posts.count > 0 or @if flag, not a bare non-boolean value.
atLeast, atMost, round, ceil, floor, default, plus raw/escape.
contains.
All text handling is byte-wise UTF-8. The compiled back-end never relies on String ==, case-folding, or Unicode collation, because those fail to *link* under Embedded Swift (the Unicode tables are absent). String equality is compiled to a byte-wise comparison; human-text collation is not this layer's job.
Behaviour hooks and the asset bundle
@style, @script, @state, and @navigation are split between build time and request time. Their heavy parts (scoped CSS, the client-script-language → JS compilation, and the Plume client runtime) are compiled once into a content-hashed bundle (plumekit bundle -o DIR, or the PlumeAssetBundle API). The render function emits only the HTML-side hooks:
- a scoped
@styleadds adata-plume-scope-…attribute to the component's tags; @statelowers to a<script type="application/json" data-plume-state>hook@navigationlowers to a static<script data-plume-navigation>marker.@scriptcompiles into the bundle; the render function emits nothing inline.- Bundle tags are injected automatically. Any
@stylesite records that the asset("name")resolves at build time to the content-hashed bundle URL,
the CSS itself goes to the bundle. The scope id is computed identically on both sides, so the bundled CSS always matches the markup.
whose initial values are computed from props at render time (JSON-encoded byte-wise).
response needs the stylesheet; any @script/@state/@navigation site records that it needs the client script. At the response boundary (Response.view), the required <link>/<script> are spliced into the document's <head>, wherever in the page the directives appeared, marked data-plume-track so client-side navigation falls back to a full load when a deploy changes the bundle hashes. Fragments (no <head>) are left untouched; a page using none of these directives loads no bundle at all. The author never writes a manual <script src="app.js"> or stylesheet link.
baked into the render function as a string literal (asset("app.js") → /app.<hash>.js; asset("logo.png") → /logo.png for your own Public/ files). Because the URL is a literal, no runtime String lookup links into the guest. In a compiled template the argument must be a string literal.
The bundle files are content-hashed: app.<hash>.css (extracted scoped @style CSS) and app.<hash>.js (@script client scripts plus the Plume client runtime).
Build-time-only features (rejected by the checker)
Using these in a compiled template is a checker error:
@imageand the responsive-image pipeline. (asset(...)is allowed; it is- Host-provided functions.
- Foundation/Unicode-backed filters and methods: case-folding
resolved to a baked URL literal at build time, as above.)
(upcase/downcase/capitalize, uppercased/lowercased), slugify, date*, json, urlEncode/urlDecode, split, replace/replaceFirst, strip/lstrip/rstrip, truncate*, sort/sortNatural, map/where, and similar.
Continuous integration
support/embedded-gate.sh is the link-and-run gate: it runs plumekit compile on Fixtures/EmbeddedGate, then builds the generated code with the Embedded-Swift Wasm SDK, links an executable, runs it under Node's WASI, and asserts the rendered bytes. Linking (not just building a library) is required because Embedded link-time failures are invisible to a library-only build.