kotlin 1.2 がリリースされて kotlin を js にコンパイルできるようになりました。というわけで Cloud Functions for Firebase に kotlin で書いたコードをコンパイルしてデプロイします。
ファイル構成
自分の場合、
Full stack kotlin プロジェクトで開発をはじめて1週間くらいたった感想 - Qiita
で作っているプロジェクトなので、次のようになっています。これから出てくるパスは適宜読み替えてください。
├── firebase
│ ├── build.gradle
│ ├── firebase.iml
│ ├── firebase.json
│ └── functions
│ ├── build
│ │ ├── classes
│ │ ├── libs
│ │ ├── public
│ │ └── tmp
│ ├── replace_depends.sh
│ ├── build.gradle
│ ├── functions.iml
│ ├── index.js
│ ├── package.json
│ └── src
│ └── main
│ └── kotlin
├── common-js
│ ├── build
│ │ ├── classes
│ │ │ └── kotlin
│ │ ├── libs
│ │ │ └── common-js.jar
│ │ └── tmp
│ │ └── jar
│ ├── build.gradle
│ ├── common-js.iml
│ └── src
│ └── main
│ └── kotlin
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle
プライベートモジュールをデプロイできない問題
先に注意点です。
実はkotlinをjsにコンパイルしてもnpm kotlinに依存しているだけであれば特に難しいことはないのですが、それ以外のkotlin関連のnpmパッケージや自分で作ったモジュールに依存している場合、 Cloud Functions には private なモジュールをデプロイできないという制限のため、デプロイ方法がちょっと難しくなります。
具体的には、コンパイルされたコードの中にrequire('kotlinx-serialization-runtime-js')と言うような、npmに含まれないモジュールへの依存関係が宣言されてしまい、その結果、関数の実行時エラーとなります。
これらを解決する必要があります。
実はコンパイルしたときに依存しているモジュールのソースコードはbuild/classes/kotlin/main/dependencies以下に展開されます。展開されたファイルを依存先として解決するようにコンパイルされたコードを改変することで、実行時エラーの回避が可能です。
今回は次のスクリプトを用意しました。
# !/usr/bin/env bash
set -eu
script_dir_path=$(dirname $(greadlink -f $0))
build_dir="${script_dir_path}/build/classes/kotlin/main/dependencies"
modules=(kotlin common-js kotlinx-serialization-runtime-js)
for ((i = 0; i < ${#modules[@]}; i++)) {
module=${modules[i]}
find "${build_dir}" -name "*.js" | xargs sed -i -e "s/require('${module}')/require('.\/${module}')/g"
}
コンパイルされたjavascriptのファイル中にある特定のrequireをnode_modulesの中ではなくパスで解決する方法に変更します。require('kotlinx-serialization-runtime-js')をrequire('./kotlinx-serialization-runtime-js')に書き換えるイメージ。
このファイルをビルドタスクの中で実行できるようにします。
ビルド定義ファイル
まずはfirebaes/functionsのpackage.jsonから
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase serve --only functions",
"shell": "firebase experimental:functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log",
"replaceDepends": "./replace_depends.sh",
"build": "../../gradlew :firebase:functions:assemble"
},
"dependencies": {
"cors": "^2.8.4",
"firebase-admin": "^5.4.3",
"firebase-functions": "^0.7.1",
"flickr-sdk": "^3.5.0"
},
"private": true
}
replaceDependsタスクを定義して、先程作ったスクリプトを呼び出せるようにしています。また、buildタスクを定義しておくことで、あとでfirebase.jsonの中からデプロイ前にビルドを行うよう設定できます。
次に、build.gradle
buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
maven { url "http://dl.bintray.com/kotlin/kotlin-dev" }
mavenCentral()
maven { url "http://oss.jfrog.org" }
}
dependencies {
classpath "com.moowork.gradle:gradle-node-plugin:1.2.0"
}
}
apply plugin: 'kotlin-platform-js'
apply plugin: "kotlin-dce-js"
apply plugin: "com.moowork.node"
apply plugin: 'kotlinx-serialization'
node {
download = true
}
repositories {
mavenCentral()
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
compile project(":common-js")
}
compileKotlin2Js {
kotlinOptions {
sourceMap = true
sourceMapEmbedSources = "always"
moduleKind = 'commonjs'
metaInfo = false
}
}
kotlin {
experimental {
coroutines 'enable'
}
}
task copyKotlinJs(type: Copy, dependsOn: compileKotlin2Js) {
def workDir = "$buildDir/classes/kotlin/main/"
from(workDir) {
include "*.js"
include "*.js.map"
}
into "$workDir/dependencies"
}
task devBuild(dependsOn: [npmInstall, copyKotlinJs])
afterEvaluate {
copyKotlinJs.dependsOn copyDependenciesKotlinJs
}
task replaceDependsPath(type: NpmTask) {
args = ["run", "replaceDepends"]
}
replaceDependsPath.dependsOn copyKotlinJs
assemble.finalizedBy replaceDependsPath
com.moowork.nodeプラグインを使ってnpmのタスクを呼び出すことができるようにしました。
copyKotlinJsを定義してビルドしたソースコードをdependencies以下にまとめるようにしています。これをやっておくとreplaceDependsPathタスクを実行するときのファイルの解決が楽。
replaceDependsPathはnpm runを行うだけのタスクですが、ビルド終了時に実行できるようにassemble.finalizeByを設定しています。
最後に、firebase関連の設定があるfirebase.jsonです。
{
"functions": {
"predeploy": "npm --prefix functions run build"
}
}
predeployでデプロイ前にビルドを実行します。
ソースコード
エントリーポイントとなるfirebase/functions/index.jsでは、ビルドしたファイルへの依存関係と関数の宣言を行います。
const functions = require('firebase-functions');
const myFunc = require("./build/classes/kotlin/main/dependencies/functions.js");
exports.hogeFunction = myFunc.hogeFunction
まとめ
ちょっと力技でしたが、kotlinで書かれたコードをCloud Funcitons for Firebase にデプロイできるようになりました。Cloud Functionsでprivate moduleが利用できない問題はAccess to private npm modules [36665861] - Visible to Public - Issue Trackerで議論されているようですので、今後進展があるかもしれません。
当面、この方法を利用してパワーで解決するのが良いのかなと思います💪💪