動機
前回の記事で紹介した View in Markdown Chrome 拡張機能に、新しいタブで開く代わりに Markdown コンテンツをクリップボードにコピーする機能を追加したいと考えました。このクリップボードへの書き込み機能の実装は、当初想定していたよりも複雑となることがわかりました。
具体的には、 manifest.json で clipboardWrite 権限を要求していてもタブにフォーカスがないと 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,
},
});
defineConfig でモジュールのオプションを利用する例その他
wxt-dev/wxtリポジトリ内のパッケージではビルドコマンドにbuildcを使用しています。このツールは monorepo を対象としたものであるため、単一のモジュールであるwxt-module-clipboardの開発には使用しませんでした。- モジュール開発時は、
example/フォルダにfile:../形式でインストールしたモジュールを利用するサンプルコードを作成し、モジュール本体と両方をbun devで watch した状態で開発を行うことをおすすめします。 webextension-polyfillを使用すべきか -browservschrome:webextension-polyfillは Chrome 拡張で、 Web 標準で規定されたbrowser.*以下の名前空間で拡張機能用の API を利用できるようにする polyfill です。- Offscreen Document API は標準化された API ではなく (w3c/webextensions#170)、 Web 標準との互換性を重視する Firefox ではこの機能が未実装となっています。したがって、
wxt-module-clipboardはwebextension-polyfillを利用しても Firefox で動作しませんでした。 webextension-polyfillを利用しても entrypoint ごとにファイルサイズが約 9KB 増加するだけの結果となることがわかったため、wxt-module-clipboardではwebextension-polyfillを利用せず、 Firefox に対応していないことを README に明記することにしました。- なお、 WXT を利用した拡張機能では、
webextension-polyfillを直接importする代わりにwxt/browserがexportする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)。
参考文献
- Manifest V2 - Permissions with warnings - Chrome Extension API Reference (注意: MV2 について、最終更新日が2012年)
- Chrome拡張でbackgroundからコピーをする方法とChrome拡張のセキュリティについて - Qiita
chrome.offscreen- Chrome Extension API Reference- Offscreen Document を利用して文字列をコピーするサンプルコード - Google 公式サンプルコード (GitHub)