Cloud Functions for Firebase に kotlin で書いたコードをデプロイする

kotlin 1.2 がリリースされて kotlin を js にコンパイルできるようになりました。というわけで Cloud Functions for Firebase に kotlin で書いたコードをコンパイルしてデプロイします。

## ファイル構成

自分の場合、

[Full stack kotlin プロジェクトで開発をはじめて1週間くらいたった感想 - Qiita](https://qiita.com/numa08/items/ff760de9dda419f20371) 

で作っているプロジェクトなので、次のようになっています。これから出てくるパスは適宜読み替えてください。

```bash
├── 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](https://www.npmjs.com/package/kotlin)に依存しているだけであれば特に難しいことはないのですが、それ以外のkotlin関連のnpmパッケージや自分で作ったモジュールに依存している場合、 Cloud Functions には private なモジュールをデプロイできないという制限のため、デプロイ方法がちょっと難しくなります。

具体的には、コンパイルされたコードの中に`require('kotlinx-serialization-runtime-js')`と言うような、npmに含まれないモジュールへの依存関係が宣言されてしまい、その結果、関数の実行時エラーとなります。

これらを解決する必要があります。

実はコンパイルしたときに依存しているモジュールのソースコードは`build/classes/kotlin/main/dependencies`以下に展開されます。展開されたファイルを依存先として解決するようにコンパイルされたコードを改変することで、実行時エラーの回避が可能です。

今回は次のスクリプトを用意しました。

```bash:firebase/functions/replace_depends.sh
#!/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から

```json:firebase/functions/puckage.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

```groovy:firebase/functions/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`です。

```json:firebase/firebase.json
{
  "functions": {
    "predeploy": "npm --prefix functions run build"
  }
}
```

`predeploy`でデプロイ前にビルドを実行します。

## ソースコード

エントリーポイントとなる`firebase/functions/index.js`では、ビルドしたファイルへの依存関係と関数の宣言を行います。

```firebase/index.js:javascript
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](https://issuetracker.google.com/issues/36665861)で議論されているようですので、今後進展があるかもしれません。

当面、この方法を利用してパワーで解決するのが良いのかなと思います💪💪