Node.js v20 で GitHubActions から Vercel へのデプロイがコケた話
Vercel が Node.js v20 無いとかエラー出してくる → Error: Found invalid Node.js Version: "20.x". Please set "engines": { "node": "18.x" } in your `package.json` file to use Node.js 18.
GAS で bot 作る時にふと npm に公開されてる便利なライブラリ達を使えたら便利だなと思い至ったのでやってみた話
今回作ったサンプルはこちら。
GAS + clasp の使い方など基本的なところは割愛するので、知らない人は @FruitRiin さんの Google App Script(GAS)をローカルで快適に編集して同期しよう! を参考にどうぞ。
スーパーエンジニアのみなさんは構成で完全に理解した思うが、webpack + babel でビルドしたものを clasp を使って GAS にぶち上げているだけ。
ディレクトリ作成、npm の初期化
$ mkdir MyBot $ cd MyBot $ yarn init $ mkdir src dist
clasp を導入して GAS と連携できるようにする
# (グローバルでもよい) $ yarn add -D @google/clasp
GAS にプロジェクト作成
$ yarn clasp login $ yarn clasp create # 既にあるプロジェクトを使うなら $ yarn clasp clone
ビルド成果物(dist
)を GAS に push
したいので、設定を書き換える
// .clasp.json { "scriptId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "rootDir": "./dist" // ←追記 }
$ mv appsscript.json dist
下記のような構成になる
. ├── .clasp.json ├── dist/ │ └── appsscript.json ├── node_modules/ ├── package.json └── src/
# TypeScript は義務、 GAS 特有の関数など補完が効いてとても楽 $ yarn add -D typescript @types/google-apps-script # ビルド用に webpack と babel 入れる $ yarn add -D webpack webpack-cli $ yarn add -D @babel/core @babel/preset-typescript babel-loader # 後ほど説明するが、作成した関数を GAS で実行可能にするために必要なプラグイン $ yarn add -D gas-webpack-plugin # eslint や prettie もお好みで入れる $ yarn add -D eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-loader # 今回は dayjs を使った Bot を動かしてみるのでいれた $ yarn add -D dayjs
ちなみにサンプルではtsconfig.json
と.eslintrc.json
は下記のようにした
{ "compilerOptions": { "target": "es5", "lib": [ "es5", "es6", "es7", ], "outDir": "./dist", "rootDir": "./src", "module": "esnext", "downlevelIteration": true, "strict": true, "noUnusedLocals": true, "esModuleInterop": true, "moduleResolution": "node" }, "include": ["./src/**/*"] }
{ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:prettier/recommended", "prettier/@typescript-eslint" ], "plugins": ["@typescript-eslint"], "parser": "@typescript-eslint/parser", "env": { "node": true, "es6": true }, "parserOptions": { "sourceType": "module" }, "rules": {} }
サンプルでは Slack の Slash command で発火する Bot (Slack App) として作成した。
もちろん GAS で動くだけなので Slack App 以外の用途でも使える。
今回の内容としては dayjs
を使って、現在時刻をコメントするシンプルなものとした。
// src/main.ts import dayjs from "dayjs"; import "dayjs/locale/ja"; dayjs.locale("ja"); export function doPost(): GoogleAppsScript.Content.TextOutput { const text = dayjs().format("YYYY年M月D日(ddd) H:m"); const data: SlackData = { text, response_type: "ephemeral", }; return ContentService.createTextOutput(JSON.stringify(data)).setMimeType( ContentService.MimeType.JSON ); } interface SlackData { text: string; response_type: "ephemeral" | "in_chanel"; }
GAS で実行可能な関数は、トップレベルな関数だけなので、index.ts
を用意し、実行対象(サンプルではdoPost
)を global
に登録する。
こうしておくと gas-webpack-plugin
が global
に登録した関数を GAS で実行可能なようにトップレベルに配置してくれる。
詳しくは gas-webpack-plugin 参照。
// src/index.ts import { doPost } from "./main"; declare const global: { [x: string]: unknown; }; global.doPost = doPost;
サンプルには無いが、GAS 上で実行選択させたくないような関数(共通処理や可読性のためにロジックを分離したような関数)は、ここに登録しなければ GAS 上で実行不可にできる。
babel はひとまず TypeScript のトランスパイルの設定を入れた
// .babelrc { "presets": [ "@babel/preset-typescript" ] }
webpack に gas-webpack-plugin と、babel を設定する。
ついでに eslint-loader
で、babel のトランスパイル実行前にコードチェックしている。
// webpack.config.js /* eslint-disable @typescript-eslint/no-var-requires */ const path = require("path"); const GasPlugin = require("gas-webpack-plugin"); module.exports = { mode: "development", devtool: false, context: __dirname, entry: "./src/index.ts", output: { path: path.join(__dirname, "dist"), filename: "index.js", }, resolve: { extensions: [".ts", ".js"], }, module: { rules: [ { test: /\.[tj]s$/, exclude: /node_modules/, loader: "babel-loader", }, { enforce: "pre", test: /\.[tj]s$/, exclude: /node_modules/, loader: "eslint-loader", }, ], }, plugins: [new GasPlugin()], };
注意点としては、出力を小さくしようと**mode: "production"
にすると動かない**のでmode: "development"
としておくこと。
関数名などが簡略化され、gas-webpack-plugin
がglobal
に登録された関数をトップレベルに配置できない。
コマンドを適当に npm scripts に設定する。
// package.json { // 省略 "scripts": { "build": "webpack", "deploy": "yarn build && clasp push", "lint": "eslint --fix --ext .ts,.js --ignore-path .gitignore ." } }
今回は上記のように一発でビルド&デプロイできるようにした
$ yarn deploy
GAS のプロジェクトを確認したら webpack + babel でビルドされたものがデプロイされている!!
$ yarn clasp open
後は実行ボタンを押すなり、トリガーを設定するなり、Slack App に登録するなりで動かすことができるようになった!!
npm に公開されているライブラリが利用できる(node_modules をGASにぶち上げる必要がない)ことは言わずもがなであるが、 ライブラリを利用しないシーンでも実は小さなメリットがある。
そもそも clasp は TypeScript に対応しており、webpack + babel を用意しなくともトランスパイルされる。
しかし import
文が複数行に展開されている場合や、import
の仕方によってはエラーとなる罠がある。
clasp push 前のローカルのコード
import * as hoge from './hoge.ts' import { fuga, bar } from './hoge.ts' function main() { hoge.foo() fuga() bar() }
clasp push 後の GAS 上のコード
// import * as hoge from './hoge.ts' // import { fuga, ← 構文エラー bar } from './hoge.ts' function main() { hoge.foo() ← hoge が宣言されていないのでエラー fuga() bar() }
複数のオブジェクトや関数を import
する場合 *
が使えないため展開する必要があるが、
量が多いと改行できない兼ね合いから可読性が極端に落ちたり、lint や prettier の設定次第では自動で改行されてしまい対応追われるウザさがある(実体験)
上記のような clasp 特有の罠を回避するためだけに webpack + babel をわざわざ導入するのは億劫ではあるが、 一度設定してしまえば他のプロジェクトにも使いまわせるので、毎度 clasp の挙動に気をつけるより圧倒的に分があると思う。
実際に私はこの構成を業務でちょっとした Bot を作って運用しているが、テストコードも書いたりしているため下記のように src
下に app
と tests
を切るようにしている。
. ├── .babelrc ├── .clasp.json ├── .git/ ├── .gitignore ├── dist/ │ └── appsscript.json ├── jest.config.js ├── node_modules/ ├── package.json ├── tsconfig.json ├── webpack.config.js └── src/ ├── app/ # ← 実行するスクリプト群 └── tests/ # ← テストコード群
Vercel が Node.js v20 無いとかエラー出してくる → Error: Found invalid Node.js Version: "20.x". Please set "engines": { "node": "18.x" } in your `package.json` file to use Node.js 18.
public ディレクトリにおいてる画像とかのパスを補完が効くようにしたい
windows はクソ(暴論)