メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

Kotlin/JSでCloud Functionsを動かす

最近Kotlin/JSを触ってます。

実はCloud FunctionsがまだJavaをサポートしていないのでKotlinでCloud Functionsを動かそうと思ったらJSに変換する必要があります。

DartでもJSに変換してCloud Functionsを動かしたことがあったので、同じ用に動かせるのではないかと思いトライしました。

sakebook.hatenablog.com

現在はEAPが稼働しているので、時期に対応されますが待ちきれない!という方には参考になると思います。

環境は次のとおりです

  • Kotlin v1.3.72
  • dukat v0.0.28
  • Node.js v13.3.0
  • Google Cloud SDK v290.0.0

Cloud Functions

まずイメージを掴むために、JSのCloud Functionsの公式サンプル実装を見てみます。

const escapeHtml = require('escape-html');

    /**
     * HTTP Cloud Function.
     *
     * @param {Object} req Cloud Function request context.
     *                     More info: https://expressjs.com/en/api.html#req
     * @param {Object} res Cloud Function response context.
     *                     More info: https://expressjs.com/en/api.html#res
     */
    exports.helloHttp = (req, res) => {
      res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
    };

これはhelloHttpという関数のサンプルです。helloHttpに、requestresponseを引数に持つ関数を代入して、レスポンスを返しているコードになります。

同じようなコードをKotlinで書いてみます。

ライブラリの追加

Kotlin 1.3.70からdependenciesに直接依存ライブラリを定義できるようになりました。

  • build.gradle.kts
dependencies {
    implementation(kotlin("stdlib-js"))
    implementation(npm("escape-html", "1.0.3"))
    implementation(npm("@types/escape-html", "0.0.20"))
}

escape-html はdukatが使えたので利用します。

  • build.gradle.kts
kotlin {
    sourceSets["main"].kotlin.srcDir("src/main/external")
    target {
        nodejs {}
        useCommonJs()
    }
}
$ dukat -d src/main/external/ build/js/node_modules/@types/escape-html/index.d.ts

helloHttp関数を作成

helloHttpラムダ式を代入しています。引数の型はJSから渡されるのでdynamicにしています。

req.query などは存在しなくてもエラーになりません。変数として扱う限りundefinedなだけです。

  • Main.kt
external val exports: dynamic

fun main() {
    exports.helloHttp = { req: dynamic, res: dynamic ->
        res.send(createMessage(req))
    }
}

private fun createMessage(req: dynamic): String {
    val message = when {
        req.query.name !== undefined -> escapeHTML(req.query.name)
        req.body.name !== undefined -> escapeHTML(req.body.name)
        else -> "World"
    }
    return "Hello $message"
}

ローカルで実行

次のコマンドでNode.jsで実行できます。

$ ./gradlew nodeRun

ただし先程の例だと何も出力されません。helloHttp関数が呼ばれていないからです。Cloud Functionsをローカルで確認するには @google-cloud/functions-frameworkを使うと確認できます。

現状、build.gradle.ktsのdependenciesからはdevDependenciesに追加できないので、npxを使ってコマンドを実行することで回避します。

$ cd build/js/packages/${PROJECT_NAME}
$ npx @google-cloud/functions-framework --target=helloHttp
npx: installed 52 in 3.789s
Serving function...
Function: helloHttp
URL: http://localhost:8080/

ローカルに立ち上がるので、アクセスするとhelloHttp関数が呼ばれます。

$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/"                                                         
Hello World

nameクエリをつけてアクセスしてみます。

$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/?name=\"Kotlin/JS\""
Hello "Kotlin/JS"

しっかりescape-htmlも動いています。

Cloud Functionsデプロイのためのプロジェクト構成

Cloud Functionsのデプロイに必要なものはJSのコードと、ライブラリを利用していたらpackage.jsonが必要です。

2つのファイルをまとめるタスクを定義します。

タスク定義

  • build.gradle.kts
tasks {
    val packaging by creating(Copy::class) {
        from("build/js/packages/${project.name}/kotlin/${project.name}.js", "build/js/packages/${project.name}/package.json")
        into("functions")
        rename { it.replace("${project.name}.js", "index.js") }
        
        doLast {
            val jsonFile = file("functions/package.json")
            val texts = jsonFile.readLines()
                .map { it.replace("kotlin/${project.name}.js", "index.js") }
            jsonFile.writeText(texts.joinToString("\n"))
        }
    }
}

functionsフォルダを生成して配置してます。jsファイルは、Cloud Functionsの制約があるのでindex.jsとなるようにリネームしています。合わせて、package.jsonのエントリーポイントをindex.jsとなるように書き換えてます。置き換え後のpackage.jsonは次のようになってます。

デプロイ先で依存の解決がされるので、npm install する必要はないです。

  • functions/package.json
{
  "main": "index.js",
  "devDependencies": {
    "source-map-support": "0.5.16"
  },
  "dependencies": {
    "kotlin": "1.3.72",
    "escape-html": "1.0.3",
    "@types/escape-html": "0.0.20"
  },
  "peerDependencies": {},
  "optionalDependencies": {},
  "bundledDependencies": [],
  "name": "kotlin-js-cloud-functions",
  "version": "1.0.0-SNAPSHOT"
}

次のように実行することでfunctionsフォルダに必要なファイルを 設置できます。

$ ./gradlew clean compileKotlinJs 
$ ./gradlew packaging

JSファイルを生成するだけならnodeRunではなくてcompileKotlinJsで生成できます。

生成してから置き換えタスクを実行します。

実行後は次のような配置になります。

$ tree -L 2
.
├── build
│   ├── js
│   ├── kotlin
│   └── tmp
├── build.gradle.kts
├── functions
│   ├── index.js
│   └── package.json
├── gradle
│   └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    └── test

デプロイ

gcloudでデプロイします。

取り掛かる前に、GCPのConfigurationが意図しているものかどうか確認しておいてください。

$ gcloud config list 

functionsフォルダに移動してからコマンドを実行します。

$ cd functions
$ gcloud functions deploy helloHttp --region=asia-northeast1 --trigger-http --runtime=nodejs8 --allow-unauthenticated 

サクッと確認したいため、--allow-unauthenticatedフラグを付けてpublicにしています。

次のようなURLが排出されると思います。

https://{REGION_NAME}-{PROJECT_NAME}.cloudfunctions.net/{FUNCTION_NAME}

まとめ

コードとしてはシンプルに導入できます。

デバッグも組み込まれてはいないですが、npxを利用することで回避できます。 デプロイに関しても、必要な処理は今回用意したタスクでまかなえるのでそこまで手間にならないと思います。

リポジトリはこちらです。

github.com

参考

Google Cloud Blog - News, Features and Announcements

Functions Framework  |  Cloud Functions Documentation  |  Google Cloud

gcloud functions deploy  |  Cloud SDK Documentation  |  Google Cloud

dukatでKotlin/JSを快適にする

前回、Kotlinで気持ちよくJSのライブラリを利用するには、モジュールを作成してあげる必要があると書きました。

sakebook.hatenablog.com

しかしこれをちゃんとやろうとすると、結構な手間になります。

定義して使い回しできるようにしている人がチラホラいます。

公式で出してくれれば揺らぎもなくてよいのになと思っていたら、公式からでてました。

github.com

dukat

d.tsの型定義ファイルからKotlinのコードを自動生成してくれます。

まだexperimentalですが、kotlin js pluginに含まれており、ビルドに組み込むことができます。

  • gradle.properties
kotlin.js.experimental.generateKotlinExternals=true
  • build.gradle.kts
plugins {
    kotlin("js") version "1.3.61"
}

repositories {
    jcenter()
}

dependencies {
    implementation(kotlin("stdlib-js"))
}

kotlin {
    sourceSets["main"].dependencies {
        // 利用したいライブラリを書く
    }
    target {
        nodejs {
            this.runTask {
                // バージョンを指定してあげる
                this.nodeJs.versions.dukat.version = "0.0.28"
            }
        }
    useCommonJs() // commonjs styleのJSを指定
    }
}

dukatは目下開発中のため、kotlin js pluginに組み込まれているバージョンは古いです。なので自分でバージョン指定を行い、最新バージョンにしてあげます。

次のコマンドでktファイルが自動生成されます。

$ ./gradlew generateExternals

生成されたktファイルは build/externals/${module}/src に配置されます。

ライブラリにd.tsファイルが同梱されているものしかktファイルは生成されません。ない場合は型定義ファイルも合わせて依存関係に追加すると使えます。

使ってみる(Plugin)

文字列操作ライブラリの voca に対して使ってみます。

github.com

型定義ファイルが別途用意されてるのでそちらも依存に加えます。こちらで検索できます。

  • build.gradle.kts
kotlin {
    sourceSets["main"].dependencies {
        implementation(npm("voca", "^1.4.0"))
        implementation(npm("@types/voca", "^1.4.0"))
    }
    ...
}

generateExternals タスクを実行すると今回は4つのktファイルが生成されます。

index.module_${library_name}kt ファイルにエントリーポイントやinterfaceなどが定義されています。ファイルの生成され方は型定義ファイルによって異なります。

今回利用する部分だけ抜粋します。

  • index.module_voca.kt
@JsModule("voca")
external val v: v.VocaStatic
  • index.v.module_voca.kt
@file:JsQualifier("v")
@file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "EXTERNAL_DELEGATION")
package v
...
external interface Chain {
    ...
    fun chain(): ExplicitChain<String>
    ...
}
...
external interface ExplicitChain<T> {
    ...
    fun upperCase(): ExplicitChain<String>
    ...
    fun trim(whitespace: String = definedExternally): ExplicitChain<String>
    ...
}

このように定義されているので、次のように利用できます。

  • main.kt
fun main() {
    println("Hello Kotlin/${v.chain("  js  ").trim().upperCase()}")
}

v.IDEなどでサジェストが効くようになっています。たまんないですね。もちろん型情報もあります。

実行すると期待通りの変換がされます。

$ ./gradlew nodeRun

> Task :generateExternals UP-TO-DATE
tests

> Task :nodeRun
Hello Kotlin/JS

現状の問題点

今回のようにうまくいけばいいのですが、experimentalなだけあってまだまだ問題はたくさんあります。

ktファイルの生成に失敗するケースが結構あります。ktファイルの生成はできたがコンパイルができないケースもあります。

また、複数ライブラリを使う場合、全てのd.tsファイルを捜査して変換を試みるので、一つでも失敗するものがあるとktファイルが生成されません。

後者はdukatをPluginを使わずに利用することで回避できます。

使ってみる(CLI)

dukatをインストールします。利用できればglobalインストールじゃなくてもnpxでも構いません。

$ npm install -g dukat

プロジェクトルートにいることを想定して、対象の型定義ファイルとktファイルの展開先を指定します。

$ dukat -d src/main/external/ build/js/node_modules/\@types/voca/index.d.ts 
nonDeclarations.v.nonDeclarations.kt
index.module_voca.kt
index.v.module_voca.kt
lib.es2015.iterable.module_dukat.kt

src/main/external/ というpathにktファイルを展開させました。src/main/kotlin/ でも良いのですが、自動生成されたファイルと分けたいので別にしました。

src/main/external/ というフォルダはソースと認識されないので、ソースとして追加します。Pluginを使ってる場合は自動で build/externals/${module}/srcをソースに追加してくれてるので問題になりませんでした。

  • build.gradle.kts
kotlin {
    sourceSets["main"].kotlin.srcDir("src/main/external")
    sourceSets["main"].dependencies {
        implementation(npm("voca", "^1.4.0"))
        implementation(npm("@types/voca", "^1.4.0"))
    }
    target {
        nodejs()
        useCommonJs()
    }
}

これで実行してみます。gradle.propertiesの kotlin.js.experimental.generateKotlinExternals=true は削除するかコメントアウトしてください。

$ ./gradlew nodeRun

> Task :nodeRun
Hello Kotlin/JS

無事に実行できました。

まとめ

dukatがあるのでKotlin/JSの未来は明るいです。ですが明るい未来はまだ遠いので、手元で使えるライブラリを選定して部分的に利用していくのが良いです。

今回動かしたリポジトリはこちらです。

github.com

参考

TypeScript Types Search

Kotlin 1.3.50 released | Kotlin Blog

GitHub - Schahen/dukatGradleDemo

kotlin/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/dukat at master · JetBrains/kotlin · GitHub

Kotlin/JSことはじめ

なんでもKotlinで解決したいと思いがちの筆者です。

Kotlin/JSを触ってみたので動かし方などまとめます。

環境は

  • Kotlin 1.3.61
  • Node 13.3.0

です

Setup

Gradle Plugin

kotlin js プラグインを使います。

  • build.gradle.kts
plugins {
    kotlin("js") version "1.3.61"
}

kotlin jsプラグインを使えば、settings.gradleでのpluginManagementは不要です。

Kotlin関連のプラグインやライブラリでは kotliin("XXX") という書き方をすることで記述をスッキリできます。中身はただの拡張関数です

Dependencies

kotlin stdlib-js ライブラリを使います。

  • build.gradle.kts
dependencies {
    implementation(kotlin("stdlib-js"))
}

Build configuration

フロント(Browser)向けのJSと、サーバ(Node)向けのJSがあるので、どちらでも動かせるようなJSを吐き出すようにします。

targetブロックではビルドする対象を指定し、tasksブロックでは既存taskのプロパティの変更を行っています。

  • build.gradle.kts
kotlin.target {
    nodejs()
    browser()
}

tasks {
    compileKotlinJs {
        kotlinOptions {
            moduleKind = "umd"
        }
    }
}

moduleKindに設定できる値は amd, commonjs , umd , plain とありますが、デフォルトは plain です。

フロントで使いたければ amd, サーバで使いたければ commonjs, 両方で使いたければ umd を選択する感じです。

今回はどちらでも動かすので umdを選択します。

main.kt

src フォルダに作成します。 - main.kt

fun main() {
    println("Hello Kotlin/JS!!")
}

Build

次のコマンドを実行します

$ ./gradlew build

build フォルダが生成され、次のような構成になります。

.
└── build
    ├── js
    │   ├── node_modules/
    │   ├── node_modules.state
    │   ├── package.json
    │   ├── packages/
    │   ├── packages_imported/
    │   └── yarn.lock
    ├── kotlin
    │   ├── compileKotlinJs/
    │   └── sessions/
    ├── libs
    │   ├── kotlin-js-example-0.0.1.jar
    │   └── kotlin-js-example-js-0.0.1-sources.jar
    ├── reports
    │   └── tests/
    └── tmp
        ├── JsJar/
        ├── expandedArchives/
        └── kotlinSourcesJar/

jsは、JavaScriptのルートフォルダのようなものです。js/packages にトランスパイルされたJSファイルが含まれています。js/packages_importedにはトランスパイルされたJSファイルが依存しているpackageが含まれています。

Run

Browser

動作確認用に適当なhtmlファイルを作成します。

プロジェクトルートに次のhtmlを作成します。

  • index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="build/js/packages_imported/kotlin/1.3.61/kotlin.js"></script>
    <script src="build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js"></script>
</head>

<body>
<p>Kotlin/JS example</p>
</body>
</html>

生成したJSファイルはkotlin.jsに依存しているので、kotlin.jsを読み込んだ後に生成した ${module}.jsを読み込みます。

ファイル名はプロジェクトのものが適応されます。

f:id:sakebook:20200218015239p:plain

ちなみに今回は詳しく取り上げませんが、 divコードから動的に生成できます。

Node

nodeコマンドで実行します。

$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
Hello Kotlin/JS!!

これでどちらでも動作確認ができました。

JavaScriptライブラリの追加

kotlinブロックの中で定義します。さらにどのsourceフォルダで利用するかも指定します。 このあたりはMPPが意識されてる感じがしますね。

次の例では dayjs を追加してます。

  • build.gradle.kts
kotlin {
    sourceSets["main"].dependencies {
        implementation(npm("dayjs", "^1.8.20"))
    }
}

npm 関数を使います。 これで build/js/node_modules/ 内にDLされます。

ライブラリの利用(Node)

ソースを次のように変更します。

  • main.kt
external fun require(module: String): dynamic

fun main() {
    val dayjs = require("dayjs")
    println("Hello Kotlin/JS!! ${dayjs()}")
}

これをnodeで実行します。

$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
Hello Kotlin/JS!! Sat, 08 Feb 2020 10:56:59 GMT

少し解説します。

external

external 修飾子を付けたものはJavaScriptからも呼び出しできるようにトランスパイルされます。そのままの名前で変換されます。

nodeで require が定義されているため今回は実行できています。これを別名にするとエラーになります。

  • main.kt
external fun req(module: String): dynamic

fun main() {
    val dayjs = req("dayjs")
    println("Hello Kotlin/JS!! ${dayjs()}")
}
$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
/USER_PATH/build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js:35
    var dayjs = req('dayjs');
                ^

ReferenceError: req is not defined

自由に命名したい場合JsNameアノテーションをつけてあげれば解決します。

  • main.kt
@JsName("require")
external fun req(module: String): dynamic

dynamic

JavaScriptの世界の型を受け止めるための便宜的な型です。

dynamicはどんな変数や関数も代入でき、実行可能です。

関数はdynamic型を返します。

未定義の関数は実行時にTypeErrorになります。未定義の変数はundefinedになります。

  • main.kt
fun main() {
    val dyn: dynamic = object{}
    println(dyn.some)
    dyn.foo = "foo"
    println(dyn.foo)
    dyn.bar = {
        println("bar")
    }
    dyn.bar()
    println(dyn.bar)
    dyn.some()
}
$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
undefined
foo
bar
function main$lambda() {
    println('bar');
    return Unit;
  }
/USER_PATH/build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js:35
    dyn.some();
        ^

TypeError: dyn.some is not a function

dynamic型の仕様についてはこちらにドキュメントがあります。

github.com

ライブラリの利用(Browser)

Browserの場合は require で読み込みは行わないのでエラーになります。

f:id:sakebook:20200218021013p:plain

Browserの場合は、 require で読み込むのをやめて追加したライブラリをscriptタグで読み込んであげると実行できます。

  • main.kt
fun main() {
    println("Hello Kotlin/JS!! ${js("dayjs()")}")
}
  • index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="build/js/packages_imported/kotlin/1.3.61/kotlin.js"></script>
    <script src="build/js/node_modules/dayjs/dayjs.min.js"></script>
    <script src="build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js"></script>
</head>

<body>
<p>Kotlin/JS example</p>
</body>
</html>

f:id:sakebook:20200218021305p:plain

js

js 関数の引数に渡した文字列は、そのままJavaScriptのコードとして実行されます。

この方法だとBrowserとNodeのどちらでも動くJavaScriptが作成できません。なので、ライブラリをModule化します。

ライブラリのModule化

external修飾子をつけているものに、更にJSModuleアノテーションを付与します。

引数にはnpmなどでインストールする名前と同じものを使います。

umd 形式の場合、JSNonModuleアノテーションもセットで必要です。

  • main.kt
@JsModule("dayjs")
@JsNonModule
external fun dayjs(): dynamic

fun main() {
    println("Hello Kotlin/JS!! ${dayjs()}")
}

これで実行可能な形式でJSファイルが生成されているので、どちらの形式でも実行可能になりました。

実際のライブラリに則している形であれば定義方法は自由です。例えば次のように定義することもできます。

  • main.kt
@JsModule("dayjs")
@JsNonModule
@JsName("dayjs")
external class DayJs(any: Any? = definedExternally) {
    fun format(): String
}

fun main() {
    println("Hello Kotlin/JS!! ${DayJs().asDynamic()}")
    println("this year         ${DayJs().asDynamic().year()}")
    println("Valentine         ${DayJs("2020-02-14").asDynamic()}")
    println("Formatted date    ${DayJs().format()}")
}
$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
Hello Kotlin/JS!! Sat, 15 Feb 2020 10:47:09 GMT
this year         2020
Valentine         Thu, 13 Feb 2020 15:00:00 GMT
Formatted date    2020-02-15T19:47:09+09:00

definedExternallyプレースホルダのようなものです。JSで定義されていてKotlin側からはわからないものがあるときに使います。

yearは実際にdayjsライブラリで定義されている関数を、dynamicから直接呼び出しています。なので型はdynamicで返ってきます。

formatも実際にdayjsライブラリで定義されているの関数なのですが、再定義することで型情報を与えることができます。

Kotlinで気持ちよくJSのライブラリを利用するには、このようにモジュールを作成して必要な関数たちを再定義してあげる必要があります。

JetBrains公式からReactのラッパーモジュールが提供されています。

github.com

Webpack

先程buildコマンドで作成したBrowser用のJSファイルは、複数必要でした。これだと、ライブラリを追加するたびにhtmlも変更しなければならないため、手間です。

Webpackを利用してビルドすることで、1つのJSファイルにまとめることができます。

次のコマンドで作成できます。このコマンドはtargetブロックで browser を宣言していないと使えないので注意が必要です。

$ ./gradlew browserWebpack

作成されたJSファイルは build/distributions/ に吐き出されます。

htmlを変更します。

  • index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="build/distributions/kotlin-js-example.js"></script>
</head>

<body>
<p>Kotlin/JS example</p>
</body>
</html>

読み込むのは1つのファイルですが、無事実行できました。

f:id:sakebook:20200218021707p:plain

このJSファイルはnodeからも実行できます。

$ node build/distributions/kotlin-js-example.js 
Hello Kotlin/JS!! Sat, 15 Feb 2020 11:36:33 GMT
this year         2020
Valentine         Thu, 13 Feb 2020 15:00:00 GMT
Formatted date    2020-02-15T20:36:33+09:00

browserRun というコマンドもあるのですが、まだバグがあり正常に終了しないのでJSファイルが作成できません。ちなみにbrowserRunで立ち上がるサーバは、 src/main/resrouces/index.html を見ます。

まとめ

今回はKotlin/JSを一から、 umd 形式でアウトプットして動かしてみました。

実際は commonjs 形式で利用することが多いと思いますが、このように、どうしてそうなるのかをきちんと理解しておけば、困ったときにデバッグしやすいと思います。

ネットで見つかるサンプルも、古いものや解説がないものばかりなのでこの記事は参考になると思います。

今回検証したリポジトリはこちらです。

github.com

参考

Kotlin to JavaScript - Kotlin Programming Language

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

GitHub - Kotlin/kotlinx.html: Kotlin DSL for HTML

GitHub - iamkun/dayjs: ⏰ Day.js 2KB immutable date library alternative to Moment.js with the same modern API

Calling JavaScript from Kotlin - Kotlin Programming Language