ts2kt を Gradle で実行する

概要

TypeScript の型定義を Kotlin ファイルに変換する Node.js スクリプトである ts2kt を Gradle で実行できるようにします。

2017/8/31 現在、ts2kt は使える状態ではありません。 生成された Kotlin ファイルでは多数のエラーが発生します。

目次

確認環境

  • IntelliJ IDEA Community 2017.2
  • Kotlin 1.1.4-2
  • Gradle 3.5
    • Groovy 2.4.10

参考情報

解説

例として Vue.js の型情報 (*.d.ts) を Kotlin ファイル (*.kt) に変換する。

手順としては以下の通り。

  1. node をプロジェクト用にダウンロード
  2. npm を使い ts2kt と vue をインストール
  3. ts2kt で *.d.ts*.kt に変換

まずは build.gradle を以下の通りに記述する。

buildscript {
    ext.gradle_node_version = '1.2.0'

    repositories {
        jcenter()
        maven {
            // gradle-node-plugin
            url "https://plugins.gradle.org/m2/"
        }
    }

    dependencies {
        classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version"
    }
}

apply plugin: 'com.moowork.node'

node {
    // node をプロジェクト用にダウンロード
    download = true
}

task ts2kt(type: NodeTask) {
    String nodeDir = "${project.projectDir}/node_modules"
    script = file("$nodeDir/ts2kt/ts2kt.js")

    String destDir = 'src/main/kotlin/org/vuejs'
    file(destDir).mkdir()

    args = ['-d', destDir] + file("$nodeDir/vue/types").listFiles().collect { it.absolutePath }
}

npm の設定として pakage.json を以下の通りに記述する。

{
  "name": "Vue.kt",
  "version": "2.4.2",
  "description": "Vue.kt that is Vue.js in Kotlin",
  "devDependencies": {
    "ts2kt": "0.0.14",
    "vue": "2.4.2"
  }
}

設定が完了したら以下のコマンドを実行する。

$ ./gradlew npmInstall ts2kt

src/main/kotlin/org/vuejs ディレクトリに Kotlin ファイルが生成される。

例えば vue.kt の一部。

external interface `T$0` {
    @nativeInvoke
    operator fun invoke(): VNode
    @nativeInvoke
    operator fun invoke(tag: String, children: VNodeChildren): VNode
    @nativeInvoke
    operator fun invoke(tag: String, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode
    @nativeInvoke
    operator fun invoke(tag: Component, children: VNodeChildren): VNode
    @nativeInvoke
    operator fun invoke(tag: Component, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode
    @nativeInvoke
    operator fun invoke(tag: AsyncComponent, children: VNodeChildren): VNode
    @nativeInvoke
    operator fun invoke(tag: AsyncComponent, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode
}
... <snip> ...
    open fun <T> `$watch`(expOrFn: (this: Vue /* this */) -> T, callback: WatchHandler<Vue /* this */, T>, options: WatchOptions? = definedExternally /* null */): () -> Unit = definedExternally
... <snip> ...

元は vue.d.ts のこの部分。

export type CreateElement = {
  // empty node
  (): VNode;

  // element or component name
  (tag: string, children: VNodeChildren): VNode;
  (tag: string, data?: VNodeData, children?: VNodeChildren): VNode;

  // component constructor or options
  (tag: Component, children: VNodeChildren): VNode;
  (tag: Component, data?: VNodeData, children?: VNodeChildren): VNode;

  // async component
  (tag: AsyncComponent, children: VNodeChildren): VNode;
  (tag: AsyncComponent, data?: VNodeData, children?: VNodeChildren): VNode;
}
... <snip> ...
  $watch<T>(
    expOrFn: (this: this) => T,
    callback: WatchHandler<this, T>,
    options?: WatchOptions
  ): (() => void);
... <snip> ...

生成された Kotlin ファイルにはいくつか問題がある。

  1. T$0 という名前が同一パッケージ内で複数定義されることになり名前が衝突する。
  2. @nativeInvoke は Deprecated
  3. VNodeChildren, Component, AsyncComponent などの型が定義できていない
    • Union Type に対応できていないため型を定義できない
  4. パラメータ名に this (Kotlin のキーワード) が使われている

Union Type に関する議論はされている様子。

discuss.kotlinlang.org

既存の JavaScript ライブラリを使うのであれば ts2kt による型情報を補足が欲しいところ。

Kotlin/JS TIPS - this の扱い

概要

JavaScript の関数中の this は関数を保持しているオブジェクトによって決定されます。 この振る舞いが Kotlin/JS を使う上での問題になります。 this を扱ういくつかの方法を説明します。 最適な方法とは言えないので、より良い方法があれば教えてください。

ソースコードは前回の記事の続きになっています。 詳しくは前回の記事を参照してください。

nosix.hatenablog.com

目次

確認環境

  • IntelliJ IDEA Community 2017.2
  • Kotlin 1.1.3-2
  • Gradle 3.5
    • Groovy 1.4.10

参考情報

解説

this の問題点

以下の JavaScript のコードは 算出プロパティとウォッチャ - Vue.js から引用したコードに少しだけ手を加えたコードである。firstNamelastName に値を設定すると fullName が算出されるサンプルである。

var vm = new Vue({
  el: '#example',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
vm.firstName = 'Taro'
vm.lastName = 'Yamada'

この処理を Kotlin/JS で書いてみる。SampleClient.kt を以下に変更する。

@JsNonModule
@JsModule("vue")
external open class Vue(option: VueOption)

external interface VueOption : Json {
    var el: Any
    var data: Json
    var computed: Json
}

fun <T : Json> Json(): T = js("({})")
fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init)

fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init))

external interface Model : Json {
    var firstName: String
    var lastName: String
}

fun run() {
    val vm: dynamic = Vue {
        el = "#example"
        data = Json<Model> {
            firstName = "Foo"
            lastName = "Bar"
        }
        computed = Json {
            // this は Json クラスのインスタンス
            this["fullName"] = {
                // this は Vue クラスのインスタンス
                this as Model // firstName, lastName を参照したいのでキャスト
                "${this.firstName} ${this.lastName}"
            }
        }
    }
    vm.firstName = "Taro"
    vm.lastName = "Yamada"
}

sample-client/index.html には以下を記述して、ブラウザで開いてみる。

<div id="example">{{ fullName }}</div>

ブラウザに表示される内容は undefined undefined である。 残念ながら上記のコードではやりたいことは行えない。 ${this.firstName} ${this.lastName}this に問題がある。

JavaScript と Kotlin での this の違い

先の例では、JavaScript の関数中の this は関数を保持しているオブジェクトを示している。 実行時に決定されるので、this は Vue クラスのインスタンスを示すことになる。

一方 Kotlin の例では、Kotlin のラムダ中の this はそのラムダの外側のスコープの this になっている。 つまり、Json クラスのインスタンスを示すことになる。

レシーバ付き関数リテラル

実行時に this を決定したいのであれば、レシーバ付き関数リテラルを使用する。 SampleClient.kt を以下に変更する。

fun run() {
    val vm: dynamic = Vue {
        el = "#example"
        data = Json<Model> {
            firstName = "Foo"
            lastName = "Bar"
        }
        computed = Json {
            // this は Json クラスのインスタンス
            this["fullName"] = fun Model.(): String {
                // this は実行時に決定される
                return "${this.firstName} ${this.lastName}"
            }
        }
    }
    vm.firstName = "Taro"
    vm.lastName = "Yamada"
}

external との併用における問題点

external interface を使うことで、firstName =, lastName = の様な簡潔な記述を実現している。 fullName も同様に簡潔な記述にするために以下のコードを追加したくなる。

external interface Computed : Json {
    var fullName: Model.() -> String
}

残念ながらコンパイルエラーとなる。 external 宣言中ではレシーバ付き関数型は禁止されているとエラーメッセージが表示される。 (禁止している理由を知っている方がいたら教えてください。)

external との併用

レシーバ付き関数リテラルを使えば this の扱いを動的に変更できるが、 external 宣言と併せて使うことができない。 external 宣言を使う場合にはレシーバなしの関数リテラルにする必要がある。 例えば、以下のとおり。

external interface Computed : Json {
    var fullName: () -> String
}

そうすると最初の問題に戻ってしまう。 回避するための苦肉の策として js("this") を使う方法がある。 コード生成時に JavaScriptthis をそのまま出力させる。 以下がその例である。

inline fun <T> thisAs(): T = js("this")

fun run() {
    val vm: dynamic = Vue {
        el = "#example"
        data = Json<Model> {
            firstName = "Foo"
            lastName = "Bar"
        }
        computed = Json<Computed> {
            // this は Json クラスのインスタンス
            fullName = {
                val self = thisAs<Model>()
                // self は実行時に決定される
                "${self.firstName} ${self.lastName}"
            }
        }
    }
    vm.firstName = "Taro"
    vm.lastName = "Yamada"
}

inline を付けないと thisAs 関数が生成され、thisAs 関数を保持しているオブジェクトが存在しないために thisAs 関数の結果は undefined になる。

ソースコード

Release 2017-08-01-164107 · nosix/kotlin-js · GitHub

Kotlin/JS TIPS - JavaScript ライブラリへの型情報の付与

概要

JavaScript ライブラリを使用する際に、型情報を付与する方法について説明します。 前回の記事で JavaScript ライブラリの使用方法を説明しましたが、 静的型付けによる恩恵を得ずに JavaScript の柔軟性を重視した使用方法としていました。 型情報を付与することによって、以下の恩恵を得られる様にしていきます。

ライブラリ開発者の手間を増やして、ライブラリ利用者の手間を減らしていきます。

前回の記事:

nosix.hatenablog.com

目次

確認環境

  • IntelliJ IDEA Community 2017.2
  • Kotlin 1.1.3-2
  • Gradle 3.5
    • Groovy 1.4.10

参考情報

解説

json 関数と Json インターフェース

JavaScript では {} によってオブジェクト生成を行う。 Kotlin/JS では json 関数によってオブジェクトを生成できる。 生成されたオブジェクトは Json インターフェース を実装したオブジェクトであり、 [] による操作を行える。 以下は SampleClient.kt の一部である。

fun run() {
    val option = json()
    option["el"] = "#example"
    option["data"] = json("message" to "Hello World")
}

生成される JavaScript は以下の様になっている。

function run() {
    var option = json([]);
    option['el'] = '#example';
    option['data'] = json([to('message', 'Hello World')]);
}

json 関数のソースコード (Kotlin) は以下になっており、js("({})") でオブジェクトを生成していることがわかる。

public fun json(vararg pairs: Pair<String, Any?>): Json {
    val res: dynamic = js("({})")
    for ((name, value) in pairs) {
        res[name] = value
    }
    return res
}

また、Json インターフェースのソースコード (Kotlin) は以下になっている。

public external interface Json {
    operator fun get(propertyName: String): Any?
    operator fun set(propertyName: String, value: Any?): Unit
}

external を使うことで JavaScript のコードをそのまま使用することができる。 get/set を定義することで [] による参照/代入を可能にしている。

型情報の付与

json 関数の仕組みを応用して以下のとおりに記述することで、Json コンストラクタに見せかけることができる。

fun Json(): Json = js("({})")

fun run() {
    val option = Json()
    option["el"] = "#example"
}

さらに、external を使うことで JavaScript のコードをそのまま使用できる性質を利用して、 [] を使わずにオブジェクトに値を設定できる様にする。 例えば、以下の様に。

fun <T : Json> Json(): T = js("({})")

external interface VueOption : Json {
    var el: Any
    var data: Json
}

external interface Model : Json {
    var message: String
}

fun run() {
    val option = Json<VueOption>()
    option.el = "#example"
    option.data = Json<Model>().apply {
        message = "Hello World"
    }
    option["computed"] = Json() // VueOption は Json インターフェースを実装しているので [] を使える
}

生成された JavaScript は以下のとおり。 external を使用した部分は簡潔なコードとなっている。 コード上に VueOptionModel インターフェースは現れない。

  function Json() {
    return {};
  }

  function run() {
    var option = Json();
    option.el = '#example';
    var $receiver = Json();
    $receiver.message = 'Hello World';
    option.data = $receiver;
    option['computed'] = Json();
  }

さらに、初期化処理を行う関数を引数 (init) にとる Json 関数を追加することで、 ()apply を省略できる。

fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init)

fun run() {
    val option = Json<VueOption> {
        el = "#example"
        data = Json<Model> {
            message = "Hello World"
        }
        this["computed"] = Json()
    }

external interface を使うことで型情報を付与し、 補完機能の恩恵を得えつつ、 誤った型の値を代入する危険性を減らすことができる。

Vue.js に適用する

さらに、Vue 関数を SampleClient.kt に追加すると、Vue の初期化が簡潔になる。

@JsNonModule
@JsModule("vue")
external class Vue(option: VueOption)

fun <T : Json> Json(): T = js("({})")
fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init)

external interface VueOption : Json {
    var el: Any
    var data: Json
}

external interface Model : Json {
    var message: String
}

fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init))

fun run() {
    val vm: dynamic = Vue {
        el = "#example"
        data = Json<Model> {
            message = "Hello World"
        }
    }
    vm.message = "Hello Kotlin World"
}

前回の記事と比べると随分と簡潔になったと思う。 しかし、検討すべき課題は残っている。 補完機能の恩恵は受けられるようになったが、型安全という面では十分とは言えない。 上記の例で言えば、elString もしくは HTMLElement であるため、Any にしている。 また、vm.message を参照するために dynamic にしている。 JavaScript の動的な側面を扱う方法については検討が必要である。

ソースコード

Release 2017-08-01-143728 · nosix/kotlin-js · GitHub

Kotlin/JS で JavaScript ライブラリを使用する

概要

Kotlin/JS から JavaScript ライブラリを使用する方法を説明します。 AMD に対応している JavaScript ライブラリを前提としています。 例として Lodash, Vue.js を使います。 作成するコードでは型付けは弱いままとして、JavaScript の柔軟性を活かした例としています。 Kotlin による静的型付けの恩恵は得られません。 静的型付けを活かした書き方は別途記事を書きます。

前回の記事の続きになっているので、必要に応じて参照してください。

ソースコード : Release 2017-07-30-211651 · nosix/kotlin-js · GitHub

nosix.hatenablog.com

目次

確認環境

  • IntelliJ IDEA Community 2017.2
  • Kotlin 1.1.3-2
  • Gradle 3.5
    • Groovy 1.4.10

参考情報

解説

JavaScript ライブラリを使用する

前回の記事では、 RequireJS を使用して JavaScript の依存関係を管理できるようにした。 RequireJS は AMD に対応している JavaScript ライブラリを管理するだけでなく、AMD に対応していないライブラリも管理できる。

本記事では、AMD に対応していないライブラリについては記載しない。 AMD に対応していないライブラリでは試行していないためである。 RequireJS にて AMD に対応していないライブラリを使用する場合には config で shim を使えば良いらしい。 (参照 : RequireJS使い方メモ - Qiita)

以降では、AMD に対応しているライブラリについて説明する。

JavaScript ライブラリを CDN 経由で使用する

ライブラリをダウンロードせず、CDN (Content Delivery Network) を使用して手軽に試したい場合がある。

例えば、Lodash を使う場合、以下のサイトで配信されており、URL が記載されている。

lodash free CDN links by jsDelivr - A super-fast CDN for developers and webmasters

RequireJS で使用するには、前回の記事で作成した require-config.js に上記サイトに記載されている URL を追記すればよい。 以下は require-config.js の内容である。

var require = {
    baseUrl: 'build/js',
    // 追加
    paths: {
        lodash: 'https://cdn.jsdelivr.net/lodash/4.17.4/lodash', // 拡張子の.jsは不要
    },
    // ^^^
    enforceDefine: true,
};

次に、Kotlin/JS のコードを記述する。 型チェックが弱いままで使用する方法である。 JavaScript のコードをほぼそのまま記述するため記述の手間は少ないが、型チェックや補完機能の恩恵を得られない方法である。

SampleClient.kt を以下に変更する。

external val lodash: dynamic

fun run() {
    println(lodash.capitalize("hello world"))
}

external val lodash: dynamic とすると、JavaScript の lodash 変数を参照できる。 external は外部、すなわち JavaScript のコードを示す。 dynamic は変数の型が動的であることを示す。

ビルドで生成される JavaScript のコードは以下になる。(一部抜粋)

  function run() {
    println(lodash.capitalize('hello world'));
  }

このコードでは lodash 変数が見つからずにエラーとなる。 また、生成された sample-client.js では lodash への依存関係が定義されない。

エラーを解決するために SampleClient.kt を以下に変更する。

@JsNonModule
@JsModule("lodash")
external val lodash: dynamic

fun run() {
    println(lodash.capitalize("hello world"))
}

依存関係を定義させるためには @JsModule を使用して、モジュール名(ライブラリ名)を指定する。

@JsModule を使用しており、かつ build.gradlemoduleKind = "plain"moduleKind = "umd" とした場合には、ビルドエラーとなる。 モジュールシステムを使用しない場合のために @JsNonModule を指定する必要がある。

前回の記事に引き続き moduleKind = "umd" としているため、 @JsModule@JsNonModule を指定している。

@JsModule を指定した場合には、生成される JavaScript コードは以下になる。(一部抜粋)

  function run() {
    println($module$lodash.capitalize('hello world'));
  }

以上でエラーは解消され、ブラウザのコンソールに Hello world が表示される。

Lodash を使う場合の注意点

Lodash を moduleKind = "plain" で使う場合には注意が必要である。 Lodash や Underscore.js では Global スコープの _ を使用する。

そのため、SampleClient.kt のコードは以下にする。

external val `_`: dynamic

fun run() {
    println(`_`.capitalize("hello world"))
}

Kotlin/JS が生成するコードでは、Closure スコープに _ を使用する。 このため、Global スコープの _ を隠蔽してしまいエラーとなる。

sample-client.js を読み込む前に _lodash に代入して、変数名も lodash にすれば対応できる。 しかし、モジュールシステムを使用した方が簡潔である。

JavaScript ライブラリをダウンロードして使用する

Gradle では JavaScript ライブラリとの依存関係定義のために WebJars を使用することができる。 前回の記事でも RequireJS との依存関係定義に使用した。

例として、Vue.js の依存関係を追加する。

sample-client/build.gradle に以下を追加する。

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
    compile "com.example:sample:1.0-SNAPSHOT"
    compile 'org.webjars:requirejs:2.3.3'
    compile 'org.webjars:vue:2.1.3' // 追加
}

次に、Vue.js の使い方に従い sample-client/index.html に以下を追記する。

<div id="example">{{ message }}</div>

message を表示するように SampleClient.kt を変更する。

@JsNonModule
@JsModule("vue")
external class Vue(option: Json)

fun run() {
    val vm = Vue(json(
            "el" to "#example",
            "data" to json(
                    "message" to "Hello World"
            )
    ))
}

比較のため JavaScript で記述する場合を記載しておく。

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello World'
  }
})

Kotlin と JavaScript の対応は以下になっている。

Kotlin JavaScript
Vue() new Vue()
json() {}
"key" to value key: value

Lodash の時との違いとしては、external val Vue: dynamic ではなく external class Vue(option: Json) としている。 Vue.js の場合にはモジュールオブジェクトは関数オブジェクトになっており、その関数はインスタンス生成に使用される。 val Vue とした上で Vue(...) とすると関数実行になるが、 class Vue とした上で Vue(...) とすればインスタンス生成になる。

client-sample/index.html をブラウザで表示すれば、Hello World と表示される。

しかし、vm 変数の型は dynamic ではなく Vue となる。 これにより、Vue クラスに定義されていないプロパティを参照できなくなる。 例えば、Vue.js では JavaScript の場合には以下のように書くことができる。

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello World'
  }
})
vm.message = 'Hello Kotlin World' 

Kotlin/JS では class Vue にプロパティを定義しなければならない。 しかし、以下の様に dynamic 型を使えばプロパティに定義しなくとも参照できる。

fun run() {
    val vm: dynamic = Vue(json(
            "el" to "#example",
            "data" to json(
                    "message" to "Hello World"
            )
    ))
    vm.message = "Hello Kotlin World"
}

ソースコード

Release 2017-07-31-193534 · nosix/kotlin-js · GitHub

Gradle で Kotlin/JS ライブラリを開発する

概要

Gradle を使用して Kotlin/JS の開発環境を構築し、Hello World を作成します。 更に、ライブラリとして配布可能にする方法、そのライブラリを使用する方法を説明します。 JavaScript のライブラリを使用する方法は別の記事で説明します。

なお、筆者は JavaScript 界については詳しくないので、誤りがあれば優しく教えてください。

目次

確認環境

  • IntelliJ IDEA Community 2017.2
  • Kotlin 1.1.3-2
  • Gradle 3.5
    • Groovy 2.4.10

参考情報

解説

環境構築

Gradle の kotlin2js plugin を使用して Kotlin/JS のコードをビルドする環境を構築する。

モジュール単位に js ファイルが生成されるため、複数のモジュールを作成する前提で構築する。 手順としては、空プロジェクトを作成し、モジュールを追加する。 モジュール名は sample として作成した。

初期設定

生成された sample/build.gradle は以下の通り。

group 'com.example'
version '1.0-SNAPSHOT'

buildscript {
    ext.kotlin_version = '1.1.3-2'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin2js'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
}

Sample.ktsrc/main/kotlin に作成する。

fun main(args: Array<String>) {
    println("Hello World")
}

module root (例では sample ディレクトリ) にてビルドを実行。

$ ./gradlew build

HTML (sample/index.html) を作成する。HTML ではビルドにより生成された JavaScript を読み込む。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>サンプル</title>
</head>
<body>

<script src="build/classes/main/sample_main.js"></script>

</body>
</html>

ここまでに作成、および生成したファイルは以下の通り。(重要な部分のみ記載。)

sample <- module root
    build
        classes
            main
                sample_main
                    root-package.kjsm
                sample_main.js
                sample_main.meta.js
    src
        main
            java
            kotlin
                Sample.kt
            resources
        test
            java
            kotlin
            resources
    build.gradle
    index.html

ブラウザで表示すると kotlin モジュールが見つからずにエラーとなる。

kotlin.js の抽出

Gradle を使う場合には jar ファイルから js ファイルを抽出する必要がある。 sample/build.gradle に以下を追記する。 (参照 : http://kotlinlang.org/docs/tutorials/javascript/getting-started-gradle/getting-started-with-gradle.html)

build.doLast() {
    // Copy *.js from jar into build directory
    configurations.compile.each { File file ->
        copy {
            includeEmptyDirs = false

            from zipTree(file.absolutePath)
            into 'build/classes/main'
            include '**/*.js'
            eachFile { details ->
                def names = details.path.split('/')
                details.path = names.getAt(names.length - 1)
            }
        }
    }
}

HTML では kotlin.js を読み込ませる記述を sample_main.js の前に追記。

<script src="build/classes/main/kotlin.js"></script>
<script src="build/classes/main/sample_main.js"></script>

以上でブラウザのコンソールに Hello World が表示される。

パスの変更

ソースコードを配置するディレクトリと生成される JavaScript が配置されるディレクトリは階層が深い。 階層を減らすことで何らかの支障をきたすかもしれないが、 今回のサンプルの範囲では問題ないため、 配置するディレクトリを変更する。 (今回のサンプルでは、ソースセットには Kotlin ソースコード以外は置かず、テストを作成していないため問題ない。)

sample/build.gradle に以下を追記する。

sourceSets {
    main.kotlin.srcDirs += "src/main"
    test.kotlin.srcDirs += "src/test"
}

def outputDir = "${projectDir}/build/js"

compileKotlin2Js {
    kotlinOptions {
        outputFile = "${outputDir}/sample.js"
    }
}

build.doLast() {
    // Copy *.js from jar into build directory
    configurations.compile.each { File file ->
        copy {
            includeEmptyDirs = false

            from zipTree(file.absolutePath)
            into outputDir // 変更
            include '**/*.js'
            eachFile { details ->
                def names = details.path.split('/')
                details.path = names[names.length - 1]
            }
        }
    }
}

ファイルの配置は以下に変更される。

sample <- module root
    build
        js
            sample
                root-package.kjsm
            kotlin.js
            kotlin.meta.js
            sample.js
            sample.meta.js
    src
        main
            Sample.kt
        test
    build.gradle
    index.html

JavaScript の配置が変更されたので HTML (sample/index.html) の内容も変更する。

<script src="build/js/kotlin.js"></script>
<script src="build/js/sample.js"></script>

生成ファイル

ビルドすることで生成されるファイルは 3 種類ある。

ライブラリ化

ビルドすることで jar ファイルが生成される。 jar ファイルには 3 種類のファイル (js, meta.js, kjsm) が全て含まれている。 kjsm ファイルが含まれていれば、IDE の型チェックが有効となる。

例えば、sample モジュールの main 関数を sayHello 関数に変更して、他のモジュール (sample-client モジュールを作成) から使用する。

まず、sample モジュールの Sample.kt を以下に変更する。

fun sayHello() {
    println("Hello World")
}

ライブラリ化する際には、依存している js ファイルを抽出する必要はない。 sample/build.gradle から以下の記述を無効化、もしくは削除する。

//build.doLast() {
//    // Copy *.js from jar into build directory
//    configurations.compile.each { File file ->
//        copy {
//            includeEmptyDirs = false
//
//            from zipTree(file.absolutePath)
//            into outputDir
//            include '**/*.js'
//            eachFile { details ->
//                def names = details.path.split('/')
//                details.path = names[names.length - 1]
//            }
//        }
//    }
//}

ビルドすると build/libs/sample-1.0-SNAPSHOT.jar が生成される。

次に、sample-client モジュールを作成し、sample モジュールと同様に設定する。 jar の依存関係を追加すると sample-client/build.gradle は以下の通りになる。

buildscript {
    ext.kotlin_version = '1.1.3-2'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin2js'

repositories {
    mavenCentral()
    // 追加
    flatDir {
        dirs '../sample/build/libs'
    }
    // ^^^
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
    compile "com.example:sample:1.0-SNAPSHOT" // 追加
    //compile 'com.example:sample:1.0-SNAPSHOT' // error!
}

sourceSets {
    main.kotlin.srcDirs += "src/main"
    test.kotlin.srcDirs += "src/test"
}

def outputDir = "${projectDir}/build/js"

compileKotlin2Js {
    kotlinOptions {
        outputFile = "${outputDir}/sample-client.js" // 変更
    }
}

build.doLast() {
    // Copy *.js from jar into build directory
    configurations.compile.each { File file ->
        copy {
            includeEmptyDirs = false

            from zipTree(file.absolutePath)
            into outputDir
            include '**/*.js'
            eachFile { details ->
                def names = details.path.split('/')
                details.path = names[names.length - 1]
            }
        }
    }
}

SampleClient.kt を以下の内容で作成する。 build/libs/sample-1.0-SNAPSHOT.jar に含まれる kjsm ファイルのおかげで補完機能が有効になっている。

fun main(args: Array<String>) {
    sayHello()
}

HTML ファイル (sample-client/index.html) を作成して JavaScript ファイルを読み込む。 sample-client.js の前に kotlin.jssample.js を読み込む必要がある。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>サンプル</title>
</head>
<body>

<script src="build/js/kotlin.js"></script>
<script src="build/js/sample.js"></script>
<script src="build/js/sample-client.js"></script>

</body>
</html>

ここまでに作成/生成したファイルを整理すると以下になる。

sample-client <- module root
    build
        js
            sample-client
                root-package.kjsm
            kotlin.js
            kotlin.meta.js
            sample.js
            sample.meta.js
            sample-client.js
            sample-client.meta.js
    src
        main
            SampleClient.kt
        test
    build.gradle
    index.html

sample-client/index.html をブラウザで開くと、ブラウザのコンソールに Hello World が表示される。

依存関係管理

HTML に script タグを並べることで各種のライブラリを使用できるが、記述順を気にする必要がある。 依存関係の管理を楽にするために、JavaScript のモジュールシステムを使用する。 Kotlin/JS が対応している JavaScript モジュールシステム (moduleKind) は以下の 4 つ。 (参照 : JavaScript Modules - Kotlin Programming Language)

  • plain
    • モジュールシステムを使わない
  • amd
    • Asynchronous Module Definition (AMD) に対応させる
    • require.js ライブラリで使用する
  • commonjs
    • CommonJS に対応させる
    • node.js/npm で使用する
  • umd
    • Unified Module Definitions (UMD) に対応させる
    • AMD, CommonJS, モジュールシステム無しの全てに対応させる
    • AMD, CommonJS の順に適用され、いずれも使用していなければモジュールシステム無しの扱いになる

ライブラリに JavaScript モジュールシステムを適用する

UMD を使用すればいずれのモジュールシステムにも対応できるので、UMD を指定してビルドする。

samplesample-client の両方の build.gradlemoduleKind を追加する。

compileKotlin2Js {
    kotlinOptions {
        outputFile = "${outputDir}/sample-client.js"
        moduleKind = "umd" // 追加
    }
}

ビルドして生成された JavaScript ファイル (sample.js, sample-client.js) の冒頭には以下の様な記述が加えられる。

moduleKind = "plain" の場合 :

if (typeof kotlin === 'undefined') {
  throw new Error("Error loading module 'sample-client'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'sample-client'.");
}
if (typeof sample === 'undefined') {
  throw new Error("Error loading module 'sample-client'. Its dependency 'sample' was not found. Please, check whether 'sample' is loaded prior to 'sample-client'.");
}
this['sample-client'] = function (_, Kotlin, $module$sample) {
  ...snip...
  return _;
}(typeof this['sample-client'] === 'undefined' ? {} : this['sample-client'], kotlin, sample);

Global オブジェクトに kotlin, sample, sample-client を登録している。

moduleKind = "umd" の場合 :

(function (root, factory) {
  if (typeof define === 'function' && define.amd)
    define(['exports', 'kotlin', 'sample'], factory);
  else if (typeof exports === 'object')
    factory(module.exports, require('kotlin'), require('sample'));
  else {
    if (typeof kotlin === 'undefined') {
      throw new Error("Error loading module 'sample-client'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'sample-client'.");
    }
    if (typeof sample === 'undefined') {
      throw new Error("Error loading module 'sample-client'. Its dependency 'sample' was not found. Please, check whether 'sample' is loaded prior to 'sample-client'.");
    }
    root['sample-client'] = factory(typeof this['sample-client'] === 'undefined' ? {} : this['sample-client'], kotlin, sample);
  }
}(this, function (_, Kotlin, $module$sample) {
  ...snip...
  return _;
}));

typeof define === 'function' && define.amd に該当すれば AMDtypeof exports === 'object' に該当すれば CommonJS、 それ以外であればモジュールシステム無しと判断している。

JavaScript モジュールシステムを使い、読み込む

AMD を使用して依存関係を管理する。 AMD を使用するために require.js を導入する。 (CommonJS も少しだけ調べたけれどブラウザで使用するには複雑に見えたので、AMD だけを記載。)

sample-client/build.gradle に以下を追記する。

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
    compile "com.example:sample:1.0-SNAPSHOT"
    compile 'org.webjars:requirejs:2.3.3' // 追加
}

ビルドすることで build/js/require.js が展開される。

require.js を使うにあたって設定ファイル (require-config.js) を作成しておく。 (参照 : RequireJS API)

var require = {
    baseUrl: 'build/js',
    enforceDefine: true,
};

設定の内容は以下のとおり。

  • build/js 以下にある JavaScript ファイルを読み込む (baseUrl)
  • JavaScript の読み込みに失敗したときはエラーとする (enforceDefine)

次に、require.js を使用するように HTML (sample-client/index.html) を変更する。

<script src="require-config.js"></script>
<script src="build/js/require.js"></script>
<script>require(['sample-client']);</script>

sample-client を指定することで、sample-client が依存している kotlinsample が読み込まれる。 読み込み先は require-config.js で指定されている。 sample, sample-client のいずれでも依存関係は Gradle に集約できている点が嬉しい。

最後に、ブラウザで開くとブラウザのコンソールに Hello World が表示される。

おまけ

sample-client は読み込むだけで main 関数が実行されてしまう。 再利用性の観点からは望ましくない。

SampleClient.kt は以下に変更する。

fun run() {
    sayHello()
}

sample-client/index.html は以下に変更する。

<script src="require-config.js"></script>
<script src="build/js/require.js"></script>
<script>
require(['sample-client'], function(client) { client.run(); });
</script>

ソースコード

Release 2017-07-30-211651 · nosix/kotlin-js · GitHub

Google App Engine スタンダード環境で Kotlin + Spring Boot を動かす

概要

2017.6.28 に Google App Engine スタンダード環境が Java8 をサポートしたと告知されました。 告知記事を読んでいると Kotlin + Spring Boot で動かすサンプルが目にとまったので、動かしてみました。

確認環境

参考情報

解説

Google Cloud Platform

  1. プロジェクトを作成

(設定手順をメモしていませんでした…。)

Google Cloud SDK

手順は以下の通り。

  1. ダウンロード
  2. アーカイブの展開
  3. 環境の初期化
  4. アカウントの初期化

ダウンロード後、ファイルの SHA256 チェックサムを検証してからファイルを展開。 install.sh を実行する前に好みのディレクトリに配置する。 筆者はユーザーのホームディレクトリに置いている (/Users/user_name/google-cloud-sdk) 。

install.sh を実行することで、インストール済みコンポーネントを確認でき、コンポーネントのインストール/アンインストール方法を知れる。 また、環境変数 PATH の自動設定を行える。

$ echo "6572e086d94ab2a6b3242a966b688a382a80a417101315d59d38ac804b5a366e  google-cloud-sdk-158.0.0-darwin-x86_64.tar.gz" | shasum -a 256 -c 
google-cloud-sdk-158.0.0-darwin-x86_64.tar.gz: OK
$ tar xvfz google-cloud-sdk-158.0.0-darwin-x86_64.tar.gz 
$ cd google-cloud-sdk
$ ./install.sh
...
Do you want to help improve the Google Cloud SDK (Y/n)? 
...
Modify profile to update your $PATH and enable shell command 
completion?

Do you want to continue (Y/n)? 
...
[/Users/user_name/.bash_profile]:  
...

途中の Y/n は全て初期設定 (Enter) を選択。~/.bash_profile にて PATH の設定が追加される。

Terminal を起ち上げ直して、以下を続行。 Java Extension コンポーネントをインストールして、SDK をアップデートする。 その後にアカウントを初期化する。

$ gcloud components install app-engine-java
$ gcloud components update
$ gcloud init
...
You must log in to continue. Would you like to log in (Y/n)? 
...
Please enter numeric choice or text value (must exactly match list 
item):  google_cloud_platform_project_id
...
API [compute-component.googleapis.com] not enabled on project 
[xxxxxxxxxxx]. Would you like to enable and retry?  (Y/n)? 
...

途中の Y/n は初期設定 (Enter) を選択。ブラウザが起動して認証を求められる。

SDK の設定諸々は以下のコマンドで確認できる。

$ gcloud info
...
Installation Root: [/Users/user_name/google-cloud-sdk]
...

IntelliJ IDEA でプロジェクト作成

  1. Gradle Project を作成
  2. appengine-web.xml 作成
    • Java8 スタンダード環境を指定
  3. build.gradle 作成
  4. Application.kt 作成
    • JsonObjectMapper の設定
    • TemplateEngine の設定
  5. JsonObjectMapper 作成
  6. Controller, Template などを作成 (省略)
  7. appengineRun タスクで実行
    • ローカル環境で動かす
  8. appengineDeploy タスクで配置
    • Google の環境で動かす

appengine-web.xml, build.gradle, Application.kt, JsonObjectMapper は以下の Gist を参照のこと。

Google App Engine Standard Environment in Java8 wi …

Serializer/Deserializer クラスは ObjectMapper の addSerializer/addDesirializer メソッドに型を合わせるために用意している。

IntelliJ IDEA の Terminal にて、以下を実行すればローカル環境で試せる。

$ ./gradlew appengineRun
...
 04, 2017 4:56:51 午前 com.google.appengine.tools.development.AbstractModule startup
情報: Module instance default is running at http://localhost:8080/
7 04, 2017 4:56:51 午前 com.google.appengine.tools.development.AbstractModule startup
情報: The admin console is running at http://localhost:8080/_ah/admin
7 04, 2017 1:56:51 午後 com.google.appengine.tools.development.DevAppServerImpl doStart
情報: Dev App Server is now running

http://localhost:8080/ をダブルクリックで選択し、コンテキストメニューOpen as URL を選択するとブラウザで開ける。 (もっと楽に起動できる方法は?)

同じく、以下を実行すれば Google Cloud Platform 環境に配置される。

$ ./gradlew appengineDeploy
...

配置後、gcloud app browser とすることでデフォルトブラウザが開くが、デフォルトブラウザの設定が不明。 OS のデフォルトは Chrome に設定されているのに、Firefox が開く。 (どこに設定が?)

他の設定もしていた build.gradle から余計な設定を削除したため、もしかしたら削除し過ぎていて問題が起こるかも。 IntelliJ IDEA に Google Cloud Tools plugin をインストールしているが、無くてもよいはずなので記載していない。 Google Cloud Tools を使うことで App Engine に配置できるはずが機能しない。 (何か設定しないとだめ?)

IntelliJ IDEA で Emacs 風キーマップを作る

概要

IntelliJ IDEA の keymap 設定を Emacs にしても、使いたい機能のショートカットが複雑で覚えられません。 そこで、Emacs 風のショートカットに設定を変更します。

筆者は最初に覚えたエディタが Emacs でしたが、その後は Emacs のライトユーザーです。 ヘビーユーザーからしたら Emacs 風ではないと思われるかもしれません。

確認環境

参考情報

解説

Emacs と言えば、ESC キーと Ctrl キーから連なるショートカットが特徴的かと思います。 IntelliJ IDEA ではショートカットキーに Second stroke を設定することにより、 Emacs 風のショートカットキーを設定できます。

私は、Ctrl-[ESC キーとして使用しています(Emacs 標準のはず)。 そのため、Ctrl-[, Ctrl-x, Ctrl-c, Ctrl-h には Second stroke を設定しています。 Ctrl-xCtrl-c で使い分けがあったかもしれませんが無視することにして設定しています。 Ctrl-h はヘルプ系のアクションを割り当てています。 また、Emacs とは異なりますが、Navigation 系のアクションを Ctrl-j (Jump のつもり) に割り当てています。 基本的には、アクションの意味を表すようにキーを選択しているつもりです(Project ならば p, Intention なら i とか)。 使用頻度の低いと思われるアクションは Ctrl-[ x で Find Action を起動して実行する方針です。

設定した内容を整理したスプレッドシートを公開しています。 設定を変更していないアクションについては記載していません。

docs.google.com

アクションの一覧は intellJ IDEA Community のソースコードから取得しています。

intellij-community/ActionsBundle.properties at master · JetBrains/intellij-community · GitHub

IntellJ IDEA の便利機能をまだまだ知らないので、こんな便利機能があるよ!という話があれば伺いたいです。 また、Emacs の標準的なショートカットキーで、このキー設定は鉄板!という話があれば伺いたいです。