Google App Scriptを自分のマシンで開発できる google clasp を使ってみる・後編

こちらの続きです。

https://qiita.com/yuskesuzki/items/4ffd0ea941a57638135f

- claspは便利。だが自由度が高すぎて環境整えるのが大変

前回、まとめにこんなことを書きましたが、このデメリットを改善するための Google aside を使ってみます。

# Google aside

Google aside (Apps Script in IDE)は Google 謹製の clasp 用テンプレートフレームワークです。実行後、いくつかの質問に回答することで、claspにちょうどいい開発環境を構築してくれます。

[https://github.com/google/aside](https://github.com/google/aside)

## asideの利用

aside はこのように使います。

```tsx
$ asdf local nodejs 22.2.0
$ npx @google/aside init
```

実行すると、いくつか質問が出てきます。
この回答結果で clasp の開発に便利なコードが生成されます。

```tsx
✔ Project Title: … calculate_sample
✔ Create an Angular UI? … No
✔ Generate package.json? … Yes
✔ Adding scripts...
✔ Saving package.json...
✔ Installing dependencies...
✔ Installing src template...
✔ Installing test template...
✔ Script ID (optional): … 
✔ Script ID for production environment (optional): … 
✔ Creating calculate_sample...

-> Google Sheets Link: https://drive.google.com/open?id=1rROKUn8XinG6puFg4wRT7CZ0FMEgOuoSo4U-CMtGX7Y
-> Apps Script Link: https://script.google.com/d/1iV_olJEdt5vMJ3UWdG_y-COeP5dFf6lPQjvYgN627Pfcd-vxtgsrR87G/edit
```

ただ、aside経由だとスタンドアローンスクリプトは作れず、必ず関連するスプレッドシートも作られます。

asideの実行により生成されたファイル・フォルダは以下の通りです。

```tsx
yusuke@suzuki-m2-Mac-mini calculate_sample_aside % ls -al
total 832
drwxr-xr-x   25 yusuke  staff     800  7  1 21:50 .
drwxr-xr-x   12 yusuke  staff     384  7  1 21:47 ..
-rw-------    1 yusuke  staff     152  7  1 21:50 .clasp-dev.json
-rw-------    1 yusuke  staff     152  7  1 21:50 .clasp-prod.json
-rw-r--r--    1 yusuke  staff       1  7  1 21:50 .claspignore
-rw-r--r--    1 yusuke  staff     114  7  1 21:50 .editorconfig
-rw-r--r--    1 yusuke  staff      44  7  1 21:50 .eslintignore
-rw-r--r--    1 yusuke  staff     398  7  1 21:50 .eslintrc.json
-rw-r--r--    1 yusuke  staff      64  7  1 21:50 .gitignore
-rw-r--r--    1 yusuke  staff      19  7  1 21:50 .prettierignore
-rw-r--r--    1 yusuke  staff     125  7  1 21:50 .prettierrc.json
-rw-r--r--    1 yusuke  staff      14  7  1 21:47 .tool-versions
-rw-r--r--    1 yusuke  staff   11358  7  1 21:50 LICENSE
-rw-r--r--    1 yusuke  staff     124  7  1 21:50 appsscript.json
drwxr-xr-x    2 yusuke  staff      64  7  1 21:50 dist
-rw-r--r--    1 yusuke  staff     246  7  1 21:50 jest.config.json
-rw-r--r--    1 yusuke  staff     570  7  1 21:50 license-config.json
-rw-r--r--    1 yusuke  staff     553  7  1 21:50 license-header.txt
drwxr-xr-x  483 yusuke  staff   15456  7  1 21:50 node_modules
-rw-r--r--    1 yusuke  staff  342627  7  1 21:50 package-lock.json
-rw-r--r--    1 yusuke  staff    1528  7  1 21:50 package.json
-rw-r--r--    1 yusuke  staff    1229  7  1 21:50 rollup.config.mjs
drwxr-xr-x    4 yusuke  staff     128  7  1 21:50 src
drwxr-xr-x    3 yusuke  staff      96  7  1 21:50 test
-rw-r--r--    1 yusuke  staff     455  7  1 21:50 tsconfig.json
```

src以下にコードのサンプルが置いてあります。

- src/index.ts

```tsx
/**
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { hello } from './example-module';

console.log(hello());
```

- src/example-modules.ts

```tsx
/**
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
export function hello() {
  return 'Hello Apps Script!';
}
```

claspコマンドは使わず、npmコマンドを介して build を行います。npm run に指定できるコマンド、どんな処理を指定してるかは package.json の内容を確認してください。

```tsx
% npm run build

> calculate-sample@0.0.0 build
> npm run clean && npm run bundle && ncp appsscript.json dist/appsscript.json

> calculate-sample@0.0.0 clean
> rimraf build dist

> calculate-sample@0.0.0 bundle
> rollup --no-treeshake -c rollup.config.mjs

src/index.ts → dist...
created dist in 612ms
```

ビルドすることでトランスコンパイルされ生成された dist/index.js が生成されます。このファイルをGASのランタイム環境に転送します。

- dist/index.js

```tsx
/**
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

function hello() {
  return "Hello Apps Script!";
}

console.log(hello());
```

dist以下のこれをGASに転送するため、deployを行います。

lint, testを実行し、dist/index.js, dist/appsscript.json を転送。

```tsx
% npm run deploy

> calculate-sample@0.0.0 deploy
> npm run lint && npm run test && npm run build && ncp .clasp-dev.json .clasp.json && clasp push -f

> calculate-sample@0.0.0 lint
> npm run license && eslint --fix --no-error-on-unmatched-pattern src/ test/

> calculate-sample@0.0.0 license
> license-check-and-add add -f license-config.json

No default format specified. Using {"prepend":"/*","append":"*/"} as backup
! Inserted license into 1 file(s)
Command succeeded
=============

WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.

You may find that it works just fine, or you may not.

SUPPORTED TYPESCRIPT VERSIONS: >=3.3.1 <5.2.0

YOUR TYPESCRIPT VERSION: 5.5.2

Please only submit bug reports when using the officially supported version.

=============
(node:44029) [DEP0180] DeprecationWarning: fs.Stats constructor is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)

> calculate-sample@0.0.0 test
> jest test/ --passWithNoTests --detectOpenHandles

 PASS  test/example-module.test.ts
  example-module
    hello
      ✓ Returns a hello message (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.815 s
Ran all test suites matching /test\//i.

> calculate-sample@0.0.0 build
> npm run clean && npm run bundle && ncp appsscript.json dist/appsscript.json

> calculate-sample@0.0.0 clean
> rimraf build dist

> calculate-sample@0.0.0 bundle
> rollup --no-treeshake -c rollup.config.mjs

src/index.ts → dist...
created dist in 491ms
(node:44296) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
└─ dist/appsscript.json
└─ dist/index.js
Pushed 2 files.
```

## 前回claspで作ったTypeScriptのコードをasideに移植

ということで、前回 clasp で作った計算プログラムを aside のプロジェクトに移植してみます。

- src/Logger.ts

```tsx
// Logger.ts

export function logMessage(message: string): void {
  Logger.log(message);
}
```

- src/Utilitiles.ts

```tsx
// Utilities.ts

export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}
```

- src/index.ts

```tsx
/* eslint-disable @typescript-eslint/no-unused-vars */
import { logMessage } from './Logger';
import { add, subtract } from './Utilities';

function main() {
  const resultAdd = add(5, 3);
  const resultSubtract = subtract(5, 3);
  //const resultSubtract2 = add("aaa", "bbb");

  logMessage(`Addition Result: ${resultAdd}`);
  logMessage(`Subtraction Result: ${resultSubtract}`);
  //logMessage(`${resultSubtract2}`);
}
```

## ついでにテストも書いちゃう

test/utilities.test.ts ファイルを作って、src/Utilities.ts のユニットテストを書いてみる。

テストツールは Jest が使われます。

ユニットテスト。GASでスプレッドシートを直に操作する等のUI処理は、テストが書きづらいので書きません。今回は計算や配列操作、データ処理を行うロジックのコードについて、ユニットテストが書いています。

- test/utilities.test.ts
```tsx
import { add, subtract } from '../src/Utilities';

describe('utilities-test', () => {
  // Path: test/utilities.test.ts
  // add function test
  describe('add', () => {
    it('add 2 number 1 (integer number)', () => {
      expect(add(2, 3)).toBe(5);
      expect(add(-2, 3)).toBe(1);
      expect(add(2, -3)).toBe(-1);
      expect(add(-2, -3)).toBe(-5);
    });

    it('add 2 number 2 (decimal number)', () => {
      expect(add(2.4, 3.5)).toBe(5.9);
      expect(add(-2.4, 3.5)).toBe(1.1);
      expect(add(2.4, -3.5)).toBe(-1.1);
      expect(add(-2.4, -3.5)).toBe(-5.9);
    });

    it('add 2 number 3 (NaN)', () => {
      expect(add(NaN, 3.5)).toBe(NaN);
      expect(add(1, NaN)).toBe(NaN);
      expect(add(NaN, NaN)).toBe(NaN);
    });
  });

  // Path: test/utilities.test.ts
  // subtract function test
  describe('subtract', () => {
    it('subtract 2 number 1 (integer number)', () => {
      expect(subtract(2, 3)).toBe(-1);
      expect(subtract(-2, 3)).toBe(-5);
      expect(subtract(2, -3)).toBe(5);
      expect(subtract(-2, -3)).toBe(1);
    });

    it('subtract 2 number 2 (decimal number)', () => {
      expect(subtract(2.4, 3.5)).toBe(-1.1);
      expect(subtract(-2.4, 3.5)).toBe(-5.9);
      expect(subtract(2.4, -3.5)).toBe(5.9);
      expect(subtract(-2.4, -3.5)).toBe(1.1);
    });

    it('subtract 2 number 3 (NaN)', () => {
      expect(subtract(NaN, 3.5)).toBe(NaN);
      expect(subtract(1, NaN)).toBe(NaN);
      expect(subtract(NaN, NaN)).toBe(NaN);
    });
  });

});

```

テストの実行。こんな感じで通りました。

```tsx
% npm run test

> calculate-sample@0.0.0 test
> jest test/ --passWithNoTests --detectOpenHandles

 PASS  test/utilities.test.ts
 PASS  test/example-module.test.ts

Test Suites: 2 passed, 2 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        0.546 s, estimated 1 s
Ran all test suites matching /test\//i.
```

## ビルド

テストも通ったので、`npm run build` でコードをビルド。

こんなコードが生成されます。

- dist/index.js
```tsx
/**
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

function logMessage(message) {
  Logger.log(message);
}

function add(a, b) {
  return a + b;
}
function subtract(a, b) {
  return a - b;
}

function main() {
  const resultAdd = add(5, 3);
  const resultSubtract = subtract(5, 3);
  logMessage(`Addition Result: ${resultAdd}`);
  logMessage(`Subtraction Result: ${resultSubtract}`);
}
```

## あえてバグを作ってみると…

ちなみに、文字列の足し算を行えるようにしてclasp版でGASにpushされてしまった件。

```tsx
function main() {
  const resultAdd = add(5, 3);
  const resultSubtract = subtract(5, 3);
  const resultSubtract2 = add("aaa", "bbb");

  logMessage(`Addition Result: ${resultAdd}`);
  logMessage(`Subtraction Result: ${resultSubtract}`);
  logMessage(`${resultSubtract2}`);
}
```

これを、aside版で行ってみると、

```tsx
% npm run build

> calculate-sample@0.0.0 build
> npm run clean && npm run bundle && ncp appsscript.json dist/appsscript.json

> calculate-sample@0.0.0 clean
> rimraf build dist

> calculate-sample@0.0.0 bundle
> rollup --no-treeshake -c rollup.config.mjs

src/index.ts → dist...
[!] (plugin rpt2) RollupError: [plugin rpt2] src/index.ts:6:31 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

6   const resultSubtract2 = add("aaa", "bbb");
                                ~~~~~

src/index.ts

    at getRollupError (/Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup/dist/shared/parseAst.js:282:41)
    at Object.error (/Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup/dist/shared/parseAst.js:278:42)
    at Object.error (/Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup/dist/shared/rollup.js:804:32)
    at RollupContext.error (/Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup-plugin-typescript2/src/context.ts:35:17)
    at /Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup-plugin-typescript2/src/diagnostics.ts:70:17
    at Array.forEach (<anonymous>)
    at printDiagnostics (/Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup-plugin-typescript2/src/diagnostics.ts:42:14)
    at typecheckFile (/Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup-plugin-typescript2/src/index.ts:67:3)
    at Object.<anonymous> (/Users/yusuke/projects/MIERUNE/gas/calculate_sample_aside/node_modules/rollup-plugin-typescript2/src/index.ts:269:5)
    at Generator.next (<anonymous>)
```

このように、npm のビルド時点で怒られ、デプロイができなくなります。ということで、claspのように、コードが間違っていても push されることはありません。

# まとめ

- 素のGASを書くのは色々ツライが、claspを使うとメリットが色々享受できる
- claspは便利だが自由度が高すぎ
- Google謹製のasideはclaspを制御するのに都合がよい

以上