clasp で Rollup.js を使用する

# はじめに

Google Apps Script 用のソースコードを Rollup.js でバンドルしてから clasp push する、ということをしてみます。

# 背景

弊社では Googleスプレッドシートと Google Apps Script を組み合わせて利用することがしばしばあります。

フロントエンドの開発では TypeScript、ES Modules の構文でコーディングを行うので、Google Apps Script でも、同様の書き方をしたいです。

clasp の公式ドキュメントに、Rollup.js を使用した書き方が記載されているので、それを試してみます。

https://github.com/google/clasp/blob/master/docs/esmodules.md

# 準備する

clasp を使用するプロジェクトを用意します。必要なライブラリをインストールしたり、設定ファイルを作成したりします。

**package.json**

```sh
npm init -y
```

**typescript**

```sh
npm install -D typescript
npx tsc --init --rootDir src/
```

**clasp**

```sh
npm install @google/clasp
npm install -D @types/google-apps-script
```

.claspignore ファイルを作成します。ここでは clasp のドキュメントにある通りの内容にします。

<details>
<summary>ソースコードを見る</summary>

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

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

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

**rollup**

```sh
npm install -D \
rollup \
@rollup/plugin-babel \
@rollup/plugin-node-resolve \
@babel/preset-env \
@babel/preset-typescript \
@babel/plugin-transform-runtime
```

clasp のドキュメントに従い、rollup.config.mjs、babel.config.js を作成します。

rollup.config.mjs で、`src/index.ts` を入力としてバンドルを作るよ、作成したファイルは `build/` に出力すると、と指定されています。

<details>
<summary>rollup.config.mjs のソースコードを見る</summary>

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

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

const preventTreeShakingPlugin = () => {
  return {
    name: 'no-treeshaking',
    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: [
    preventTreeShakingPlugin(),
    nodeResolve({
      extensions,
    }),
    babel({ extensions, babelHelpers: "runtime" }),
  ],
};
```
</details>

<details>
<summary>babel.config.js のソースコードを見る</summary>

```js: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"
  ]
};
```
</details>

rollup.config.mjs のファイル名は、rollup.config.js としていると、`npx rollup -c` を実行したときに以下のエラーが出力されるので、拡張子を mjs にします。

```
(node:13234) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
[!] RollupError: Node tried to load your configuration file as CommonJS even though it is likely an ES module. To resolve this, change the extension of your configuration to ".mjs", set "type": "module" in your package.json file or pass the "--bundleConfigAsCjs" flag.
```

babel.config.js では、ドキュメントの通りの内容だと、`npx rollup -c` を実行した時に以下のエラーが出力されたので、plugins の内容を追加しています。

```
[!] (plugin babel) RollupError: You must use the `@babel/plugin-transform-runtime` plugin when `babelHelpers` is "runtime".
```

**Apps Script**

弊社では Googleスプレッドシートと連携して使うことが多いので、Googleスプレッドシートに連携させて Apps Script を作成します。ただしこの記事では Googleスプレッドシートの機能は使いません。

```sh
npx clasp create --type sheets --title "SampleSpredsheet-01"
```

# ローカルのソースコードを結合する

## index.ts に関数を定義している場合

準備ができたので、Rollup.js でソースコードのバンドルを作ってみます。

Google Apps Script から実行するスクリプトを `runScriptFirst` という名前で、index.ts に定義します。

index.ts から、別のファイル util.ts を参照します。

**ソースコード**

```ts:src/util.ts
export function print (message: string): void {
  console.log(message);
}
```

```ts:src/index.ts
import * as Util from './util';

function runScriptFirst (): void {
  Util.print('this is the first script.');
}
```

バンドルします。

```sh
npx rollup -c
```

build/index.js が作成され、以下のように出力されました。なるほど、JavaScript に変換され、かつ、二つのファイルの内容が一つにまとまって出力されています。

```js:build/index.js
function print(message) {
  console.log(message);
}

function runScriptFirst() {
  print('hello, world.');
}
```

これを Google Appsc Script に push します。

```sh
npx clasp push
```

![20231129-01_ScriptFirst.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/27479e3d-db0e-0318-4328-56ac619a5273.png)

スクリプトの一覧に `runScriptFirst` が表示されました。

これを実行してみます。

![20231129-02_ScriptFirstLog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/cc00f7c5-85d1-c289-7e63-e3f7a6cb90f8.png)

実行できました。

## 関数ごとにファイルを分割している場合

2つのソースファイルに、それぞれ runScriptFirst、runScriptSecond の関数を定義します。

```ts:src/script-first.ts
import * as Util from './util';

export function runScriptFirst (): void {
  Util.print('this is the first script.');
}
```

```ts:src/script-second.ts
import * as Util from './util';

export function runScriptSecond (): void {
  Util.print('this is the second script.');
}
```

これらを含めたバンドルを作成するにはどのようにすればよいのでしょうか。

正しい方法がわかりませんが、一応以下のように書いてみました。

```ts:src/index.ts
import { runScriptFirst } from './script-first';
import { runScriptSecond } from './script-second';

runScriptFirst;
runScriptSecond;
```

![20231129-03_ScriptSecond.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/ecd9961c-90ce-e091-3907-80bbe70486cc.png)

2つの関数が、どちらもスクリプトの一覧に表示されました。

# npm パッケージを組み込む

## dayjs

dayjs を使用してみます。

```sh
npm install dayjs
```

```ts:src/util.ts
import dayjs from 'dayjs/esm/index'

export function getNowText (): string {
  return dayjs().format('YYYY/MM/DD HH:mm:ss')
}

export function print (message: string): void {
  console.log(message);
}
```

```ts:src/index.ts
import * as Util from './util';

function runScriptDayjs (): void {
  Util.print(Util.getNowText());
}
```

`build/index.js` には、上記のソースコードと、dayjs のソースコードが含まれた内容が出力されています。`npx clasp push` して実行してみます。

![20231129-04_ScriptDayjsLog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/b37b9a42-bb80-444f-8820-b2e39d943386.png)

実行できました。

## lodash

lodash の map を使用してみます。仮に、数値の配列を与えると、それらの値を 2 倍にした配列に変換するコードを書いてみます。

```sh
npm install lodash-es
npm install -D @types/lodash-es
```

```ts:src/index.ts
import * as Util from './util';
import map from 'lodash-es/map';

function runScriptLodash (): void {
  const result = map([1, 2, 3], (v) => v * 2);
  Util.print(result.toString());
}
```

![20231129-05_ScriptLodashLog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/4e7eda8a-6220-8cc5-9e84-c72a26c08037.png)

実行できました。

build/index.js のファイルサイズも小さく抑えられているようです。

## lodash の chain

もはや clasp も Rullup.js も関係ありませんが、lodash の chain を使ってみます。

chain は単独では使用することができないので、代わりに使いたい機能だけを組み込んだ独自の chain 関数を定義します。

以下の記事を参考にしました。

https://stackoverflow.com/questions/45462193/how-to-use-chain-with-lodash-es-while-supports-tree-shaking

```ts:src/custom-chain.ts
import map from 'lodash-es/map';
import type _t from 'lodash';

interface LodashCustomWrapper<T> {
  map: <TResult>(iteratee: _t.ArrayIterator<T, TResult>) => LodashCustomWrapper<T>,
  value: () => any,
}

const chain = <T>(input: T[]) => {
  let value: any = input;
  const wrapper = {
    map: <TResult>(iteratee: _t.ArrayIterator<T, TResult>) => {
      value = map(value, iteratee) as TResult[];
      return wrapper;
    },
    value: () => value,
  };
  return wrapper as LodashCustomWrapper<T>;
}

export default chain;
```

```ts:src/index.ts
import * as Util from './util';
import chain from './custom-chain';

function runScriptLodash (): void {
  const result = chain([1, 2, 3]).map((v) => v * 2).value();
  Util.print(result.toString());
}
```

これで動きました、が、この書き方は大変だ...

# おわりに

Google Apps Script のコードを import/export を使って書くことができるようになりました。npm パッケージも使うことができました。

jest を使ってテストコードも書くことができるので、これで安心して Google Apps Script のコードもメンテナンスできそうです。