Cordova Pluginのフックスクリプトについて

# イントロダクション
 Cordova Pluginで自前の処理を組み込みたい場合に利用する、フックスクリプトについてまとめました。

Cordovaプラグインについての記事

- [基本事項](https://qiita.com/KNaito/items/4a49ba098115085d74fb)
- [JavaScript部分の実装](https://qiita.com/KNaito/items/af7b61f83888c95f0c7b)
- [Native部分の実装iOS] ... TODO
- [Native部分の実装Android] ... TODO
- [フックスクリプト](https://qiita.com/KNaito/items/65587f5d51974e8b4adf)  <- _この記事_
- [プラグインのテスト] ... TODO

## plugin.xmlへの記述

フックスクリプトは次のように記述します。

```xml
  <hook type="after_plugin_add" src="scripts/afterPluginAdd.js" />
  <hook type="before_plugin_rm" src="scripts/beforePluginRm.js" />
  <hook type="after_plugin_install" src="scripts/afterInstall.js" />
  <hook type="before_plugin_uninstall" src="scripts/beforeUninstall.js" />
```

フックポイント(type)は、公式サイトに一覧があります。https://cordova.apache.org/docs/ja/latest/guide/appdev/hooks/

よく使う上記4つについて説明すると

* after_plugin_add 何かしらプラグインをaddした時に呼ばれる。
* before_plugin_rm 何かしらプラグインをrmする前に呼ばれる。
* after_plugin_install 本プラグインをインストールした時に呼ばれる。
* before_plugin_uninstall 本プラグインをアンインストールした時に呼ばれる。

この他によく使うのは

* before_prepare プリペア前
* after_prepare プリペア後
* before_compile コンパイル前
* after_compile コンパイル後

です。

### ディレクティブ

hookディレクティブは、通常は次のようにplugin.xmlのpluginディレクティブ以下に記述します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<plugin id="cordova-plugin-my-plugin" version="1.0.0" xmlns="http://apache.org/cordova/ns/plugins/1.0" >
  <name>My Plugin</name>
  <hook type="after_plugin_add" src="scripts/afterPluginAdd.js" />
  <hook type="before_plugin_rm" src="scripts/beforePluginRm.js" />
  <hook type="after_plugin_install" src="scripts/afterInstall.js" />
  <hook type="before_plugin_uninstall" src="scripts/beforeUninstall.js" />
  ...
</plugin>
```


prepareやcompileに関連するhookは、platform以下に記述しても動作しますが、その場合明示的にplatformを指定したprepareやcompileでないと動作しません。

例えば、

```xml
<platform name="android">
  <hook type="after_prepare" src="scripts/afterPrepare.js" />
</platform>
```

としてしまうと、

```
$ cordova prepare android
```

のときは`afterPrepare.js`が実行されますが

```
$ cordova prepare

```
ときは`afterPrepare.js`が実行されないので注意してください。


srcは、どこに書いてもOKです。hooksディレクトリは、古い仕様のフックスクリプト(plugin.xmlに記述しなくても、Directory名に応じて自動でcordovaによって実行されるフックスクリプト)と紛らわしいので、個人的にはscriptsディレクトリを作成して使うことが多いです。

### プラットフォームとの関係

`context.opts.platforms` に、フックスクリプトに対する有効なプラットフォームが入ります。
例えば、今、androidとiosがプラットフォームとして登録されていた場合、`cordova prepare` を実行すると

```
context.opts.platforms = [ 'android', 'ios' ]
```

となります。

　ただし、androidとiosが登録されていたとしても、`cordova prepare ios`としてiosだけのプリペアを実行すると、iosだけがフックスクリプトに対する有効なプラットフォームとなり

```
context.opts.platforms = [ 'ios' ]
```

となります。

そこで、例えば、`cordova prepare` であれ `cordova prepare ios` であれ、iosを含む時にだけ実行したい場合は、

```
if (context.opts.platforms.includes('ios')) {
  // iosの時の処理
}
```

のように記述すると良いと思います。

## フックスクリプトの書き方

基本は

```js
module.exports = function (context) {
 // 処理
};
```

`context`は、Cordovaの情報が入っています。具体的内容はフックポイントによって多少異なりますが、例えば、`cordova prepare`であれば

```json
{
 hook: 'after_prepare',
 opts: { [後述] },
 cmdLine: '[現在実行しているcordovaコマンド]',
 cordova: { [cordovaオブジェクト] }
}
```

となります。cordovaオブジェクトは、各種cordovaコマンドを呼び出すためのメソッドから成り立っています。

`after_plugin_add`,`before_plugin_rm`,`after_plugin_install`,`before_plugin_uninstall`のフックポイントの場合、今のプロジェクトに特定のプラットフォームがあるかどうかは、次のようなコードで判定出来ます。(他のフックポイントについては未検証なので、各自で調べてみてください）

androidプラットフォームがあるかどうかの確認

```js
if ( context.opts.cordova.platforms.indexOf("android") >=0 ) {
 // Androidプラットフォームがある場合の処理
}
```

# context.optsについて

context.optsには、以下のような情報が入っています。

```json
{
  "cordova": {
    "platforms": [
      "android"
    ],
    "plugins": [
      "cordova-plugin-whitelist",
      "mobi.monaca.plugins.debugger"
    ],
    "version": "6.2.0"
  },
  "plugin": {
    "id": "[プラグインID]",
    "pluginInfo": {
      "filepath": "[プラグインへのフルパス]",
      ...
    },
    "platform": "android",
    "dir": "[プラグインへのフルパス]"
  },
  "projectRoot" : "[プロジェクトへのフルパス]"
}
```

特に、`config.opts.plugin.id`で、今動作しているフックスクリプトのプラグインIDが取得することが出来るのは重宝します。

# CordovaConfigについて

contextには、プロジェクトのルートディレクトリの情報は入っていますが、config.xmlに記述されている情報はありません。そこで、ConfigParserを使って以下のように取得します。

```js
      var ConfigParser = context.requireCordovaModule('cordova-common').ConfigParser;
      var cfg = new ConfigParser("config.xml");
```

cfgオブジェクトを使い、例えば、アプリのpackageネームを取得したい場合は

```js
var packageName = cfg.android_packageName() || cfg.packageName();
```

とします。

ConfigParserの詳しい仕様・使い方は
https://github.com/apache/cordova-common/blob/master/src/ConfigParser/ConfigParser.js
を参照してください。

cordova-commonのその他のオブジェクトについては、
https://github.com/apache/cordova-common
を参照してください。

2つほどサンプルを紹介します

### 1. Preferenceを取得する

config.xmlの中に

```xml
<preference name="MY_SETTINGS" value="MY_VALUES" />
```
のような設定があったとすると、フックスクリプトから次のように取得することができます。

```js
var myValues = cfg.getPreference("MY_SETTINGS");
```

各プラットフォームごとの設定が欲しい場合は、

```js
var myValues = cfg.getPreference("MY_SETTINGS","android");  // iosの場合、androidの箇所をiosにする
```

とします。この場合、プラットフォームディレクティブの下に当該preferenceがあればそれを取得し、
なければ、グローバルのに当該preferenceがあればそれを取得するようになります。


### 2. プラグインのvariablesで設定した設定値を取得する。

```
$ cordova plugin add sample_plugin --variable HOGE_SDK_VERSION=1234
```

このような形で設定したvariableは、config.xmlに次のような形式で設定されます

```xml
<plugin name="sample_plugin" spec="...">
  <variable name="HOGE_SDK_VERSION" value="1234" />
</plugin>
```

これをフックスクリプトから取得するためには、次のようにします。

```js
var plugin = cfg.getPlugin(context.opts.plugin.id);
if (plugin) {
  var variables = plugin.variables;
  if (variables) {
    var hogeSdkVersion = variables["HOGE_SDK_VERSION"];
    // hogeSdkVersionを使った書き込みなど
  }
}
```

これを用いることで、プラグインの中で利用するSDKのバージョンを、プロジェクト側で変更したり
する機能を実装することができます。

やや複雑な処理になりますが、https://github.com/dpa99c/cordova-android-play-services-gradle-release は
このようなやり方でgoogle-play-servicesのバージョンを変更しています。非常に勉強になります。（だだし、このプラグインでは上記のConfigParserは利用せず、自前でconfig.xmlを読み込み、xml2jsでパースしています。)

# プロジェクトのプラットフォーム以下にあるJSモジュールを組み込む

pathを解決して、requireすることで利用することが出来ます。例えば、platform/ios以下にあるcordova/API.jsを読み込みたい場合は、次のようにフックスクリプトを実装します。

```js:sample_hook.js
module.exports = function (context) {   
    var projectRoot = context.opts.projectRoot;

    var path = require('path');
    var pathAPI = path.resolve(projectRoot, 'platforms', 'ios', 'cordova', 'Api.js');
    var API = require(pathAPI);  // platforms/ios/cordova/API.jsを読み込む
    var api = new API('ios')     // APIのインスタンスを作成する。
    console.log(api.locations);  // apiのlocationsを表示する。
};
```

上記フックスクリプトをafter_prepareフックポイントで設定する

```xml
    <hook type="after_prepare" src="sample_hook.js"/>
```

ことにより、
次のような値を取得することが出来ます。

```json
{ root: '[プロジェクトへの絶対パス]/platforms/ios',
  www: '[プロジェクトへの絶対パス]/platforms/ios/www',
  platformWww: '[プロジェクトへの絶対パス]/platforms/ios/platform_www',
  configXml: '[プロジェクトへの絶対パス]/platforms/ios/HelloCordova/config.xml',
  defaultConfigXml: '[プロジェクトへの絶対パス]/platforms/ios/cordova/defaults.xml',
  pbxproj: '[プロジェクトへの絶対パス]/platforms/ios/HelloCordova.xcodeproj/project.pbxproj',
  xcodeProjDir: '[プロジェクトへの絶対パス]/platforms/ios/HelloCordova.xcodeproj',
  xcodeCordovaProj: '[プロジェクトへの絶対パス]/platforms/ios/HelloCordova',
  cordovaJs: 'bin/CordovaLib/cordova.js',
  cordovaJsSrc: 'bin/cordova-js-src' }
```

## 応用1

 Androidのプラットフォームバージョンを取得する

```
module.exports = function (context) {
  var projectRoot = context.opts.projectRoot;

  var path = require('path');
  var pathVersion = path.resolve(projectRoot, 'platforms', 'android', 'cordova', 'version');
  console.log(pathVersion);
  var version = require(pathVersion);

  console.log(version);

  // console.log(context.cordova.projectMetadata.getPlatforms(context.opts.projectRoot));
};
```

これを、cordova prepareのフックスクリプトとして入れると

```
{ version: '7.2.0-dev' }
```

こういう結果を取得することが出来る。

また、

```
console.log(context.cordova.projectMetadata.getPlatforms(context.opts.projectRoot));
```

を使った場合、プラットフォームをSEMVER形式で指定して組み込んだ場合は

```
{ state: 'fulfilled',
  value: [ { name: 'android', version: '^7.1.1' } ] }
```

にようになるが、ファイルパスで指定した場合は

```
{ state: 'fulfilled',
  value: [ { name: 'android', src: 'file:../cordova-android' } ] }
```

となってしまう。

# その他のライブラリの利用

## Cordova 6.5以前の場合
公式のやり方は特にないと思います。
自分は、必要なライブラリは、scriptsディレクトリの下にnode_modulesディレクトリを作成して、直接そこに置いています。

## Cordova 7.1以後の場合
 プラグインにpackage.jsonが必須になったため、プラグイン自体も自身が利用したいJSモジュールをpackage.json経由で組み込むことが出来るようになりました。
 プラグインのpackage.jsonに記述したJSモジュールは、フックスクリプトからも利用出来ます。


# 非同期処理

q.jsを使って行うことが出来る。Qオブジェクトは、requireCordovaModuleを使って取得できる。

```js
module.exports = function (context) {
  var Q = context.requireCordovaModule("q");
  var deferred = Q.defer();
  // 非同期処理
  do_something_async(function (result) {
    if (result.isSuccess) {
      deferred.resolve();
    } else {
      deferred.reject();
    }
  });
  return deferred.promise;
};
```
Cordova 9以後では、`requireCordovaModule("q");` が使えないかもしれないので、プラグインごとに `package.json` に　`q.js` を組み込むように指定して、フックスクリプト側では 
```
 var Q = require('q');
```
のが良いと思います。あるいは、JSのPromiseをそのまま使ってください。