はじめに
claspによってGoogle Apps Scriptのローカル開発は非常に快適になったわけですが、claspだけではまだクリアできない課題もあります。
というわけで、GASプロジェクトのローカル開発を更に快適にするための環境を『学びながら』構築します。
今回の環境構築に際して実現したいことは↓これらです。
- npmパッケージを使えるようにする
- TypeScriptで書けるようにする
- ESLintで構文チェックする
- Prettierでコード整形する
- テストを書けるようにする
- Renovateで自動アップグレード(おまけ)
では、順番に課題をクリアしていきましょう。
clasp
claspはGASをローカル開発するためのCLIです。
clasp pull
、 clasp push
、 clasp clone
などのコマンドでローカルの*.js
ファイルとGoogle Drive上のGASプロジェクトが連携できます。
clasp
の導入は公式のREADMEに従えばOKです(なげやり)
サンプルプロジェクトをclone
予めGASのプロジェクトを作り、clasp clone
します。
$ clasp clone <sciptID>
.clasp.jsonを追記
後々Webpackでバンドルしたファイルをclasp push
したいので、rootDir
を設定しておきます。
{
"scriptId": "***********",
"rootDir": "dist/"
}
これでひとまずローカルで開発するスタート地点に立てました。
Webpack
まず最初にnpmパッケージを使えるようにするという課題をクリアするために、Webpackを導入します。
Webpackはモジュールの依存関係を解決して1つの(あるいはいくつかの)*.js
ファイルにまとめてくれる優れものです。以下の記事が大変参考になりました。
webpack 4 入門
では早速インストールしましょう。
Webpack本体だけではなく、webpack-cli
というwebpack
コマンドを実行するためのパッケージもインストールします。
$ yarn add webpack webpack-cli --dev
無事Webpackをインストールできたらディレクトリを整理します。
root
├── node_modules
├── dist
│ └── appsscript.json
├── src
│ └── index.js
├── .clasp.json
├── .gitignore
├── package.json
└── yarn.lock
ここで今更ですが、これから構築したい開発環境の方針の確認です。
今回は、./src内でアプリケーションを作る → それらをWebpackでまとめたものを./distに出力 → まとめたものをclasp push
する、という流れで開発できる環境を整えていきます。
ではそんな環境のためにWebpackの設定を続けます。
gas-webpack-pluginを導入
Webpackの設定ファイルを作成する前に、もう一つgas-webpack-pluginというパッケージが必要なので、これもインストールします。
$ yarn add gas-webpack-plugin --dev
このプラグインは何者なのか。READMEにはこう記述されています。
In Google Apps Script, it must be top level function declaration that entry point called from google.script.run. When
gas-webpack-plugin
detect a function assignment expression toglobal
object. it generate a top level function declaration statement.
GASではgoogle.script.runから関数を呼び出すためにトップレベルに定義している必要があるのですが、Webpackでバンドルすると、私達が記述した関数はWebpackの関数の中で定義されてしまいます。
gas-webpack-pluginを使うと、global
オブジェクトのメソッドとして定義した関数をバンドルしたファイルのトップレベルで宣言してくれます。
このプラグインも踏まえて、webpack.config.js
を作成します。
const path = require("path");
const GasPlugin = require("gas-webpack-plugin");
module.exports = {
mode: "development",
devtool: "none",
entry: {
app: "./src/index.js"
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js"
},
plugins: [new GasPlugin()]
};
Webpackの最低限の設定はこれでできているので、GASのプログラムを作成して試してみます。
import { say } from "./say";
global.myFunction = () => {
say("Hello");
};
export const say = str => {
Logger.log(str);
};
ではWebpackでバンドルします。
$ yarn webpack
これで./dist/app.jsが出力されました!続けて、
$ clasp push
clasp push
も上手く行ったら、clasp open
を実行、ブラウザでGASの関数を実行してログを確認します。
↓こんなログが出力されていれば成功です!
[20-03-11 07:40:24:187 PDT] Hello
npm scriptsを作成
いちいちyarn webpack
とclasp push
を入力するのは面倒なので、package.json
のscripts
にコマンドを登録しておきます。
{
"devDependencies": {
"gas-webpack-plugin": "^1.0.2",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
},
"scripts": {
"build": "yarn webpack && clasp push"
}
}
これで今後はyarn build
一発でビルドしてプッシュまでしてくれます。
余談
yarn webpack
したらpackage.json
にライセンス書いてないよ!って怒られた。
公開する気がないなら、"license": "UNLICENSED"
にしておくといいらしい。
{
"license": "UNLICENSED",
// 略
}
npmパッケージを使ってみる
実際にnpmパッケージを使えるか、lodashで試してみます。
まずはインストール。
$ yarn add lodash --dev
lodashのchunk
メソッドを使った簡単なプログラムを作ります。
import { say } from "./say";
import { chunkSample } from "./chunk";
global.myFunction = () => {
say("Hello");
};
global.chunkFunction = () => {
chunkSample();
};
import _ from "lodash";
export const chunkSample = () => {
Logger.log(_.chunk(["a", "b", "c", "d", "e"], 2));
}; // [[a, b], [c, d], [e]] というログが表示されるはず
ではビルドしてプッシュします。
$ yarn build
ではブラウザでGASの関数を実行してログを確認します。
↓期待通りのログが出力されました。成功ですね!
[20-03-11 14:45:53:177 PDT] [[a, b], [c, d], [e]]
TypeScript
次はTypeScriptを使えるようにします。
実はclaspがTypeScriptに対応していて、*.ts
をclasp push
すると自動で*.js
(というか*.gs
)にトランスパイルしてくれるんですが、今回せっかくなので(?)、ts-loaderでjsにしてからclasp push
します。
というわけで必要なパッケージをインストールします。
$ yarn add typescript ts-loader @types/google-apps-script --dev
ではts-loderのREADMEを見つつ、設定を追加していきます。
エントリーポイントのファイルの拡張子もちゃんと.ts
に書き換えましょう。私は最初忘れてました。
const path = require("path");
const GasPlugin = require("gas-webpack-plugin");
module.exports = {
mode: "development",
devtool: "none",
entry: {
app: "./src/index.ts"
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js"
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
loader: "ts-loader"
}
]
},
plugins: [new GasPlugin()],
resolve: {
extensions: [".ts", ".js"]
}
};
続いてTypeScriptをトランスパイルする際の設定ファイル、tsconfig.json
も作ります。
この設定は以下の記事を主に参考にさせていただきました。
tsconfig.jsonの全オプションを理解する(随時追加中)
{
"compilerOptions": {
"sourceMap": false,
"target": "es2017",
"module": "es2015",
"typeRoots": ["./node_modules/@types/"],
"moduleResolution": "node"
},
"include": ["src/**/*.ts"]
}
ここまで設定したら、./src内の*.js
ファイルを*.ts
に書き換えます。
とりあえず拡張子を変えるだけでいいんですが、index.ts
のglobal
が定義されていないよ、と怒られます。
import { say } from "./say";
import { chunkSample } from "./chunk";
// ↓ここが怒られる
global.myFunction = () => {
say("Hello");
};
// 略
これの対処として、独自の型ファイルを作成してみます。
index.d.tsを作る
どこに配置するのが最良なのかわからないですが、とりあえず./src/typings内に配置。
declare
文で変数を宣言します。
declare const global: any;
これで怒られなくなりました。ではyarn build
します。
$ yarn build
無事にビルドとプッシュができました。
余談
Webpackでモジュールをバンドルする際にTree Shakingというイカしたテクニックがあります。
これは、使われていないコード(デッドコード)をバンドルせず、出力されるファイルをスリムにする手法です。以下の記事が大変参考になりました。
webpackのTree Shakingを理解して不要なコードがバンドルされるのを防ぐ
今回の私のように*.ts
を*.js
にトランスパイルしている場合は、tsconfig.json
で"module": "es2015"
と設定し、webpack
をproduction
モードで実行すればTree Shakingされるはず。後で試してみたい。
ESLint
続いて、JavaScriptの構文をチェックしてくれるESLintを導入します。
例によってまずは必要なパッケージをインストールします。
$ yarn add eslint eslint-loader --dev
eslint-loader
はWebpackでバンドルする際にコードを検証するためのローダーです。
ESLintの設定ファイルも作るのですが、今回はyarn eslint --init
コマンドで設定ファイルを作成しました。
ESLintの詳細な設定をまだわかっていないので、「とりあえずお任せで」って感じです。
出来上がったのがこちら。
{
"env": {
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {}
}
webpack.config.js
にもeslint-loader
の設定を追加します。
const path = require("path");
const GasPlugin = require("gas-webpack-plugin");
module.exports = {
// 略
module: {
rules: [
// 略
{
enforce: "pre",
test: /\.ts$/,
exclude: /node_modules/,
loader: "eslint-loader"
}
]
},
// 略
};
Webpackでは、module.rules
の配列に記述したローダーを下から(配列の後ろから)順番に実行していきます。なのでts-loader
でトランスパイルするより先に構文チェックしたいならeslint-loader
を配列の後ろに書かなければいけません。
また、enforce
というプロパティに"pre"
を設定しておくと、配列の順番に関係なく先に実行させることも可能です。今回はこの書き方にします。
導入したESLintがちゃんと構文エラーを検出してくれるのか確認します。
そのために.eslintrc.json
とindex.ts
を少し書き換えます。
{
// 略
"rules": {
"no-unused-vars": "error"
}
}
// 略
const unusedVars = "error";
"no-unused-vars"
というルールを"error"
に設定しました。これは文字通り、定義して使われていない変数をチェックするルールです。
そしてindex.ts
にこのルールを犯すようなコードを記述しました。これでyarn webpack
を実行します。
$ yarn webpack
yarn run v1.22.0
$ /Users/********/node_modules/.bin/webpack
// 略
ERROR in ./src/index.ts
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):
/Users/********/src/index.ts
12:7 error 'unusedVars' is assigned a value but never used no-unused-vars
✖ 1 problem (1 error, 0 warnings)
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
期待通り、構文エラーを検出してくれるようになりました!
Prettier
続いて大人気のコードフォーマッターPrettierを導入します。
改行やインデントを整えたり、シングルクォーテーションとダブルクォーテーションどちらにするか等のルールを設定してコードをきれいにしてくれます。
まずはインストールします。
$ yarn add prettier --dev
次に設定ファイルを作ります。ルートディレクトリに.prettierrc
を作り、中身は公式のBasic Configurationをそのまま使おう、と思いましたが個人的な好みにあまり合わない設定だったので色々と編集してこんな感じにした。
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"bracketSpacing": true
}
Prettier自体の導入はこれで完了なんですが、ESLintと併用するためのパッケージをインストールします。以下の記事を参考にさせていただきました。
Prettier 入門 ~ESLintとの違いを理解して併用する~
$ yarn add eslint-config-prettier eslint-plugin-prettier --dev
ESLintとPrettierを併用するために.eslintrc.json
も追記します。
{
"env": {
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:prettier/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"prettier/prettier": ["error"]
}
}
これでeslint --fix
を実行した時にPrettierによるコードをきれいにしてくれます。
$ yarn eslint --fix ./src/**/*.ts
せっかくコマンドで整形できるようにしましたが、私はエディタでコード保存時に整形してくれるようにします。(以下の設定はVScodeを使っている方向けです)
ルートディレクトリに.vscode
というディレクトリを作り、settings.json
の中にエディタの設定を記述します。
Prettierに関係ない設定もたくさん書いていますが。
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.tabSize": 2,
"editor.wordWrap": "on",
"eslint.enable": true,
"eslint.packageManager": "yarn",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"typescript.tsdk": "node_modules/typescript/lib",
"[markdown]": {
"files.trimTrailingWhitespace": false
},
"editor.fontSize": 14,
"editor.formatOnSave": true
}
これでコードを保存するたびにきれいに整形してくれるようになりました。やったー!
Jest
続きまして、JavaScript用テストフレームワークのJestを導入します。
まずは必要なパッケージをインストール。
$ yarn add jest @types/jest @types/node --dev
例によって、Jestの設定ファイルを作りたいところですが、JestのTypeScript対応について少し考えます。
公式ドキュメントではBabel使ってTypeScriptもサポートしてるよ!と書かれていますが、同時に以下のような記述もあります。
Because TypeScript support in Babel is transpilation, Jest will not type-check your tests as they are run. If you want that, you can use ts-jest.
つまりBabel使うとテストコードの型チェックはしないということっぽいです。
型チェックもしたければts-jestを使えということなので、ts-jest公式も参考にしながら続きの設定もしていきます。
まずはts-jestをインストールします。
$ yarn add ts-jest --dev
続いて、以下のドキュメントを参考にしつつ、Jestの設定ファイルjest.config.js
を作成します。
Configuration | ts-jest
Configuring Jest
Jest - TypeScript Deep Dive 日本語版
module.exports = {
moduleFileExtensions: ["ts", "tsx", "js", "json", "jsx", "node"],
roots: ["./src"],
preset: "ts-jest",
};
テストを書いて実行してみる
JestのGetting Startedを参考に、簡単なテストを書いて実行してみます。
export const sum = (a: number, b: number): number => a + b;
import { sum } from "./sum";
describe("sum test", () => {
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
});
ご覧の通り、2つの引数を足すだけの非常に簡単な関数です。
ついでに、package.json
の"scripts"
にコマンドを追加します。
{
// 略
"scripts": {
"test": "jest",
"build": "yarn webpack && clasp push"
}
}
ではテストを実行してみます。
$ yarn test
yarn run v1.22.4
// 略
PASS src/sum.test.ts
sum test
✓ adds 1 + 2 to equal 3 (2ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.021s
Ran all test suites.
✨ Done in 1.82s.
無事にパスできました!
余談
テストを実行した際に以下の警告が出ました。
message TS151001: If you have issues related to imports, you should consider setting esModuleInterop
to true
in your TypeScript configuration file (usually tsconfig.json
). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.
モジュールのインポートに関する課題があるなら、tsconfig.json
のesModuleInterop
をtrue
にするのも検討してごらん?
って意味っぽいので少し調べたところ、CommonJSモジュールとESモジュール間の相互運用を可能にするオプションのようです。以下の記事を参考にさせていただきました。
esModuleInterop オプションの必要性について
Renovate(おまけ)
せっかく作ったボイラープレートですから、いざ使おうとしたときにパッケージを最新の状態にしておくたいものです。
というわけでRenovateを使ってみます。
導入についてはこちらの記事が大変参考になります。
フロントエンド開発でパッケージのアップデートを継続的におこなう - Renovate
かなり柔軟に設定できるようですが、今のところはpatchを自動でマージする設定だけしておきます。
{
"extends": ["config:base"],
"patch": { "automerge": true }
}
これでパッケージが更新されたときにプルリクエストが来るようになりました。
おわりに
これでかなり(個人的には)快適にGASのローカル開発ができるようになりました
kozukata1993/gas-boilerplate
今回の記事を執筆するにあたって、以下の記事を参考にさせていただきました。
Google Apps Script をローカル環境で快適に開発するためのテンプレートを作りました
GAS を npm パッケージ + Webpack + TypeScript で開発する