メモ2ブログ

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

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

dartdocでAPIドキュメントを作成する

Dartで書かれたプロジェクトであれば、 dartdoc でドキュメントが自動生成できます。

$ dartdoc

自分が作りたかった理由はPluginのscoreを上げたかったからなのですが、ローカルで試すに当たり、Dart自体の、Pluginには不要なドキュメントが生成されてしまいました。

なのでそれを除外するコマンドが次です。

$ dartdoc --exclude 'dart:async,dart:collection,dart:convert,dart:core,dart:developer,dart:io,dart:isolate,dart:math,dart:typed_data,dart:ui,dart:ffi,dart:html,dart:js,dart:js_util'

まだ泥臭い指定をしなければ一緒に生成されてしまうみたいです。

確認したバージョンは次のとおりです。

$ dartdoc --version
dartdoc version: 0.29.1

今開発中のバージョンが 0.30.1まで出てますが、関連した修正は無さそうです。気軽に対象のディレクトリが指定できる形だと楽な気がしますね。

参考

dartdoc | Dart

dartdoc command should not generates docs for dart-sdk · Issue #1949 · dart-lang/dartdoc · GitHub