Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
set -euo pipefail
./kotlin test --platform jvm \
-m kinetica-compiler \
-m kinetica-gradle-plugin \
-m kinetica-runtime \
-m kinetica-markdown \
-m kinetica-router \
Expand Down Expand Up @@ -202,7 +203,9 @@ jobs:
macos:
name: macOS native
runs-on: macos-latest
timeout-minutes: 30
# 40 rather than 30: this job also publishes the release modules and builds the Gradle
# consumer fixture.
timeout-minutes: 40
steps:
- uses: actions/checkout@v6

Expand Down Expand Up @@ -245,6 +248,11 @@ jobs:
-m kinetica-motion \
-m kinetica-theme

# Runs here rather than in the JVM job: it publishes kinetica-runtime & co to the local
# Maven repository, and their macosArm64 targets cannot be built on a Linux runner.
- name: Gradle plugin consumer check
run: node scripts/verify-gradle-plugin.mjs

# Smoke-build the native macOS sample: compiles kinetica-appkit (the AppKit renderer) and
# links a macosArm64 executable. GUI launch is not automated (no display in CI); the build
# itself exercises the full toolchain path: K2 plugin + Native IR + AppKit interop.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ See [`docs/README.md`](docs/README.md).
| `kinetica-router` / `-forms` / `-motion` / `-data` / `-persist` / `-theme` / `-markdown` | first-party batteries |
| `kinetica-test` | headless component test harness |
| `kinetica-compiler` | K2 compiler plugin — mandatory: frame/slot ordinals, skip transform, FIR authoring rules, server/client boundary |
| `kinetica-gradle-plugin` | `io.heapy.kinetica` for Gradle consumers: applies the compiler plugin to every compilation, `kinetica { }` options, version-matched runtime dependencies |
| `samples/` | browser apps, four-way Game of Life comparison, server-components demo, annotated (compiler-plugin) sample |
| `docs/` | the documentation site + Docker packaging |
| `examples/gradle-ssr` | standalone Gradle 9.7 consumer of the released artifacts: SSR + island hydration + the SEO metadata that goes with it |
Expand Down
2 changes: 1 addition & 1 deletion bench-jvm/module.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ settings:
kotlin:
compilerPlugins:
- id: io.heapy.kinetica.compiler
dependency: io.heapy.kinetica:kinetica-compiler:0.3.0
dependency: io.heapy.kinetica:kinetica-compiler:0.4.0
options:
moduleId: bench-jvm

Expand Down
9 changes: 7 additions & 2 deletions common.module-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ settings:
- -Xexpect-actual-classes
compilerPlugins:
- id: io.heapy.kinetica.compiler
dependency: io.heapy.kinetica:kinetica-compiler:0.3.0
dependency: io.heapy.kinetica:kinetica-compiler:0.4.0

repositories:
# Serves two roles: the compiler plugin is consumed from here, and published modules can be
Expand All @@ -22,7 +22,12 @@ settings@android:
namespace: io.heapy.kinetica

dependencies:
- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1
# Both leak into Kinetica's public API — EffectScope extends CoroutineScope, and the companions
# of @Serializable types like Role implement kotlinx.serialization's SerializerFactory — so
# consumers must get them on the compile classpath. Without `exported` the toolchain publishes
# every dependency as runtime-scoped, and a Gradle consumer cannot compile against Kinetica.
- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1: exported
- org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0: exported

test-dependencies:
- org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1
28 changes: 24 additions & 4 deletions docs/docs-site/resources/docs/compiler-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fun AnnotatedApp() {

## Enabling it

<!-- code: common.module-template.yaml, samples/annotated/module.yaml, kinetica-compiler/src/KineticaCommandLineProcessor.kt (pluginOptions) -->
<!-- code: common.module-template.yaml, samples/annotated/module.yaml, kinetica-compiler/src/KineticaCommandLineProcessor.kt (pluginOptions), kinetica-gradle-plugin/src/KineticaGradlePlugin.kt -->

Every Kinetica module applies the shared template (`common.module-template.yaml`), which wires:

Expand All @@ -49,7 +49,7 @@ settings:
kotlin:
compilerPlugins:
- id: io.heapy.kinetica.compiler
dependency: io.heapy.kinetica:kinetica-compiler:0.3.0
dependency: io.heapy.kinetica:kinetica-compiler:0.4.0
options:
moduleId: my-app
serverSourceSet: serverMain
Expand All @@ -58,8 +58,28 @@ settings:

Further 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; `warning` downgrades them). See
`samples/annotated` for the working wiring.
`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](/docs/getting-started) adds — one name per compiler option:

```kotlin
// 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

Expand Down
45 changes: 45 additions & 0 deletions docs/docs-site/resources/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,51 @@ fun main() {
open the page. JVM apps (`product: jvm/app`) run with `./kotlin run -m my-server` and package to
an executable jar with `./kotlin package`.

## From Gradle

<!-- code: kinetica-gradle-plugin/src/KineticaGradlePlugin.kt, scripts/fixtures/gradle-plugin-consumer/build.gradle.kts, examples/gradle-ssr -->

Kinetica is built with the toolchain but consumed from any Kotlin build. For Gradle, the
`io.heapy.kinetica` plugin does the wiring:

```kotlin
// settings.gradle.kts — a fresh project resolves plugins from the portal only
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
```

```kotlin
// build.gradle.kts
plugins {
kotlin("multiplatform") version "2.4.10"
id("io.heapy.kinetica") version "0.4.0"
}

repositories {
mavenCentral()
}

kotlin {
jvm()
js { browser() }
}
```

That is the whole setup. The plugin applies the mandatory
[compiler plugin](/docs/compiler-plugin) to every compilation of every target and adds
`kinetica-runtime` to `commonMain` — plus `kinetica-browser` to a JS target's main source set — at
its own version. `kinetica { addRuntimeDependencies = false }` hands the dependencies back to you;
everything else the plugin exposes is on the compiler-plugin page.

Kotlin **2.4.10** is the version Kinetica is published with. klib metadata is not forward
compatible, so a mismatch fails the compilation — the plugin warns about it before that happens.
The plugin itself needs Gradle 8.11+ and a JDK 17+ Kotlin daemon; the compiled application still
targets whatever your toolchain says.

## Components are plain functions

<!-- code: kinetica-runtime/src/Annotations.kt (UiComponent), kinetica-runtime/src/ComponentScope.kt (state, each) -->
Expand Down
41 changes: 22 additions & 19 deletions examples/gradle-ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ everybody, rendered on the server, with the interactive parts hydrated as island
user-agent sniffing, no bot-only rendering path.

It is also a consumer test of the release: nothing here is built from this repository's sources —
the `io.heapy.kinetica:*:0.3.0` artifacts come from Maven Central, and the build system is plain
the `io.heapy.kinetica:*:0.4.0` artifacts come from Maven Central, and the build system is plain
Gradle 9.7.0, not the Kotlin Toolchain the rest of the repo uses.

## Run
Expand Down Expand Up @@ -44,32 +44,35 @@ the distinction does not matter — the content is in the first response either

## Gradle wiring worth copying

Kinetica is compiler-plugin-only and does not publish a Gradle subplugin yet, so the plugin jar is
resolved through its own configuration and passed to every Kotlin compilation:
Kinetica is compiler-plugin-only: without its K2 plugin on the compilation, `state`/`event` calls
throw `MissingKineticaPluginException` at runtime and the `@UiComponent` authoring rules stop
being enforced at compile time. From 0.4.0 that wiring is one plugin id:

```kotlin
val kineticaCompiler = configurations.resolvable("kineticaCompiler") { isTransitive = false }
dependencies { add(kineticaCompiler.name, libs.kinetica.compiler) }

val kineticaPluginArgument = kineticaCompiler.flatMap { configuration ->
configuration.elements.map { jars -> "-Xplugin=${jars.single().asFile.absolutePath}" }
}
tasks.withType<KotlinCompilationTask<*>>().configureEach {
compilerOptions.freeCompilerArgs.add(kineticaPluginArgument)
plugins {
kotlin("multiplatform") version "2.4.10"
id("io.heapy.kinetica") version "0.4.0"
}
```

If the wiring is ever lost, the failure is loud rather than silent: the plugin's FIR checkers stop
running, and `state`/`event` calls throw `MissingKineticaPluginException` — but note the plugin
also *is* the thing that enforces `@UiComponent` call rules at compile time, so treat a build that
suddenly stops reporting those errors as suspicious.
It applies the compiler plugin to every compilation of every target and adds `kinetica-runtime` to
`commonMain` and `kinetica-browser` to `jsMain` at its own version — which is why neither appears
in this build's dependency blocks. `kinetica { addRuntimeDependencies = false }` hands those back
to you, and the same block carries the compiler options (`moduleId`, `serverSourceSet`,
`clientSourceSet`, `sourcePipeline`, `transforms`, `checks`).

The plugin resolves through Maven Central, not the Gradle Plugin Portal, so `settings.gradle.kts`
lists `mavenCentral()` in `pluginManagement.repositories` — a fresh project has only the portal
there and would fail with `UnknownPluginException`.

Other notes on the build:

- Kotlin **2.4.10** matches the version Kinetica 0.3.0 was published with; klib metadata is not
forward compatible, so do not bump one without the other.
- `kotlinx-serialization-json` is declared explicitly: Kinetica exposes it as a runtime-scoped
transitive dependency, which is not on the compile classpath.
- Kotlin **2.4.10** matches the version Kinetica 0.4.0 was published with; klib metadata is not
forward compatible, so do not bump one without the other. The plugin warns when they diverge.
- The Kotlin compile daemon needs JDK 17 or newer: the compiler plugin is loaded into it.
- `kotlinx-serialization-json` is declared explicitly because this example's own code builds
island props and JSON-LD with it. Code that only uses Kinetica does not need the declaration —
0.4.0 puts serialization and coroutines on the compile classpath.
- Repositories live in `build.gradle.kts`, not `settings.gradle.kts` — the Kotlin/JS plugin adds
its own Node.js repository to the project, which a settings-only setup rejects or shadows.
- The configuration cache is off: Kotlin/JS compile tasks in KGP 2.4.10 are not yet compatible.
Expand Down
34 changes: 7 additions & 27 deletions examples/gradle-ssr/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)

import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask

plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
// Everything Kinetica needs: the mandatory K2 compiler plugin on every compilation of every
// target, plus kinetica-runtime in commonMain and kinetica-browser in jsMain at the same
// version. Before 0.4.0 this file resolved the plugin jar itself and pushed -Xplugin into
// each KotlinCompilationTask by hand.
alias(libs.plugins.kinetica)
}

repositories {
Expand All @@ -13,21 +16,6 @@ repositories {
mavenCentral()
}

// The Kinetica compiler plugin ships as a plain jar (no Gradle subplugin yet), so it is resolved
// through its own configuration and handed to every Kotlin compilation as -Xplugin=<jar>.
val kineticaCompiler = configurations.resolvable("kineticaCompiler") {
isTransitive = false
}

dependencies {
add(kineticaCompiler.name, libs.kinetica.compiler)
}

val kineticaPluginArgument: Provider<String> =
kineticaCompiler.flatMap { configuration ->
configuration.elements.map { jars -> "-Xplugin=${jars.single().asFile.absolutePath}" }
}

kotlin {
jvmToolchain(21)

Expand All @@ -48,9 +36,8 @@ kotlin {

sourceSets {
commonMain.dependencies {
implementation(libs.kinetica.runtime)
// Kinetica exposes kotlinx.serialization at runtime only; island props and JSON-LD
// are built here, so the compile-time dependency is declared explicitly.
// Island props and JSON-LD are built by this example's own code, so it declares the
// serialization library it uses directly rather than leaning on Kinetica's.
implementation(libs.kotlinx.serialization.json)
}
jvmMain.dependencies {
Expand All @@ -65,16 +52,9 @@ kotlin {
implementation(kotlin("test"))
implementation(libs.ktor.server.test.host)
}
jsMain.dependencies {
implementation(libs.kinetica.browser)
}
}
}

tasks.withType<KotlinCompilationTask<*>>().configureEach {
compilerOptions.freeCompilerArgs.add(kineticaPluginArgument)
}

// One `./gradlew jvmRun` builds the browser island too: the webpack output is packed into the
// server's resources, where Ktor serves it from /static.
tasks.named<ProcessResources>("jvmProcessResources") {
Expand Down
9 changes: 4 additions & 5 deletions examples/gradle-ssr/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
[versions]
kotlin = "2.4.10"
kinetica = "0.3.0"
kinetica = "0.4.0"
ktor = "3.5.1"
serialization = "1.11.0"
slf4j = "2.0.17"

[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
kinetica-runtime = { module = "io.heapy.kinetica:kinetica-runtime", version.ref = "kinetica" }
kinetica-browser = { module = "io.heapy.kinetica:kinetica-browser", version.ref = "kinetica" }
# Plain jar of the K2 plugin, wired into every Kotlin compilation as -Xplugin=<jar>.
kinetica-compiler = { module = "io.heapy.kinetica:kinetica-compiler", version.ref = "kinetica" }
# kinetica-runtime and kinetica-browser are deliberately absent: the io.heapy.kinetica plugin
# adds them at its own version, together with the mandatory compiler plugin.

ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
Expand All @@ -23,3 +21,4 @@ slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kinetica = { id = "io.heapy.kinetica", version.ref = "kinetica" }
5 changes: 5 additions & 0 deletions kinetica-compiler/module.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ repositories:
publish: true

settings:
jvm:
# The plugin jar is loaded into the consumer's Kotlin compile daemon, whose JVM this project
# does not control — a JDK 17 daemon (the common case for Android and Spring builds) cannot
# load the toolchain's default Java 21 bytecode and fails with UnsupportedClassVersionError.
release: 17
kotlin:
version: 2.4.10
languageVersion: 2.4
Expand Down
2 changes: 1 addition & 1 deletion kinetica-compiler/src/CompilerContract.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.heapy.kinetica.compiler

public object KineticaCompilerContract {
public const val pluginId: String = "io.heapy.kinetica.compiler"
public const val pluginVersion: String = "0.3.0"
public const val pluginVersion: String = "0.4.0"
public const val optionModuleId: String = "moduleId"
public const val optionServerSourceSet: String = "serverSourceSet"
public const val optionClientSourceSet: String = "clientSourceSet"
Expand Down
4 changes: 2 additions & 2 deletions kinetica-compiler/test/CompilerModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class CompilerModelTest {
@Test
fun compilerContractListsResponsibilitiesAndDescriptorDefaults() {
assertEquals("io.heapy.kinetica.compiler", KineticaCompilerContract.pluginId)
assertEquals("0.3.0", KineticaCompilerContract.pluginVersion)
assertEquals("0.4.0", KineticaCompilerContract.pluginVersion)
assertEquals("moduleId", KineticaCompilerContract.optionModuleId)
assertEquals("serverSourceSet", KineticaCompilerContract.optionServerSourceSet)
assertEquals("clientSourceSet", KineticaCompilerContract.optionClientSourceSet)
Expand Down Expand Up @@ -743,7 +743,7 @@ class CompilerModelTest {

val transforms = generated.getValue("generated/io/heapy/kinetica/generated/KineticaComponentTransforms.kt").text
assertTrue("public const val KineticaGeneratedCompilerPluginId: String = \"io.heapy.kinetica.compiler\"" in transforms)
assertTrue("public const val KineticaGeneratedCompilerPluginVersion: String = \"0.3.0\"" in transforms)
assertTrue("public const val KineticaGeneratedCompilerPluginVersion: String = \"0.4.0\"" in transforms)
assertTrue("public val KineticaGeneratedComponentTransforms: List<ComponentTransformRegistration>" in transforms)
assertTrue("componentFqName = \"app.ShopScreen\"" in transforms)
assertTrue("name = \"title\"" in transforms)
Expand Down
Loading