Build-time compilation of templates into Java source, replacing today's runtime-parsed + reflection-driven template engine.
Related: #28 (template validation) — the validator is a smaller, additive answer to a subset of what this issue solves. Compilation is the more comprehensive and structurally honest direction.
The vision
Each template becomes a real Java class at build time. The class encapsulates everything the template is — not just rendering, but bindings, dependencies, validation, metadata. The template stops being a string-driven runtime artifact and becomes a unified declarative compilation unit that the rest of the framework can interact with as Java.
Implications:
- Rendering is a direct method call (no parser at runtime, no KVC reflection, no string lookups in the hot path)
- Bindings are typed Java expressions.
$customer.name becomes component.getCustomer().getName() — the compiler sees the reference; refactoring tools follow it; the type system validates it
- Bindings as declarations. The template class exposes its bindings as introspectable metadata: which bindings exist, their types, their cardinality, whether they're settable, whether they're required
- Dependencies declared. Sub-components, resources, and inherited templates referenced by this template are visible at the class level — not hidden inside string parsing
- Validation is the Java compiler. No separate validator pass; type/symbol errors fail compilation, with clean error messages mapped back to template source
- Tooling Just Works. IDE rename, find-references, click-through navigation — because everything is real Java referencing real Java
What compilation buys us
Correctness
- Stringly-typed bindings stop being an Achilles heel. The compiler verifies every reference.
- Refactoring Java methods automatically catches stale template references at build time.
- Type errors (
<wo:if condition=\"\$someString\"> where the binding returns String, not boolean) fail at build time, not request time.
- Typos in bindings fail at build time.
Performance
- No runtime template parsing. Parse once at build, never again.
- Binding evaluation is direct method calls, not reflection. Especially impactful in deep iteration.
- Stack traces show real lines in real classes, not deep KVC resolver frames.
Developer experience
- IDE navigation works across the template/Java boundary because there is no boundary anymore at the bytecode level.
- Generated classes are inspectable when debugging.
- Error messages can be richer because compilation has more context than runtime parsing.
Architectural simplification
- The bridge layer (WO compatibility) doesn't need parallel rendering pipelines — both WO and ng-objects templates compile through the same generator to the same element model.
- Runtime gets simpler (no parser, no reflection-driven KVC in the hot path).
- The render redesign and template compilation reinforce each other — generating against a redesigned element model is cleaner than retrofitting compilation onto today's model.
Introspection / declarative power
- Tools (linters, doc generators, dependency analyzers) can inspect template classes the same way they inspect any Java class
- A component's API surface (what it accepts as bindings, what it exposes) becomes a real Java thing — not a convention buried in strings
- Build-time analyses ("which components reference resource X", "which templates use component Y", "which bindings have changed type since v1") become tractable
- The template class can declare what it requires, what it provides, what it changes — making the implicit explicit
The pipeline
.wo bundle (HTML + WOD)
→ parsed at build time
→ generated Java source
→ compiled to .class (same javac as the rest of the project)
→ loaded normally
→ executed via direct method calls
The runtime stops doing template parsing entirely. Templates ship as .class files like everything else. Live reload in dev mode is a separate concern (see below).
Sketch of generated code
For <wo:str value=\"\$customer.name\" /> against component MyPage:
public class MyPage_Template implements NGTemplate<MyPage> {
@Override
public void render(MyPage component, NGOutput out, NGRenderContext ctx) {
out.appendEscaped(component.getCustomer().getName());
}
// Bindings exposed as introspectable metadata
@Override
public List<BindingDescriptor> bindings() { ... }
}
The exact shape is open — bindings(), dependencies(), validation hooks, etc. — but the principle is: every aspect of the template is a Java concept the rest of the system can see and act on.
What needs to be designed
The generator
- WOD/HTML parser already exists (reuse)
- Every element type needs a code-generation strategy:
<wo:str> → out.appendEscaped(expr)
<wo:if> → if (expr) { ... }
<wo:repetition> → for (T item : list) { ... }
- Sub-component invocation →
subComponent.render(out, ctx)
- Action bindings → method-reference or delegate registration
- Operators (
@count, @sum.salary) → library calls
- Iteration variable scoping (typed correctly based on generic types of the list)
- Action handler binding (how does an action defined in Java connect to a
\$action binding in the template?)
- Genuinely dynamic paths (KVC against
Map<String, Object> or polymorphic chains) need a fallback — explicit annotation or naming convention, with KVC at runtime for opt-in dynamics
Error message mapping
- A failure in the generated class needs to surface as a template-source error: "in MyPage.wo at line 42, `$customer.name` doesn't resolve"
- Source maps from generated Java back to original template positions
- Without this, error messages are actively worse than today's runtime errors
Dev-mode story
- Recompiling every template on every save kills the dev loop
- Either (a) in-memory regeneration + recompilation per template (JTE-style), or (b) a runtime interpreter mode for dev with the compiler kicking in for builds, or (c) some smart incremental approach
- This is meaningful design work; not skippable
Generated-code readability
- Developers will look at generated files when debugging
- Readability is craft work — needs consistent style, helpful comments referencing source lines, clean structure
- Bad generated code hurts adoption
Hot-reload
- Templates change frequently during development
- Build-time generation introduces latency that needs careful handling
- See dev-mode story
Prior art
The JVM template-compilation space is well-developed:
- JStachio — Mustache compiled to Java. Zero reflection, type-safe, error messages mapped back to template source.
- JTE — Full templating language compiled to Java. Hot reload in dev mode via in-memory recompilation. Modern, actively developed.
- Rocker — Similar approach, focus on performance.
- Quarkus Qute — Compiled with reflection fallback for dynamic cases.
The consensus across these frameworks is that typed compilation is the right answer for JVM HTML templating. ng-objects following this approach is following an established direction, not blazing a trail — applied to a component model rather than a pure templating language.
Timing
This is not M2 work. M2 should focus on the security/redesign blockers and ship the template validator (#28) as a smaller, additive answer to binding-validation.
M3 or beyond is where template compilation lives. Three reasons:
- The render pipeline is being redesigned in M3 (see docs/render-redesign.md). Generating against a redesigned element model is much cleaner than retrofitting.
- The dev-mode/hot-reload design is real work that benefits from dedicated focus.
- The WO bridge work in M2 establishes patterns the compilation work depends on.
The compilation effort and the render redesign are natural co-features. Designing them together produces a cleaner result than sequencing them.
Why this is worth taking seriously
Beyond the immediate "catch binding errors at build time" benefit, template compilation transforms what a template is in the framework:
- From: a string-driven runtime artifact that the framework interprets, where bindings are KVC paths and dependencies are implicit
- To: a Java class with first-class API, where rendering is a method, bindings are typed declarations, dependencies are visible, and the whole template is something the rest of the framework (tooling, IDE, build, runtime) can introspect and act on
That shift opens design space that isn't reachable from the current architecture — declarative component APIs, build-time analysis, sophisticated tooling, optimized rendering paths, simpler runtime. The validator (#28) catches a class of bugs; this transforms the model.
Worth exploring carefully, with proper design work, not rushed.
Build-time compilation of templates into Java source, replacing today's runtime-parsed + reflection-driven template engine.
Related: #28 (template validation) — the validator is a smaller, additive answer to a subset of what this issue solves. Compilation is the more comprehensive and structurally honest direction.
The vision
Each template becomes a real Java class at build time. The class encapsulates everything the template is — not just rendering, but bindings, dependencies, validation, metadata. The template stops being a string-driven runtime artifact and becomes a unified declarative compilation unit that the rest of the framework can interact with as Java.
Implications:
$customer.namebecomescomponent.getCustomer().getName()— the compiler sees the reference; refactoring tools follow it; the type system validates itWhat compilation buys us
Correctness
<wo:if condition=\"\$someString\">where the binding returns String, not boolean) fail at build time, not request time.Performance
Developer experience
Architectural simplification
Introspection / declarative power
The pipeline
The runtime stops doing template parsing entirely. Templates ship as
.classfiles like everything else. Live reload in dev mode is a separate concern (see below).Sketch of generated code
For
<wo:str value=\"\$customer.name\" />against componentMyPage:The exact shape is open —
bindings(),dependencies(), validation hooks, etc. — but the principle is: every aspect of the template is a Java concept the rest of the system can see and act on.What needs to be designed
The generator
<wo:str>→out.appendEscaped(expr)<wo:if>→if (expr) { ... }<wo:repetition>→for (T item : list) { ... }subComponent.render(out, ctx)@count,@sum.salary) → library calls\$actionbinding in the template?)Map<String, Object>or polymorphic chains) need a fallback — explicit annotation or naming convention, with KVC at runtime for opt-in dynamicsError message mapping
Dev-mode story
Generated-code readability
Hot-reload
Prior art
The JVM template-compilation space is well-developed:
The consensus across these frameworks is that typed compilation is the right answer for JVM HTML templating. ng-objects following this approach is following an established direction, not blazing a trail — applied to a component model rather than a pure templating language.
Timing
This is not M2 work. M2 should focus on the security/redesign blockers and ship the template validator (#28) as a smaller, additive answer to binding-validation.
M3 or beyond is where template compilation lives. Three reasons:
The compilation effort and the render redesign are natural co-features. Designing them together produces a cleaner result than sequencing them.
Why this is worth taking seriously
Beyond the immediate "catch binding errors at build time" benefit, template compilation transforms what a template is in the framework:
That shift opens design space that isn't reachable from the current architecture — declarative component APIs, build-time analysis, sophisticated tooling, optimized rendering paths, simpler runtime. The validator (#28) catches a class of bugs; this transforms the model.
Worth exploring carefully, with proper design work, not rushed.