github varabyte/kobweb v0.24.1

2 hours ago

This is a jam packed release featuring long-planned tweaks to the network APIs as well as new lucide and material symbol icons!

In the previous release, we revisited the backend network APIs to support multipart requests, but due to backwards compatibility, we could not yet touch the frontend APIs, as the method names we wanted to use were already taken. Instead, we deprecated them.

In this release, those frontend changes are now done! Potentially, there may be some codebases out there which might break with this change, but they would have been ignoring scary deprecation warnings for a while now. As these methods are not used by most Kobweb users, we decided to incrementally update the release version instead of put out a new major version.

If you are a developer with a fullstack website and you get warnings after updating, please review the Notes section below about migrating. We did our best to tell the IDE how to automatically update your deprecated code to the latest versions of the API, but it doesn't always do the best job.

Important

Planning to upgrade? Review instructions in the README.

Changes

Frontend

  • Icons!

    • Added support for Google Material Symbols
    • Added support for Lucide
    • See the Notes section below for more details.
  • Introduced a slew of Kotlin-idiomatic network request APIs.

    • See the Notes section below for more details.
  • CSS Modifiers

    • Added support for setting various animation properties directly, e.g. Modifier.animation { delay(...) }
    • Added object-position property
    • Added text-wrap property
    • Added tab-size property
    • Added image-rendering property
    • Added image-orientation property
    • Added missing Transform.None value (and global keywords, e.g. Transform.Inherit)
  • Improved CSSPosition output, now producing simpler output

    • Before, CSSPosition values were expanded to their most specific 4-arg version, but now we shorten when possible.
    • e.g. CSSPosition(Edge.Left) is now just "left", and not "left 0% top 50%"

Worker

  • Made network APIs more flexible so they could be called from a worker script.

    • In a worker environment, accessing window throws a runtime exception. So now....
      • In a site: window.fetch(...)
      • In a worker: self.fetch(...)
  • Added support for launching coroutines inside a worker context

    • In a site: CoroutineContext(window.asCoroutineDispatcher()).launch { ... }
    • In a worker: CoroutineContext(self.asCoroutineDispatcher()).launch { ... }

Backend

  • You can now expose system properties from your build script to your backend server

    • This feature is kind of analogous to app globals for the frontend
    • See the Notes section below for more details.
  • Added support for multiple headers on the same request

    • Technically, requests can have a list of headers associated with a key, but we were collapsing them down to one.
  • When you run kobweb run --layout=static in dev mode, it will now return 404 on dynamic pages.

    • This should help avoid confusion that new users sometimes have where they have a dynamic page that would get correctly served for them locally during development time, only for their hosting service provider to return a 404 in prod.

Gradle

  • You can now specify an explicit path to a browser that gets used by the export step.
    • If set, will skip the normal downloading step that occurs on first export.
    • This can be useful in CIs (which create environments from scratch each time) or in any environment where you know that you already have a browser present that you can use.
    • You can set this value via environment variable: KOBWEB_EXPORT_BROWSER_PATH or directly in your site's build script via kobweb.app.export.browser.path.

Notes

Material Symbols

Kobweb has supported Material Design Icons for a long time, but Google deprecated those and replaced them with Material Symbols instead. If you are using MDI in your own project, you may want to consider migrating over if possible, as presumably MDI is mostly stale now and future work will go into MS.

# gradle/libs.versions.toml

[libraries]
silk-icons-ms = { module = "com.varabyte.kobwebx:silk-icons-ms", version.ref = "kobweb" }
// site/build.gradle.kts

kotlin {
    sourceSets {
        jsMain.dependencies {
            implementation(libs.silk.icons.ms)
        }
    }
}

At that point, you can reference them in your code! Material Symbol icon methods begin with the Ms prefix.

For example, if you wanted to use the mail icon, find the corresponding method with the same name:

MsMail()

You can also pass in a style, with one of three values (where outline is the default):

enum class MsIconStyle {
    OUTLINED,
    ROUNDED,
    SHARP;
}

Lucide Icons

Check out the official page to see their list of icons.

To use these in your project, simply add the following dependency to your build script:

# gradle/libs.versions.toml

[libraries]
silk-icons-lucide = { module = "com.varabyte.kobwebx:silk-icons-lucide", version.ref = "kobweb" }
// site/build.gradle.kts

kotlin {
    sourceSets {
        jsMain.dependencies {
            implementation(libs.silk.icons.lucide)
        }
    }
}

At that point, you can reference them in your code! Lucide icon methods begin with the Lucide prefix.

For example, if you wanted to use the mail icon, find the corresponding method with the same name:

LucideMail()

The full API for that method is:

@Composable
fun LucideMail(
    modifier: Modifier = Modifier, size: CSSLengthValue = 1.em, strokeWidth: Number = 2, color: CSSColorValue? = null,
)

so feel free to play with those values in your own site.

Backend system properties

If you are running a kobweb server and want to expose some values to it from the build script, we made this easy:

kobweb {
   app {
      server {
         systemProperties.put("example.key", "example.value")
      }
   }
}

Then, anywhere inside your jvmMain server code:

val value = System.getProperty("example.key")!!
assertEquals("example.value", value)

We are using this new feature in the todo template example (e.g. kobweb create examples/todo). This allows us to make it easy for users to toggle the implementation of the backend datastore from in-memory to a MongoDB database. Feel free to check out the project if you are curious!

Revisiting Network APIs

The stdlib fetch API is very powerful but full of dynamic, type-unsafe code. Kobweb's network APIs are suspend friendly and type-safe.

However, in the earliest days of Kobweb development, I made an incorrect assumption and oversimplified what I would accept for a request's payload body. Specifically, I only accepted a byte array for it. However, there are several different data types that web request APIs traditionally accept, including multipart bodies, json, html text, and blobs.

Therefore, this version of Kobweb introduces a new RequestBody class which you construct with one of the various provided bodyOf factory methods. So where before you would pass raw bytes into one of the Kobweb network methods, now you pass in the output of a bodyOf call instead.

Of course there is a bodyOf for raw bytes. But we also support wrapping the other types as well (blobs, etc.)

And instead of returning raw bytes back, we now return a Response object (a standard class in web APIs). You can pull raw bytes out of its body payload by using our new bodyAsBytes extension method:

// ======== LEGACY ========
val bodyBytes: ByteArray = ...
val responseBytes = window.fetch(HttpMethod.POST, bodyBytes)

// ======== MODERN ========
val bodyBytes: ByteArray = ...
val body = bodyOf(bodyBytes)
val response = window.fetch(HttpMethod.POST, body)
val responseBytes = response.bodyAsBytes()

// Or, as a one-liner
val responseBytes = window.fetch(HttpMethod.POST, bodyOf(bodyBytes)).bodyAsBytes()

A little more verbose, admittedly, but now users can work with Response objects directly, which is a lot more powerful.

Real-world example: multipart request

To showcase a real example, the following code was introduced in the last release demonstrating how to send a multipart request:

// Using stdlib window fetch

val formData = FormData().apply {
   append("file", file, file.name)
   append("description", "Kobweb multipart test")
}
val requestInit = RequestInit(method = "POST", formData)
val response = window.fetch("/api/multipart", requestInit).await()

Now, I would write it as follows, ditching the RequestInit object and the need to await the response (since Kobweb's fetch method is suspend aware):

// Using Kobweb window.http

val formData = FormData().apply {
   append("file", file, file.name)
   append("description", "Kobweb multipart test")
}
val response = window.http.post("/api/multipart", bodyOf(formData))

Migrating deprecated network methods

We deprecated a lot of network methods in this release, since, as we discussed in the prior section, we settled on the approach we should have used in the beginning: take in a rich request body type and return a Response object.

If you are a user caught up in this, we apologize for the inconvenience.

In the previous release, we pushed people away from using, say, fetch, which originally returned raw bytes, and migrated them to use fetchBytes instead (same function, new name). Now, here we are abandoning raw bytes! And pushing users back to the original method, except now it takes in a rich body type and outputs a Response. Users are expected to call bodyAsBytes on top of the Response object to get those raw bytes out again.

For every method we deprecated, we included logic to help the IDE try to migrate the code for you. However, sometimes, it just really sucks at it, so manual migration / tweaking may be required.

In the following table, we'll use the post method to demonstrate before and after migrations, but keep in mind this same transition would apply for any of the HTTP verbs (get, put, delete, etc.).

Deprecated Modern
window.fetchBytes(HttpMethod.POST, url, bytes) window.fetch(HttpMethod.POST, bodyOf(bytes)).bodyAsBytes()
window.tryFetchBytes(HttpMethod.POST, url, bytes) window.tryFetch(HttpMethod.POST, bodyOf(bytes)) { bodyAsBytes() }
window.http.postBytes(url, bytes) window.http.post(bodyOf(bytes)).bodyAsBytes()
window.http.tryPostBytes(url, bytes) window.http.tryPost(bodyOf(bytes)) { bodyAsBytes() }
window.http.postBytes(url, bytes) window.http.post(bodyOf(bytes)).bodyAsBytes()
window.http.tryPostBytes(url, bytes) window.http.tryPost(bodyOf(bytes)) { bodyAsBytes() }
window.api.postBytes(url, bytes) window.api.post(bodyOf(bytes)).bodyAsBytes()
window.api.tryPostBytes(url, bytes) window.api.tryPost(bodyOf(bytes)) { bodyAsBytes() }
window.api.postBytes(url, bytes) window.api.post(bodyOf(bytes)).bodyAsBytes()
window.api.tryPostBytes(url, bytes) window.api.tryPost(bodyOf(bytes)) { bodyAsBytes() }

As you can see, the new versions are a bit more verbose, but by separating out the logic for making the request and reading the response, we get a lot more power with fewer methods.

Serialization extensions

Not many people know that Kobweb also provides an artifact (com.varabyte.kobwebx:kobwebx-serialization-kotlinx) that adds extensions for network requests that are Kotlinx serialization aware.

We also revisited those methods and simplified them as well.

For the following table, imagine we had the following serializable classes:

// Request goes from client -> server
@Serializable
class Req {
   /* ... properties ... */
}

// Response goes from server -> client
@Serializable
class Res {
   /* ... properties ... */
}

In the following table, note that a common change is moving the Res response class out from the initial request signature into a trailing bodyAs<T> call.

Deprecated Modern
window.http.postBytes<Req>(url, req) window.http.post<Req>(url, req).bodyAsBytes()
window.http.tryPostBytes<Req>(url, req) window.http.tryPost<Req>(url, req) { bodyAsBytes() }
window.http.post<Req, Res>(url, req) window.http.post<Req>(url, req).bodyAs<Res>()
window.http.post<Res>(url) window.http.post(url, body = null).bodyAs<Res>()
window.http.tryPost<Req, Res>(url, req) window.http.tryPost<Req>(url, req) { bodyAs<Res>() }
window.http.tryPost<Res>(url) window.http.tryPost(url, body = null) { bodyAs<Res>() }
window.api.postBytes<Req>(url, req) window.api.post<Req>(url, req).bodyAsBytes()
window.api.tryPostBytes<Req>(url, req) window.api.tryPost<Req>(url, req) { bodyAsBytes() }
window.api.post<Req, Res>(url, req) window.api.post<Req>(url, req).bodyAs<Res>()
window.api.post<Res>(url) window.api.post(url, body = null).bodyAs<Res>()
window.api.tryPost<Req, Res>(url, req) window.api.tryPost<Req>(url, req) { bodyAs<Res>() }
window.api.tryPost<Res>(url) window.api.tryPost(url, body = null) { bodyAs<Res>() }

The above tables certainly makes it look like this would be a very disruptive change indeed, but in practice, not too many people use fullstack Kobweb, and even for those that do, some of these methods are pretty niche, so we don't expect most users to even notice a single warning!

Thanks

  • @Ayfri for their Lucide icons work
  • @digitalby for Material Symbols
  • @k4i6 for identifying the missing functionality of multiple header values and providing the feature.

The community really benefits from contributions like this. Thank you so much!


Full Changelog: v0.24.0...v0.24.1

Don't miss a new kobweb release

NewReleases is sending notifications on new releases.