風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

Chrome拡張機能にてシークレットウィンドウとbackgroundとのメッセージ送受信時の注意


ハマったよ……

Twitter メディアローダ
furyu.hatenablog.com
にて、Google Chrome のシークレット ウィンドウ上で実行すると、ファイルダウンロード時に

失敗 - ファイルがありません

となって正常にダウンロードできない、という現象が発生した

シークレット ウィンドウでだけ発生するという訳の分からない状態だったので、途方に暮れていたのだが……。

何が起こっていたのか?

background → content_scripts に、ZIP化したコンテンツの Blob URL を sendResponse() で送信する仕組みにしていたのだが、なぜか受け取った content_scripts 側(シークレット ウィンドウ)で、その Blob URL にアクセスできない状態(404発生)になっていた。

原因

Manifest - Incognito - Google Chromeによると、manifest.json に指定できる "incognito" キーについて、シークレット ウィンドウ(匿名タブ)で動かせる拡張機能では"spanning"もしくは"split"が指定でき、

  • "spanning"(デフォルト)
    → 匿名タブ(ウィンドウ)と、その他の(一つのプロセス上で共有される)コンテキスト(これにはoptions_uiやbackgroundページも含む)とがあり、匿名タブは共有プロセスにアクセスできない
  • "split"
    →匿名タブ(と対応する background ページ)は、(匿名でないタブからみて)独立したプロセス上で動作し、メッセージ等のリソースもプロセス内のコンテキストのもののみを参照できる(それ以外のコンテキストとはやり取りできない)
    対応しているbackgroundとは共通のリソースにアクセスできると思われる

となっているように読める。
英語の解読に自信はないので、読み取り間違っているかもしれない。その場合はご指摘を……。

なので、manifest.json で明示していなかったために "spanning" モードで動いていた本拡張機能では、background で作成した Blob URL に匿名タブからアクセスできなかったのだと思われる。

対策

manifest.json に

,   "incognito" : "split"

を追加。

念のため、backgroundでの ZIP 化を無効化するオプションも付けておいた

副作用

Firefox では、"incognito" キーはサポートしているが、選択肢として "split" がサポートされていない
このため、manifest.json を Chrome と共通にしていると、アドオン登録の検証時に

"/incognito" should be equal to one of the allowed values
エラー: Your JSON file could not be parsed.

というエラーが発生し、撥ねられてしまう。
やむを得ず、Chrome と Firefox とで manifest.json を分ける羽目に……せっかく共通化していたのに(苦笑)。

拡張機能のアイコンをSVGで作ろうとInkscapeを入れてみたところ、はまった点


前書き

最近、近傍ツイート検索
furyu.hatenablog.com
の拡張機能用のアイコンをリニューアルした。

実は、2014年6月当時にやっつけで作ったまま(厳密には2016年2月にマイナーチェンジしているが)だったので、いつか作り直そう……と思いつつ、面倒なので長らく放置していた。

(2014/6/9作成)(2016/2/1微修正)

それが先日、ふと「アイコンを SVG で作ってみよう」と思い立ち、初めて Inkscape なる OSS のベクトル画像ドローソフトをインストールしてみた。
ところが、まっとうに使い始める前に例によって色々とはまってしまったので*1、それらの点についてメモ書きしておく。
導入したバージョンは、Inkscape 0.92.2 (5c3e80d, 2017-08-06)

はまった点

Python のエラー……?

Inkscape がデフォルトで出力する SVG ファイル(Inkscape SVG フォーマット)は色々と情報が入っていてサイズが大きいので、これを圧縮する方法を調べていると……

SVGのエクスポート – Inkscape@JP

標準で、『最適化 SVG』というフォーマットがあり、これはかなりサイズが小さくなるっぽい。

で、『名前を付けて保存』メニューから、その出力を試そうとしたら……

The fantastic lxml wrapper for libxml2 is required by inkex.py and therefore this extension. Please download and install the latest version from http://cheeseshop.python.org/pypi/lxml/, or install it through your package manager by a command like: sudo apt-get install python-lxml

Inkscapeは、実行したスクリプトから追加データを受け取りました。スクリプトはエラーを返しませんでしたが、実行結果が意図しないものになっていることを示唆している可能性があります。

というエラーが出てしまい、保存に失敗する。

Inkscape が自分でインストールする Python 2.7 と、別件で既にインストールされていた Python 2.7 と競合しているのかと思って、PYTHONHOME や PYTHONPATH の設定変更や削除、C:\Python27 のリネーム等、いろいろと試してみたが、

Traceback (most recent call last):
  File "C:\Python\Lib\site-packages/site.py", line 73, in <module>
    __boot()
  File "C:\Python\Lib\site-packages/site.py", line 3, in __boot
    import os
ImportError: No module named os

とかいったエラーに変わるだけで、どう設定してもうまくいかない。

結局、いったん Inkscape をアンインストールしたあと、インストール時のコンポーネント選択で、
f:id:furyu-tei:20171202111555p:plain
☑ Python 2.7 のチェックを外してインストールしてみると、上記エラーが出なくなった。

なお、代わりに、自身が構築している Python 2.7 の環境上に、いくつかのライブラリを手動でインストールしておく必要があるので注意。

> python -m pip install lxml numpy scour
デフォルトは mm 単位で、A4 サイズ……?

インストールした Inkscape を起動すると、新規ドキュメントが開くが、デフォルトではこれがサイズの単位が mm で、かつ、A4 サイズになっている。
f:id:furyu-tei:20171202113915p:plain

ドキュメントのプロパティ(ファイル(F)→ドキュメントのプロパティ(D))はこんな感じ。
f:id:furyu-tei:20171202111611p:plain

この設定ではアイコンを作るのには不便そうなので、Chrome 拡張機能の基本サイズ 96px × 96px にしようと思い、
f:id:furyu-tei:20171202113835p:plain
のように設定。
※変更したのは、

  • 『Display units』→ px
  • 『カスタムサイズ』→幅(W):96.0000、高さ(H):96.0000、単位(N):px
  • 『拡大縮小』→ Scale x:1.00000
  • 『Background』→☑ Checkerboard background(チェックを付ける)
  • 『境界線』→☑ 描画より前面に境界線を表示する(T)(チェックを付ける)、□ 境界線に影を表示する(S)(チェックを外す)

これで、ドキュメントは
f:id:furyu-tei:20171202113901p:plain
といった感じになる。

よく見てみたら、ドキュメントのプロパティの『ページサイズ』に、"Icon 48x48" といったものもあった。

ただ、毎回設定するのは大変なので、デフォルトを変えたい。

Setting up page template - InkscapeForum.com

などを参考に、↑のように設定変更したドキュメントを

%UserProfile%\AppData\Roaming\inkscape\templates

の下に、default.ja.svg というファイル名で保存しておく。
すると、新規ドキュメントを開いたときに、上記設定が反映されるようになる。

『最適化 SVG』形式で保存すると……画面いっぱいのアイコンが?!

苦戦しつつもアイコンを描き終わり、『最適化SVG』形式で保存し、これをブラウザで表示してみると……
f:id:furyu-tei:20171202115705p:plain
……でかっ!!

エディタで当該 SVG ファイルを見てみると……

<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg version="1.1" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

viewBox は指定されているものの、width と height が抜けている……?
そこで、SVG タグを修正し

<svg version="1.1" viewBox="0 0 96 96" width="96" height="96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

「width="96" height="96" 」を付け加えたところ、

f:id:furyu-tei:20171202120010p:plain
なんとか普通に表示されるようになった。

なお、『Inkscape SVG』形式や、『プレーン SVG』形式の場合には、width・height は入っているため、『最適化 SVG』形式の場合の不具合かもと考えている。
ちなみに、

  • 『カスタムサイズ』→幅(W):25.4000、高さ(H):25.4000、単位(N):mm

にしておくと、『最適化 SVG』であっても width・height が入り、

<svg width="25.4mm" height="25.4mm" version="1.1" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

見た目上の表示もまっとうになる。
ただし、ピクセルとの変換が面倒。

他のサイズで出力するには……?

96px × 96px で作成しているが、例えば Chrome 拡張機能用には、48px、96px、128px というサイズのアイコンが必要になる。
しかも、128pxは、96px のアイコンの周囲を 16px の透過ピクセルで囲んだもの。

SVG 形式の場合
ドキュメントのプロパティをその都度変更しつつ、名前を付けて保存してもよいが、96px の最適化 SVGを出力しておき、これをコピーして、テキストエディタで width、height、viewBox を書き換える方が簡単かも。
※ 96px

<svg version="1.1" viewBox="0 0 96 96" width="96" height="96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

※ 48px

<svg version="1.1" viewBox="0 0 96 96" width="48" height="48" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

※ 128px(周囲16pxは透過)

<svg version="1.1" viewBox="-16 -16 128 128" width="128" height="128" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

ちなみに、viewBox と width・height の関係は、以下のサイトの解説が分かりやすかった。
old-pine.net

私的に、こんな↓感じだと理解。
f:id:furyu-tei:20171203003948p:plain
間違っていたらご指摘を。
なお、preserveAspectRatioの指定によって、viewBox を width-height の矩形に投影する際の動作が異なってくる模様。
developer.mozilla.org

追記:@rikuoさんにご紹介いただいたわかりやすい記事
www.sarasoueidan.com

Interactive SVG Coordinate System


PNG 形式の場合
Inkscape の『PNG 画像にエクスポート(E)』(Shift+Ctrl+E)により出力するのが簡単。
通常は、『画像サイズ』欄に出力したいサイズを入力して、エクスポートするだけでよい。
ただし Chrome 拡張機能の 128px の場合には、エクスポート領域で、透過ピクセル部分を含んだ x0、y0、x1、y1(-16, -16, 112, 112)を指定してやる必要がある。
f:id:furyu-tei:20171202124108p:plain

サゲ


苦労して(?)作成した SVG ファイルも、Chrome 拡張機能の manifest.json や、拡張機能のアイコン登録画面などでは、指定できない罠。

*1:絵心が無いとかそういう、人間のソフトウェアに依存したどうしようもない理由ではなく

近傍ツイート検索で最近リツイートしたユーザーを表示する機能を追加しました(version 0.2.6.100)


前書き

近傍ツイート検索
furyu.hatenablog.com
にて、個別ツイートをリツイートした最近のユーザーを表示する機能を追加しました。
また、該当するユーザーが RT 前後の 10 分間にツイートしていた内容(概要)を表示することも出来るようになりました。

早い話が、『RtRT』("(Find the) Reference to ReTweet")や、
リツイート直後のツイートを表示するやつみたいなことを、ユーザースクリプトや拡張機能でもやりたかったのです……サイトに行って調べるのが面倒なので(苦笑)。
とは言っても色々と制限が厳しく、Webサービスみたいにはいきませんが……。
調子に乗ってあちこちのツイートで試していると API 制限(時間当たりの回数制限)に引っかかるかも知れません。悪しからずご了承ください。

新機能について

当然ながら、ブラウザに近傍ツイート検索の最新版がインストールされていることが前提です。
まだの方は、お使いのブラウザ環境に合わせて
github.com
chrome.google.com

近傍ツイート検索 – Firefox 向けアドオン

からインストールしてください。

使い方

インストール/更新後に Web 版公式ツイッターを開くと、リツイートされているツイートの下の方に [Re:RT] というボタンがついています。
f:id:furyu-tei:20171125034556p:plain

これをクリックすると、当該ツイートを RT したユーザーのうち、最近行った方々が表示されます。
f:id:furyu-tei:20171125034607p:plain

ユーザーのプロフィールの右側に [↓↑] というボタンが表示されますが、
f:id:furyu-tei:20171125034626p:plain

これをクリックすると、
f:id:furyu-tei:20171125034640p:plain

RT 前後 10 分間のツイート概要が表示されます。
RTした人は、その前後に当該ツイートについて言及していることもよくある、という経験則に基づきます。

オプションとしては、表示するユーザー数を変更できます。
f:id:furyu-tei:20171125034653p:plain
これは、最大 100 ユーザーまでです。
Twitter API の仕様による制限です。

注意書き

更新の際に無効化されてしまう

上記の機能に関して、拡張機能に新たな権限が追加されています。
f:id:furyu-tei:20171127045119p:plain

0.2.5.200以前バージョンから更新された場合、いったん拡張機能が無効化され、有効化しようとすると、

「近傍ツイート検索」の最新バージョンは、さらに許可が必要なため無効になっています。

のようにメッセージが表示されて、再度有効にするかどうかの確認があります。
f:id:furyu-tei:20171127034910p:plain

[再度有効にする]ボタンを押してください。
上記は Chrome の場合ですが、Firefox の場合にも同様のダイアログが表示されることがありますので、その場合は[更新(U)]ボタンを押してください。

※キャンセルを押してしまった場合などは……
 Google Chrome → 拡張機能の画面 chrome://extensions/ (「≡」(右上のハンバーガーメニュー)→「その他のツール(L)」→「拡張機能(E)」)を開き、「近傍ツイート検索」を探して、「□ 有効にする」のチェックボックスにチェックを入れてください。
 Firefox Quantum → アドオンマネージャー about:addons ([Ctrl]+[Shift]+[A]、「≡」(右上のハンバーガーメニュー)→アドオン)を開き、「近傍ツイート検索」を探して、[有効化]ボタンを押してください。

ここで、Google Chrome の場合には

・twitter.com の全サイト、twitter.com 上にある自分のデータの読み取りと変更

とあるので(特に『変更』と出てしまっているので)不安に思われる方もいらっしゃるようですが、「ユーザーデータには変更を加えてはいません」のでご安心ください。
Firefox Quantum の場合、同様のダイアログでは、「・twitter.com の保存されたデータへのアクセス」という表現になっていると思います。
作者本人がそう言っても不安だ・信用できない、という方は、公開しているソースコードをご覧になるか、使用をお控えください。

これは、今回の機能の中で、「個別ツイートをリツイートした最近のユーザー」の情報を取得するために、新たにアクセス権を追加したためです。
Twitter 側に設定してある本拡張機能用のアクセス権限は、「読み取り専用」(Read-only)としているのですが、
f:id:furyu-tei:20171127035554p:plain
拡張機能には「読み取り専用」という権限はなく、アクセスを許可しようとすると自動的に「読み取りと変更」ということになってしまいます。

なお、上記の権限に関して、実際に使用している Twitter の API は以下の通りです。

POST oauth2/token — Twitter Developers
GET application/rate_limit_status — Twitter Developers
GET statuses/retweets/:id — Twitter Developers

余談

ブラウザ拡張機能用に background で ZIP 化するためのライブラリを試作(Chrome拡張機能/Firefox Quantum WebExtensions 用)


前書き

WebExtensions について調べていると、Promise を使用して云々……という記述が出てきて、今さらながらに Promise というものの存在を知りました(ヲイ。
慣れれば使い勝手が良さそうなので、練習を兼ねて、ブラウザ拡張機能の background で ZIP 化することが出来るようなライブラリを試作してみました。
github.com
習作なので、いつも以上に動作保証できません。ご利用は計画的に(汗)。

概要

content_scripts に対し、ZipRequest クラスを提供します。
ZipRequest#open()/file()/generate()/close() という一連の関数にて、background に対してメッセージを送ることで ZIP 化に関する指示を出し、background からの応答メッセージで結果を受け取り、content_scrips に返します。
background での ZIP 化には、JSZip を使用しています。

比較する意味もあって、content_scripts 用には Promise を使ったもの(Promise版・zip_request.js) と、使わないもの(コールバック版・zip_request_legacy.js)とがあります。
background 用のもの(zip_worker.js)は共通です。

サンプル

はてなブログ("*://*.hatenablog.com/*")を開くと、画像を適当な数選んで ZIP 化・ダウンロードする、という迷惑な(汗)サンプルコード(抜粋)です。
サンプルソースコード全文は、こちらをご覧ください

並列処理

複数のファイルを同時並行で取得しながらアーカイブする処理です。
コールバック版(zip_request_legacy.js)の場合

'use strict';

( function () {

var zip_request = new ZipRequest(),
// (中略)

zip_request.open();

files.forEach( function ( file ) {
    var url = file.src || file.href,
        filename = get_filename( url );
    
    console.log( '[start]', url, filename );
    
    zip_request.file( {
        url : url,
        filename : filename,
        zip_options : {
            date : new Date( '2017-01-01' )
        }
    }, function ( result ) {
        console.log( '[result]', url, filename, result );
    } );
} );

zip_request.generate( 'blob', function ( response ) {
    zip_request.close();
    
    // 以下、Aタグのdownload 属性を使ったダウンロード処理
} );

} )();

Promise版(zip_request.js)の場合

'use strict';

( async function () {

let zip_request = new ZipRequest(),
// (中略)

await zip_request.open();

await Promise.all(
    files.map( async ( file ) => {
        let result,
            url = file.src || file.href,
            filename = get_filename( url );
        
        console.log( '[start]', url, filename );
        
        result = await zip_request.file( {
            url : url,
            filename : filename,
            zip_options : {
                date : new Date( '2017-01-01' )
            }
        } )
        .catch( result => { return result } ); // Promise.all() を停止させないための対策
        
        console.log( '[result]', url, filename, result );
        
        return result;
    } )
);

let response,
    download_link;

response = await zip_request.generate( 'blob' );

await zip_request.close();

// 以下、Aタグのdownload 属性を使ったダウンロード処理

} )();

コールバック版のライブラリ内で多少工夫をしていることもあり、並列処理に関しては、一見したところそれ程違いは無いかもしれません。
コールバック版では、例えば file() に対応する応答が background からまだ来ない状態で generate() が呼ばれたとしても、全てのファイルについて結果が返るのを待って background に要求を出すようにしているため、上記の書き方が可能。ただし、close() については、generate() のコールバック後に呼び出す必要あり。

直列処理

ファイルを一つずつ順番に取得しながら(逐次)アーカイブする処理です。
コールバック版(zip_request_legacy.js)の場合

'use strict';

( function () {

var zip_request = new ZipRequest(),
// (中略)

zip_request.open( function ( result ) {
    var file_index = 0;
    
    function zip_files() {
        if ( files.length <= file_index ) {
            zip_request.generate( 'blob', function ( response ) {
                zip_request.close();
                
                // 以下、Aタグのdownload 属性を使ったダウンロード処理                                
            } );
            return;
        }
        
        var file = files[ file_index ++ ],
            url = file.src || file.href,
            filename = get_filename( url );
        
        console.log( '[start]', url, filename );
        
        zip_request.file( {
            url : url,
            filename : filename,
            zip_options : {
                date : new Date( '2017-01-01' )
            }
        }, function ( result ) {
            console.log( '[result]', url, filename, result );
            
            zip_files();
        } );
    }
    
    zip_files();
} );

} )();

Promise版(zip_request.js)の場合

'use strict';

( async function () {

let zip_request = new ZipRequest(),
// (中略)

await zip_request.open();

for ( let file of files ) {
    // files.map( async ( file ) => { ... } ) は使えないことに注意
    // ※ map() では、コールバック関数の戻り値が Promise object になり、直列処理されない
    let url = file.src || file.href,
        filename = get_filename( url ),
        result;
    
    console.log( '[start]', url, filename );
    
    result = await zip_request.file( {
        url : url,
        filename : filename,
        zip_options : {
            date : new Date( '2017-01-01' )
        }
    } )
    .catch( result => { return result } ); // エラーで停止させないための対策
    
    console.log( '[result]', url, filename, result );
}

let response,
    download_link;

response = await zip_request.generate( 'blob' );

await zip_request.close();

// 以下、Aタグのdownload 属性を使ったダウンロード処理

} )();

こちらは、Promise 版のメリットが出ていると思います。
コールバック版は処理の流れが一見解りにくいのに対し、Promise 版では上から下への自然な流れで解りやすくなっています。

はまった点など

  • Promise.all() は、並列実行中の Promise オブジェクトが一つでもエラーになると異常終了してしまう(catchされてしまう)ため、中断したくない場合、それぞれの Promise で reject() ではなく resolve() を呼び、戻り値によって判別するようにする
  • Array#map() 等のコールバック処理を持つものは、直列処理では使用できない(コールバックの結果が Promise オブジェクトで返されるため)
  • Promise の resolve() や reject() は、呼んだ後も続きが実行される(実行されないようにするには、直後に return が必要)
  • background における、browser/chrome.runtime.onMessage.addListener() のコールバック関数内で、非同期の処理を呼んでから sendResponse()を返す場合、コールバック関数の戻り値に true を設定する必要がある(chrome.runtime - Google Chrome
  • content_scripts と background 間のやり取り(sendMessage()/sendResponse())では、JSON で基本的にはシリアライズ可能なオブジェクトしか渡せない……ところが、渡せるオブジェクトの種類に、ブラウザ間で差異がある(関数オブジェクトは Firefox で NG、Blob が渡せるのは Firefox のみ、等)
  • background で URL.createObjectURL() により得られた Blob URL を content_scripts に送ると、Chrome では download 属性付き A タグでダウンロード可能なのに対し、Firefox や MS-Edge では不可(Firefoxについては、なぜか Blob がそのまま content_scripts に送れるため、そちらで Blob URL に変換することで対応している)
  • MS-Edge では、作成した ZIP をダウンロードさせる術が見つからない

Promise/async/awaitやclassの書き方でもっとはまると思っていたが、これらはそれ程でもなかった代わりに、拡張機能の仕様やブラウザ間の細かい差異の方が難解。

Chrome 拡張機能を Microsoft Edge の拡張機能にも対応させようとして挫折した件


前書き

承前。
furyu.hatenablog.com
furyu.hatenablog.com
せっかくだから、Firefox Quantum に対応できた拡張機能を、MS-Edge でも動かしたいと欲張ったのだが……見事に挫折した(哀)。

修正方法等

manifest.json

「マニフェスト解析エラー: 'author' フィールドが見つかりません。」と出たので、author フィールドを追加。

browser.* ネームスペース対応

WebExtensions 用に、予め対応してあった。

PATH の問題

オプション画面にて、chrome.browserAction.setIcon( { path : icon_path } ) を使ってアイコンを変化させる処理を書いてあったところ、MS-Edge ではアイコンが正常に表示されなくなった。
調べてみたところ、どうも、アイコンの PATH の指定が、

  • Chrome 拡張機能 / Firefox WebExtensions → 呼び出し元の HTML(オプション画面)からの相対パス
  • MS-Edge 拡張機能 → manifest.json のあるフォルダからの相対パス

のように違いがあるようで、場合分けが必要だった。

未解決問題

以下は、Microsoft Edge 41.16299.15.0 / Microsoft EdgeHTML 16.16299 で発生

fetch() や XMLHttpRequest で ArrayBuffer を使用すると未定義のエラーが発生

ユーザーコンテキスト(content_scripts)で fetch() を使用すると、

  SCRIPT65535: 未定義のエラーです。

どうやら、同様の現象が他でも見られるみたい。
Fetch API in Extension SCRIPT65535 error - Microsoft Edge Development
https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/attachment/14192157/8236868/

同様に、XMLHttpRequest で、xhr.responseType を 'arraybuffer' に設定した場合に、xhr.response を参照しようとするとエラーとなってしまう。

  SCRIPT65535: 未定義のエラーです。

いったん、responseType を 'blob' にして受けてやると response は参照できるので、これを ArrayBuffer 化することは可能だった。
ただ、その場合でも、JSZip の アーカイブ処理(JSZip#generateAsync)で失敗してしまう。

よって、MS-Edge の拡張機能では、Twitter 原寸びゅーや Twitter メディアダウンローダで使用している JSZip による ZIP 化が出来ず、ZIP ダウンロード機能が無効化してしまう。
特に、メディアダウンローダの方はほぼすべての機能が使えなくなってしまう……(哀)。

バックグラウンドでのダウンロードができない

バックグラウンドコンテキスト(background)内では、現状、ファイルのダウンロードができない模様。

Overarching issues
The following known issues span across the extension platform and will be fixed in the near future:
: (中略)
・Triggering a download via a hidden anchor tag will fail from background scripts. This should be done from an extension page instead.

Extensions - Supported APIs - Microsoft Edge Development | Microsoft Docs
  • browser.downloads API は存在しない(2017/04/11現在のロードマップで、"Under consideration" になっている
  • a タグの download 属性を使ったダウンロードも不可(click()してもダウンロードされない)
  • navigator.msSaveOrOpenBlob() 等も使えない
    SCRIPT16386: インターフェイスがサポートされていません
  • tabs.create() で新たにタブを開いてそちらでやろうとしても、background から開いた場合には上記の不具合が継承されてしまう

ということで、八方ふさがり。
Twitter 原寸びゅーのコンテキストメニューからの「原寸画像を保存」が実現出来ない。
background から content_scripts 宛に sendMessage() して、そちらで処理ならできるかも?でも面倒くさそう……。

余談

というわけで、自作拡張機能を MS-Edge に対応させることは、少なくとも現状では諦めた。
まぁ、出来た拡張機能を公式に公開するすべは今のところ無さそうだし良いけど。

それにしても MS-Edge は、拡張機能を開発しやすいとはお世辞にも言えないな……。

  • 拡張機能関連のメニューが自動的に隠れてしまうため、アクセスしずらい
    "about:flags"みたいに、タブに独立して出す方法はある?
  • 開発者ツールでデバッグしていると固まりやすく、またかなりの確率でブラウザごと落ちてしまう
  • 上記の fetch() のように、拡張機能のコンテキストでのみ動作しない不具合が多い

それでもめげず(?)、便利な拡張機能を公開されている限られた方々には頭が下がる訳だが。

少なくとも自分は、当面 MS-Edge では開発したくない。
幸い、MS-Edge にも Tampermonkey があるので、当方の拡張機能を使いたい方は、Tampermonkey を入れて、ユーザースクリプト版をお使いいただきたい。
とか言っていたら、MS-Edge 版の Tampermonkey が TweetDeck では異常が発生し、ユーザースクリプトを動かせない不具合を見つけてしまったり……。