LoginSignup
7
13

More than 3 years have passed since last update.

Google Apps Script のローカル開発環境を学びながら構築する

Last updated at Posted at 2020-03-14

はじめに

claspによってGoogle Apps Scriptのローカル開発は非常に快適になったわけですが、claspだけではまだクリアできない課題もあります。
というわけで、GASプロジェクトのローカル開発を更に快適にするための環境を『学びながら』構築します。

今回の環境構築に際して実現したいことは↓これらです。

  • npmパッケージを使えるようにする
  • TypeScriptで書けるようにする
  • ESLintで構文チェックする
  • Prettierでコード整形する
  • テストを書けるようにする
  • Renovateで自動アップグレード(おまけ)

では、順番に課題をクリアしていきましょう。

clasp

claspはGASをローカル開発するためのCLIです。
clasp pullclasp pushclasp cloneなどのコマンドでローカルの*.js ファイルとGoogle Drive上のGASプロジェクトが連携できます。

claspの導入は公式のREADMEに従えばOKです(なげやり)

サンプルプロジェクトをclone

予めGASのプロジェクトを作り、clasp cloneします。

$ clasp clone <sciptID>

.clasp.jsonを追記

後々Webpackでバンドルしたファイルをclasp pushしたいので、rootDirを設定しておきます。

./.clasp.json
{
  "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 to global object. it generate a top level function declaration statement.

GASではgoogle.script.runから関数を呼び出すためにトップレベルに定義している必要があるのですが、Webpackでバンドルすると、私達が記述した関数はWebpackの関数の中で定義されてしまいます。

gas-webpack-pluginを使うと、globalオブジェクトのメソッドとして定義した関数をバンドルしたファイルのトップレベルで宣言してくれます。

このプラグインも踏まえて、webpack.config.jsを作成します。

./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のプログラムを作成して試してみます。

./src/index.js
import { say } from "./say";

global.myFunction = () => {
  say("Hello");
};
./src/say.js
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 webpackclasp pushを入力するのは面倒なので、package.jsonscriptsにコマンドを登録しておきます。

./package.json
{
  "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メソッドを使った簡単なプログラムを作ります。

./src/index.js
import { say } from "./say";
import { chunkSample } from "./chunk";

global.myFunction = () => {
  say("Hello");
};

global.chunkFunction = () => {
  chunkSample();
};
./src/chunk.js
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に対応していて、*.tsclasp pushすると自動で*.js(というか*.gs)にトランスパイルしてくれるんですが、今回せっかくなので(?)、ts-loaderでjsにしてからclasp pushします。
というわけで必要なパッケージをインストールします。

$ yarn add typescript ts-loader @types/google-apps-script --dev

ではts-loderのREADMEを見つつ、設定を追加していきます。
エントリーポイントのファイルの拡張子もちゃんと.tsに書き換えましょう。私は最初忘れてました。

./webpack.config.js
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の全オプションを理解する(随時追加中)

./tsconfig.json
{
  "compilerOptions": {
    "sourceMap": false,
    "target": "es2017",
    "module": "es2015",
    "typeRoots": ["./node_modules/@types/"],
    "moduleResolution": "node"
  },
  "include": ["src/**/*.ts"]
}

ここまで設定したら、./src内の*.jsファイルを*.tsに書き換えます。
とりあえず拡張子を変えるだけでいいんですが、index.tsglobalが定義されていないよ、と怒られます。

./src/index.ts
import { say } from "./say";
import { chunkSample } from "./chunk";

// ↓ここが怒られる
global.myFunction = () => {
  say("Hello");
};
// 略

これの対処として、独自の型ファイルを作成してみます。

index.d.tsを作る

どこに配置するのが最良なのかわからないですが、とりあえず./src/typings内に配置。
declare文で変数を宣言します。

./src/typings/index.d.ts
declare const global: any;

これで怒られなくなりました。ではyarn buildします。

$ yarn build

無事にビルドとプッシュができました。

余談

Webpackでモジュールをバンドルする際にTree Shakingというイカしたテクニックがあります。
これは、使われていないコード(デッドコード)をバンドルせず、出力されるファイルをスリムにする手法です。以下の記事が大変参考になりました。
webpackのTree Shakingを理解して不要なコードがバンドルされるのを防ぐ

今回の私のように*.ts*.jsにトランスパイルしている場合は、tsconfig.json"module": "es2015"と設定し、webpackproductionモードで実行すればTree Shakingされるはず。後で試してみたい。

ESLint

続いて、JavaScriptの構文をチェックしてくれるESLintを導入します。
例によってまずは必要なパッケージをインストールします。

$ yarn add eslint eslint-loader --dev

eslint-loaderはWebpackでバンドルする際にコードを検証するためのローダーです。

ESLintの設定ファイルも作るのですが、今回はyarn eslint --initコマンドで設定ファイルを作成しました。
ESLintの詳細な設定をまだわかっていないので、「とりあえずお任せで」って感じです。

出来上がったのがこちら。

./.eslintrc.json
{
  "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の設定を追加します。

./webpack.config.js
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.jsonindex.tsを少し書き換えます。

./.eslintrc.json
{

  // 

  "rules": {
    "no-unused-vars": "error"
  }
}

./src/index.ts
// 略

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をそのまま使おう、と思いましたが個人的な好みにあまり合わない設定だったので色々と編集してこんな感じにした。

./.prettierrc
{
  "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も追記します。

./.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に関係ない設定もたくさん書いていますが。

./.vscode/settings.json
{
  "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 日本語版

./jest.config.js
module.exports = {
  moduleFileExtensions: ["ts", "tsx", "js", "json", "jsx", "node"],
  roots: ["./src"],
  preset: "ts-jest",
};

テストを書いて実行してみる

JestのGetting Startedを参考に、簡単なテストを書いて実行してみます。

./src/sum.ts
export const sum = (a: number, b: number): number => a + b;
./src/sum.test.s
import { sum } from "./sum";

describe("sum test", () => {
  test("adds 1 + 2 to equal 3", () => {
    expect(sum(1, 2)).toBe(3);
  });
});

ご覧の通り、2つの引数を足すだけの非常に簡単な関数です。
ついでに、package.json"scripts"にコマンドを追加します。

./package.json
{
  // 

  "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.jsonesModuleInteroptrueにするのも検討してごらん?
って意味っぽいので少し調べたところ、CommonJSモジュールとESモジュール間の相互運用を可能にするオプションのようです。以下の記事を参考にさせていただきました。
esModuleInterop オプションの必要性について

Renovate(おまけ)

せっかく作ったボイラープレートですから、いざ使おうとしたときにパッケージを最新の状態にしておくたいものです。
というわけでRenovateを使ってみます。

導入についてはこちらの記事が大変参考になります。
フロントエンド開発でパッケージのアップデートを継続的におこなう - Renovate

かなり柔軟に設定できるようですが、今のところはpatchを自動でマージする設定だけしておきます。

renovate.json
{
  "extends": ["config:base"],
  "patch": { "automerge": true }
}

これでパッケージが更新されたときにプルリクエストが来るようになりました。

おわりに

これでかなり(個人的には)快適にGASのローカル開発ができるようになりました:grinning:
kozukata1993/gas-boilerplate

今回の記事を執筆するにあたって、以下の記事を参考にさせていただきました。
Google Apps Script をローカル環境で快適に開発するためのテンプレートを作りました
GAS を npm パッケージ + Webpack + TypeScript で開発する

7
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
13