Edited at

GAS を npm パッケージ + Webpack + TypeScript で開発する


概要

npm パッケージを使いつつ TypeScript で GAS を書いていく環境を作ったので、整理を兼ねてメモします。


前提


  • clasp を使って GAS を開発している。

  • モダンなフロントエンドの開発手法を理解している。


ゴール


  • GAS をローカル環境上で Babel, TypeScript を使って開発していける環境の構築

  • Spreadsheetの onOpen, onInstall 等にも関数をバインドできる


説明しない事


  • clasp とは

  • Node.js, npm, Babel, TypeScript

  • Webpack の使い方


環境構築手順


clasp に管理させるディレクトリを変更する

GASの開発は普通にやっていくと root ディレクトリ直下に全ファイルが配置される形になるかと思いますが、npmのエコシステムを使う場合は package.jsonnode_modules/ が配置されたりして管理が難しくなっていくので、以下のようにディレクトリ構造を変更してしまいます。

~/

├ dist/
│ └ appsscript.json
├ src/
├ .clasp.json
├ .claspignore
├ ...

clasp には dist/ 配下をリリース対象のスクリプトと認識させるため、 .clasp.json"rootDir" というフィールドを追加します。 (https://github.com/google/clasp#project-settings-file-claspjson)


.clasp.json

{

"scriptId": "<SCRIPT_ID>",
"rootDir": "./dist"
}

clasp pushdist/ 配下のみが 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

BryceHQ/es3ify-webpack-plugin: A simple webpack plugin to es3ify your code for old versions of ie, such as ie8.

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 を使っています。


webpack.config.js

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()]
};



.babel.rc

{

"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["ie 8"]
}
}
],
"@babel/preset-typescript"
]
}


tsconfig.json

{

"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 オブジェクトのプロパティに代入していきます。

この時に型定義が無いかもなのですが、今回そこらへんは適当にやってしまいました。


src/index.ts

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 を使うことで、 webpackwatch モードのように変更の度にビルドと clasp push を行えるようになります。


package.json

{

"scripts": {
"build": "node_modules/.bin/webpack",
"watch": "node_modules/.bin/watch 'npm run build && clasp push' ./src"
}
}

以上です。