TypeScript でユーザースクリプトを書いた
TypeScript で NicoNicoFavlist というユーザースクリプトを書いたので、その時の備忘録などを記しておきます。
https://github.com/kotas/niconico-favlist
ユーザースクリプトを TypeScript で書く
ユーザースクリプトを動かすためのブラウザ拡張の多くは、オリジナルの先駆者である Greasemonkey と互換性のある API セット を提供しています。
TypeScript で上記の API を扱うのであれば、その定義ファイル (.d.ts) が必要となるのですが、見当たらなかったので自分で作りました。
https://github.com/borisyankov/DefinitelyTyped/blob/master/greasemonkey/greasemonkey.d.ts
(半) 公式リポジトリに pull req も出しておいたらマージされてました。やったね。
ビルドフロー
書いた TypeScript を最終的にユーザースクリプトにビルドする必要があるので、自動化のために grunt を導入しています。
TypeScript -> JS
TypeScript -> JS 変換は grunt-typescript という grunt プラグインがありますので、以下のような感じで設定書いておけば動きます。簡単ですね。
typescript: userscript: src: ['src/userscript/main.ts'] dest: 'compiled/userscript.js' options: comments: false
src
に指定したファイルが中で <reference>
で参照しているファイルも勝手にコンパイル対象となります。src/**/*.js
のように指定してもいいんですが、こうすると必要ないファイルの結合を防ぐ事ができます。
dest
を .js
付きのファイル名にしておけば、1ファイルに結合してくれます。(もちろんファイル構造を維持したまま .ts -> .js の map もできます)
HTML / CSS の埋め込み
ユーザースクリプトでは、最終的に1つの JavaScript ファイルにまとめる必要があるため、HTML や CSS などの静的ファイル類も JS ファイルに埋め込む事にしました。
Handlebars などのテンプレートエンジンを使っても良かったのですが、テンプレートとしてではなく静的ファイルとしてで十分だったので、超簡単なものを自作しました。
色々考えた結果、ファイル結合をしてくれる grunt-contrib-concat を使う事にしました。設定は以下のように書いています。
concat: resources: files: 'compiled/resource.js': ['resources/*.html', 'resources/*.css'] options: banner: "var Resource = { html: {}, css: {} };\n" process: (content, path) => name = path.replace(/^.+\/|\..+$/g, '') content = JSON.stringify(content.toString().replace(/^\s+|\s+$/g, '').replace(/\s*\n\s*/g, "\n")) if /\.css$/.test(path) "Resource.css['#{name}'] = #{content};" else "Resource.html['#{name}'] = #{content};"
options.banner
は結合後のファイルのヘッダーとして付与される文字列です。
options.process
は結合されるファイルそれぞれについて、ファイルの内容とパスを引数にして呼び出され、return した文字列が結合される内容として使われます。
上記を実行すると、resources/
以下の html と css が compiled/resource.js
に、以下のような形で出力されます。
var Resource = { html: {}, css: {} }; Resource.html['hello'] = "<div class=\"hello\">\nHello\n</div>"; Resource.html['world'] = "<div class=\"world\">\nworld!\n</div>"; Resource.css['hello'] = ".hello {\ncolor: red;\n}"; Resource.css['world'] = ".world {\ncolor: red;\n}";
あとは、これを以下のように読み込めるようにしました。
var $hello: JQuery = Resource.load('hello'); var $world: JQuery = Resource.load('world');
TypeScript 側のコードは以下です。ポイントは export declare var html
のように declare
で宣言のみしている事で、これにより上記で作られる JS ファイルでの設定を上書きしないようにしています。
ユーザースクリプト用の加工
ユーザースクリプトとして動かすには、以下の点に気を付ける必要があります。
専用のヘッダーは、そのまま TypeScript ファイルに書いておいてもいいのですが、「ファイルの先頭」である必要があったり、バージョン情報など package.json
に書いてある情報と重複したりするので、テンプレートから動的に生成して、コンパイル済み JS ファイルに grunt でくっつける事にしました。
また、グローバル名前空間の汚染については、スクリプト全体を (function() { })()
みたいな即時関数で囲えばいいんですが、あいにく TypeScript 単体では出来ないので、これも grunt でやる事にしました。
以下のようなテンプレートを作っておき
etc/userscript/header.txt
// ==UserScript== // @name <%= pkg.name %> // @version <%= pkg.version %> // @author <%= pkg.author %> // @copyright <%= pkg.copyright %> // @description <%= pkg.description %> // @namespace http://example.com/ // @include http://example.com/ // ==/UserScript==
etc/userscript/intro.txt
(function () {
etc/userscript/outro.txt
})();
同じく grunt-contrib-concat の設定を書き足します。
concat: userscript: files: 'dist/niconicofavlist.user.js': [ 'etc/userscript/intro.txt' 'compiled/resource.js' 'compiled/userscript.js' 'etc/userscript/outro.txt' ] 'dist/niconicofavlist.meta.js': [] options: banner: grunt.file.read 'etc/userscript/header.txt'
options.banner
に指定する事で、自動的に grunt.template
で処理されるので、テンプレート内の <%= pkg.version %>
などは package.json
に書いておいた情報がそのまま埋め込まれます。
ついでに、.meta.js
というヘッダーのみが含まれるファイルを生成しています。この URL を @updateURL としてヘッダーに書いておくと、@version
を見てアップデートを確認してくれます。
終わりに
TypeScript は便利なのでみんな使えばいいと思います><
Node 学園祭では存在感が薄くて残念でしたがw