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。