概要
npm パッケージを使いつつ TypeScript で GAS を書いていく環境を作ったので、整理を兼ねてメモします。
前提
- clasp を使って GAS を開発している。
- モダンなフロントエンドの開発手法を理解している。
ゴール
- GAS をローカル環境上で Babel, TypeScript を使って開発していける環境の構築
- Spreadsheetの
onOpen
,onInstall
等にも関数をバインドできる
説明しない事
- clasp とは
- Node.js, npm, Babel, TypeScript
- Webpack の使い方
環境構築手順
clasp に管理させるディレクトリを変更する
GASの開発は普通にやっていくと root ディレクトリ直下に全ファイルが配置される形になるかと思いますが、npmのエコシステムを使う場合は package.json
や node_modules/
が配置されたりして管理が難しくなっていくので、以下のようにディレクトリ構造を変更してしまいます。
~/
├ dist/
│ └ appsscript.json
├ src/
├ .clasp.json
├ .claspignore
├ ...
clasp には dist/
配下をリリース対象のスクリプトと認識させるため、 .clasp.json
に "rootDir"
というフィールドを追加します。 (https://github.com/google/clasp#project-settings-file-claspjson)
{
"scriptId": "<SCRIPT_ID>",
"rootDir": "./dist"
}
clasp push
で dist/
配下のみが push されるようになります。
確認してませんが、多分 pull も dist 配下に降ってくるんじゃないかと思います。
Babel, TypeScript 周り
最低限以下を入れます。 (Webpack, loader その他は適宜入れてください)
@babel/core
@babel/preset-env
@babel/preset-typescript
@types/google-apps-script
typescript
Webpack 環境構築
必要なプラグイン
gas-webpack-plugin
fossamagna/gas-webpack-plugin: Webpack plugin for Google Apps Script
GAS の関数は global
に登録されるようトップレベルで関数宣言をする必要がありますが、 Webpack で 1 ファイルにバンドルすると Webpack の関数スコープに閉じ込められてしまい global
から参照出来ない形で出力されてしまいます。
gas-webpack-plugin
はこれを解決する物で、コード中の global
オブジェクトへの代入を検出して Webpack のバンドルファイルにトップレベルの関数宣言を注入してくれます。
es3ify-webpack-plugin
GAS は ECMAScript の 5 とか 5.1 とかで書かないと動作しないみたいなんですが、以下のようにビルドツール、バンドラーを使って ES5 として出力されるコード自体の一部が動作しない場合があります。
exports.default = foo;
これは次のように書き直す事で GAS のエディタに怒られなくなります。
exports['default'] = foo;
しかし自動生成ファイルを手作業で修正するのはさすがに無いので、 es3ify を使って予約語まわりをいい感じにしてもらいます。
ちなみに es3ify-webpack-plugin
の README には es3ify-loader
使えって書いてありますが、今回そっちは試さなかったのでここでは es3ify-webpack-plugin
を前提に書いていきます。
設定ファイル
こんな感じになりました。 TypeScript のトランスパイルには @babel/preset-typescript
を使っています。
const path = require('path');
const GasPlugin = require('gas-webpack-plugin');
const Es3ifyPlugin = require('es3ify-webpack-plugin');
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
context: __dirname,
entry: {
main: path.resolve(__dirname, 'src', 'index.ts')
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js'
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.[tj]s$/,
loader: 'babel-loader'
}
]
},
plugins: [new GasPlugin(), new Es3ifyPlugin()]
};
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["ie 8"]
}
}
],
"@babel/preset-typescript"
]
}
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [
"es2015",
"es2016",
"es2017",
"es2018"
],
"allowJs": true,
"declaration": false,
"outDir": "./dist",
"rootDir": "./src",
"downlevelIteration": true,
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["./src/**/*"],
"exclude": [".git", "node_modules"]
}
tsconfig.json はまだ理解してない部分も多いから冗長かも。
環境構築は以上になります。
開発
これでモダンなフロントエンドと同じやり方で開発を進められるようになりましたが、エントリーポイントだけちょっと対応が必要になります。
GAS で登録したい関数を global
オブジェクトのプロパティに代入していきます。
この時に型定義が無いかもなのですが、今回そこらへんは適当にやってしまいました。
import { MyFunc } from "./my-func"
// プロパティが無いと言われるのを防ぐ程度の型定義
declare const global: {
[x: string]: any ;
}
global.onOpen = function(e: any) {
return MyFunc(e)
}
global.onInstall = function(e: any) {
global.onOpen(e)
}
webpack
コマンドでビルドし、 clasp push
でデプロイ完了です。
watch
npm パッケージの watch
を使うことで、 webpack
の watch
モードのように変更の度にビルドと clasp push
を行えるようになります。
{
"scripts": {
"build": "node_modules/.bin/webpack",
"watch": "node_modules/.bin/watch 'npm run build && clasp push' ./src"
}
}
以上です。