@gasboost/vite
v1.2.2
Published
A Vite plugin for building @gasboost/app applications for Google Apps Script.
Maintainers
Readme
@gasboost/vite
@gasboost/app で構築した Google Apps Script アプリケーションを、Vite でビルド・ローカル開発するためのプラグインです。
@gasboost/vite は用途ごとに2つの Vite Plugin を提供します。
build— Google Apps Script 向けの production builddev— ローカル開発時の RPC 実行
インストール
pnpm add @gasboost/app
pnpm add -D @gasboost/vite vitenpm:
npm install @gasboost/app
npm install -D @gasboost/vite viteQuick Start
まず @gasboost/app でバックエンドを定義します。
// src/server.ts
import { AppsScript } from "@gasboost/app";
const app = new AppsScript()
.get(() => {
return HtmlService.createHtmlOutput("Hello");
})
.post((request) => {
return ContentService.createTextOutput(request.text());
})
.call("sum", (a: number, b: number) => a + b)
.call("getUser", async (id: string) => ({
id,
name: "Taro",
}));
export default app;gasboost() から build と dev を取得し、1つの vite.config.ts の中で mode に応じて使い分けます。
frontend と backend は成果物と build graph が異なるため、同時に1つの Vite build へ混在させません。
// vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
import { gasboost } from "@gasboost/vite";
const { build, dev } = gasboost({
entry: "src/server.ts",
envDir: ".env",
});
export default defineConfig(({ mode }) => {
if (mode === "server") {
return {
plugins: [build],
build: {
outDir: "dist",
emptyOutDir: false,
},
};
}
return {
plugins: [react(), viteSingleFile(), dev],
build: {
outDir: "dist",
},
};
});この例では、frontend build と backend build を mode で分離します。
{
"scripts": {
"dev": "vite",
"build:client": "vite build --mode client",
"build:server": "vite build --mode server",
"build": "pnpm build:client && pnpm build:server"
}
}vite
↓
gasboost:dev
vite build --mode client
↓
frontend build
vite build --mode server
↓
gasboost:buildfrontend 側の React、single-file 化などの設定は利用側プロジェクトの責務です。
gasboost()
const { build, dev } = gasboost({
entry: "src/server.ts",
envDir: "config",
});戻り値:
{
build: Plugin;
dev: Plugin;
}entry
必須です。
AppsScript を定義して default export している entry file を指定します。
gasboost({
entry: "src/server.ts",
});envDir
任意です。
build 時に Vite が環境変数ファイルを読み込むディレクトリを指定します。
gasboost({
entry: "src/server.ts",
envDir: "config",
});Build Plugin
build は vite build のときだけ動作し、GAS backend build を担当します。
frontend / backend を同じ vite.config.ts で扱う場合は、server 用 mode のときだけ plugins に含めてください。
const { build } = gasboost({
entry: "src/server.ts",
});
export default defineConfig(({ mode }) => {
if (mode === "server") {
return {
plugins: [build],
};
}
return {
plugins: [],
};
});build plugin は次の処理を担当します。
- entry file の静的解析
AppsScriptに登録された GET / POST / RPC の検出- GAS が認識するグローバル関数宣言の生成
- GAS 向け Vite build configuration
- 環境変数の読み込み
グローバル関数の生成
例えば次のアプリケーションを定義した場合:
const app = new AppsScript()
.get(...)
.post(...)
.call("getUser", ...)
.call("sum", ...);
export default app;build plugin は GAS が認識するためのグローバル関数宣言を生成します。
function doGet() {}
function doPost() {}
function getUser() {}
function sum() {}これらは関数名を Google Apps Script に認識させるための宣言です。
実際のハンドラ登録と dispatch は @gasboost/app がランタイム上で行います。
@gasboost/vite
static analysis
global function declarations
↓
@gasboost/app
runtime registration
dispatch静的解析
entry file には1つの AppsScript インスタンスを定義し、default export します。
const app = new AppsScript();
app.get(...);
app.call("getUser", ...);
export default app;チェーン形式にも対応しています。
const app = new AppsScript()
.get(...)
.post(...)
.call("getUser", ...);
export default app;RPC 名は文字列リテラルで指定する必要があります。
app.call("getUser", handler);次のような動的な名前は静的解析の対象外です。
const name = "getUser";
app.call(name, handler);build 時に GAS のグローバル関数名を確定する必要があるためです。
検証
静的解析時には、曖昧なアプリケーション定義をエラーとして扱います。
- GET ハンドラの重複
- POST ハンドラの重複
- RPC 名の重複
- entry 内の複数
AppsScriptインスタンス - default export されていない
AppsScript
GAS 向け build configuration
現在の build 設定:
- target: ECMAScript 2019
- output format: CommonJS
- output directory:
dist entryを build input として利用
環境変数
Vite 標準の環境変数機構を利用します。
config/
├── .env
├── .env.development
└── .env.productionconst { build } = gasboost({
entry: "src/server.ts",
envDir: "config",
});アプリケーションでは通常の Vite と同様に参照できます。
const apiUrl = import.meta.env.VITE_API_URL;Dev Plugin
dev は Vite Dev Server 上でのみ動作します。
frontend の開発用 Vite config に追加して利用します。
const { dev } = gasboost({
entry: "src/server.ts",
});
export default defineConfig({
plugins: [dev],
});dev plugin は、GAS にデプロイしなくてもローカル環境から AppsScript の RPC を実行できるエンドポイントを Vite Dev Server に追加します。
Local RPC
ローカル RPC のエンドポイントは次の形式です。
POST /__gasboost/{rpcName}例えば、
const app = new AppsScript().call("sum", (a: number, b: number) => a + b);
export default app;に対して、
POST /__gasboost/sum
Content-Type: application/json{
"args": [1, 2]
}を送信すると、内部では次の dispatch が実行されます。
app.dispatch("sum", 1, 2);レスポンス:
3RPC Request
Request Body は次の形式です。
{
args: unknown[];
}例えば複数の引数を持つ RPC:
app.call(
"example",
(id: number, name: string, active: boolean, options: { value: number }) => {
// ...
},
);に対して、
{
"args": [
1,
"Taro",
true,
{
"value": 4
}
]
}のように送信できます。
引数なし RPC
Request Body が空の場合は、引数なし RPC として扱われます。
POST /__gasboost/noArgs内部では次のように dispatch されます。
app.dispatch("noArgs");明示的に送る場合は次でも構いません。
{
"args": []
}RPC Response
@gasboost/app の dispatch() が返す AppsScriptResponse.contents を、そのまま HTTP Response Body として返します。
例えば、
app.call("getUser", () => ({
id: 1,
name: "Taro",
}));に対するレスポンスは:
{
"id": 1,
"name": "Taro"
}となります。
Response の Content-Type は:
application/json; charset=utf-8です。
Async RPC
非同期 RPC にも対応しています。
app.call("loadUser", async (id: string) => {
const user = await loadUser(id);
return user;
});dev plugin は dispatch() の完了を待ってからレスポンスを返します。
開発中の module loading
RPC リクエストごとに、指定された entry を Vite の ssrLoadModule() で読み込みます。
POST /__gasboost/sum
↓
ssrLoadModule("src/server.ts")
↓
AppsScript.dispatch("sum", ...)そのため、Vite Dev Server 上の最新のサーバーコードを利用して RPC を実行できます。
Error Response
不正な Request Body
JSON として不正な body や、args が配列でない body は 400 を返します。
{
"args": "invalid"
}レスポンス例:
{
"error": {
"name": "InvalidRpcRequestError",
"message": "Invalid RPC request body. Expected { args: unknown[] }."
}
}JSON 自体が不正な場合も 400 です。
POST 以外
Local RPC endpoint は POST のみ受け付けます。
GET /__gasboost/sumは 405 になります。
{
"error": {
"name": "MethodNotAllowedError",
"message": "Only POST is allowed."
}
}未登録 RPC
存在しない RPC を呼び出した場合は 500 を返します。
POST /__gasboost/unknown{
"error": {
"name": "Error",
"message": "Function unknown is not registered."
}
}Handler Error
RPC handler 内で例外が発生した場合も 500 として JSON で返されます。
{
"error": {
"name": "TypeError",
"message": "handler failed"
}
}Local RPC の対象 path
次の形式だけを RPC として扱います。
/__gasboost/{rpcName}例えば以下は対象です。
/__gasboost/getUser
/__gasboost/sum一方、次のような path は RPC として扱いません。
/__gasboost/
/__gasboost/foo/bar
/api/users通常の Vite middleware chain に処理を渡します。
query string が付いていても RPC 名は正しく解決されます。
/__gasboost/hello?foo=barURL encoded な RPC 名も decode されます。
/__gasboost/hello%20worldClient Transport
@gasboost/client を利用している場合、dev plugin はローカル開発時の transport を自動で Local RPC に切り替えます。
アプリケーション側では development / production を判定する必要はありません。
import { appsScriptClient } from "@gasboost/client";
import type { App } from "./server";
const { client } = appsScriptClient<App>();
const result = await client.sum(1, 2);Vite Dev Server 上では、appsScriptClient() に transport が指定されていない場合、dev plugin が自動的に FetchTransport を適用します。
appsScriptClient()
↓
FetchTransport
↓
POST /__gasboost/{rpcName}
↓
AppsScript.dispatch()そのため、ローカル開発用に transport を切り替えるコードを書く必要はありません。
// 不要
appsScriptClient({
transport: import.meta.env.DEV
? new FetchTransport({
endpoint: "/__gasboost",
})
: new AppsScriptTransport(),
});production build では dev plugin による差し替えは行われません。
GAS 上では appsScriptClient() のデフォルトである AppsScriptTransport がそのまま利用されます。
appsScriptClient()
↓
AppsScriptTransport
↓
google.script.runつまり、同じ application code のまま local development と GAS production の両方で利用できます。
const { client } = appsScriptClient<App>();また、明示的に transport を指定した場合はその transport が優先されます。
const { client } = appsScriptClient<App>({
transport: customTransport,
});dev plugin が明示指定された transport を上書きすることはありません。
HtmlTemplate variables
GAS の HtmlTemplate では、<?= variable ?> を使用してサーバー側の値を HTML に埋め込めます。
<script>
const variables = "<?= variables ?>";
</script>GAS 上では HtmlTemplate によって評価されますが、Vite Dev Server では GAS のテンプレート処理が実行されません。
@gasboost/vite では、template オプションを指定することで、開発時にこれらの template variables を置換できます。
import { gasboost } from "@gasboost/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
gasboost({
entry: "./src/backend/main.ts",
template: {
variables: JSON.stringify({
scriptId: "local-script-id",
isSetupCompleted: true,
isTermsAccepted: true,
}),
},
}).dev,
],
});例えば、次の HTML は、
<script>
const variables = "<?= variables ?>";
</script>Vite の開発環境では次のように変換されます。
<script>
const variables =
'{"scriptId":"local-script-id","isSetupCompleted":true,"isTermsAccepted":true}';
</script>複数の template variable も指定できます。
gasboost({
entry: "./src/backend/main.ts",
template: {
environment: "development",
userName: "Tiger",
},
});<p>Environment: <?= environment ?></p>
<p>User: <?= userName ?></p>template の型は Record<string, string> です。
値の serialize は @gasboost/vite では行いません。オブジェクトなどを埋め込む場合は、利用側で JSON.stringify() などを使用して文字列へ変換してください。
template による置換は Vite Dev Server でのみ行われます。production build では値を埋め込まず、GAS 上で実際の HtmlTemplate が template expression を評価します。
build と dev の責務
gasboost()
│
├─ build
│ ├─ AppsScript の静的解析
│ ├─ GAS グローバル関数生成
│ ├─ GAS 向け build config
│ └─ backend production build
│
└─ dev
├─ Vite Dev Server middleware
├─ Local RPC endpoint
├─ entry の ssrLoadModule
└─ AppsScript.dispatchfrontend と backend を1つの vite.config.ts で扱う場合は、mode で分岐します。
const { build, dev } = gasboost({
entry: "src/server.ts",
});
export default defineConfig(({ mode }) => {
if (mode === "server") {
return {
plugins: [build],
};
}
return {
plugins: [dev],
};
});build を通常の frontend build に含めると、server entry が Vite の build input になるため、frontend と backend の build は分離してください。
GAS ランタイム
@gasboost/vite は、build 時と local RPC 実行時に server entry を実際に評価します。
一般的な GAS API は、デフォルトのローカルランタイムから提供されます。
import { gasboost } from "@gasboost/vite";
export default gasboost({
entry: "./src/server.ts",
});entry の初期化時に追加の GAS API が必要な場合は、runtime から差し込めます。
import { SpreadsheetAppStub } from "@gasboost/sheetorm";
import { gasboost } from "@gasboost/vite";
export default gasboost({
entry: "./src/server.ts",
runtime: {
SpreadsheetApp: SpreadsheetAppStub,
},
});runtime に渡した値は、デフォルトのローカルランタイムを上書きします。
このランタイムは build 時の entry 評価と、local RPC 実行時の両方で利用されます。
基本的には、module 初期化時に必要な API だけを差し込み、実際の GAS API 呼び出しは handler 実行時まで遅延させることを推奨します。
ランタイム解析
GET / POST / RPC の登録状態は、entry ファイルを静的解析するのではなく、実際に評価された AppsScript インスタンスから取得します。
そのため、import 先、ヘルパー関数、loop、AppsScript.calls() 経由の登録にも対応できます。
const app = new AppsScript();
for (const [name, handler] of Object.entries(handlers)) {
app.call(name, handler);
}
export default app;関連パッケージ
@gasboost/app— GAS バックエンドランタイムと RPC 定義@gasboost/client— フロントエンド側の型安全 RPC クライアント
License
MIT
