5

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?

Jac
  • 610
  • 3
  • 13
  • 1
    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(...) }`? – Thank you Apr 24 '20 at 23:46
  • 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) – Animesh Sahu Apr 25 '20 at 04:52
  • 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. – Jac Apr 25 '20 at 07:26
  • The only solution I found is the following one but it doesn't work as expected: https://discuss.kotlinlang.org/t/kotlin-js-doesnt-add-new-keyword-where-needed/14986 I'll try to make few experiments with that and share the solution. – Jac Apr 25 '20 at 07:28

1 Answers1

3

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.

Jac
  • 610
  • 3
  • 13