Ruby で Chrome Extension が作れる unloosen を (多分) 最速で試してみた

## これはなに

[Develop chrome extension with ruby.wasm - RubyKaigi 2023](https://rubykaigi.org/2023/presentations/3yUma%E2%82%8Brb.html) で発表された、 [unloosen](https://github.com/aaaa777/unloosen) (Ruby + wasm で Chrome extension を作るフレームワーク) を使ってみたので、設定方法と使用感を紹介する記事です。 (多分これが一番早いと思います)

作ったサンプル実装は↓にあります。

https://github.com/tomoasleep/unloosen-example

## unloosen とはなにか

[unloosen](https://github.com/aaaa777/unloosen) は [ruby.wasm](https://github.com/ruby/ruby.wasm) を利用して Chrome extension を作るフレームワークです。単純な Chrome extension のラッパーになるだけでなく、書きやすくするためのヘルパーメソッド等を提供していて、以下の通りに、1つの Ruby ファイルにまとめることが出来たりします。

```ruby
require "unloosen"

content_script site: "www.example.com" do
  h1 = document.querySelector("h1")
  h1.innerText = "Hello unloosen!"

  chrome.runtime.onMessage.addListener do |message|
    h1 = document.querySelector("h1")
    h1.innerText = message
    true
  end
end

popup do
  submit_button = document.querySelector("button#submit")
  submit_button.addEventListener "click" do |e|
    form_text = document.querySelector("input#title").value

    # This is workaround: in popup, JS.eval cannot be used.
    query_object = Utils.build_js_object(active: true, currentWindow: true)
    chrome.tabs.query(query_object) do |tabs|
      tab = tabs.at(0)

      chrome.tabs.sendMessage(
        tab[:id],
        form_text,
      )
    end

    e.preventDefault
  end
end
```

### unloosen はどのように Ruby で Chrome Extension を書けるようにしているか

unloosen は以下の図の流れで、 Ruby コードを上手く Chrome Extension として使えるようにしています。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/993adc71-8d7a-7650-d6fe-05c29b604cf6.png)
>
> Ref: https://github.com/aaaa777/unloosen

大まかに起動の流れを説明すると、以下の通りになります。


1. manifest.json (Chrome 拡張の設定ファイル) で unloosen の提供する起動用コードを指定
1. unloosen の提供する起動用コードが、 ruby.wasm, unloosen gem (Chrome 拡張用の DSL などを提供) を読み込み
1. unloosen の提供する起動用コードが、拡張の作者が書いた Ruby コード (`app.rb`) を読み込み
1. ruby.wasm が app.rb 内の処理を実行

ユーザーが書く必要があるのは、 manifest.json と app.rb (あと popup するときは表示用の HTML ファイル) だけです。 こういう感じで、js を1行も書かずに Chrome Extension を作ることが出来ます。

## unloosen を作った Chrome Extension の作り方

今回サンプルとして、超簡単な Chrome Extension を作ってみました。

https://github.com/tomoasleep/unloosen-example

popup を表示してページを書き換えるだけの超簡単な Chrome Extension です。

![change-title.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/978f5b29-8be7-865f-fb55-5e77983fae86.png)

とりあえずこの extension をいじることで Chrome Extension を動かすことは出来ます。
詳しく知りたい人向けに、これを元に説明します。

### 1. unloosen の起動用ファイルをダウンロードして配置する

unloosen を使う Chrome 拡張には、以下の起動用ファイルが必要になります。

* entrypoint となるコード群
  * `loader-content-script.esm.js`
  * `module-content-script.esm.js`
  * `module-popup.esm.js`
  * `module-sandbox.esm.js`
  * `module-background.esm.js`
* unloosen gem を含んだ ruby.wasm

これらは [unloosen-ruby-loader
](https://www.npmjs.com/package/unloosen-ruby-loader) という npm package にまとめられているので、これをダウンロードするのが良いでしょう。

https://www.npmjs.com/package/unloosen-ruby-loader

### 2. 各種設定ファイルを書く

1. でダウンロードしたファイルを参照するように、 `manifest.json` (Chrome 拡張の設定ファイル) , `unloosen.config.json` (unloosen 用の設定ファイル) を書きます。

```manifest.json
{
  "manifest_version": 3,
  "name": "unloosen-example",
  "description": "A demo extension of unloosen (https://github.com/aaaa777/unloosen)",
  "version": "0.0.1",
  "content_scripts": [
    {
      "js": [
        "node_modules/unloosen-ruby-loader/dist/entry/loader-content-script.esm.js"
      ],
      "matches": ["http://www.example.com/"]
    }
  ],
  "action": {
    "default_popup":"popup.html"
  },
  "permissions": [
    "activeTab"
  ],
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
  },
  "web_accessible_resources": [
    {
      "resources": [
        "*",
        "node_modules/unloosen-ruby-loader/dist/**"
      ],
      "matches": ["<all_urls>"]
    }
  ]
}
```

```unloosen.config.json
{
  "application": "app.rb",
  "ruby.wasm": "node_modules/unloosen-ruby-loader/dist/ruby.wasm",
  "content-script-entry": "node_modules/unloosen-ruby-loader/dist/entry/module-content-script.esm.js",
  "sandbox-entry": "node_modules/unloosen-ruby-loader/dist/entry/module-sandbox.esm.js",
  "popup-entry": "node_modules/unloosen-ruby-loader/dist/entry/module-popup.esm.js"
}
```

popup 機能を利用したい場合は、以下の通りに

```popup.html
<html>
<head>
  <script src="/node_modules/unloosen-ruby-loader/dist/entry/module-popup.esm.js" type="module"></script>
  <style>
    body{
      width: 300px;
      height: 300px;
    }
  </style>
</head>
<body>
  <p id="tabnum"></p>
  <form id="form">
    <fieldset>
      <legend>new h1 content</legend>
      <label><input type="text" name="title" id="title"></label>
      <button id="submit">Replace h1 content</button>
    </fieldset>
  </form>
</body>
</html>

```

### 3. app.rb を書く

1, 2 で必要な設定は済んだので、実装に必要なコードを `app.rb` に書いていきます。

```app.rb
require 'unloosen' # 各種ヘルパーメソッド等を定義

content_script site: "www.example.com" do
  # ページを開いたときの処理を書く

  # h1 = document.querySelector("h1")
  # h1.innerText = "Hello unloosen!"
end

popup do
  # ツールバーの拡張のボタンを押したときの処理を書く

  # submit_button = document.querySelector("button#submit")
  # submit_button.addEventListener "click" do |e|
  #   form_text = document.querySelector("input#title").value
  #   ...
  # end
end
```

### 4. Unpacked Extension としてインストール

https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked を元に `manifest.json` があるディレクトリを 「Unpacked Extension」 として読み込みます。

これで Ruby で実装した Chrome Extension がインストールできました :tada: 

## 触ってみての感想

まず、全く js のコードを書かずに Ruby だけで Chrome Extension を書ける、というのはかなり体験として面白いです。 js とそのビルドのことを全く考えたくないタイプの人間が Chrome Extension を書きたいときに重宝するのと、 かなり設定が簡略化されているので [ruby.wasm](https://github.com/ruby/ruby.wasm) を試しに触って見るための入り口として有用だと感じました。(最初の設定が今後ジェネレーター等で簡略化されればさらに試しやすくなりそう)

ただ、 実際に書く際に ruby.wasm のクセ, Chrome Extension の仕様でハマることもそこそこありました。(unloosen はかなり書きやすくなるようにケアしているものの)、これらの仕様を知らなくても書けるというわけではなく、ある程度これらについて知っておく必要はあります。

* Ruby 内で JS オブジェクトを使う際のクセ (例: null を表す `JS::Object` は falsy にならない、 Hash はそのまま js メソッドの引数として渡せない、 ...etc)
* CSP に阻まれて popup 中で `JS.eval` が使えない、などの制約
* デバッグの難しさ (デバッガが使えたり、sourcemap が使えると…！)

ただ、 [ruby.wasm](https://github.com/ruby/ruby.wasm) を学ぶ材料として unloosen はハードルが低く、自分も今回始めて unloosen を通じて [ruby.wasm](https://github.com/ruby/ruby.wasm) を初めて使う機会になりになり、その過程で、 ruby.wasm がどういうことが出来るか、どこに課題があるかを知ることが出来て楽しかったです :v: 

[ruby.wasm](https://github.com/ruby/ruby.wasm) の入り口としては面白いですし、今後の発展も楽しみな分野なので、試しに潜ってみる感覚で使ってみるのはどうでしょうか。

--

この記事は 「[【RubyKaigi 2023連動イベント】みんなでRubyの知見を共有しよう](https://qiita.com/official-events/4d2a9eed42dbe0a4af21)」 の参加記事です。 RubyKaigi で学んだことや、面白かった内容の共有、その他 Ruby の知見を是非投稿してみませんか？ :raising_hand: 

https://qiita.com/official-events/4d2a9eed42dbe0a4af21