TL;DR
これ真似てください.
概要
- GASのJSエンジンがRinhoからV8に更新された.したがってES6+コードが動作するはずである.
- とはいえ,node.jsなどと比較して,いくつか制約がある.例えばクラスのフィールド宣言は未対応のようだ.参考:
- ところで未対応の機能もその理由も様々あるようだが,おそらくフィールド宣言周りの問題は,Google Closure Compilerのような内製パーサーに起因するものだというのが専らの下馬評である.参考:
- 一方で,bun標準のbundlerはES5へのダウンレベリングに対応しておらず,これに限りesbuild(およびGAS用プラグイン)を使う必要あり.今回は諸事情でesbuild-gas-pluginは辞めesbuild-plugin-gas-generatorを使った.
前提知識に関して参考:
私について
JSまわりは忌避してきたのだが,一念発起してWeb技術にも触れてみようと思い,そのエコシステムつながりでGASを使ってみた,という状況.
したがってあまりJSまわりのエコシステムには詳しくない.悪しからず.
ここではECMAScript 2015以降のコードを、現行および旧バージョンのブラウザや環境において下位互換性のあるJavaScriptに変換することをダウンレベリングと呼ぶ.
また,ダウンレベリング含むトランスパイル,バンドルなどを含めビルドと呼ぶ.
目的
よくあるGoogle CalendarからDiscord Webhookへ,予定を通知するBotを,GASで実装しようとした.
問題
これをclaspとbunで書こうとしたところ,TSからJSへのトランスパイルでハマった.コードがpushできない.
どうやら上述の通りクラスのフィールド宣言が含まれているので,ビルド設定をいじらなくてはならない.
しかし,よくよく調べてみると,bunには本稿執筆時点でダウンレベリング機能が存在していない.bunのリポジトリにもissueが立っているが,今のところ動きがないようだ.
計画
三案ある.
まず1案だが,不快.機械的にどうにかできることをわざわざ人間の注意力に任せるのは,短期的な解決にはなるが中長期的な技術的負債になる.というか自分のリポジトリで訳の分からないオレオレルールを作りたくない.却下.
次に2案.1よりマシだがビルドが二段階になるのはいただけない.いざデバッグなどする場合,コードの追跡が難しくなりそうだし,時間がかかる.タイプ量も増えそうだ.却下.
最後3案.ツールが増えるのは本意ではないが,esbuildはGASのビルドに関して実績があり,ビルドの段階が増えるわけでもない.これで行こうと思う.
対処
前掲した記事にもあるように,esbuild+esbuild-gas-pluginを使ってみる.
現状以下のようなディレクトリ構成であるので
.
├── .git
├── .vscode
├── dist
├── node_modules
├── src
├── .clasp.json
├── .gitignore
├── appsscript.json
├── bun.lock
├── package.json
└── tsconfig.json
プロジェクトルート直下にbuild.tsを作成し,以下のように記述した
import esbuild from "esbuild";
import { GasPlugin } from "esbuild-gas-plugin";
esbuild.build({
entryPoints: ["src/main.ts"],
bundle: true,
outfile: "dist/main.js",
target: "es2021",
plugins: [GasPlugin],
}).catch(e => {
console.error(e);
process.exit(1))
};
// このcatch無意味な気がする
以下を参考にした.
いくつかエラーが出たので,潰していたのだが,一つ難しいエラーに見舞われた.
├╴ Type '{ name: string; setup({ onEnd, initialOptions }: PluginBuild): void; }' is not assignable to type 'Plugin'.
│ Types of property 'setup' are incompatible.
│ Type '({ onEnd, initialOptions }: PluginBuild) => void' is not assignable to type '(build: PluginBuild) => void | Promise<void>'.
│ Types of parameters '__0' and 'build' are incompatible.
│ Type 'import("/home/USER/WORKSPACE/notificationBot/node_modules/esbuild/lib/main").PluginBuild' is not assignable to type 'import("/home/USER/WORKSPACE/notificationBot/node_modules/esbuild-gas-plugin/node_modules/esbuild/lib/main").PluginBuild'.
│ The types of 'initialOptions.packages' are incompatible between these types.
│ Type '"bundle" | "external" | undefined' is not assignable to type '"external" | undefined'.
調べてみると,これはesbuild-gas-pluginがesbuildを自分のnode_modulesに固定で抱え込んでいて、利用者側のesbuildバージョンと食い違いが起きることに発端するようだ.
このコメントではオーナーの@mahaker氏が回避策としてESMによる読み込みを辞め,CJSとしてインポートすることにより,型読み込みを握りつぶす方法を紹介している.1
もう一つ,別件で問題がある.
バンドルする以上,グローバル変数が衝突することを防ぐため全体をIIFEで囲むわけであるが,逆にグローバルスコープに露出させる必要がある関数をどのように露出させるのか,という問題である.
これは公式には特に明言されていないのだが,いくつかの資料が示す通り2,グローバルオブジェクト3に対してエントリーポイントを与える必要がある.
function main() {
// content;
}
(globalThis as any).main = main
以上を踏まえ,別のプラグインを検討してみたい.
esbuild-plugin-gas-generatorは自動でexportされた関数やクラスを収集してエントリーポイントを作ってくれる.さらに内部でesbuildを抱え込んでもいないため,先ほど述べたような無駄な回避策を弄する必要もない.
...明確な短所としては,2年ほど更新がない.(結果論だが今のところ困っていないので...)
ということで,本プラグインに乗り換えたことにより,上述の問題二つが一気に解決できた.build.tsは以下のようになった.
後書き
JSの難しさの9割を担っているエコシステムについて,調べるいい機会になった.
ES6が実用段階に入った段階で触れ,そして絶望して逃げだしたあの時よりはだいぶ標準化が進んだ気がするが,それでもまだまだだと感じる.
esbuild-gas-pluginの問題について,もっと多くの人が直面しているはずだと思ったのだが,自分が調べた限りあまり記事になっていない.人口がいないのか.思うに,手軽さがGASの良さであるのに,あえてローカルで開発環境を作り,TSで書いてビルドしようだなんて大掛かりなことをしようとするの自体,GASの立て付けからして本末転倒であるような気がする.claspが(一応)非公式扱い4なのもこれが理由なのかも,と思った.
-
@types/nodeのCJSに対する型定義がすべてanyになっているため.ハックすぎる. ↩
-
TypeScript+clasp+esbuildでGASのローカル開発をもっと便利に/プロジェクトのセットアップ,【GAS】Clasp環境でesbuildバンドルするなど ↩
-
実行環境によって異なるが,当然この場合Node.js由来の
globalに登録することになる.と思っていたのだが,globalThisという標準化された手法があるらしく,今後こちらを使うべきだろう. ↩ -
@google/claspによる. ↩