kotlin-frontend-plugin のソースコードを読んで要約してみた

概要

以下の記事の続きです。以下の記事では、ソースコードの内容をクラス単位で整理しました。 クラス単位では全体像が見えづらいため、図とリストを使って全体を要約します。

nosix.hatenablog.com

目次

確認環境

  • Kotlin 1.1.4
  • kotlin-frontend-plugin 0.0.21

github.com

解説

タスクの依存関係

タスクの dependsOn による依存関係を整理すると以下の図のようになる。

データフロー

各タスクと一部のクラスでの処理と入出力の関係を図にすると以下のようになる。 前者は npm 関連のタスク、後者は webpack 関連のタスク。

requiredDependencies には以下の 3 つの依存関係が含まれている。

  • webpack (version: *, scope: DevelopmentScope)
  • webpack-dev-server (version: *, scope: DevelopmentScope)
  • source-map-loader (version: *, scope: DevelopmentScope)
    • kotlinFrontend.sourceMaps が true の場合のみ

入出力の関係

全てのタスクの入力と出力の関係を以下のリストに整理する。

  • 環境変数
    • PATH
      • node コマンドのパスとして使われる
  • プロパティ
    • org.kotlin.frontend.node.dir
      • node コマンドのパスとして使われる
  • project
    • name
      • “$buildDir/package.json” の name に使われる
    • version
      • “$buildDir/package.json” の version に使われる
  • sourceSets
    • main.output.resourcesDir
      • 設定している場合には、"$buildDir/package.json" と同じファイルが resourcesDir に作成される
  • dependencies
    • compile Kotlin/JS library
      • “$buildDir/node_modules_imported” に展開される
    • compile project
      • “$buildDir/webpack.config.js” の resolve に使われる
  • compileKotlin2Js
    • kotlinOptions.outputFile
      • sourceSets.main.output に追加される
      • “$buildDir/package.json” の name, main に使われる
      • “$buildDir/webpack.config.js” の context, entry value, resolve に使われる
      • “$buildDir/WebPackHelper.js” の bundlePath, moduleName に使われる
      • “$buildDir/webpack-dev-server-run.js” の bundlePath, moduleName に使われる
  • kotlinFrontend
    • sourceMaps: Boolean = false
      • “$buildDir/WebPackHelper.js” の sourceMap に使われる
      • “$buildDir/webpack-dev-server-run.js” の sourceMap に使われる
    • downloadNodeJsVersion: String = “”
      • 空でなければ nodejs-download タスクを行う
      • nodejs-download.version に設定される
    • nodeJsMirror: String = “”
      • nodejs-download.mirror に設定される
    • define(name: String, value: Any?)
      • “$buildDir/webpack.config.js” の defined (plugin) に使われる
      • “$buildDir/.run-webpack-dev-server.txt” の exts に記録される
  • npm
    • dependencies: MutableList
      • dependency(name: String, version: String = “*”) で設定する
      • “$buildDir/package.json” の dependencies に使われる
    • developmentDependencies: MutableList
      • devDependency(name: String, version: String = “*”) で設定する
      • “$buildDir/package.json” の devDependencies に使われる
    • versionReplacements: MutableList
      • replaceVersion(name: String, version: String) で設定する
  • webpackBundle
    • bundleName: String = project.name
      • “$buildDir/webpack.config.js” の entry key に使われる
    • sourceMapEnabled: Boolean = kotlinFrontend.sourceMaps
      • “$buildDir/WebPackHelper.js” の sourceMap に使われる
      • “$buildDir/webpack-dev-server-run.js” の sourceMap に使われる
    • contentPath: File? = null
      • “$buildDir/WebPackHelper.js” の contentPath に使われる
      • “$buildDir/webpack-dev-server-run.js” の contentPath に使われる
    • publicPath: String = “/”
      • “$buildDir/webpack.config.js” の publicPath に使われる
      • “$buildDir/WebPackHelper.js” の publicPath に使われる
      • “$buildDir/webpack-dev-server-run.js” の publicPath に使われる
    • port: Int = 8088
      • “$buildDir/WebPackHelper.js” の port に使われる
      • “$buildDir/webpack-dev-server-run.js” の port に使われる
      • “$buildDir/.run-webpack-dev-server.txt” の port に記録される
    • proxyUrl: String = “”
      • “$buildDir/WebPackHelper.js” の proxyUrl に使われる
      • “$buildDir/webpack-dev-server-run.js” の proxyUrl に使われる
    • stats: String = “errors-only”
      • “$buildDir/WebPackHelper.js” の stats に使われる
      • “$buildDir/webpack-dev-server-run.js” の stats に使われる
    • webpackConfigFile: Any? = null
      • 設定されている場合、webpack-config タスクを行わない
      • 設定されている場合のみ、webpack-helper タスクを行う
      • “$buildDir/WebPackHelper.js” の webPackConfig に使われる
      • “$buildDir/webpack-dev-server-run.js” の webPackConfig に使われる
  • “$projectDir/package.json.d” 配下の JSON ファイル
    • “$buildDir/package.json” にマージされる
  • “$projectDir/webpack.config.d” 配下の JavaScript ファイル
    • “$buildDir/webpack.config.js” の拡張スクリプトとして使われる

まとめ

kotlin-frontend-plugin を使うにあたって重要なファイルは、以下の 4 つであると思われる。

  • node コマンド
  • package.json
  • webpack.config.js
  • webpack-dev-server-run.js

kotlin-frontend-plugin では、 package.json の設定により環境を整え、 webpack.config.js の設定により JavaScript のバンドルを作成し、 webpack-dev-server-run.js の設定により webpack-dev-server を起動する。 これらを使う上で、node コマンドは必須である。

これら 4 つのファイルに関わる主要な入力は以下のようになっている。

  • node コマンド
    • プロパティ org.kotlin.frontend.node.dir でパスを設定
      • 最優先されるパス
      • 別途 node.js をインストールして使用する場合に使うとよい
    • ~/.gradle/nodejs
      • Gradle でインストールさせる場合のパス
      • kotlinFrontend.downloadNodeJsVersion を指定することで有効になる
    • 環境変数 PATH でパスを設定
  • package.json
    • project の name, version
    • dependencies で compile 指定した Kotlin/JS library が node_modules として展開される
    • compileKotlin2Js.kotlinOptions.outputFile が name, main に使用される
    • npm の dependencies, developmentDependencies で依存モジュールを指定する
    • “$projectDir/package.json.d” 配下の JSON ファイル で設定を拡張する
  • webpack.config.js
    • dependencies で compile 指定した Project が resolve に含まれる
    • compileKotlin2Js.kotlinOptions.outputFile が複数の設定に使用される
    • kotlinFrontend の define(name: String, value: Any?) メソッドで plugin の設定を追加する
    • webpackBundle の bundleName, publicPath, webpackConfigFile
      • webpackConfigFile を指定すれば、指定したファイルが使用される
      • webpackConfigFile を指定しなければ、生成したファイルが使用される
    • “$projectDir/webpack.config.d” 配下の JavaScript ファイルで設定を拡張する
  • webpack-dev-server-run.js
    • compileKotlin2Js.kotlinOptions.outputFile が bundlePath, moduleName に使用される
    • kotlinFrontend の sourceMaps が source-map-loader を追加する
    • webpackBundle の各設定
    • スクリプトのなかでは HotModuleReplacement の設定を追加する

また、実行されるコマンドは以下のとおりであり、設定が上手くいかに場合には以下のコマンドを試すとよい。

// npm-install
npm install

// webpack-bundle
node $buildDir/node_modules/webpack/bin/webpack.js --config $buildDir/webpack.config.js

// webpack-run
node $buildDir/webpack-dev-server-run.js

// webpack-stop (実際には curl は使用せず URL にアクセスしている)
curl http://localhost:$port/webpack/dev/server/shutdown

このあたりの関係を押さえておくと設定に迷うことが少なくなるのではないかと思う。

kotlin-frontend-plugin のソースコードを読んでみた

概要

kotlin-frontend-plugin は Kotlin/JS を用いてのフロントエンド開発を助ける Gradle プラグインです。 node.js を使うようになっており、webpack, karma などの node.js ライブラリを使った開発を助けます。 Kotlin の公式プラグインですが、現在は EAP となっています。

文書が少なく、何をどのように行っているのか分からなかったため、ソースコードを読みました。 本記事では、ソースコードから読み取った処理の要点を紹介します。 各々の関係を把握するには適していないため、別途図を作成する予定です。

目次

確認環境

  • Kotlin 1.1.4
  • kotlin-frontend-plugin 0.0.21

github.com

解説

class FrontendPlugin

org.jetbrains.kotlin.gradle.frontend.FrontendPlugin で行われている処理を書き記す。

project.extensions に kotlinFrontend (型は KotlinFrontendExtension) を追加する。

KotlinFrontendExtension で指定されている設定項目は以下のとおり。

  • sourceMaps: Boolean = false
  • downloadNodeJsVersion: String = “”
  • nodeJsMirror: String = “”

project.tasks に以下のタスクを追加する。

  • packages (group: build)
  • bundle (group: build)
  • run (group: run)
  • stop (group: run)
  • nodejs-download (型は NodeJsDownloadTask)
    • kotlinFrontend.downloadNodeJsVersion が空でない場合のみ追加
    • タスクの以下の項目を設定する
      • version = kotlinFrontend.downloadNodeJsVersion
      • mirror = kotlinFrontend.nodeJsMirror (nodeJsMirror の設定が存在する場合のみ)

タスクに依存関係を追加する。 ATask.dependsOn(BTask)BTask > ATask として書くと以下になる。

nodejs-download > packages
packages > compileKotlin2Js
packages > compileTestKotlin2Js
packages > bundle
packages > run
compileKotlin2Js > compileTestKotlin2Js
compileKotlin2Js > bundle
compileKotlin2Js > run
bundle > assemble
stop > clean

NpmPackageManager に対して以下を行う。

  • apply(task packages)
    • packages タスクを渡しているが使用されていない
  • install(project)
    • buildFinished & not failure & taskGraph is null の場合のみ実行される

kotlinFrontend extension で設定した *Bundle に対応する Bundler に対して以下を行う。 Bundler は webpack (WebPackBundler) か rollup (RollupBundler) のいずれか、もしくは両方。 * 部分は webpack か rollup を指定する。

  • apply(project, NpmPackageManager, task bundle, task run, task stop)

Launcher に対して以下を行う。 Launcher は WebPackLauncher, KtorLauncher, KarmaLauncher の 3 つ。

  • apply(NpmPackageManager, project, task run, task stop)

sourceSets.main.output に compileKotlin2Js.kotlinOptions.outputFile を登録する)。 但し、compileKotlin2Js.kotlinOptions.outputFile が存在するときのみ。

以降では、NpmPackageManager、Bundler から 1 つ (WebPackBundler)、Launcher から 1 つ (WebPackLauncher) 、 関連しているタスクについての調査結果を書く。 (全て調べるのは大変なので)

nodejs-download: NodeJsDownloadTask

~/.gradle/nodejs ディレクトリに node.js をダウンロードする。

node 実行ファイルのパスを “$buildDir/nodePath.txt” に書き込む。

class NpmPackageManager

org.jetbrains.kotlin.gradle.frontend.npm.NpmPackageManager で行われている処理を書き記す。

apply

project.extensions に npm (型は NpmExtension) を追加する。

NpmExtension で指定されている設定項目は以下のとおり。

  • dependencies: MutableList
  • developmentDependencies: MutableList
  • versionReplacements: MutableList

設定するためのメソッドは以下のとおり。

  • dependency(name: String, version: String = “*”)
  • devDependency(name: String, version: String = “*”)
  • replaceVersion(name: String, version: String)

以下のいずれかの条件に該当する場合のみ、後に示す様々な処理を行う。

条件:

  • npm.dependencies が空ではない
  • npm.developmentDependencies が空ではない
  • “$projectDir/package.json.d” ディレクトリが存在する
  • requiredDependencies が空ではない
    • require メソッドで設定される

処理:

compile.dependencies に NpmDependenciesTask.results のディレクトリを追加する。 (AbstractFileCollection として追加されており、具体的なディレクトリは遅延評価されると思われる。)

project.tasks に以下のタスクを追加する。

  • npm-preunpack (型は UnpackGradleDependenciesTask)
    • タスクの以下の項目を設定する
      • dependenciesProvider = requiredDependencies (を返す関数)
  • npm-configure (group: NPM, 型は GeneratePackagesJsonTask)
    • タスクの以下の項目を設定する
      • dependenciesProvider = requiredDependencies (を返す関数)
      • packageJsonFile = “$buildDir/package.json
      • npmrcFile = “$buildDir/.npmrc”
  • npm-install (group: NPM, 型は NpmInstallTask)
    • タスクの以下の項目を設定する
      • packageJsonFile = “$buildDir/package.json
  • npm-index (型は NpmIndexTask)
  • npm-deps (型は NpmDependenciesTask)
  • npm (group: NPM)

タスクに依存関係を追加する。

nodejs-download > npm-configure
npm-preunpack > npm-configure
npm-configure > npm-install
npm-install > npm-index
npm-index > npm-deps
npm-deps > npm
npm > packages

install

以下のタスクのうち、state が EXECUTED, SKIPPED, UP-TO-DATE ではないタスクの execute() を呼ぶ。

  • UnpackGradleDependenciesTask (= npm-preunpack)
  • GeneratePackagesJsonTask (= npm-configure)
  • NpmInstallTask (= npm-install)
  • NpmIndexTask (= npm-index)
  • NpmDependenciesTask (= npm-deps)

npm-* タスク

npm-preunpack: UnpackGradleDependenciesTask

npm.dependencies、npm.developmentDependencies、dependenciesProvider の返す Dependency、 いずれかの依存関係が存在するときのみ以下を行う。

compile configuration (build.gradle の compile 指定の依存設定) のうち、 Kotlin Java Script Library を “$buildDir/node_modules_imported” ディレクトリに展開する。 展開する際に package.json を生成する。

resultNames (型は MutableList) に展開したライブラリの名前、バージョン、URLが保存される。 resultNames の内容は “$buildDir/.unpack.txt” に書き込まれる。

npm-configure: GeneratePackagesJsonTask

npm.dependencies、npm.developmentDependencies、dependenciesProvider の返す Dependency、 いずれかの依存関係が存在するときのみ以下を行う。

“$buildDir/package.json” ファイルを生成する。package.json の内容は以下のとおり。

  • name
    • moduleNames が 1 つならば、その名前
      • moduleNames は compileKotlin2Js.kotlinOptions.outputFile のファイル名 (拡張子除く) のリスト
    • 1 つでなければ、project.name
    • いずれの名前も使えなければ、noname
  • version
    • project.version
  • main
    • moduleNames が 1 つならば、その名前
    • 1 つでなければ、null
  • dependencies
    • 以下の 3 つを合わせた内容
      • npm.dependencies
      • resultNames (npm-preunpack タスクの結果) から生成された Dependency (RuntimeScope に設定)
      • dependenciesProvider が生成した Dependency (RuntimeScope のみ)
  • devDependencies
    • 以下の 2 つを合わせた内容
      • npm.developmentDependencies
      • dependenciesProvider が生成した Dependency (DevelopmentScope のみ)
    • “$projectDir/package.json.d” ディレクトリ配下の JSON ファイルの内容がマージされる
      • マージ順序はファイル名により決定される (拡張子前の数字で整列、数字がなければ 0 扱い)

“$buildDir/.npmrc” ファイルを生成する。.npmrc の内容は以下のとおり。

progress=false
# cache-min=3600

sourceSets.main.output.resourcesDir が設定されている場合には、 “${sourceSets.main.output.resourcesDir}/package.json” にも書き込む。

npm-install: NpmInstallTask

npm install を実行する。

使用する npm コマンドは以下の順番で検索される。

  1. org.kotlin.frontend.node.dir プロパティで指定したパス
  2. “$buildDir/nodePath.txt” (NodeJsDownloadTask.nodePathTextFile) のパス
  3. 環境変数 PATH で指定したパス

npm-index: NpmIndexTask

“$buildDir/.modules.with.kotlin.txt” と “$buildDir/.modules.with.types.txt” を生成する。

“$buildDir/node_modules” のファイルを検査して、モジュールの絶対パスを .modules.with.*.txt ファイルに保存する。 kotlin と types それぞれで、以下に該当するファイルを探し、[module] の絶対パスを保存する。

  • kotlin
    • [module].jar
    • [module]/META-INF/*.kotlin_module
    • [module]/*.meta.js (但し、// Kotlin.kotlin_module_metadata で始まる)
  • types
    • [module]/typings.json
    • [module]/package.json (但し、typings をキーに含む)

npm-deps: NpmDependenciesTask

“$buildDir/.modules.with.kotlin.txt” に記載されているディレクトリを results に読み込む。 results は NpmPackageManager の apply メソッドで使用される。

object WebPackBundler

apply(project, NpmPackageManager, task bundle, task run, task stop) で行われている処理を書き記す。

NpmPackageManager.require により以下のモジュール依存関係を追加する。

  • webpack (version: *, scope: DevelopmentScope)
  • webpack-dev-server (version: *, scope: DevelopmentScope)
  • source-map-loader (version: *, scope: DevelopmentScope)
    • kotlinFrontend.sourceMaps が true の場合のみ

project.tasks に以下のタスクを追加する。

  • webpack-config (型は GenerateWebPackConfigTask)
  • webpack-helper (型は GenerateWebpackHelperTask)
  • webpack-bundle (groupd: webpack, 型は WebPackBundleTask)

タスクに依存関係を追加する。

webpack-config > webpack-helper
webpack-config > webpack-bundle
webpack-helper > webpack-bundle
RelativizeSourceMapTask > webpack-bundle
webpack-bundle > bundle

webpack-* タスク

webpack-config: GenerateWebPackConfigTask

kotlinFrontend extension で WebPackExtension が設定されており、 WebPackExtension.webpackConfigFile が設定されていない場合のみ以降の処理を行う。

以下に示す “$buildDir/webpack.config.js” を生成する。 <Part A|B|C> は後述する内容が挿入される。

'use strict';

var webpack = require('webpack');

var config = <Part A>;

var defined = <Part B>;
config.plugins.push(new webpack.DefinePlugin(defined));

module.exports = config;

<Part C>

Part A には以下の内容を持つ JSON が挿入される。

  • context: compileKotlin2Js.kotlinOptions.outputFile の親ディレクト
  • entry: 以下の内容を持つ JSON
    • WebPackExtension.bundleName: “./${compileKotlin2Js.kotlinOptions.outputFile (拡張子を除く)}”
  • output: 以下の内容を持つ JSON
    • path: “$buildDir/bundle”
    • filename: “[name].bundle.js”
    • chunkFilename: “[id].bundle.js”
    • publicPath: WebPackExtension.publicPath
  • module: { rules: [ ] }
  • resolve: { modules: <resolveRoots> }
    • <resolveRoots> は以下の内容を持つリスト
      • buildDir をカレントとしたときの、compileKotlin2Js.kotlinOptions.outputFile の親ディレクトリへの相対パス
      • “node_modules”
      • “$buildDir/node_modules”
      • compile configuration (build.gradle の compile 指定の依存設定) で指定された ProjectDependency から以下を抽出
        • buildDir をカレントとしたときの、compileKotlin2Js.outputFile の親ディレクトリへの相対パス
        • 但し、現状のコードだとタスク名を !contains(“test”) でフィルタしているため、compileTestKotlin2Js.outputFile の親ディレクトへの相対パスも含まれる
  • plugins: [ ]

Part B には kotlinFrontend extension の define(name: String, value: Any?) メソッドで設定された内容を持つ JSON が挿入される。

Part C には “$projectDir/webpack.config.d” 配下のファイルの内容が以下の形式で挿入される。 it はファイルを指す。 挿入順序はファイル名により決定される (拡張子前の数字で整列、数字がなければ 0 扱い)

// from file ${it.path}
<ファイルの内容>
WebPackExtension の設定

kotlinFrontend extension には、bundle メソッドと allBundles メソッドが存在する。 いずれも BundleConfig を設定するメソッドである。

  • bundle(id: String, configure: BundleConfig.() -> Unit)
    • id: Bundler の種別 (webpack, rollup)
    • id で指定した Bundler の設定を行う
  • allBundles(block: BundleConfig.() -> Unit)
    • 全ての Bundler の共通設定を行う

BundleConfig のインスタンスは WebPackBundler クラスで生成される WebPackExtension インスタンスである。 WebPackExtension クラスは BundleConfig インターフェースを実装している。

kotlinFrontend extension にて webpackBundle { ... } を実行すると bundle("webpack") { ... } が実行される。

WebPackExtension で設定できる項目は以下のとおり。

  • bundleName: String = project.name
  • sourceMapEnabled: Boolean = project.kotlinFrontend.sourceMaps
  • contentPath: File? = null
  • publicPath: String = “/”
  • port: Int = 8088
  • proxyUrl: String = “”
  • stats: String = “errors-only”
  • webpackConfigFile: Any? = null

webpack-helper: GenerateWebpackHelperTask

WebPackExtension.webpackConfigFile が設定されている場合のみ以降の処理を行う。

“$buildDir/WebPackHelper.js” を生成する。内容は以下のとおり。

module.exports = <JSON>

<JSON> には以下の内容を持つ JSON が挿入される。

  • port: WebPackExtension.port
  • shutDownPath: “/webpack/dev/server/shutdown” (WebPackRunTask.ShutDownPath)
  • webPackConfig: WebPackExtension.webpackConfigFile
  • contentPath: WebPackExtension.contentPath
  • proxyUrl: WebPackExtension.proxyUrl (空ならば null に設定される)
  • publicPath: WebPackExtension.publicPath
  • sourceMap: kotlinFrontend.sourceMaps && WebPackExtension.sourceMapEnabled
  • stats: WebPackExtension.stats
  • bundlePath: compileKotlin2Js.kotlinOptions.outputFile
  • moduleName: compileKotlin2Js.kotlinOptions.outputFile のファイル名 (拡張子を除く)

webpack-bundle: WebPackBundleTask

node $buildDir/node_modules/webpack/bin/webpack.js --config $buildDir/webpack.config.js を実行する。

WebPackExtension.webpackConfigFile が設定されている場合は、"$buildDir/webpack.config.js" は設定したファイルが指定される。

object WebPackLauncher

apply(NpmPackageManager, project, task run, task stop) で行われている処理を書き記す。

project.tasks に以下のタスクを追加する。

  • webpack-run (group: webpack, 型は WebPackRunTask)
    • タスクの以下の項目を設定する
      • start = true
  • webpack-stop (group: webpack, 型は WebPackRunTask)
    • タスクの以下の項目を設定する
      • start = false

タスクに依存関係を追加する。

webpack-config > webpack-run
RelativizeSourceMapTask > webpack-run
webpack-run > run
webpack-stop > stop

webpack-run: WebPackRunTask

node $buildDir/webpack-dev-server-run.js を実行する。 実行する際に、起動するサーバーの情報を JSON 形式で “$buildDir/.run-webpack-dev-server.txt” に保存する。

“$buildDir/webpack-dev-server-run.js” はサーバー起動前に生成される。 kotlin-frontend-plugin 中の resource である kotlin/webpack/webpack-dev-server-launcher.js をテンプレートとして、 テンプレート中の require('$RunConfig$') を GenerateWebpackHelperTask で生成している JSON に置き換える。 但し、JSON 中の内容において、WebPackExtension.webpackConfigFile が設定されていない場合は “$buildDir/webpack.config.js” が使用される。

サーバーの起動に成功すると “$buildDir/.run-webpack-dev-server.txt” が生成される。 以下の内容を持つ JSON ファイルである。

  • port: WebPackExtension.port
  • exts: kotlinFrontend extension の define メソッドで設定した内容
  • hashes: 以下の内容を持つ JSON
    • webpack.config.js: ファイルの SHA1
    • webpack-dev-server-run.js: ファイルの SHA1

webpack-stop: WebPackRunTask

http://localhost:$port/webpack/dev/server/shutdown にリクエストを送信する。 $port は “$buildDir/.run-webpack-dev-server.txt” が保持している port。

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 に以下を追記する。 (参照 : Getting Started with Kotlin and JavaScript with Gradle - Kotlin Programming Language)

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