SmartHR Tech Blog

SmartHR 開発者ブログ

Google Apps Scriptで、社内プロダクトのnpmライブラリの利用状況をスプレッドシートに出力してみた話

こんにちは!SmartHRのプロダクトエンジニアの@diescakeです!

今日は「Google Apps Scriptで、社内プロダクトのnpmライブラリの利用状況をスプレッドシートに出力してみた話」を題材にしつつ、主にGoogle Apps Script(以降GAS)の開発環境周りの話をします。技術の分野としてはWebフロントエンド(以下フロントエンド)に関連した話が多くなります。

全体の構成図はこんな感じです!

fuga
ソースコード管理から、スプレッドシートに反映されるまでのデータフロー図

大まかな構成・データフローは上図のような感じで、主な技術スタックとしては、GAS + Clasp + TypeScript + esbuildを採用しました。

この図を左側から見ていくと、まずGitHubでソースコードを管理していて、昨今のフロントエンド開発と同様に、TypeScriptでコードを書き、必要に応じてnpmに公開されたライブラリを利用しています。

このTypeScriptで実装されたスクリプトをesbuildでコンパイルしつつ、ランタイムに依存しているnpmライブラリがあれば1つのJavaScriptファイルにバンドルします。

続いて、esbuildによって出力されたJavaScriptファイルを、Claspを利用してGASにpushして反映します。GASコンソールからこのスクリプトを実行することで、スプレッドシートが更新される仕組みです。

実際の運用としては、GASコンソールのトリガーによって定期実行されるようになっていて、デイリーでスプレッドシートが自動更新されるようになっています。

最後に、GASからGitHubのpackage.jsonへアクセスするために、GASコンソールから追加・参照が可能なスクリプトプロパティ(環境変数のようなイメージ)に、参照先package.jsonのURLやトークンを登録しています。

詳しくは後述していきます!

出力されたスプレッドシートはこんな感じです!

ライブラリの利用状況を整理したスプレッドシート(主にReact関連のライブラリ)
ライブラリの利用状況を整理したスプレッドシート(主にReact関連のライブラリ)

ライブラリの利用状況を整理したスプレッドシート(主にtextlint関連のライブラリ)
ライブラリの利用状況を整理したスプレッドシート(主にtextlint関連のライブラリ)

スプレッドシートのA列にライブラリの名前がアルファベット順に並んでいて、1行目に各プロダクトの名前が表示されます。 ライブラリの名前とプロダクトの名前が交差するセルに採用しているバージョンが表示される感じです。

package.jsonを眺めたことがあれば、割と直感的に理解できるのではと思います。

また、上述しましたが、GASコンソールのトリガー設定によって、デイリーで最新の状態が反映されるようになっています。便利!!

GASコンソールのトリガー設定。午前4〜5時に一度実行している
GASコンソールのトリガー設定。午前4〜5時に一度実行している

このGASの実装は公開できていませんが、この記事最後の「サンプルコードの紹介」にて、ローカルに用意したpackage.jsonを比較した結果をCSV出力できるので、お手元でExcelやスプレッドシートへ反映して眺めることができます。ご興味があればぜひどうぞ!

なんでやろうと思ったの?

まず、背景の話から入ると、SmartHRというサービスはユーザーから見ると、1つのWebアプリケーションのように振る舞っていますが、実際には複数のWebアプリケーションから成り立っています。

SmartHRの開発体制。各プロダクト一覧と大まかな技術スタック
SmartHRの開発体制

例えば、この図の右側を見ると「文書配付」「年末調整」「分析レポート」…と機能が並んでいますが、それぞれ独立したWebアプリケーションとして実装され、それぞれに原則1つの開発チームがアサインされています。ソースコードの管理上も別のリポジトリに分かれています。

SmartHRでは各プロダクトにおける技術選定を担当チームにが決定しているため、プロダクト固有の事情・ドメイン・メンバーのバックグラウンドによって採用しているライブラリに個性があったり、そのアップデートへの追随状況がまちまちということもあります。

こういった背景もあり、社内のプロダクト全体を俯瞰してライブラリの利用状況を比較するとどういう傾向にあるのかはずっと気になっていました。

また、実用性の観点だと、ライブラリのアップデートにおけるBREAKING CHANGESへの対応方針を考えるうえで、他プロダクトの状況を参考にすることも多いので、GitHubを検索して状況を確認する手間が削減できると便利だな〜〜、と思うことはありました。

とある昼下がりの午後。いつものようにpackage.jsonと穏やかな1日を過ごしていると電流が走りました。

「…あれ?もしかして、package.jsonってJSONなのでは…!?」 (それはそう)

JSONということは、労せずJavaScriptから読み込み自由にデータを加工できます。なので、複数のpackage.jsonを用意すれば比較するスクリプトは簡単に書けそう!などと考え、勢いで書きはじめたのでした。

これらの技術スタックを採用してみた感想

今回GASを採用しようと思った一番の理由は、誰でもライブラリの利用状況をすぐに確認できる状況を作りつつ、それが継続的に最新の状態を維持する仕組みがすぐに実現できそうだったからです。

🤔 「うーん、誰でもライブラリの利用状況をすぐに確認できる状況ってなんやろか」
→ 「出力は表形式になりそうだし、ひとまずスプレッドシートなら共有・確認もしやすそう」
→ 「スプレッドシートならGASかなぁー」

くらいのテンションです。

以下に、実際どうだった?という話をメリット・デメリットとして並べていきます。

メリット

  • スプレッドシートは確認しやすく、共有もしやすかった
    • 元々の目論見は達成できたと思っています。
  • 出力先のスプレッドシートと親和性が高く大体何でもできる(できそう)
    • 当然のことながら、Google謹製ということもあって、直接スプレッドシートを操作するAPIが充実しています。
    • カラムの幅を調整したり、スプレッドシートのタイトルに更新日時を含めたりなど、使いやすく・見やすくするための調整は大体できそうです。
  • インフラの管理が不要
    • インフラ構築のために、あらためて特別な設定が不要です。
    • メンテナンスフリーなので(今のところ)運用も楽です。
  • 権限管理の考慮事項が少ない
    • プライベートな情報なので権限周りには気を使うところですが、Google workspaceの仕組みにそのまま乗っかれるので考慮が必要な点が少なくすみました。
  • トリガーの設定が充実している
    • 定期実行など実行トリガーの条件が充実していて、GASのコンソールから簡単に設定・管理できて便利です。
    • こういったトリガーが充実していると、ふと浮かんだアイデアを形にしやすいのでかなり大きな推しポイントです。
  • 使い慣れた環境を利用して開発できた
    • GitHubでソースコードを管理しつつ、使い慣れたエディタで、オートフォーマッター、リンターが使えるのは素敵!
      • ただし、GASのオンラインエディタも新コンソールに移行して大きく改善されているので、ちょっとGASを使いたいくらいであれば十分快適になっている気がします。
    • npmライブラリが使える!使えるよ!

デメリット

  • GAS環境固有の事情で何かとハマりがち
    • TypeScriptのおかげで、「ハマり」は軽減できていると感じつつも、それでもやっぱりハマる。
      • 例えば、 GASにはURLクラスがない ので、これに依存しているライブラリを利用する場合は、polyfillを用意しないとランタイムエラーになるということなどありそうです。
    • 一般的なWebアプリケーション開発と比べてオンライン上に知見が少ないので、トラブルシューティングに要する時間が長くなりがちという問題もあります。
  • 開発環境の構築に手間がかかる
    • いわゆるボイラープレートの構築が大変問題です。
    • 環境をゼロから構築する場合、ある程度フロントエンド開発の経験がないと「やりたいことの実装」に辿り着く前に挫折する気がします。

個々の技術についてもう少し詳しくみる

個々の技術についてもう少し掘り下げて触れていきます。

Claspについて

github.com

Develop Apps Script projects locally using clasp (Command Line Apps Script Projects).

Claspとは、Google公式のツールで、GASアプリケーションをローカルで開発可能にするためのCLI(Command Line Interface)です。 Claspによって提供されるCLIを利用して、ターミナルからコマンドでGoogleにログインしたり、ローカルのソースコードを指定したGASのプロジェクトへプッシュできます。

コマンドの詳細はClaspの公式ドキュメントを確認いただければと思いますが、雑に利用例をピックアップすると以下のようなイメージです。

# Googleアカウントにログイン
$ clasp login

# 新規のGASプロジェクトを生成
$ clasp create

# GASにローカルのソースコードを反映
$ clasp push

# ブラウザでGASプロジェクトを開く
$ clasp open

ログインやプロジェクトの生成は対話形式で進むのでとりあえずコマンド叩けば、あとは導かれるままに初期設定ができます。これは便利〜。 また、実際に利用した感覚としては、npmにログインしたり、ローカルのソースコードをnpmにpublishする体験と近いように感じました。なので、もしnpmにライブラリを公開した経験があれば違和感なく活用できる気がします。

clasp pushとclasp deployの違いについて

Claspにはpushの他にdeployというコマンドがあります。 初見でちょっと悩んだので簡単に触れると、それぞれ以下のような役割を持っています。

  • clasp push:ローカルのソースコードをGASに反映するコマンド
  • clasp deploy:GASに反映されているソースコードを機能(WebアプリケーションやAPI、ライブラリなど)として公開するためのコマンド

今回の用途ではソースコードをGASに反映して、トリガーから定期実行できれば十分なのでdeployは利用せず、pushのみの運用となりました。

Claspのローカルインストールについて

公式ドキュメントではClaspをグローバルインストールする手順を紹介していますが、ローカルインストールしてnpmスクリプトからclaspにアクセスする方法でも問題なく利用できました。

以下のようなイメージです。

$ yarn add -D clasp
# もしくは
$ npm install --save-dev clasp
/* package.json */

{
  ...(略)
  "scripts": {
    "clasp-login": "clasp login",
    "clasp-logout": "clasp logout",
    "clasp-push": "clasp push"
  }
  ...(略)
}

なので、もしあなたが先祖より伝わる家訓によってnpmのグローバルインストールを禁じられていたとしても安心して利用できると思います。

ただし、clasp loginコマンドによって生成されたGoogleアカウントの認証情報はユーザーディレクトリ配下の ~/.clasprc.json に保存され、ローカル(リポジトリ単位)ではなくグローバルに扱われる点だけ注意が必要です。

Claspを採用するうえでの注意点

というわけで、Claspによって、GASのソースコードをリポジトリ管理しやすくなり、慣れ親しんだローカルの開発環境を利用できるのでとてもオススメ……、ではあるのですが、1つ大きめの注意点があります。

現在(2022/06/27)Claspのメンテナンスが止まっていて、2021/08/31のコミットを最後にメンテナーの動きが見られない点です。

github.com

今時点ではClaspは正常に動作しているものの、GASのアップデートに伴い動作しなくなったり、Clasp自体の脆弱性対応が行なわれないといった可能性がありますので、今後のGoogleの動向に注意が必要です。

GASの開発におけるTypeScriptについて

昨今は普段の開発でもTypeScriptを書かれている方は多いと思います。となると、GASもTypeScriptで書きたくなるのが人情ってものですよね!?

GASの開発にTypeScriptを採用するメリットは一般的なWebアプリケーションの開発とそう違いませんが、普段からGASを書いていてAPIに精通している人は少ない(要出典)と思うので、その点ではTypeScript + GASの型定義(@types/google-apps-script)によって、「ハマり」を低減できる恩恵は大きいと思っています。

また、エディタ上にサジェストされるGASのメソッドやプロパティを眺めてどんなことができそうか察して試せるメリットもとても大きく、実現したいアイデアを形にするのにとても役立ちます。

GASの処理系でTypeScriptをそのまま動かすことはできないので、何らかの方法でJavaScriptにコンパイルした成果物をpushする必要があるのですが、その方法は2つあります。

  1. Claspを利用してTypeScriptで実装されたソースコードをGASにpushする方法
  2. 自前でTypeScriptをコンパイルして、生成されたJavaScriptのソースコードをGASにpushする方法

今回は最終的に2を採用しました!

1. Claspを利用してTypeScriptで実装されたソースコードをGASにpushする方法

まず1つ目についてです。

実は先に紹介したClaspがTypeScriptをサポートしています。clasp pushの対象に *.ts ファイルがあると自動でコンパイルして、その成果物をGASにpushしてくれます。

Googleの公式ドキュメントでもこの方法が紹介されています。

developers.google.com

この方法は手軽に導入できて楽という反面、上述したとおりClaspのメンテナンスが止まっている影響で、TypeScriptが最新のバージョンに追随できていないという問題があります。

2. 自前でTypeScriptをコンパイルして、生成されたJavaScriptのソースコードをGASにpushする方法

2つ目は、自前でTypeScriptをコンパイルする環境を作る方法です。

環境構築が必要になるのでひと手間かかりますが、GASでnpmライブラリを利用する場合は、どのみちモジュールバンドラーが必要になるので、これと合わせてTypeScriptをコンパイルする環境を作ってしまうのもありかと思います。

esbuildについて

esbuildとはGo言語で実装されたモジュールバンドラーです。

github.com

他には一般的なWebアプリケーション開発ではwebpackやRollupなどが利用されていると思います。

GASの実装にあたって、いくつかランタイムに動作するnpmライブラリを利用したかったため、モジュールをバンドルして1ファイルにまとめてアップロードする方針を取りました。Claspだけでは実現できない点です。

今回は機能的に求められることも少なく、ターゲットもGASの上で動けば十分なので、ビルドが早くシンプルに利用できそうなesbuildを採用しました。

また、「GASにおけるTypeScriptについて」でも説明したとおり、ClaspがTypeScriptをサポートしているため、特にランタイムに動かしたいライブラリがなければモジュールバンドラーの導入は不要です。

GAS向けにesbuildを利用するうえでの注意点

ここで、GAS向けにesbuildを利用するうえでの注意点が1つあります。

GASのコンソールから特定の関数を選択、もしくはトリガーから実行するためには、ソースコードのトップレベルに関数を露出させる必要がありますが、普通にビルドすると実装が無名関数でラップされてしまうのでGASから実行できなくなってしまう点です。

GASのコンソールで実行対象の関数を選択している様子
GASのコンソールで実行対象の関数を選択している様子

これを回避するために、以下のesbuild-gas-pluginを導入しました。

github.com

順を追って以下に詳しく説明していきます。

まず、以下のTypeScriptで記述されたmain.tsをesbuildでコンパイルすると…

/* main.ts */

export const hello = () => console.log('ぽえ〜ん')

以下のようなmain.jsが出力されます。

/* main.js */

(() => {
  // src/main.ts
  var hello = () => console.log('ぽえ〜ん');
})();

このmain.jsはグローバルスコープを汚染しないよう無名関数によって実装がラップされていることがわかります。これによってGASからはhelloという関数を認識できず、実行できなくなっています。

そこで、このesbuild-gas-pluginを導入してコンパイルすると以下のような結果が出力されます。

/* main.js */

var global = this;

(() => {
  // src/main.ts
  var hello = () => console.log('ぽえ〜ん');
})();

無名関数の外(トップレベル)に global という変数定義が追加され、無名関数の内側(実装側)から間接的に this にアクセスできるようになりました。

このglobalというグローバル変数にhello関数をぶら下げるように元のソースコードを変更してみます。

/* main.ts */

// @ts-expect-error
global.hello = () => console.log('ぽえ〜ん')

未定義のglobalにアクセスするのでTypeScriptによって怒られが発生します。おとなしくしてもらいました。 すると、以下ような結果が出力されます。

/* main.js */

var global = this;
// src/main.ts
function hello() {
}
(() => {
  // src/main.ts
  global.hello = () => console.log('ぽえ〜ん');
})();

トップレベルにhelloという関数が生成されたことで、GASのコンソールから実行できるようになりました!

この生成されたhello関数自体は空実装になっていますが、globalを通じて関数の実装を上書きしているので、無名関数内のhelloが呼び出されるようになります。ぽえ〜ん。

少々複雑に見えるかもしれませんが、GASのコンソールやトリガーからはトップレベルに定義した関数しか呼び出せないという点だけ抑えておければよいと思います!

どう実装したの?

すでになかなかのボリュームと化しつつありますが、GAS周りの技術選定の話だけで終わってしまってはあまり真新しさもないので、「複数のpackage.jsonを比較した結果をCSV出力するスクリプト」の実装とサンプルコードを紹介します。

複数のpackage.jsonを比較した結果をCSV出力する

実装1ファイルのシンプルなスクリプトですが、comparing-dependencies という名称で、GitHub・npmに公開しています。

github.com

APIの使い方はとてもシンプルでこういうイメージです!

import { createCsv } from 'comparing-dependencies'

const packageJsons = [
  {
    name: 'prj_01',
    dependencies: {
      react: '^17.0.2',
      dayjs: '^1.10.3',
    },
    devDependencies: {
      webpack: '^5.51.1',
    },
  },
  {
    name: 'prj_02',
    dependencies: {
      react: '^17.0.2',
      dayjs: '^1.10.2',
      'react-redux': '^7.2.4',
    },
    devDependencies: {
      webpack: '^5.47.1',
    },
  },
  {
    name: 'prj_03',
    dependencies: {
      moment: '^2.29.1',
      webpack: '^4.46.0',
    },
  },
]

const csv = createCsv(packageJsons)

console.log(csv)

このコードを実行すると以下のような結果が出力されます。

"library","prj_01","prj_02","prj_03"
"dayjs","^1.10.3","^1.10.2",""
"moment","","","^2.29.1"
"react","^17.0.2","^17.0.2",""
"react-redux","","^7.2.4",""
"webpack","^5.51.1","^5.47.1","^4.46.0"

createCsvの引数には、何らかの方法で取得したpackage.jsonたちをそのまま配列で流し込むことを想定しています。

createCsvはオブジェクトのname, dependencies, devDependenciesを参照していて、比較結果をCSV形式の文字列で出力します。内部的にはdependenciesdevDependenciesは特に区別せずマージして扱っています。

また、createCsvの引数の型にも指定しているpackage.jsonファイルの型付けはtype-festPackageJsonを参照しています。

type-festは普段なかなか利用しないような型定義も実装されていて便利!

サンプルコードの紹介

最後に、このリポジトリには実行可能なサンプルコードがあります。

examples/inputs配下に置いた*.jsonを読み込み、比較結果をresult.csvとして出力するようになっていますので、「うちのプロダクトもnpmライブラリの利用状況気になるな────」と興味を持たれた方は、このサンプルコードを活用してもらえるのが一番手軽かと思います!

利用手順はREADME.mdのとおりですが、簡単にフォローするとこんな感じです。

  1. examples/inputs 配下にpackage.jsonたちを置きます。
examples/inputs/
  |- example_package_01.json
  |- example_package_02.json
  |- example_package_03.json
  1. 以下のコマンドでサンプルコードを実行します。
$ yarn run-example
  1. result.csvが出力されます!!!
examples/result.csv

おわりに

というわけで、「社内プロダクトのnpmライブラリの利用状況をスプレッドシートに出力してみた話」を通して、主にGASの開発環境周りの話を紹介してきました。

今回、GASの実装を公開できていないので、ふわっとした読み物になってしまった感があるのですが、また後日リポジトリを公開できたタイミングに紹介できればと思います。(遅筆なのに自分を追い込んでいくスタイル)

冒頭に整理しているメリット・デメリットを総括すると、GASに触れていると何かとハマる点はあるものの、TypeScriptの導入や開発環境を整備することで開発体験は大きく改善できます。

特に、Webフロントエンド周りの開発経験があれば馴染みやすく、SlackのワークフローやZapier等では実現が難しいちょっと凝った自動化を実現したいときに便利で、業務効率化の幅が広がるように思います!

興味があればぜひ試してみてください!

さて、業務効率化!といえば…

SmartHRは人事・労務の業務効率化を実現し、働くすべての人の生産性向上を支える 「クラウド人事労務ソフト」です。

本記事では、SmartHRのプロダクト開発に微塵も関係ない話をしましたが、SmartHRでは、さまざまなプロダクトの課題に一緒に向き合っていくエンジニアを募集しています!

SmartHRのエンジニア採用サイトもありますので、よければぜひ覗いてみてください!

hello-world.smarthr.co.jp