Skip to content

Instantly share code, notes, and snippets.

@x7ddf74479jn5
Last active July 29, 2022 08:10
Show Gist options
  • Save x7ddf74479jn5/09c57e6d469aea0057442cdaf7d805a7 to your computer and use it in GitHub Desktop.
Save x7ddf74479jn5/09c57e6d469aea0057442cdaf7d805a7 to your computer and use it in GitHub Desktop.
GASローカル開発メモ

GASローカル開発メモ

リンク

GAS Reference

google/clasp

方針

GASをローカルで開発するため@google/claspを利用する。 TypeScriptで書き、esbuildでバンドルする。 環境変数を扱うためdotenvを入れる。 prettiereslintを入れる。

考察

メリット

  • git管理ができる
  • TypeScriptで開発できる
  • npmのパッケージが利用できる(制限あり)
  • モジュール分割できる

デメリット

  • ドキュメント・事例が少ない
  • claspのメンテナンスが滞っている(2022/07/14現在で最終更新2021/08)
  • ローカルでデバッグができない

環境構築

pnpm add -g @google/clasp
clasp login
mkdir clasp-example
cd clasp-example
pnpm init --yes
pnpm add -D @types/google-apps-script esbuild esbuild-gas-plugin typescript
pnpm add -D dotenv prettier eslint eslint-config-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser
clasp create
./
├ .clasp.json
└ appsscript.json

設定ファイル

esbuildの出力先はbuildでファイル名がmain.jsappsscript.jsonがプロジェクトルートにあるとclasp pushがなぜかコケた。build内に入れると動く。

.gitignore

node_modules/
.clasp.json
.env
build/main.js

.claspingnore

**/**
!appsscript.json
!main.js

.clasp.json

{
  "scriptId": "<AppScriptのID>",
  "rootDir": "./build"
}

<AppScriptのID>はFormやSpreadSheetのプロジェクトの設定からコピペ。

apppsscript.json

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.currentonly",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/gmail.compose",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/script.scriptapp",
    "https://www.googleapis.com/auth/forms.currentonly",
    "https://www.googleapis.com/auth/forms"
  ]
}

oauthScopesはGAS Referenceから必要なスコープを探す。

環境変数

定義しておくとコード中で補完が効く。

env.d.ts

export {};

declare global {
  const process: {
    env: {
      [key: string]: string;
      // Define type for custom environmental variables here:
      // NODE_ENV: "development" | "production";
      HOGE: string;
    };
  };
}

ビルドスクリプト

.envに書いた環境変数はprocess.env.hogeでコード中から参照する。esbuildで置換するため必ずこの形式にする。 const { hoge } = process.envではエラーが出る。

build.js

/* eslint-disable @typescript-eslint/no-var-requires */
const { build: runBuild } = require("esbuild");
const dotenv = require("dotenv");
const { parsed: loadedEnvs } = dotenv.config();
const pkg = require("./package.json");
const { GasPlugin } = require("esbuild-gas-plugin");

/** @type {string[]} */
const options = process.argv.slice(2);

const debug = options.includes("--debug");
const noMinify = options.includes("--no-minify");
const watch = options.includes("--watch");

console.log("Running esbuild with following options:");
console.table({
  debug,
  noMinify,
  watch,
});

/** @type {import('esbuild').BuildOptions} */
const buildOptions = {
  entryPoints: ["./src/main.ts"],
  bundle: true,
  target: "esnext", // Lowers target to support ESNext syntax

  banner: {
    js: [
      "/*! DO NOT EDIT DIRECTLY. */",
      `/*! This code is generated from ${pkg.repository.url}/tree/main by clasp. */`,
    ].join("\n"),
  },

  define: {
    DEBUG: debug,

    // Replace `process.env.FOO` with variables written in `.env` file
    ...Object.fromEntries(
      Object.entries(loadedEnvs ?? {}).map(([key, value]) => [
        `process.env.${key}`,
        JSON.stringify(value),
      ])
    ),
  },
  outfile: "./build/main.js",
  watch: watch,
  minify: !noMinify,
  minifyIdentifiers: false,
  logLevel: "info",
  plugins: [GasPlugin],
};

runBuild(buildOptions).catch((error) => {
  console.error(error);
  process.exit(1);
});

参考

関数の公開

GAS上で扱う関数はglobalに公開しなければならない。

main.js

const greeting = () => {
  Logger.log("Hello World");
};

// @ts-expect-error
(global as any).greeting = greeting;

esbuild-gas-pluginを使っている場合、main.gsの冒頭に関数定義だけ出力される。使わない場合、自前でビルドスクリプトに書かなければいけないっぽい。

トリガー

トリガーもコードで記述できる。

const setOnFormSubmitTrigger = (fn: string) => {
  const sheet = SpreadsheetApp.getActive();
  ScriptApp.newTrigger(fn).forSpreadsheet(sheet).onFormSubmit().create();
};

トリガーをセットする関数を作り、GASダッシュボードから実行するとコードファーストでトリガーを設定できる。

const main = () => {
  resetTriggers();
  setTriggers();
};

注意事項

  • スプレッドシートのonFormSubmitとフォームのonFormSubmitはAPIが違う。
  • イベント型のトリガー(doGet, onFormSubmit...)の引数のeの型がない。
  • コンテナバインドスクリプトをまたがったAppscriptを作ることはできない?(スプレッドシートシートのスクリプトからはフォームのスクリプトのAPIに触れない)
  • getActive()などの関数は紐付いたフォームやスプレッドシートのものを取得する。(つまり現在書いているコンテナバインドスクリプトのものオンリー)
  • clasp runでローカルからプッシュしたスクリプトを実行できるらしいが、.clasprc.jsonが見つからないと怒られる。(ユーザーディレクトリ直下にclaspの設定ファイルがある。.clasprc.jsonもそこにあるが、プロジェクトルートまでしか探そうとしない)
  • npmの資産が使えるが、URLを扱うライブラリはGASと互換性がないため使えないっぽい(要検証)。
  • GASのエンドポイントURLはデプロイするたび変わるため、Slack Botを作る場合など公開URLをデプロイごとに更新することが必要。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment