⚡️

GAS HtmlServices + Vue3 + Vite + Clasp でのデプロイ後エラー対策

2024/08/26に公開

こんにちは、luthです

ノンプログラマーですが、社内ツール開発でワンオフ物としてWebAppをGASで公開していたりします
今回は、Vue@3.4.0にバージョンアップした際に数時間引っかかったエラーの対応についてです

経緯はこちら
https://zenn.dev/luth/scraps/90e0c8d8db82a2

後述のnpmパッケージはこちら
https://www.npmjs.com/package/vite-plugin-vue-googleappsscript

環境情報

  • GoogleAppsScript [HtmlServices]
  • Node.js
  • @google/clasp
  • vue@^3.4.0
    ※@3.3.x以下では発生なし
  • vite
  • vite-plugin-singlefile

*上記構成については下記記事を参照させていただきました、ありがとうございました…!
https://engineer.retty.me/entry/2022/12/22/150035

課題点

運用中のVueアプリのメンテナンスで、Vue@3.4.0へのバージョンアップを検証していました
ローカル開発ではブラウザで問題なく動いたため、claspでGASにアップロードし、デプロイテストをしたところ、何もマウントされない状態になってしまいました

コンソールを見ると、下記エラーで止まってマウントまで至らなかったようです

開発者ツール内コンソール
Uncaught SyntaxError: Invalid destructing assignment target

このエラー自体はデフォルト引数に関するエラーです
ただ、ローカルでのデフォルト引数の設定自体は問題ありませんでしたし、
何よりビルド後のローカルのhtmlファイルをブラウザで開いても、このエラーは出ずに正常処理されます

宇宙猫🐈になりかけながらも、デプロイ後のソースを追ったところ、ローカルとの差分は以下の通りでした

ローカルファイル内 vue@3.4.0以降
i = `https://vuejs.com/errors/#runtime-${n}`
デプロイ後のブラウザで開いた際
i = `https://            /* 謎の改行 */
${T}`, message: "alert", // 本来はかなり先のコード

GASはWebアプリをブラウザで描画する際、生のHTML+JSを利用するわけではなく、2重のiframeを使って埋め込むことで、アプリ利用者向けのセキュリティ担保を目指しています
おそらくは、そのデプロイ後のソースを読み込んでiframe内で展開する時に、ソースがめちゃ削られてる感じです

Vue@3.3.xの時のビルドソースでは以下のようになっていましたので、GASがソース内のURLを、セキュリティ上の理由だかで削る正規表現がめちゃ広めなものなんでしょう…

vue@3.3.x でのbuild版
i = n;

対策plugin

URLが置換されるのが問題なら事前にURLを潰そう、ということで、
Viteプラグイン用に下記のように記述してみました

plugins/vite-plugin-vueOnGoogleAppsScript.ts
import type { Plugin } from 'vite';
export interface ReplaceRule {
  from: string | RegExp;
  to: string;
}
export const PRESET_REPLACE_MASTER: Array<ReplaceRule> = [
  {
    from: /\=`https:\/\/vuejs\.org\/errors\/#runtime-\$\{(.+?)\}`/g,
    to: '=$1',
  },
  {
    from: /\=`https:\/\/vuejs\.org\/errors{0,1}-reference\/#runtime-\$\{(.+?)\}`/g,
    to: '=$1',
  },
  // for scriptlet of apps script
  {
    from: /"(\<\?\!{0,1}\={0,1}.+?\?\>)"/g,
    to: "'$1'",
  },
];

export const vueOnGas = (
  replaceMaster: Array<ReplaceRule> = PRESET_REPLACE_MASTER,
): Plugin => ({
  name: 'vue-on-gas',
  generateBundle(_outputOptions, outputBundle) {
    const chunkNames = Object.keys(outputBundle);
    chunkNames.forEach((chunkName) => {
      const chunk = outputBundle[chunkName];
      replaceMaster.forEach(({ from, to }) => {
        const isMatch =
          typeof from === 'string'
            ? chunk.code.indexOf(from) !== -1
            : from.test(chunk.code);
        if (isMatch) {
          console.info(
            `[Vue on GoogleAppsScript plugin] match with pattern: ${from.toString()}`,
          );
          chunk.code = chunk.code.replace(from, to);
        }
      });
      outputBundle[chunkName] = chunk;
    });
  },
});

viteの設定に組み込むのはこんな感じ
置換設定を変えたい場合は引数にマスタを入れてあげれば置換処理に使えます

vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { viteSingleFile } from 'vite-plugin-singlefile';
import { vueOnGas } from './plugins/vite-plugin-vueOnGoogleAppsScript';

export default defineConfig({
  plugins: [
    vue(),
    viteSingleFile(),
    vueOnGas(),
  ],
  build: {
    outDir: 'dist',
  },
});

というnpmパッケージ

そんな内容のnpmパッケージを公開しましたので、
よろしければご利用くださいませ。

https://github.com/luthpg/vite-plugin-vueOnGoogleAppsScript
https://www.npmjs.com/package/vite-plugin-vue-googleappsscript


単純な置換処理ですが、プラグインにできたことでビルド設定さえすれば以後は気にせずに開発~デプロイを実行できるようになりました
GAS+Vue+Viteの組み合わせはニッチな気がしますが、同構成をご利用の方、よろしければご利用ください…!

Discussion