Instantiate Javascript classes that expect "new" keyword on KotlinJS
Asked Answered
I

1

6

considering the following javascript code (partially taken from Apollo Server documentation), it creates an instance of ApolloServer and start it.


const {ApolloServer} = require('apollo-server')

const server = new ApolloServer({ ... });

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

Now consider to replicate the same behaviour using KotlinJS. Firstly, Kotlin doesn't have the "new" keyword and calling ApolloServer() as expected, won't work but raise an error (TypeError: Class constructor ApolloServer cannot be invoked without 'new').

// We can banally represent part of the code above like:
external fun require(module: String): dynamic
val ApolloServer = require("apollo-server").ApolloServer

// ApolloServer is a js class

Declaring an external class like:

external open class ApolloServer() {
    open fun listen(vararg opts: Any): Promise<Any>
    operator fun invoke(): Any
}

and set it as ApolloServer type doesn't help.

How do we replicate "new ApolloServer()" call?

Icicle answered 24/4, 2020 at 23:43 Comment(4)
sorry I don't know Kotlin but if it has JS interop there has to be an idiomatic way to call new. One band-aid fix could be to implement function createServer(...) { return new ApolloServer(...) }?Lida
I don't know about K/JS but you probably need to call it without the new keyword it'll instantiate a new instance. Because there's no new keyword in Kotlin (atleast what we do in Kotlin/JVM)Angell
Thankyou: Potentially I could make other js files that contain factories like createServer and then import them. That was the same conclusion I thought initially, but there must be a way to avoid this practice. AnimeshSahu: I do use Kotlin in JVM too and started to use it with node.js. It's very powerful but it lacks of documentation. Many times I have to analyze the javascript output in order to understand how it works because there isn't much explained around.Bendick
The only solution I found is the following one but it doesn't work as expected: discuss.kotlinlang.org/t/… I'll try to make few experiments with that and share the solution.Bendick
I
4

To solve this problem I found an interesting approach based on JsModule annotation. We need to create a Kotlin file that represent that javascript module we want to import, in my case "apollo-server".

@file:JsModule("apollo-server")
@file:JsNonModule
package com.package

import kotlin.js.Promise

external interface ServerInfo {
    var address: String
    var family: String
    var url: String
    var subscriptionsUrl: String
    var port: dynamic /* Number | String */
        get() = definedExternally
        set(value) = definedExternally
    var subscriptionsPath: String
    var server: Any
}

external open class ApolloServer(config: Any? /* ApolloServerExpressConfig & `T$0` */) : Any {
    open var httpServer: Any
    open var cors: Any
    open var onHealthCheck: Any
    open var createServerInfo: Any
    open fun applyMiddleware()
    open fun listen(vararg opts: Any): Promise<ServerInfo>
    open fun stop(): Promise<Unit>
}

With the above code we are basically describing what we expect to find in the apollo-server module and how to map it into Kotlin.

In our Kotlin main function we don't have to specify any require(...) but just use our ApolloServer class like:

    ApolloServer(null).listen().then {
       console.log(it)
    }

Using this approach Kotlin would transpile it correctly, using the new keyword in javascript.

Transpiled version extract:

  function main$lambda(it) {
    console.log(it);
    return Unit;
  }
  function main() {
    (new ApolloServer(null)).listen().then(main$lambda);
  }

This code is just an example, ApolloServer won't be initialized without a proper configuration, this case, for example, contains a nullable configuration.

Icicle answered 26/4, 2020 at 22:31 Comment(3)
I want to implement this with the library JSZip, but when I call it it says is not a constructor. Do you happen to know how to do this? Declaration: external open class JSZip { open fun loadAsync(data: Blob, vararg options: Any) : Promise<JSZip> open fun file(name: String) : ZipObject }. Usage: JSZip().loadAsync(file).then(onFulfilled = { zip -> /*code here*/ }).Runoff
@Runoff had you managed to fix it?Efficacy
@Efficacy Sorry for the late reply, I had to look it up at work. It seems I dropped the JS approach, transfered the file to the backend and unzipped it there. Sorry :/Runoff

© 2022 - 2024 — McMap. All rights reserved.