0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OSS開発で目指す快適すぎるclasp開発環境

0
Posted at

こんにちは🦔

GoogleAppsScript(GAS)界隈では、npmライブラリ「@google/clasp」を利用したローカル開発を行うことも、一般的になってきたと思います

ただ、GASでフロントエンドやTypeScriptなどでのモダンなアーキテクチャを活用するための環境構築には、GAS特有な制約やエコシステムの違いに何度も心を折られることでしょう

「TypeScriptで書きたい」
「ReactやVueを使いたい」
「GitHub Actionsでデプロイしたい」

そうしてコードを書き始めてみると、

「これはGASのAPIだからローカルだとundefinedエラーになっちゃう…」
「ローカルでexportしてGASにアップロードすると、GASだとexportできなくてエラーになっちゃう…」
「ローカルだと動くフロントエンドなのに、なぜかGAS環境だとWebUIが真っ白に…」

そんな次々と襲いかかるエラーとの格闘で日が暮れ、諦めてしまうことも珍しくないと思います

そんなGAS開発のツラいポイントを解消するために、これまで自作してきたOSSライブラリが開発サイクルを一通りカバーできるまで揃いました
実際にどの工程でどのような課題を解決するのか、具体的なコード例を交えて紹介します!

[create] プロジェクトジェネレーター: @ciderjs/gasbombe

解消したい課題

  • TypeScriptの設定
  • バンドルツール(Viteやesbuild)
  • claspの設定
  • さらにはUIフレームワークとの統合…

これらを、GAS環境に最適化させた上で、モダンな構成で整えようとすると、インストールすべきパッケージは膨大になり、初期設定だけで疲弊します
最初からベストプラクティスが欲しいですよね

ライブラリが行うこと

@ciderjs/gasbombeは、対話型のCLIでこれらのセットアップを一括で行うジェネレーターです

npx @ciderjs/gasbombe

コマンドを実行し、使いたいパターン(React, Vue, 素のJavaScript、など)や、claspの設定方法を選ぶだけ。
現代のエコシステムの恩恵を受けながら、すぐに設定済みのプロジェクトディレクトリが生成されます

実現されるDevEx

プロパンガスのガスボンベのように、開発に必要な詰め合わせが即座に手に入り、現代のエコシステムの恩恵を受けながら、すぐにGASツールの開発に着手できます

[dev] サーバとクライアントを型で繋ぐ: @ciderjs/gasnuki

解消したい課題

google.script.runを使うと、サーバサイド関数を呼び出せたり、URLが操作できたりと、現代のWebアプリ開発に欠かせない機能が利用できます

ただ、このgoogle.script.runAPIは、やや古めかしいJavaScriptでして、

  • @types/google-apps-scriptを導入しても型が効かない
  • もちろん自分で実装したサーバサイド関数の型もない
  • Promiseでの提供がない(コールバック形式)
  • ローカル開発時に動かない

という手強い壁があります

また、データの受け渡しで JSON.parseを通すと型情報が失われ、すべてanyになってしまいます
これを一々手動でアサーションしていっては、簡単にサーバサイドとフロントエンドの型の不一致も起きていきますよね

ライブラリが行うこと

@ciderjs/gasnukiは、サーバーサイドのコードを解析して型定義を生成し、クライアント側ではPromiseベースのラッパーを提供します
* Vite設定やCLI使用方法については、ライブラリのREADMEをご覧ください

サーバー側 (GAS)

server/index.ts
import { serialize } from "@ciderjs/gasnuki/json";

export const getUserData = (id: string) => {
  // オブジェクトの型情報を保ったままJSON化
  return serialize({ id, name: "Alice", updatedAt: new Date() });
};

クライアント側 (React/Vueなど)

src/lib/server.ts
import { serialize } from "@ciderjs/gasnuki/json";
import {
  getPromisedServerScripts,
  type PartialScriptType,
} from '@ciderjs/gasnuki/promise';
import type { ServerScripts } from '~/types/appsscript';

// ローカル開発で動くサーバモックを定義
const mockupFunctions: PartialScriptType<ServerScripts> = {
  // getUserData関数のシミュレート
  getUserData: async (id) => {
    await new Promise(resolve => setTimeout(resolve, 500)); // ネットワーク遅延を模した待機
    return serialize({ id, name: 'test user', updatedAt: new Date() });
  },
};

export const server = getPromisedServerScripts<ServerScripts>({ mockupFunctions, parseJson: true });
src/App.tsxなど
import { server } from "@/lib/server";

const fetchData = async () => {
  // 関数名が補完され、引数や戻り値に型がつく
  // 戻り値の Date オブジェクトも自動的に復元される
  const user = await server.getUserData("001");
  console.log(user.name); 
};

実現されるDevEx

IDEの補完を効かせながら、型安全にバックエンド関数を呼び出せます
また、ローカル開発時には自動で自分で定義したモックに切り替わるため、デプロイなしでUIの動作確認が可能です

[dev] ライブラリの型定義: @ciderjs/dgs

解消したい課題

GASライブラリをローカルのTypeScript環境で扱おうとすると型定義が存在せず、導入をためらうことがあります
また、Webエディタならエディタのライブラリ導入機能で補ってくれているappsscript.jsonへの依存関係の記述も、手作業だと困難を極めます

ライブラリが行うこと

@ciderjs/dgsは、主要なGASライブラリのインストールと型定義の導入を支援するCLIツールです

npx @ciderjs/dgs install

対話形式でGASライブラリを選択すると、@google/claspを利用して最新バージョンを確認した上で、appsscript.jsonを更新し、ローカルには型定義ファイル(d.ts)を配置します

実現されるDevEx

GASエディタの機能に依存していた外部ライブラリの追加が、ローカル環境で実現できます
また、外部ライブラリも型安全に利用できるようになり、ライブラリ管理のストレスが大幅に軽減されます

[dev] GAS環境用ルーター: @ciderjs/city-gas

解消したい課題

GASのWebアプリはURLパスによるルーティングが自由に行えません
また、ローカル開発中のブラウザ(window.history)と本番のGAS環境(google.script.history)ではAPIが異なるため、コードの共通化が必要です
そのため、既存のルーターライブラリ(react-routerなど)が利用できず、自力でルーティング実装をする必要があります

ライブラリが行うこと

@ciderjs/city-gasは、この環境差異を吸収するクエリ駆動ファイルベースルーターです
Zodスキーマを使ったクエリパラメータの型定義とバリデーションも内蔵しています
標準でReact、Vue3に対応しています

src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createRouter } from '@ciderjs/city-gas';
import { RouterProvider } from '@ciderjs/city-gas/react';
// 自動生成されるルート定義をインポート
import { pages, specialPages, dynamicRoutes } from './generated/routes';

// ルーター初期化
const router = createRouter(pages, { specialPages, dynamicRoutes });

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
);
src/pages/search/[id].tsx
import { z } from 'zod';
import { useNavigate, useParams } from '@ciderjs/city-gas/react';

// スキーマ定義
export const schema = z.object({
  q: z.string(),
  index: z.coerce.number().optional(), // URL文字列を数値に変換
  sort: z.enum(['date', 'relevance']).optional(),
});

export default function SearchPage() {
  // params は { id: string; q: string; index?: number; sort?: "date" | "relevance" } と型推論される
  const params = useParams('/search');

  const navigate = useNavigate();

  const handleClick = () => {
    // 第1引数: ルート名(補完あり)
    // 第2引数: パラメータ(schemaに基づき型チェックあり)
    navigate('/article/[id]', { id: params.id });
    
    // オプションで replace も可能
    // navigate('/', {}, { replace: true });
  };

  return (
    <div>
      <h1>Search: {params.q}</h1>
      <p>Page: {params.index ?? 0}</p>
      <button onClick={handleClick}>Open article: {params.id}</button>
    </div>
  );
}

実現されるDevEx

実行環境を意識せずにルーティングを実装でき、GASの世界観に型安全なルーターライブラリを登場させられます

[test] GAS固有オブジェクトをモックする: @ciderjs/vitest-plugin-gas-mock

解消したい課題

SpreadsheetApp 等のGAS固有オブジェクトがNode.js環境には存在しないため、それらを含んだコードのロジックだけを単体テストしたくとも、実行後即座にエラーになります
これらを手動ですべてモックするのは膨大な手間と不完全性が付きまといます

ライブラリが行うこと

@ciderjs/vitest-plugin-gas-mockは、事前に@types/google-apps-scriptを分析してありますので、
Vitest実行時にGASの完全性の高い型定義情報を元にグローバルオブジェクトのモックを自動生成・注入します

// vitest.config.ts
import { mockGas } from "@ciderjs/vitest-plugin-gas-mock";

export default defineConfig({
  plugins: [mockGas()],
});

// テストコード
test("スプレッドシート名の取得", () => {
  // 深いメソッドチェーンも自動的にモックが繋がる
  const name = SpreadsheetApp.getActive().getName();
  expect(name).toBeDefined();
});

実現されるDevEx

GASのAPIを含んでいても、ロジックだけを検査することを可能とし、コード品質の担保を助けます
また、深いメソッドチェーンも自動的に解決されるため、テストコード側で事細かにモックを定義する必要がなくなります

[build] GAS環境特有のエラー対策: vite-plugin-google-apps-script

解消したい課題

GASのランタイムは、テンプレートリテラル内の改行や、URL文字列などの解釈において特殊な挙動を示し、ビルド後のコードがクラッシュする原因になります
また、GASスクリプトレット(<?!= ... ?>)がダブルクォーテーションに囲われていると、内部のJSONデータが壊れてしまうため、こちらもクラッシュの原因となります

ライブラリが行うこと

このプラグインは、Vite内部で圧縮に利用できるTerserを強制利用させることで、テンプレートリテラル内の改行を、改行コード\nに置き換えることが可能です
また、デプロイ時にエラーの原因となる文字列や記号をビルド時に自動でエスケープ・クリーンアップします

実現されるDevEx

GAS特有のランタイム制限を意識せず、Viteなどの高速なビルドツールを安心して利用し、GAS環境でクラッシュしないフロントエンド開発に専念できます

[build] バックエンドに不要なexport句を削除: rolldown-plugin-remove-export

解消したい課題

GASのサーバーサイドはファイル間でグローバルスコープを共有する仕様ですが、exportをしないとvitestなどのテストに利用できないです
ただ、そのためにexport句をいれると、バンドルツールを通してもビルドファイルにも残ってしまうexport文が実行時の構文エラーを引き起こします。

ライブラリが行うこと

このプラグインは、rolldown(あるいはrollup)でのバンドル後のコードから、GASには不要なexport文を削除し、GASでそのまま動く形に整形します

実現されるDevEx

モジュールシステムで開発しながら、GASの特殊な実行環境に合わせたコードを自動生成できます

[push] GASでもCI/CDを実現: @ciderjs/clasp-auth

解消したい課題

GitHub Actions等での自動デプロイにおいて、clasp loginはブラウザでの対話的な認証を前提としているため、ヘッドレスなCI環境ではそのまま実行できません
そのため、claspによるGAS環境へのアップロードはローカルマシンで対話的にログイン・pushをする必要がありました

ライブラリが行うこと

@ciderjs/clasp-authは、ローカルの認証情報を暗号化してGitHub Secretsにアップロードし、CI実行時に復元する仕組みを提供します

# ローカルで実行してSecretsに登録
npx @ciderjs/clasp-auth upload owner/repo
.github/workflows/push.yaml
name: Push to GAS
 on:
   push:
     branches: [ "main" ]

 jobs:
   deploy:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-node@v4
         with:
           node-version: 20
 
       - name: Setup clasp auth
         uses: ciderjs/clasp-auth@v0.2.0
         with:
           json: ${{ secrets.CLASPRC_JSON }}

       - name: Install clasp
         run: npm install -g @google/clasp

       - name: Push to GAS
         run: clasp push

実現されるDevEx

煩雑な認証情報の管理から解放され、安全かつ簡単にGASの自動デプロイ環境を構築できます
これによりチーム開発時にも「mainブランチにマージしたら、自動でGASにデプロイ」なども実現可能です


これら一連のツールを組み合わせることで、環境構築から開発、テスト、そしてデプロイまで、
GAS開発の「当たり前」だった不便さを一つずつ解消していくことで、
モダンな開発体験を損なうことなく、GAS環境のための本来のロジック開発に集中できる環境が整ってきました

つまり、特別な知識が必要な「GAS開発者」ではなく、一人の「エンジニア」として、GitHub ActionsによるCI/CDやTypeScriptによる型安全な開発といった、Web開発における普通のベストプラクティスをそのままGASの世界に持ち込めます

これらのツールが、皆さんの開発を少しでも軽やかにするきっかけになれば幸いです

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?