[Clasp] Google Apps Script で npm install した package を利用する

この記事は [Google Apps Script Advent Calendar 2021](https://qiita.com/advent-calendar/2021/google-apps-script) の 11 日目の記事です

## はじめに

GAS に頼るのは最後の手段にしたいというのが個人的な考えですが、
[google/clasp](https://github.com/google/clasp)により TypeScript で記述できるようになったことで
自分の中で GAS に対するハードルがだいぶ下がりました。（TypeScriptの書き味が一番好きです）

今回はそんな clasp を使って、`npm install` した package を利用する方法を記載してみます。

## 前提

- Node.js および npm を利用できること（本記事では npm を使いますが、yarn に置き換えられる方はそれでも大丈夫かと思います）
- google/clasp がインストール済であること
  - ([参考リンク](https://github.com/google/clasp#install) / コマンド: `npm install -g @google/clasp` )

### 注意点

- 最後に GAS 上でプロジェクトを開くとわかりますが、1 つの JavaScript ファイルに bundle されるため、目当ての関数を探すのが面倒にはなります。
- また、GAS 上コードを直接編集することはほぼできない感じになります。
- clasp push する前には必ず、 `npm run build` (rollup 実行)が必要です。
- 後から気づいたのですが、ドキュメントからのリンクの[こちら](https://github.com/atti187/esmodules)にも完成形があります。本記事はそれを 0 からやっていく手順です。

## 手順

### clasp プロジェクトのセットアップ

この辺りは初歩的なところなので読み飛ばしても OK です。また、clasp のスタート手順については、[README](https://github.com/google/clasp#readme) を読むと情報が正確です。

```shell
# (未だの場合のみ) clasp の認証を通しておく
clasp login

# 適当にプロジェクトディレクトリを作成してnpm init
cd projectdir
npm init

# GASのプロジェクト構成を作成
clasp create --type standalone
```

### TypeScript 化

これは本題とは少し関係がないところですが、TypeScript で書きたいので[ドキュメント](https://github.com/google/clasp/blob/master/docs/typescript.md)に従ってセットアップします。

次のコマンドで形定義を install して

```shell
npm i -D @types/google-apps-script
```

直下に `tsconfig.json` を作成すれば OK です。
tsconfig.json は clasp のドキュメントによると、変更するなという項目もあるので注意。
一例を載せておきます。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "None",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "lib": ["esnext"],
    "experimentalDecorators": true
  },
  "include": ["src"]
}
```

### スクリプトを記述

ここから本筋の、`npm install` した package を利用したコードを書いていきます。
とりあえず `moment` を使ってみるので install してコードを作成します。

moment を install して

```shell
npm i moment
```

src ディレクトリに `index.ts` を記述

```typescript:src/index.ts
import moment from "moment";

const main = () => {
  const now = moment();
  Logger.log(now.format());
};
```

この時点で、 `clasp push` コマンドを打つと GAS をアップロードできますが、 `clasp open` で GAS プロジェクトを開いて実行しても、エラーになるはずです。GAS 上でも動かせる JavaScript ファイルになるよう、bundler を設定します。

### module bundler (rollup.js) を設定

ローカルで書いたソースコードから、GAS 上でも実行できるファイルを生成するために rollup.js を使います。  
ちょっと難しそうですが、[clasp のドキュメント](https://github.com/google/clasp/blob/master/docs/esmodules.md)に書いてあることをやるだけです。

まず、[rollup.js](https://rollupjs.org/guide/en/#installation) を install します。

```shell
npm i -D rollup
```

次に、rollup の config ファイル `rollup.config.js` を作成します。一旦[ドキュメント](https://github.com/google/clasp/blob/master/docs/esmodules.md)からコピペするだけです。

```javascript:rollup.config.js
import { babel } from "@rollup/plugin-babel";
import { nodeResolve } from "@rollup/plugin-node-resolve";

const extensions = [".ts", ".js"];

const preventThreeShakingPlugin = () => {
    return {
      name: 'no-threeshaking',
      resolveId(id, importer) {
        if (!importer) {
            // let's not theeshake entry points, as we're not exporting anything in Apps Script files
          return {id, moduleSideEffects: "no-treeshake" }
        }
        return null;
      }
    }
  }

export default {
  input: "./src/index.ts",
  output: {
    dir: "build",
    format: "esm",
  },
  plugins: [
    preventThreeShakingPlugin(),
    nodeResolve({
      extensions,
    }),
    babel({ extensions, babelHelpers: "runtime" }),
  ],
};
```

あわせて、babel の config ファイルを作成します。

```javascript:babel.config.js
module.exports = {
  presets: [
    [
      // ES features necessary for user's Node version
      require("@babel/preset-env").default,
      {
        targets: {
          node: "current",
        },
      },
    ],
    [require("@babel/preset-typescript").default],
  ],
   plugins: ["@babel/plugin-transform-runtime"]
};
```

:::note
`plugins` はドキュメントの例にはありませんが、必要なので追加しておきます。
:::

この状態で rollup を実行すると色々とエラーが出ることがわかるので対応します。（実行前にエラーがわかるようになりたい人生だった）

```shell
# 必要な依存関係を解決する (上記のjs内で使ってるplugin等々)
npm i -D @rollup/plugin-babel @rollup/plugin-node-resolve @babel/preset-env @babel/preset-typescript @babel/plugin-transform-runtime
```

`babel.config.js` に必要な plugin がないよと怒られるので編集追加します。

```diff:babel.config.js

+  plugins: ["@babel/plugin-transform-runtime"],
```

あと少しです。`rollup.config.js` に一部追加します。  
node_modules 以下の package.json から main ファイルを解決してくれるのですが、 `jsnext:main` という様式で書かれている package があり、それに対応するための追記です。（`jsnext:main` 以外は ドキュメント記載のデフォルト値を入れてます。）

```diff:rollup.config.js
     nodeResolve({
       extensions,
+      mainFields: ["jsnext:main", "module", "main"],
     }),
```

ここまでできたら、`package.json` に build 用のコマンドを追加して実行してみましょう。

```diff:package.json
   "scripts": {
+    "build": "rollup -c"
   },
```

```shell
npm run build
```

成功すると `created build` みたいな出力になるはずです。

### clasp のデプロイ

ここまできたらあと少しです。
bundle された js ファイル以外がアップロードされないように、`.claspignore`を作成します。

```ignore:.claspignore
# ignore all files…
**/**

# except the extensions…
!appsscript.json
# and our transpiled code
!build/*.js

# ignore even valid files if in…
.git/**
node_modules/**
```

そして以下のコマンドを実行するとアップロードできます。

```shell
# スクリプトのアップロード
clasp push
```

`clasp open` などで GAS プロジェクトを開き、 `main` 関数を実行すると、npm install した moment を使ったコードが実行できることが確認できるかと思います。
お疲れ様でした！

## 終わりに

今回のソースコード完成形はこちらにあります。 (https://github.com/suzukenz/sandbox/tree/master/advent-gas-2021)

記事にすると結構長くなりましたが、一度わかれば構築はすぐで、便利な package を使えたり
import / export が使えたりとメリットが多いので、自分は GAS を作るときはしばらくこれでやってみるつもりです。
また、clasp を使ってローカルに開発環境を作ることで、eslint などの便利なツールの恩恵も受けられます。その辺もいつか触れられたらと思います。


最後まで読んでいただきありがとうございました。