GASをwebpackでビルドして管理する
- # GAS
TL;DR
Google Apps Script(以下、GAS)は、手軽に色々できて便利なんですけど、開発環境周りがしんどいです
一応、google/clasp を使って、
- ローカルに持ってきてお好きなエディタで
- TypeScript で書ける
って感じにはできますが、ファイルを跨いだインポートができません。これがまじで致命的で。
外部のパッケージを使えないのはまだ良いとしても、複数の GAS プロジェクトを跨いだ部品の共通化とかもできません。
例えば、Slack に通知するなら Slack クライアントは共通部品になるけど、そういうのもプロジェクト毎に書かないといけないわけです。
一応ライブラリっていう他のプロジェクトの関数を利用できる機能はあるんですが、GAS からポチポチするとグローバルの名前空間にライブラリの関数が追加される形なので、あまり好ましくないです。
でまあ、『ブラウザ版 JS にモジュールがないから無理矢理バンドルしちゃえ!』って webpack のアプローチがこっちでもできるので、やっていきます
Node 依存の実装を使ってないライブラリなら外部パッケージも利用できます。
ディレクトリ構成
ディレクトリ構成はこんな感じです
$ tree . -L 3 -I node_modules
.
├── @types
│ ├── globals.d.ts
│ └── vendor.d.ts
├── bin
│ ├── clone.sh
│ └── push.sh
├── jest.config.js
├── lib
│ └── # 共通化した部品
├── package.json
├── projects
│ ├── project1 # GASプロジェクト①
│ │ ├── appsscript.json
│ │ ├── dist
│ │ └── index.ts
│ └── project2 # GASプロジェクト②
│ ├── appsscript.json
│ ├── dist
│ └── index.ts
├── tests
│ ├── # テスト
│ └── tsconfig.json
├── tsconfig.json
└── webpack.config.js
project1, project2 のように projects 下に gas プロジェクトを配置していって、それぞれのディレクトリ下の dist にバンドルを吐き出します
私の場合は、
- Trello の API クライアント
- Slack の outcoming webhook によるメッセージ送信
- DB 代わりのスプレッドシート (利用メンバー一覧とメタ情報, etc)
とかとかを lib に切り分けていて、必要に応じて利用できるようにしてあります
webpack 設定を書く
GitHub - fossamagna/gas-webpack-plugin: Webpack plugin for Google Apps Script
こちらを使って、GAS 向けにビルドができます
必要なパッケージを取得します
$ yarn add -D webpack copy-webpack-plugin gas-webpack-plugin typescript ts-loader
projects/<ProjectName>/index.ts
を自動的に webpack のエントリにする形で、webpack 設定を書きます
const path = require('path');
const fs = require('fs');
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const GasPlugin = require('gas-webpack-plugin');
const projectsPath = path.resolve(__dirname, 'projects')
const targets = fs.readdirSync(projectsPath, { withFileTypes: true })
.filter(path => path.isDirectory() && !path.name.startsWith('.'))
.map(path => path.name)
const entries = {}
targets.forEach(dirName => {
entries[dirName] = path.resolve(projectsPath, dirName, 'index.ts')
})
module.exports = {
mode: 'production',
entry: entries,
output: {
path: path.resolve(projectsPath),
filename: "[name]/dist/bundle.js",
},
resolve: {
extensions: [
'.ts',
'.js'
],
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, 'tsconfig.json')
})
]
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
],
},
plugins: [
new CopyWebpackPlugin({
patterns: [
...targets.map(dirName => ({
from: `projects/${dirName}/**/*.json`,
to: `${dirName}/dist/[name].[ext]`
})),
]
}),
new GasPlugin(),
],
};
これで、ビルド($ webpack
) すると、各 GAS プロジェクトディレクトリ下に GAS に読ませる用の dist
が生成されます
GAS 用の型定義を適用する
GAS 依存の関数等の型定義は @types:google-apps-script
で提供されます
$ yarn add -D @types/google-apps-script
型定義には、console の名前重複問題があるので、対策が必要です
こちらの Issue で示されてるように、$ npm install
のタイミングで console
をコメントアウトします
{
"scripts": {
"postinstall": "replace-in-file \"declare var console\" \"//declare var console\" node_modules/@types/google-apps-script/google-apps-script.base.d.ts",
}
}
GAS プロジェクトをクローンする
準備はできたので、GAS プロジェクトを引っ張ってきます
$ yarn add -g clasp
初利用なら、GAS のユーザー設定 から Google Apps Script API
を有効化する必要があります
クローン用のスクリプトを準備しておきます
#!/bin/bash
printf "Input Project Name >> "; read project_name
mkdir ${project_name} && cd ${project_name} && touch index.ts
clasp clone --rootDir './dist'
{
"scripts": {
// ...
"clone": "bin/clone.sh",
}
}
これで、$ yarn clone
すれば GAS プロジェクトを引っ張ってこれます
$ yarn clone
yarn run v1.22.10
$ bin/clone.sh
Input Project Name >> hoge
? Clone which script? # 対象のプロジェクトを選ぶ
Warning: files in subfolder are not accounted for unless you set a '.claspignore' file.
Cloned 2 files.
└─ ./dist/appsscript.json
└─ ./dist/コード.js
Not ignored files:
└─ dist/appsscript.json
└─ dist/コード.js
Ignored files:
✨ Done in 11.52s.
プロジェクトを push する
同様に push 用スクリプトを置きます
#!/bin/bash
yarn build && cd projects/$1 && clasp push
{
"scripts": {
// ...
"push": "bin/push.sh",
}
}
これで、$ yarn push <ProjectName>
すれば反映できます
$ yarn push hoge
yarn run v1.22.10
$ webpack
[webpack-cli] Compilation finished
# ...
└─ dist/appsscript.json
└─ dist/bundle.js
Pushed 2 files.
✨ Done in 29.15s.
gas プロジェクトに実行可能な関数を生やす
global に生やした関数が gas から実行できる関数になります
global.gasFunc = () => {
console.log('hello world')
}
一応これで $ clasp push
すれば gasFunc
を実行することができますが、型の問題があります
export {}
type Any = any
declare global {
namespace NodeJS {
interface Global extends Any { }
}
}
無理矢理だけど、こんなんで通ります
その他いろいろ
単体テストをちゃんとする
- GAS は、ランタイムをローカルに作れない
- webpack はヒューマンリーダブルなコードを吐き出さないので GAS エディタ上でのデバッガを利用できない
って感じなので、動作確認しようと思ったら、いちいち GAS にあげて console.log でデバックするしかありません
つまり、単体テストをちゃんと書かないと開発効率が死にます
GAS 依存の実装(UrlFetchApp.fetch()
, SpreadsheetApp.openById()
, えとせとらえとせとら)からデータなりを取り出す部分とそれ以外のロジックをしっかり切り分けておき、後者をローカルのランタイムでテストできるようにしておくことが大事です
じゃないとまじでコードベースが増えてきたときにしんどいです
ソースコードに載せられない情報を置く
API のキーとか、ソースコードに書くべきでない値は、ファイル > プロジェクトのプロパティ > スクリプトのプロパティ
にキーとバリューのセットで置いておいて、以下の形で取得できます
const props = PropertiesService.getScriptProperties().getProperties();
props.API_KEY; // でアクセスできる
※ 最近 GAS のエディタが新しくなりまして、新エディタだとこれがないっぽいです。一時的に旧エディタに戻せば使えます
終わりに
d-kimuson/gas-webpack-workspace-boiler
一応使ってるソースから汎用的でないソースを外したリポジトリを用意したので、パッケージのバージョン情報等はこちらを参照してください