👋

ViteでらくらくUserScript開発

2024/04/07に公開

時々使っているサイトの機能になにか足りないものや不便であったり、見づらかったりする時はないでしょうか。その場合は、ユーザースクリプト(UserScript)を利用することで、容易に機能を拡張したり、UIをカスタマイズしたりできます。

ユーザースクリプトとは?

ユーザースクリプトとは、ウェブサイトやアプリに利用者(ユーザー)が自分で追加・注入するカスタムのプログラムです。例えば、よく使われるユーザースクリプトの例として、SNSに検索フィルターを追加したり、動画サイトにループ再生機能を追加したり、様々な機能制限を解消したり、有害なコンテンツを除去したりすることができます。

少しでもJavaScriptができれば、自作できますし、GreasyforkOpenUserJS
などでは、他の人が作ったスクリプトが沢山あります。ただし、注意すべきは安易に人が提供するスクリプトをインストールすることは極めて危険です。なぜなら、スクリプトによって、あなたがみているサイトの全部を変更したり、秘密を自由にアクセスしたり、場合によっては記録できたりしますので、コードをしっかり確認し、信頼できるソースからだけインストールするようにしてください。

いづれにしても、ある程度JavaScriptやブラウザに対する理解力がなければ、ユーザースクリプトを使わない方が無難でしょう。あるいは、後述のように自分で作ることもできますが、それでもやはり充分な理解がなければ、不正アクセスと判定されてしまうケースがあるので、ご注意ください。

また、より複雑な機能であれば、ブラウザの拡張機能を使うこともできます(これもViteで開発できます)。しかし、UserScriptのAPIも充分暴露されてくれているので、ブラウザのバーにものを追加したり、ローカルにアクセスしたりしない限りの簡単なものであれば、UserScriptで充分です。

ユーザースクリプトを使うには?

ユーザースクリプトを使うには、多くの場合ブラウザの拡張機能やプラグインをインストールしておく必要があります。一部のブラウザでは既にユーザースクリプトのサポートが内蔵されている場合もあります。例えばChromeであればTampermonkeyがあります。ユーザースクリプトを実行するための拡張機能は、ユーザースクリプト・マネジャーと呼ばれ、最初はFirefoxのGreasemonkey「油猿」に因んで、ユーザースクリプト系のアプリはグリーズやモンキーなどのように肖ることが多いです。

ユーザースクリプトを作るには?

簡単なユーザースクリプトであれば、Tampermonkeyなどの管理画面で直打ちすればいいです。テストしたり検証したりする時は、ブラウザに備えられているデベロッパー・ツールなどを用いても結構です。しかし、VSCodeやNeovimなどのエディターによる自動補完やハイライト機能、npmパッケージやTypeScript、ESM Module、ユニットテスト、HMR、UIフレームワークなどを利用した現代的な開発体験を行ったりする際には、やはりそれだけでは不十分です。

もちろんTampermonkeyなどにはファイルシステムからファイルを読み取ったり、あるいはローカルサーバーを利用して開発することもできますが、やはりリフレッシュが必要だったり、バンドリングがめんどくさかったりするので、本記事では現代的なフロントエンド開発するツールbun+Vite+vite-plugin-monkeyによる開発の流れをご紹介します。

また、Githubにてサンプルコードを公開しておりますので、ご参照ください。
https://github.com/mkpoli/monkey-example

インストール

Tampermonkeyのインストール

ChromeやEdgeなどChromiumベースのブラウザを利用する場合、以下からTampermonkeyをインストールします。
https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ja

bunのインストール

BunはJavaScriptのランタイムであり、パッケージマネジャーであり、テストであり、非常に動作が軽く早く、Unix系のOSだけでなく最近はWindowsでもサポートされるようになっているので、以下の公式サイトからインストールしてみてください。Bunを使わない場合は、Node.js+npmなど伝統的な方法でもいけますが、Bunの方が楽です。

Linux/macOS

curl -fsSL https://bun.sh/install | bash

Windows

powershell -c "irm bun.sh/install.ps1 | iex"

プロジェクト設定

以下のコマンドを実行すると、プロジェクト名が聞かれます。

bun create monkey

今回は適当にmonkey-exampleとします。

? Project name: » monkey-example

フレームワーク選択で、色々できますが、今回は簡単なものを試すだけなので、特に上下キーを押さず、エンターを押しEmptyを選択します。

TypeScriptを押します。

VSCodeなどのエディターでフォルダーを開きます。

cd monkey-example
code .

するとこのようなフォルダー構造になります。

エントリー・ポイントsrc/main.tsはこうなっているはずです。

指示された通り、インストール・起動します。

bun install
bun run dev

すると、自動的にTampermonkeyの画面が開かれます。

内容を確認してみると自動的にコンパイルされたユーザースクリプトがマウントされるコードを確認できます。

Installをクリックします。すると、Googleにアクセスして、デベロッパーコンソールを確認したらhello worldの文字が出るはずです。

筆者の環境ではなぜかこのようにAccess-Control-Allow-Private-NetworkのCORSポリシー違反になっていますが、ここのIssueの回答でのやり方を行うと解決されます。

色々弄ってみる

まず、package.jsonvite.config.tsで然るべき設定を行います。

vite.config.ts
import { defineConfig } from 'vite';
import monkey from 'vite-plugin-monkey';
export default defineConfig({
  plugins: [
    monkey({
      entry: 'src/main.ts',
      userscript: {
        icon: 'https://vitejs.dev/logo.svg',
        namespace: 'npm/vite-plugin-monkey',
        match: ['https://www.google.com/'],
        license: 'MIT',
        description: {
            "en": "Take over the world!",
            "ja": "世界覇権を果たす",
        }
      },
    }),
  ],
});

スタイルファイルの読み込み

例えば、CSSスタイルファイルを導入することで、フォントを変えたり、「I'm feeling lucky」を消したりできます。

main.ts
// @ts-ignore isolatedModules

// スタイルを導入
import './style.css';

// コンソールにプリントする
console.log('You are my sunshine')
src/style.css
@import url('https://fonts.googleapis.com/css2?family=Rampart+One&display=swap');

* {
  font-family: 'Rampart One', sans-serif !important;
}

input[name='btnI'] {
  display: none;
}

テキストを弄る

テキストを弄って検索ボタンに猿を表示したりできます。

main.ts
// 検索ボタンの前後の猿を表示する
const searchButtons = document.querySelectorAll<HTMLButtonElement>('form[role="search"] input[name="btnK"]');
for (const button of Array.from(searchButtons)) {
  button.value = `🐵 ${button.value} 🙈`;
}

リアルタイム時間表示

新しくsrc/time.tsを作って、モジュールをインポートして、Googleの直下に時間を表示します。

src/time.ts
export function updateTime(element: HTMLTimeElement): void {
  const now: Date = new Date();
  const timeString: string = now.toLocaleTimeString();
  element.textContent = timeString;
  element.setAttribute('datetime', now.toISOString());
}

export function createTimeElement(): HTMLTimeElement {
  const timeElement: HTMLTimeElement = document.createElement('time');
  timeElement.id = 'current-time';
  updateTime(timeElement);
  return timeElement;
}
src/main.ts
// Googleロゴの下に時間を表示する
import { createTimeElement, updateTime } from './time';
const googleLogo = document.querySelector<HTMLImageElement>('img[alt="Google"]');
if (googleLogo) {
  const time = createTimeElement();
  time.style.display = 'block';
  time.style.textAlign = 'center';
  time.style.marginTop = '-0.85rem';
  setInterval(() => updateTime(time), 1000);
  googleLogo.insertAdjacentElement('afterend', time);
}

謎のボールを表示

src/main.ts
(() => {
  const app = document.createElement('div');
  document.body.append(app);
  return app;
})().innerHTML = `
<div class="scene">
  <div class="ball"></div>
</div>
<style>
  .scene {
    position: fixed;
    top: 0;
    bottom: 0;
    right: 0;
    width: 200px;
    z-index: 9999;
    pointer-events: none;
  }

  .ball {
    width: 50px;
    height: 50px;
    background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
    box-shadow: 0 0 10px 0 #f6d365, 0 0 20px 0 #fda085;
    border-radius: 50%;
    position: absolute;
    left: 50%;
    top: 0;
    transform: translateX(-50%);
    bottom: 0;
    animation: bounce 2s infinite cubic-bezier(0.280, 0.840, 0.420, 1);
  }

  @keyframes bounce {
    0%, 100% {
        transform: translateY(10vh);
    }
    50% {
        transform: translateY(70vh);
    }
  }
</style>
`;

桜吹雪を表示

src/main.ts

(() => {
  const sakura = document.createElement('div');
  sakura.classList.add('sakura');
  document.body.append(sakura);

  const style = document.createElement('style');
  style.innerHTML = `
    .sakura {
      position: fixed;
      top: 0;
      bottom: 0;
      right: 0;
      left: 0;
      z-index: 99999;
      pointer-events: none;
    }

    @keyframes fall {
      0% {
        transform: translateY(0vh);
      }
      100% {
        transform: translateY(100vh);
      }
    }

    @keyframes rotate {
      0% {
        transform: rotate(-100deg) rotateX(0deg);
      }
      100% {
        transform: rotate(100deg) rotateX(360deg);
      }
    }
  `;
  document.head.append(style);

  const petal = `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="17.5" viewBox="0 0 60 105" fill="none" class="petal"><path d="M34.7787 15.6339L31.8005 1.73564C31.4623 0.157496 29.5133 -0.394533 28.4583 0.826835C11.0216 21.0118 -13.2755 65.2958 10.9968 103.467C11.5493 104.336 12.698 104.626 13.589 104.11C49.4912 83.3179 58.7369 30.2871 59.0283 2.30435C59.0461 0.599669 57.0465 -0.254352 55.7754 0.881589L38.067 16.7061C36.9225 17.7288 35.1003 17.1346 34.7787 15.6339Z" fill="#FEDCFF"/></svg>`;

  for (let i = 0; i < 25; i++) {
    const petalParent = document.createElement('div');
    petalParent.classList.add('petal');
    petalParent.style.animation = `rotate ${Math.random() * 5 + 10}s linear infinite alternate`;
    petalParent.style.animationDelay = `${Math.random() * 5}s`;

    const petalElement = document.createElement('div');
    petalElement.innerHTML = petal;
    petalElement.style.position = 'fixed';
    petalElement.style.top = '-10vh';
    petalElement.style.left = `${Math.random() * 100}vw`;
    petalElement.style.opacity = `${Math.random() * 0.5 + 0.5}`;
    petalElement.style.animation = `fall ${Math.random() * 5 + 10}s linear infinite`;
    petalElement.style.animationDelay = `${Math.random() * 5}s`;

    petalParent.append(petalElement);
    sakura.append(petalParent);
  }
})();

ビルド

以下のコマンドを実行すると、dist/フォルダーにビルドされたファイルが生成されます。これをシェアしたり、Greasyforkに上げたりすることができます。

bun run build

自動リリース

リリースは手動でも良いのですが、以下の手順を行わなければならないため、とても面倒くさいです。

  1. package.jsonのバージョンをSemVer準拠で上げます。
  2. あげたものを適切なコミットメッセージでコミットします。
  3. コミットメッセージに対しバージョンの名前でタグします。
  4. コミットとタグの両方をpushします。
  5. GitHubなどでReleaseを作ります。

毎回このような手順を手動でやると、たとえマニュアルを用意したとしても、ヒューマンエラーは避けられません。なので、npmのバージョンあげ用nツールやGitHub ActionsなどのCI/CD機能を利用し、自動的なバージョンリリースを目指します。

バージョンアップ

バージョンアップには今回bumpp-versionというものを使います。似たような機能のパッケージはたくさんありますが、Conventional Commitsを強制したり、npmレポジトリに公開することを目指しているものが多く、UserScriptにはあまり適していません。また、最も有望視しているのはかの有名なAnthony Fu氏がforkしたbumppbumpp-versionとは無関係です)ですが、まだバグがいくつか残っているので、とりあえず設定が自由ではないがちゃんと動くbumpp-versionにします。

bun i -D bumpp-version

bumpp-versionをインストールした後、"scripts"欄で"version"スクリプトを"bumpp-version"と指定します。

package.json
                "preview": "vite preview",
                "check": "svelte-check --tsconfig ./tsconfig.json",      
                "format": "biome format --write .",
-               "lint": "biome lint --write ."
+               "lint": "biome lint --write .",
+               "version": "bumpp-version"
        },
        "devDependencies": {
                "@biomejs/biome": "1.9.3",
@@ -17,6 +18,7 @@
                "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
                "@tsconfig/svelte": "^5.0.4",
                "@types/codemirror": "^5.60.15",
+               "bumpp-version": "^1.0.2",
                "svelte": "^5.0.0-next.264",
                "svelte-check": "^4.0.5",
                "svelte-preprocess": "^6.0.3",

GitHubリリース

GitHubのActionsを使って、ビルド環境の設定、自動コンパイル、ビルドされたアーティファクト(.user.jsファイル一個のみをリリース)とともにReleaseを作ります。以下のファイルを追加します。注意:レポジトリのSettingsでRead onlyをRead and Writeにしないと権限エラーができるのでお忘れなく。

.github/workflows/release.yml
name: Create Draft Release

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Install dependencies
        run: bun install

      - name: Run build
        run: bun run build

      - name: Release
        uses: softprops/action-gh-release@v2
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: 'dist/monkey-example.user.js'

共有サイトで公開

GitHubのリリースがあれば比較的に手間が少なく公開することができますが、自動化が一番望ましいです。

Greasy Fork

WebHookでできるそうです。
https://greasyfork.org/en/users/webhook-info

OpenUserJS

(要調査)

まとめ

いかがだったでしょうか?ぜひ試しにUserScript作ってみてください!

Discussion