KustomExport: a KSP generator of JS facade

Related tags

Kotlin KustomExport
Overview

Status: Experimentation

KustomExport: a KSP generator of JS facade

Motivation

Providing a nice JS API can sometimes be complex from a Kotlin Multiplatform Project.

A common example, let's say you want to expose an object to iOS, Android and Web that is defined by:

data class SomeDataObject(
    val timestamp: Long,
    val state: StateEnum, // enum class StateEnum { IDLE, RUNNING }
    val idList: List<String>
)

Android and iOS will be happy with that, but there is no proper way to export that simple object to Js/Typescript today:

  • Long will not produce a number but a kotlin.Long (doc), but web developers usually use number to store timestamp.
  • Enums are not handled yet (KT-37916) and so exported as any
  • List could be better in 99% of cases if it was exported in Arrays

There are good reasons why it's not supported by KotlinJs directly, but it's not practical to provide a clean Typescript API.

Technical approach

While changing the typescript output is probably the more efficient way, it's usually a bit simpler to generate some Kotlin Facades to do the work. For example, the previous class could be cleaned with:

// jsMain/StateEnumJs.kt
// Export the enum in a class, so it's providing a real class in JS instead of 'any'
@JsExport
class StateEnumJs internal constructor(internal val stateEnum: StateEnum) {
    val name: String = stateEnum.name
}
fun StateEnumJs.import(): StateEnum = value
fun StateEnum.export(): StateEnumJs = Encoding(this)
// Object that exposes all possible values of the enum (note the 's')
@JsExport
object StateEnumsJs {
    val IDLE: StateEnumJs = StateEnum.IDLE.exportEncoding()
    val RUNNING: StateEnumJs = StateEnum.RUNNING.exportEncoding()
}

// jsMain/SomeDataObjectJs.kt
@JsExport
class SomeDataObjectJs(
    val timestamp: Double,// -> number in Typescript
    val state: StateEnumJs,
    val idList: Array<String>
)
fun SomeDataObjectJs.import(): SomeDataObject = ...
fun SomeDataObject.export(): SomeDataObjectJs = ...

This way, you expose a more typical Typescript API, but it's a lot more boilerplate that you've to write, hence the use of KSP to generate this boilerplate.

If you write similar facades yourself, this generator could help you avoid writing them manually. Please open issues to discuss your needs!

Note that it's adding more code, so if you're exposing already a lot of classes, you should be prepared to a significant increase in the JS bundle size. It's the cost to have a great Typescript API with KotlinJs today.

Current status

The current project is partially tested (unit tests + Typescript integration tests in samples). What we can generate today:

  • Long to number (by using toLong/toDouble, so be careful with precision issues here!)
  • List<...> to Array<...> and it's ready to support more collections, please open a ticket with your needs.
  • enum classes
  • class / data class (equals/toString/componentX methods are removed)
  • functions and dynamic properties are wrapped (ex val rand: Long get() = Random.nextLong() will be wrapped and called again each time the exposed object is called in Typescript)
  • interfaces

What we don't support yet:

  • generics (quite tricky as we need to export/import an unknown object, not sure about feasability)
  • abstract/open/sealed class (not written yet, no technical blockers so far)

You can have a look to the Samples to have a feel of how it can be used.

Licence

/*
 * Copyright 2021 Deezer.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
Comments
  • Support Kotlin 1.6.20

    Support Kotlin 1.6.20

    RC2 is available, after testing it quickly I noticed some weird stuff going on regarding data class wrapping, resulting in Cannot read properties of undefined kind of errors.

    (It may be due to our trick with 2nd internal constructor to preserve the first ctor original via tricky use of dynamic.)

    opened by glureau 3
  • Exceptions are actually duplicated

    Exceptions are actually duplicated

    By default Kotlin generate exceptions in the js code but they are not visible in the Typescript code.

    By exposing exceptions like CancellationException for Typescript, KustomExport actually generates a duplicated class in the js code. If passing one exception via a method is properly handled, if exception is thrown it's not wrapping the exception. So eventually we ends up with CancellationException and CancellationException_1 in the js code, and the instanceof is failing.

    Proposed solution: use external + @JsExport instead of redefining those exception classes.

    opened by glureau 3
  • Plugin-based architecture?

    Plugin-based architecture?

    This project is super cool!

    I'm curious if you'd explore a plugin-based architecture.

    As an example, we have enums declared like:

    enum class MyEnum(val value: String) {
      OPTION_A("option_a"),
      OPTION_B("option_b");
    }
    

    We'd love to write a custom rule that lets us export MyEnum as the underlying value (as a lightweight String), rather than a heavier class.

    We also have other enum classes that use Ints.

    opened by ankushg 3
  • Support mixing @JsExport and @KustomExport

    Support mixing @JsExport and @KustomExport

    Kotlin 1.6.20 brings the support of enum, so using @JsExport on simple enums could be good for performance. Right now the processor expects that all dependencies are also annotated with @KustomExport, but it doesn't check if the classes actually use it or not, leading to project not able to build.

    Goal is to scan the dependencies annotations and be able to support JS Export automatically.

    enhancement 
    opened by glureau 2
  • Sealed class

    Sealed class

    Exporting sealed class with KotlinJs make the code executable but not compliant with strict typescript rules. (The generated class is not abstract but with abstract fields for example, creating issues on typescript.)

    https://youtrack.jetbrains.com/issue/KT-39193

    We could generate an abstract class instead: the sealed is important for Kotlin side, but it has no class hierarchy limitation on the JS/TS word, so it's probably nothing more than an abstract class for the facade.

    opened by glureau 1
  • Remove Exception support?

    Remove Exception support?

    A previous experimentation has been pushed on Exceptions.

    It was practical for our use case at the time but it's leading to more and more subtle issues that we don't want to support. Also it worked only when passing the exception in a method/property, but it cannot be wrapped when a throw is done from Typescript, meaning that a try/catch on the Kotlin will still have to deal with the Typescript Error and not the wrapped exception.

    From our learnings, we think it's better to have explicit API with types that don't extends Exception. And if we need to pass it, we'll let KotlinJs deal with it and not trying to interfere here. But for that we need https://github.com/deezer/KustomExport/issues/19 and https://github.com/deezer/KustomExport/issues/17 to be fixed.

    As a bonus, it should also reduce the bundle size.

    opened by glureau 1
  • Coroutines doesn't support immediate cancellation

    Coroutines doesn't support immediate cancellation

        const abortController = new AbortController();
        const { signal: abortSignal } = abortController;
        var promise = longCompute(abortSignal); // Get a Promise from Kotlin
        abortController.abort(); // Won't work
        
        // Workaround: await just a bit so the promise Kotlin is actually created and listen the `onabort`
        await new Promise((resolve) => {
          setTimeout(resolve, 0);
        });
        abortController.abort(); // Works
    

    The first call to abort() should work.

    opened by glureau 1
  • Exported enum instance not guaranteed

    Exported enum instance not guaranteed

    Hi,

    Feel free to rename this issue so it is clearer for you 😉 .

    With this Kotlin enum:

    @KustomExport
    public enum class Bar() {
        FIRST, SECOND
    }
    

    And this TypeScript code:

    const bar = fooInstance.bar // Bar_FIRST
    if (bar === Bar_FIRST) {
      console.log('First');
    }
    

    The enum instance returned on fooInstance is not guaranteed to be an exported instance of Bar (for example in this case there will be no console.log).

    (Hi @glureau 👋 )

    bug 
    opened by neovov 1
  • Handle value class

    Handle value class

    0.1.1 release actually consider value class as a class:

    • if you don't use the annotation on this class, other classes will expect a generated class with the name of the value class, but it doesn't exist
    • if you do use the annotation, as it considers it as a class, it'll not work, or generate boilerplate and that's not what we expect from a value class

    I think we should generate a typealias for annotated value class, so that it's closer to the expected output of a value class.

    enhancement 
    opened by glureau 1
  • Suspend : facade suspend method to Promise

    Suspend : facade suspend method to Promise

    Currently handled like a normal method, suspend is simply ignored.

    A naive/first version could be to wrap it in a Promise with GlobalScope.promise {...}

    enhancement 
    opened by glureau 1
  • Enum : Use the given String parameter to fill the value

    Enum : Use the given String parameter to fill the value

    Currently we can define an enum like that

    enum class Foo(val value: String) {
      ONE("one"), TWO("hey")
    }
    

    But once wrapped, there is no way to get the parameters. We don't want to support all super-powers of Kotlin enums right now. Using the given parameter if there is only one string, and returning the value in the facade could be enough.

    opened by glureau 1
  • `Map<>` is not supported as a data class property

    `Map<>` is not supported as a data class property

    A data class that has a map property with type arguments does not get translated correctly. For example:

    @KustomExport
    data class MapHolder(val map: Map<String, String>) 
    

    Will result in:

    2 type arguments expected for interface Map<K, out V>
    
    opened by baconz 0
  • Annotating sealed class should export all child classes

    Annotating sealed class should export all child classes

    If you annotate a sealed class with JsExport, it will export all of the child classes by default. KustomExport does not behave the same way. I found that:

    @KustomExport
    sealed class Foo {
        data class Bar(val numbers: List<Long>) : Foo()
    }
    

    Will fail with a compiler error. It seems the only way to export the child classes is to do:

    @KustomExport
    sealed class Foo
    
    @KustomExport
    data class Bar(val numbers: List<Long>) : Foo()
    
    opened by baconz 1
  • Support companion object methods

    Support companion object methods

    In some cases, we want to be able to provide companion object methods, for example to build the class. The class itself could have a private constructor, like in this example:

    @JvmInline
    internal value class DurationMs private constructor(internal val value: Long) {
        init {
            require(value >= 0)
        }
    
        operator fun plus(t: DurationMs): DurationMs {
            return from(value + t.value)
        }
    
        fun toSecond(): Int = (value / 1000).toInt()
    
        companion object {
            fun from(value: Long) = DurationMs(value)
        }
    }
    
    opened by glureau 0
  • Handling unsigned types

    Handling unsigned types

    Do you folks think @JsExport will support mapping UInt to numeric as in the future? https://kotlinlang.slack.com/archives/C0B8L3U69/p1639058809157600

    Handling unsigned types should be quite easy to do, if we still go with the rules of mapping to number by default.

    enhancement 
    opened by glureau 0
  • Export enum as String/Int

    Export enum as String/Int

    Exporting enum to a class to be able to map additional properties and functions is increasing the JS bundle size. An alternative could be to export an enum as a string for example:

    @KustomExport(exportMode = ExportMode.Enum.asString)
    enum class Direction {
        NORTH, SOUTH, WEST, EAST
    }
    

    We could have a default mode to auto that could choose the mode asString or asInt if there is only one String/Int parameter, and choose the full wrapper when there is more than 1 constructor param or a method.

    Full fledge wrapper
    import sample._enum.Direction as CommonDirection // The annotated enum in commonMain
    
    @JsExport
    public class Direction internal constructor(
        internal val `value`: CommonDirection
    ) {
        public val name: String = value.name
    }
    
    public fun Direction.importDirection(): CommonDirection = value
    
    public fun CommonDirection.exportDirection(): Direction = Direction(this)
    
    @JsExport
    public object Directions {
        public val NORTH: Direction = CommonDirection.NORTH.exportDirection()
    
        public val SOUTH: Direction = CommonDirection.SOUTH.exportDirection()
    
        public val WEST: Direction = CommonDirection.WEST.exportDirection()
    
        public val EAST: Direction = CommonDirection.EAST.exportDirection()
    }
    
    
    Export as String
    import sample._enum.Direction as CommonDirection // The annotated enum in commonMain
    
    typealias Direction = String
    
    public fun Direction.importDirection(): CommonDirection = CommonDirection.valueOf(this)
    
    public fun CommonDirection.exportDirection(): Direction = this.name
    
    @JsExport
    public const val Directions_NORTH: Direction = "NORTH"
    @JsExport
    public const val Directions_SOUTH: Direction = "SOUTH"
    @JsExport
    public const val Directions_WEST: Direction = "WEST"
    @JsExport
    public const val Directions_EAST: Direction = "EAST"
    

    Bundle size (js) : 428 chars saved (no compressed) when exporting as strings (vs the full fledged export). A big part of the gain come from removing the object Directions and using (const) val instead, if we keep an object Dimensions the gain is only 14 chars. (So it's probably better to remove the object for the full fledged export, also const as no impact on bundle size.)

    Limitations:

    • only available when there is no methods/properties (we may be able to use the 1st param instead of name when only one param is defined)
    • fun goTo(d: Direction) will be exported as goTo(direction: string); (as expected). It allows passing any string value, but a bad string will generate an IllegalStateException, possibly crashing the app.

    Eventually there is an issue today with KSP/KotlinJsIr on multi-modules where external dependencies are not resolvable (WIP). With the current implementation, we expect a class not resolvable to be from another module, and it could be an issue with typealias resolution (not tested yet).

    Also a little note about enums, Kotlin 1.6.20 should export enums without an additional layer... https://youtrack.jetbrains.com/issue/KT-37916

    standby 
    opened by glureau 1
Releases(v0.8.0)
  • v0.8.0(Dec 20, 2022)

  • v0.7.0(Dec 20, 2022)

  • v0.6.1(Jun 7, 2022)

    What's Changed

    • Add SimulatorArm64 target for tvOS and watchOS by @antpa in https://github.com/deezer/KustomExport/pull/25

    New Contributors

    • @antpa made their first contribution in https://github.com/deezer/KustomExport/pull/25

    Full Changelog: https://github.com/deezer/KustomExport/compare/v0.6.0...v0.6.1

    Source code(tar.gz)
    Source code(zip)
  • v0.6.0(Apr 22, 2022)

    What's Changed

    • Upgrade to Kotlin 1.6.21
    • Upgrade to Kotlin Coroutines 1.6.1
    • Mixing annotations @JsExport & @KustomExport (experimental)

    https://github.com/deezer/KustomExport/pull/24 by @glureau

    Full Changelog: https://github.com/deezer/KustomExport/compare/v0.5.0...v0.6.0

    Source code(tar.gz)
    Source code(zip)
  • v0.5.0(Apr 19, 2022)

    What's Changed

    • Remove kustom exceptions by @glureau in https://github.com/deezer/KustomExport/pull/23

    (discussion: https://github.com/deezer/KustomExport/issues/20 )

    Full Changelog: https://github.com/deezer/KustomExport/compare/v0.4.2...v0.5.0

    Source code(tar.gz)
    Source code(zip)
  • v0.4.2(Apr 15, 2022)

  • v0.4.1(Feb 24, 2022)

    Expose CancellationException.

    TimeoutCancellationException has internal fields and internal constructor so it cannot be exported/imported like the other exceptions. For now it's only visible for Typescript but are not mapped.

    Source code(tar.gz)
    Source code(zip)
  • v0.4.0(Feb 24, 2022)

  • v0.3.1(Feb 24, 2022)

  • v0.3.0(Feb 24, 2022)

  • v0.2.0(Feb 8, 2022)

  • v0.1.1(Jan 26, 2022)

Owner
Deezer
Deezer
Swift-friendly api generator for Kotlin/Native frameworks

MOKO KSwift KSwift it's gradle plugin for generation Swift-friendly API for Kotlin/Native framework. Kotlin sealed interface/class to Swift enum Kotli

IceRock Development 226 Dec 28, 2022
Simple, fast, efficient logging facade for Android apps

µlog Simple, fast, and efficient logging facade for Android apps. Inspired by Timber and Logcat. Features Lazy message evaluation Pluggable backends (

Danny Lin 9 Oct 21, 2022
Mirai-device-generator - Mirai Device Generator with kotlin

Mirai Device Generator Mirai DeviceInfo 生成器 作为插件运行时会提供 BotConfigurationAlterer 服

cssxsh 46 Jan 1, 2023
Kotlin Multiplatform (pending KSP support) snapshot (klip) manager for tests

KLIP Kotlin Multiplatform (pending KSP support) snapshot (klip) manager for tests Modules core - runtime library processor - ksp-based annotation proc

Martynas Petuška 23 Nov 25, 2022
android webview loader using ksp

KSPWebViewLoader ?? @WebViewBuilder Annotation can be automating your webview settings. (WIP) How to use @WebViewBuilder( url = "https://www.googl

sehee Jeong 8 Apr 8, 2022
Exploring Kotlin Symbol Processing - KSP. This is just an experiment.

KSP example Exploring Kotlin Symbol Processing - KSP. This is just an experiment. Project contains 2 modules Processing Example Processing module is t

Merab Tato Kutalia 12 Aug 23, 2022
Generate helper methods for compose navigation using KSP

Compose NavGen Generate helper methods for compose navigation using KSP. ?? You can try it now, but it's still under development. ?? TODO Support defa

Kenji Abe 6 Feb 5, 2022
KSP annotation processor for Toothpick

toothpick-ksp KSP annotation processor for Toothpick. All credits go to olivierperez/ksp for the initial work on a KSP processor. Bear in mind this is

null 0 Oct 19, 2021
A collection of code generators powered by ksp.

AutoKsp A collection of code generators powered by ksp. status: working in progress Projects AutoGradlePlugin - Generate gradle plugin properties file

shenghaiyang 0 Nov 8, 2021
KSP extension for the kotlin-maven-plugin

kotlin-maven-symbol-processing Extension for the kotlin-maven-plugin to support Kotlin Symbol Processing (KSP). Usage To use this extension, add the d

Dyescape 19 Dec 18, 2022
Kotlin Symbol Processing (KSP) sample project

Kotlin Symbol Processing (KSP) Sample Project Sample annotation processor created with Kotlin Symbol Processing (KSP) API. The repository supplements

Pavlo Stavytskyi 33 Dec 23, 2022
Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP)

Mockative Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP). Installation Mockative uses KSP to generate

Mockative 121 Dec 26, 2022
Ksp-di-library - Small library for DI in KMM apps

DI-KSP Small library for DI in KMM apps. Uses KSP for processing DI annotations:

Anna Zharkova 3 Feb 6, 2022
Ktorm KSP extension to help generate boilerplate code.

Ktorm KSP extension to help generate boilerplate code. It can automatically generate Table objects through entity classes, while making entities defined by data classes easier to use, and supports custom extension code generation logic.

KTORM.ORG 24 Dec 30, 2022
glide's ksp compiler ,use kotlin symbol processor

glide-ksp glide's ksp compiler ,use kotlin symbol processor requirements library version kotlin >= 1.6.10 ksp 1.6.10-1.0.2 usage add jitpack repositor

Mistletoe 24 Oct 17, 2022
Utility library that utilizes KSP to generate Room type converter classes.

Roomie Roomie is an annotation processing library that utilizes KSP to geaRoomie is an annotation processing library that utilizes KSP to generate TypeConverter classes for Room. TypeConverter classes most often involve same boiler-plate code and Roomie makes it really easy to quickly create them with a single annotation.nerate TypeConverter classes for Room. TypeConverter classes most often invol

Chukwuka Eze 12 Aug 26, 2022
KSP-based library that generates lists from your annotation usages

ListGen, Generate Lists From Functions That Have @Listed Annotations! Welcome to ListGen! ListGen is a KSP-based library that can generate lists (and

Adib Faramarzi 24 Dec 6, 2022
Ktorfit - a HTTP client/Kotlin Symbol Processor for Kotlin Multiplatform (Js, Jvm, Android, iOS, Linux) using KSP and Ktor clients inspired by Retrofit

Ktorfit is a HTTP client/Kotlin Symbol Processor for Kotlin Multiplatform (Js, Jvm, Android, iOS, Linux) using KSP and Ktor clients inspired by Retrofit

Jens Klingenberg 637 Dec 25, 2022
BindsAdapter is an Android library to help you create and maintain Adapter class easier via ksp( Kotlin Symbol Processing).

BindsAdapter BindsAdapter is an Android library to help you create and maintain Adapter class easier via ksp( Kotlin Symbol Processing). Installation

Jintin 5 Jul 30, 2022
A ksp library to automatically generate navigation functions for jetpack compose.

Compose/Navigation/Generator ⚠️ This library is still under development and not considered stable! Content Introduction Usage Example: Single destinat

Steffen Eckardt 4 Sep 13, 2022