yuki.games / blog

Published at: 2025-10-27 Updated at: 2025-10-29

WXT をモジュールで拡張する

動機

前回の記事で紹介した View in Markdown Chrome 拡張機能に、新しいタブで開く代わりに Markdown コンテンツをクリップボードにコピーする機能を追加したいと考えました。このクリップボードへの書き込み機能の実装は、当初想定していたよりも複雑となることがわかりました。

具体的には、 manifest.jsonclipboardWrite 権限を要求していてもタブにフォーカスがないと navigator.clipboard.writeText が使えないこと、また、Background Script からは Clipboard API を直接使えないという制約があるためです (GoogleChrome/chrome-extensions-samples@11898ee)。これらの問題を回避するには Offscreen Document を使用する必要があり、実装が少し複雑になります。

そこで、このクリップボード書き込み処理を View in Markdown 拡張機能のコア機能として組み込むのではなく、他の拡張機能でも再利用できるよう、 WXT のモジュール機構を利用した独立したパッケージとして作成することにしました。

WXT モジュールの作り方

Note

以後の説明では、 bun を JavaScript Runtime / Package Manager / Bundler に使用しています。

WXT のモジュール機構の詳細については WXT Modules (公式ドキュメント) を、モジュールの実装例については wxt-dev/wxt/packages@fad5ab6 をご参照ください。
また、作成した wxt-module-clipboard のソースコードやドキュメンテーションは yukidaruma/wxt-module-clipboard (GitHub) をご参照ください。

このセクションでは、 WXT モジュールを作成するうえでわかりにくい点について説明します。

client bundle でのみ利用されるコードを分割する

wxt (フレームワーク本体) のコードはブラウザから直接 import される前提で書かれたものではありません。モジュールを定義する (defineWxtModule を呼び出す) コードをブラウザで利用するコードから import すると、 bundle size が増大してしまったり、ビルドエラーが発生したりします。この問題を避けるため、クライアント側でのみ使用するコードは専用の entrypoint (wxt-module-clipboard/client) から import できるようにしました。

// wxt-module-clipboard/index.ts
import "wxt";
import { addWxtPlugin, defineWxtModule } from "wxt/modules";

export default defineWxtModule<ClipboardModuleOptions>({
  // ...
});
// wxt-module-clipboard/client.ts
export async function copyToClipboard() {}
export async function copyToClipboardViaOffscreen() {}
// モジュールの機能を利用するコード
import { copyToClipboard } from "wxt-module-clipboard/client";

特定の entrypoint だけで実行されるスクリプトを作成する

WXT には entrypoints/ 以下に作成した特定のファイル名のファイルが自動的に Bundler による Bundle の対象となる、 entrypoint と呼ばれる仕組みがあります。

モジュールから entrypoint にスクリプトを挿入するには addWxtPlugin を使用します。

// wxt-module-clipboard/index.ts
import "wxt";
import { addWxtPlugin, defineWxtModule } from "wxt/modules";

export default defineWxtModule<ClipboardModuleOptions>({
  setup(wxt, options) {
    const pluginModuleId = "wxt-module-clipboard/background-plugin";
    addWxtPlugin(wxt, pluginModuleId);
  },
})

ただし、このままだとすべての entrypoint にスクリプトが挿入されてしまいます。例えば chrome.runtime.onMessage.addListener を利用してメッセージのリスナを登録するコードの場合、同じリスナが複数のコンテキストで登録される問題が生じます。

そこで、特定の entrypoint でのみ必要となるスクリプトについては、 import.meta.env.ENTRYPOINT を使用した条件分岐を追加し、その entrypoint でのみ実行されるようにします。この条件分岐は Bundler の静的解析による Tree-shaking / Dead Code Elimination の対象となり、スクリプトを必要としない entrypoint からコードを完全に削除することができます。

// modules/background-plugin.ts
import { setupClipboard } from "./client";

export default () => {
  if (import.meta.env.ENTRYPOINT !== "background") return;

  setupClipboard();
};

Warning

UPDATE (2025-10-29) import.meta.env.ENTRYPOINT が Side Panel で使用できない問題が WXT のユーザーにより報告されていること、将来的に import.meta.env.ENTRYPOINT が廃止される可能性があることが WXT の開発者によってコメントされていることにご注意ください (wxt-dev/wxt#1611)。

@aklinker1: That is because they’re internal variables used by WXT. It’s actually impossible to use vite define’s for import.meta.env.ENTRYPOINT, specifically for HTML entrypoints. Since they’re built in a single vite build, there’s no way to specify individual values for each entrypoint. This is something I intend to fix before v1.0, but it will likely be via a global variable, not an env var.

モジュールにオプションを追加する

一般的なプラグインシステム (例: Vite の plugin) では plugins: [somePlugin(config)] の形 (オブジェクト) によりプラグインを追加するため、インラインで設定を行うことができます。一方、 WXT のモジュール機構は modules: ["some-module"] の形で名前を渡すことで読み込むため、モジュールを設定可能にする方法が異なっています。

WXT の設定ファイルでモジュールのオプションを設定できるようにするには、 TypeScript の Module Augmentation 機能を利用して WXT の InlineConfig 型の定義を拡張します。

// wxt-module-clipboard/index.ts
declare module "wxt" {
  export interface InlineConfig {
    clipboard?: {
      optionalPermissions?: boolean;
    };
  }
}

export default defineWxtModule<ClipboardModuleOptions>({
  configKey: "clipboard", // InlineConfig の key と合わせる
  setup(wxt, options) {
    // options でモジュールの設定にアクセスできる
    const useOptionalPermissions = options?.optionalPermissions ?? false;
  },
});

これにより、ユーザーは WXT の設定ファイルでモジュールのオプションを指定できるようになります。

// wxt.config.ts
export default defineConfig({
  modules: ["wxt-module-clipboard"],
  clipboard: {
    optionalPermissions: true,
  },
});
WXT の defineConfig でモジュールのオプションを利用する例

その他

  • wxt-dev/wxt リポジトリ内のパッケージではビルドコマンドに buildc を使用しています。このツールは monorepo を対象としたものであるため、単一のモジュールである wxt-module-clipboard の開発には使用しませんでした。
  • モジュール開発時は、 example/ フォルダに file:../ 形式でインストールしたモジュールを利用するサンプルコードを作成し、モジュール本体と両方を bun dev で watch した状態で開発を行うことをおすすめします。
  • webextension-polyfill を使用すべきか - browser vs chrome:
    • webextension-polyfill は Chrome 拡張で、 Web 標準で規定された browser.* 以下の名前空間で拡張機能用の API を利用できるようにする polyfill です。
    • Offscreen Document API は標準化された API ではなく (w3c/webextensions#170)、 Web 標準との互換性を重視する Firefox ではこの機能が未実装となっています。したがって、 wxt-module-clipboardwebextension-polyfill を利用しても Firefox で動作しませんでした。
    • webextension-polyfill を利用しても entrypoint ごとにファイルサイズが約 9KB 増加するだけの結果となることがわかったため、 wxt-module-clipboard では webextension-polyfill を利用せず、 Firefox に対応していないことを README に明記することにしました。
    • なお、 WXT を利用した拡張機能では、 webextension-polyfill を直接 import する代わりに wxt/browserexport する browser を使用することになっています。

npm に公開する

汎用的な形でモジュールを作成することに成功したため、作成したパッケージを npm に公開することにしました (wxt-module-clipboard on npm)。 npmjs.com の Access Tokens ページでトークンを作成後、 npm publish の代わりに bun publish を使用して npm にビルドしたパッケージをアップロードしています。

bun run build
NPM_CONFIG_TOKEN=npm_xxxx bun publish

bun publish の完了後に yukidaruma/wxt-module-clipboard リポジトリを clone して動作確認を行いました。 example/ フォルダ内で、 npm i, npm run dev / bun i, bun dev を利用して、ビルドした拡張機能がそれぞれ正常に動作することを確認しています。

あとがき

今回作成した wxt-module-clipboard を利用すれば、 Chrome 拡張におけるクリップボードへの書き込みを簡単に実装できるようになります。拡張機能を開発していて、クリップボードへの書き込みを実装する機会があればぜひご利用ください。また、今回の記事が WXT モジュール機構の理解や WXT モジュールの開発の一助となれば幸いです。

現時点では Chrome のみにしか対応していませんが、 WXT の import.meta.env.FIREFOX を利用することで、単一のコードベースで Firefox にも対応できるかもしれません。 Firefox への対応については、将来の課題としています (yukidaruma/wxt-module-clipboard#1)。

参考文献