Compiler plugin
The compiler plugin (io.heapy.kinetica.compiler) is a standard part of the toolchain story: it removes the ceremony that hand-written Kinetica code carries (explicit key = arguments) and generates the cross-boundary artifacts for server components. It is a K2 CompilerPluginRegistrar whose core is a set of IR transforms plus FIR checkers; with the opt-in sourcePipeline: psi it additionally analyzes sources before compilation and emits generated Kotlin.
Annotations
All annotations live in kinetica-runtime, so app code needs no extra dependency:
@UiComponent
fun Badge(label: String) {
text("Static badge")
text(label)
}
@Preview("Annotated app")
@UiComponent
fun AnnotatedApp() {
var label by state { "Inbox" } // no key: the compiler assigns a stable SlotId
Badge(label)
}| Annotation | Purpose |
|---|---|
@UiComponent | stable SlotId generation, desugaring, render skipping (skippable = false opts out), static hoisting |
@Preview(name) | registers a preview entry for tooling |
@ServerComponent / @ClientComponent | marks the server/client boundary for code splitting |
@ServerAction(invalidates = […]) | declares a typed server action and its invalidation keys |
Enabling it
Every Kinetica module applies the shared template (common.module-template.yaml), which wires:
# module.yaml
settings:
kotlin:
compilerPlugins:
- id: io.heapy.kinetica.compiler
dependency: io.heapy.kinetica:kinetica-compiler:0.4.0
options:
moduleId: my-app
serverSourceSet: serverMain
clientSourceSet: clientMainFurther options and their defaults: sourcePipeline: lightTree (psi turns on source generation), transforms: all (off is the IR kill switch for debugging), and checks: error (FIR authoring-rule diagnostics; the only other value is off, which unregisters the checkers — there is no severity downgrade). See samples/annotated for the working wiring.
In a Gradle build the same options live in the kinetica { } block that the io.heapy.kinetica plugin adds — one name per compiler option:
// build.gradle.kts
kinetica {
moduleId = "my-app"
serverSourceSet = "jvmMain"
clientSourceSet = "jsMain"
sourcePipeline = "psi" // passed to JVM compilations only
transforms = "all"
checks = "error"
}sourcePipeline = "psi" is the one place the two build systems differ: the PSI pipeline exists only in the JVM compiler pipeline, so in a multiplatform project the Gradle plugin passes it to the JVM compilations and withholds it everywhere else, where it would fail the build.
IR passes
Four transforms run in order on every backend:
- Template extraction — a
hostwhose content is a single dynamictext(…)over constant props becomes atemplateNodereferencing a hoisted static skeleton; the browser renderer patches such nodes through a clone-and-fill fast path. - Static hoisting — constant
propsOf(…)argument lists and static leaf hosts are interned as file-level fields, so re-renders reuse one instance instead of reallocating. - Skippable components —
@UiComponentbodies get an inputs-equal fast path (skippableNode) that re-emits the previous subtree when arguments and read cells are unchanged. - Frames — the ordinal assignment described below.
What it generates
Generated symbols land in io.heapy.kinetica.generated: component transforms, preview entries, server-action registrations + stubs + a dispatcher, the client-component manifest, and route codecs. Declared responsibilities: slot-id generation, @UiComponent desugaring, skipping, static hoisting, the server/client boundary, server-action stubs, diagnostics, hot-reload protocol, preview entries.
The plugin is mandatory
Kinetica does not have a plugin-less mode. The IR pass (which runs on every backend — JVM, JS, wasm, native) assigns each slot- and event-consuming call site a static ordinal inside a per-component frame, wraps content lambdas into frame regions, and stages child ordinals for component calls. State identity is decided at compile time: there are no string keys, no positional cursors, and the collision bug class (two branches landing on one slot) is impossible by construction. A call site the plugin never rewrote fails fast at runtime with MissingKineticaPluginException.
Consequences for authoring:
- every function that calls
state/derived/effects/events/boundaries must be a@UiComponent fun ComponentScope.X(...); - slot calls directly inside loops or multi-run lambdas (
List(n) { … },repeat,map) are not supported — useeach(items, key = { … })orkeyed(key) { … }, which disambiguate iterations by user key; render { }content should call components; the lambda itself is wrapped into a frame region by the plugin (its parameter type carries@UiComponent).
The annotation-driven generation output (previews, server-action dispatchers, manifests) remains early-stage and JVM-only via the sourcePipeline: psi option; hand-written equivalents, as shown in Server components, are the supported path for those features today.