Skip to content

turansky/seskar

Repository files navigation

CI Status CI Status Gradle Plugin Portal Maven Central Kotlin

Seskar

Seskar is a Gradle plugin that provides useful additions for Kotlin/JS projects.

Setup

To add Seskar to your project, you need to add the following configuration to your project's build.gradle.kts:

plugins {
  kotlin("multiplatform") version "2.1.0"
  id("io.github.turansky.seskar") version "3.75.0"
}

Kotlin/JS requirements

Lazy functions

// App.kt
suspend fun main() {
    console.log("App start!")
  
    val value = if (Random.nextDouble() > 0.5) {
        createCalculationWithHeavyLibrary()
    } else {
        42
    }

    console.log("Value: $value")
}

// createCalculationWithHeavyLibrary.kt
/**
 * - Function will be located in separate JS chunk
 * - Chunk will be loaded when function will be called first time
 */ 
@Lazy
val createCalculationWithHeavyLibrary = LazyFunction<Int> {
    val calculator = HeavyCalculator()
    calculator.calculate()
}

React

Lazy components

// Content.kt
@Lazy
val Content = FC {
    MyHeavyComponent1()
    MyHeavyComponent2()
}

// App.kt
val App = FC {
    Header()

    Suspense {
        Content()
    }

    Footer()
}
Examples
  1. Lazy app
  2. Kotlin Wrappers Example

Conditional rendering

Seskar generates keys for child elements to prevent problems with conditional rendering. As a result, in the following example Content child state won't be reset after showHeader property change.

val App = FC {
    val showHeader = useShowHeader()

    if (showHeader)
        Header() // generated: key = "@rdk/5"

    Content()    // generated: key = "@rdk/7"
    Footer()     // generated: key = "@rdk/8"
}

Dependencies

When a project uses the Kotlin/JS compiler, value classes are autoboxed. If a value class is used as a dependency of a React hook (e.g., in useMemo, useState or useEffect), a new class will be created on every rendering pass, which causes infinite re-rendering.

To prevent this, Seskar disables autoboxing for value class dependencies in hooks. It also converts Long values to String.

Seskar supports Duration by default.

Example
value class Count(
    private val value: Int,
)

val Counter = FC {
    val count: Count = useCount()

    useEffect(count) {
        println("Count changed: $count")
    }
}
Without plugin
function Counter() {
    var count = useCount()

    useEffect(
        () => {
            println(`Count changed: $count`)
        },
        // AUTOBOXING
        [new Count(count)],
    )
}
With plugin
function Counter() {
    var count = useCount()

    useEffect(
        () => {
            println(`Count changed: $count`)
        },
        // NO AUTOBOXING
        [count],
    )
}

Unions

AS-IS

Use enum constant as union value

// TypeScript
type Align = 'TOP' | 'LEFT' | 'BOTTOM' | 'RIGHT'
// Kotlin

sealed external interface Align {
    companion object {
        @JsValue("TOP")
        val TOP: Align

        @JsValue("LEFT")
        val LEFT: Align

        @JsValue("BOTTOM")
        val BOTTOM: Align

        @JsValue("RIGHT")
        val RIGHT: Align
    }
}

println(Align.TOP)  // 'TOP'
println(Align.LEFT) // 'LEFT'

Kebab case

// TypeScript
type LayoutOrientation = 'top-to-bottom'
    | 'left-to-right'
    | 'bottom-to-top'
    | 'right-to-left'
// Kotlin
import seskar.js.JsValue

sealed external interface LayoutOrientation {
    companion object {
        @JsValue("top-to-bottom")
        val TOP_TO_BOTTOM: LayoutOrientation

        @JsValue("left-to-right")
        val LEFT_TO_RIGHT: LayoutOrientation

        @JsValue("bottom-to-top")
        val bottomToTop: LayoutOrientation

        @JsValue("right-to-left")
        val rightToLeft: LayoutOrientation
    }
}

Snake case

// TypeScript
type LayoutOrientation = 'top_to_bottom'
    | 'left_to_right'
    | 'bottom_to_top'
    | 'right_to_left'
// Kotlin
import seskar.js.Case

sealed external interface LayoutOrientation {
    companion object {
        @JsValue("top_to_bottom")
        val TOP_TO_BOTTOM: LayoutOrientation

        @JsValue("left_to_right")
        val LEFT_TO_RIGHT: LayoutOrientation

        @JsValue("bottom_to_top")
        val bottomToTop: LayoutOrientation

        @JsValue("right_to_left")
        val rightToLeft: LayoutOrientation
    }
}

Custom

Use String or Int constant as union value

String
// TypeScript
type Align = 't' | 'l' | 'b' | 'r'
// Kotlin
import seskar.js.JsValue

sealed external interface CustomAlign {
    companion object {
        @JsValue("t")
        val TOP: CustomAlign

        @JsValue("l")
        val LEFT: CustomAlign

        @JsValue("b")
        val BOTTOM: CustomAlign

        @JsValue("r")
        val RIGHT: CustomAlign
    }
}

println(CustomAlign.TOP)  // 't'
println(CustomAlign.LEFT) // 'l'
Int
// TypeScript
type GRAPH_ITEM_TYPE_NODE = 1
type GRAPH_ITEM_TYPE_EDGE = 2
type GRAPH_ITEM_TYPE_PORT = 3
type GRAPH_ITEM_TYPE_LABEL = 4

type GraphItemType = GRAPH_ITEM_TYPE_NODE
    | GRAPH_ITEM_TYPE_EDGE
    | GRAPH_ITEM_TYPE_PORT
    | GRAPH_ITEM_TYPE_LABEL
// Kotlin
import seskar.js.JsRawValue

""
sealed external interface GraphItemType {
    companion object {
      @JsRawValue("1")
        val NODE: GraphItemType

      @JsRawValue("2")
        val EDGE: GraphItemType

      @JsRawValue("4")
        val PORT: GraphItemType

      @JsRawValue("8")
        val LABEL: GraphItemType
    }
}

println(GraphItemType.EDGE) // 2
println(GraphItemType.PORT) // 4