YAML/JSONでコマンド定義が書けるテンプレートジェネレータを作っている
この記事はNodeJS Adventcalendar 2016 の19日目の記事です。 もうクリスマスということで、大幅に遅れてしまって申し訳ございません。
色々とnodejsのCLIや、electronやwebなどを作っていく中で、僕の利用するスタックはだいたい同じなので、毎回package.jsonから書いていくのだるいなーというところから、YAML/JSONで定義ができる(Yet Another Yeoman的な)ツール作ろう、と思って書いて、それからずっと放置してました。 良い機会 & やる気を取り戻すためにもちょっと紹介します。
hiaというテンプレートジェネレータです。
Getting Started
hiaを利用して、簡単なジェネレータを作ってみました。 これはreact + flux-util + flowtypeのテンプレートを作ってくれます。
npm install -g @joe-re/hia-react-fluxutil
でinstallできます。
あとは以下のように適当なディレクトリを作り、hia-flux init
コマンドを叩いてみてください。
$ mkdir hia-flux-sample $ cd hia-flux-sample $ hia-flux init created src/components/Counter.jsx created src/actions/CounterAction.js created src/stores/CounterStore.js created src/containers/CounterContainer.jsx created src/routers/AppRouter.jsx created src/AppDispatcher.js created src/index.html created src/index.jsx created .flowconfig created .babelrc created webpack.config.js created package.json
ファイルが作成されたら、npm install
の後にnpm start
を叩くことで、アプリケーションが動きます。
YAML定義の例
実際にどんな感じのyamlを定義するのか、というとこんな感じです。
--- basedir: ./test command: hia subcommands: test:view: description: generate view template. input: true templates: src: fixtures/component.ejs script: fixtures/scripts/test.js output: dir: test/dist filename: '[name].jsx' args: feature_name: aliase: f description: Feature name. It is used as second directory name.(If you specify calendar, create 'test/dist/calendar/[name].jsx') require_args: aliase: r description: require args. required: true question_args: aliase: q description: This is Question Section. before: fixtures/scripts/before.js question: true default_args: aliase: d description: set default value. default: 'default value'
コマンド定義の説明
command: hia
でcommand名を定義しています。
subcommands: test:view:
みたいな感じでsubcommandを定義しています。つまりこれでhia test:view
コマンドが定義されました。
このコマンドはinput:true
を設定しているので、inputを受け付けます。
つまりhia test:view <INPUT>
という形式のコマンドとなります。
最後にargsセクションで以下のような定義をしています。
args: feature_name: aliase: f description: Feature name. It is used as second directory name.(If you specify calendar, create 'test/dist/calendar/[name].jsx') require_args: aliase: r description: require args. required: true question_args: aliase: q description: This is Question Section. before: fixtures/scripts/before.js question: true default_args: aliase: d description: set default value. default: 'default value'
これらはそれぞれ、CLIのオプションとして渡せます。 --feature_name hoge
ならfeature_nameというオプションでhogeとして受け取れます。ここで必須にしたいパラメータは required: true
で表現します。default valueの設定もできます。
ちなみにこれらの定義をしただけで、なんとなく良い感じのhelpが表示されるようにしています。
$ hia -h Easy and customizable generator system for creating template. Usage: hia <SUBCOMMAND> <INPUT> -h, --help Show list available subcommands and some concept guides. -c, --config specify config file path. Subcommands: hia component <INPUT> generate view template. -t, --text text on your component.
テンプレートのrenderの定義の説明
テンプレートはejsを解釈してrenderします。inputやargsセクションの定義で受け取った値は、そのままinputならinput、argsならそれぞれのオプション名が変数名になります。
templatesセクションには、{ src: 'ファイルパス', name: 'ファイル名の文字列' }
というオブジェクトの配列を設定できます。このsrcにejsのファイルパスを設定します。ここで設定したnameは、outputセクションで{ filename: '[name].js' }
と設定することで出力時の名前として使うことができます。
scriptセクションを設定することで、render直前にスクリプトを挟むこともできます。scriptは以下のように記述します。
module.exports = function script(params) { params.subcommand.output.dir += '/exchanged'; params.cliParams.input += 'Exchanged'; return params; };
paramsはsubcommandとcliParamsの2つのオブジェクトを持っています。 引数で受け取ったオブジェクトを、変更してreturnで返すことでrenderされる直前にパラメータを変更することが可能です。
subcommandはhia.yaml(json)で定義したコマンドの設定を含みます。上の例では出力先ディレクトリを変更しています。 cliParamsはinputやargsで受け取ったparameterを持っています。
所感
1人でやっている分には、初回設定が楽になるわー、ぐらいの感覚で使えているのだけど、大勢の人に使ってもらおうと思うと完全に雑な作りなので、もっと改良していきたい。具体的にはwiredep的な機能付けて、templateに追加でコードが書けるような仕組み欲しいなー、とか思っている。 やっていきを継続して行きたい。
SpectronからElectronアプリケーションのMenuを操作する
この記事は Electron Advent Calendar 2016 の12日目の記事です。
SpectronはElectronアプリケーションのためのE2Eテストツールです。
SpectronはElectronのChrome Driverを通じてアプリケーションの操作を実行できるのですが、メニューの操作には現状対応していません。(詳しくは後述します。)
そこでspectron-fake-menuというSpectronからメニューの操作ができるnpmを作成しました。
使い方
https://github.com/joe-re/spectron-fake-menu/tree/master/example にサンプルアプリケーションがあります。
ちょっと変なアプリですが、メニューからカウントのインクリメント、デクリメントをするカウンターです。
テストコードでは、インクリメントとデクリメントをspectron-fake-menuを使って操作し、成功していることを検証しています。
以下のように、SpectronのApplicationクラスのインスタンスをapplyメソッドに渡すことで、spectron-fake-menuのセットアップは完了します。
const Application = require('spectron').Application; const fakeMenu = require('spectron-fake-menu'); const app = new Application({ path: electron, args: [ path.join(__dirname, '.') ] }); fakeMenu.apply(app); // apply fake menu
その後は、fakeMenu.clickMenuメソッドでMenuのクリックイベントを発火させることができます。clickMenuメソッドの引数にはラベルの文字列を渡します。
const fakeMenu = require('spectron-fake-menu'); fakeMenu.clickMenu('Count', 'Increment'); // trigger 'Count -> Increment' fakeMenu.clickMenu('Count', 'Decrement'); // trigger 'Count -> Decrement'
そもそもなぜSpectronからMenuの操作ができないのか
以下のIssueがあります。
Testing menu interaction · Issue #21 · electron/spectron · GitHub
require('electron').remote.Menu.getApplicationMenu() API does not serialize to JSON so it can't currently be fetched via Spectron.
MenuのAPIがJSONにserializeされていないので、process間通信が必要なSpectronでは操作が難しいというのがその理由のようです。 同じ様に、dialogモジュールの操作も出来ません。
Can't interact with dialog · Issue #23 · electron/spectron · GitHub
アプリケーションの操作をChrome Driverを通じて行うのが基本理念のSpectronでは、これらのnativeなAPIを必要とする操作に対するサポートはまだ不十分、というのが実情です。
spectron-fake-menuではどうしているのか
Mocking electron modules · Issue #94 · electron/spectron · GitHub
のIssueで紹介されている方法なのですが、Electronは--requireオプションで、Electronの起動直前にスクリプトを差し込むことができます。
これを利用して、Mainプロセスにspectron-fake-menu用のイベントを受け取る口を仕込んでいます。
require('electron').ipcMain.on('SPECTRON_FAKE_MENU/SEND', (_e, labels) => { const item = findItem(require('electron').Menu.getApplicationMenu().items, labels); item.click(); });
あとは簡単で、clickMenuメソッドでそのイベントを中継してMainプロセスに通知すれば良いだけです。
function clickMenu(...labels) { _app.electron.ipcRenderer.send('SPECTRON_FAKE_MENU/SEND', labels); }
実際、この一連の処理のコードは30行程度しかありません。
つらいところ: メニューの操作は非同期処理になる
Exampleのテストコードは以下のようになっています。
function waitForChangeCount(app, count) { return new Promise((resolve, _reject) => { // Since the click event is asynchronous processing in the native layer, wait for the count chang on DOM. // If the expected change does not occur, the test will fail with a timeout. const timer = setInterval(() => { app.client.getText('#count').then((text) => { if (text === count) { clearInterval(timer); resolve(); } }); }, 1000); }); } desctibe('decrement', function() { // ...省略 it('decrement count', () => { return app.client.waitForExist('#count') .then(() => { fakeMenu.clickMenu('Count', 'Decrement'); return waitForChangeCount(app, '-1'); }) .then(() => assert.ok(true)); });
waitForChangeCountメソッドの処理にとてもつらい感が現れています。 clickMenuメソッドでメニューのクリックを発火してから画面上にその変化が現れるまでが非同期なので、タイマーをセットしてDOMの変化を監視しています。
nativeなレイヤーでのclickイベントのやり取りがそもそも非同期なので、外部から処理を差し挟んで同期APIにするのが非常に難しいです。(どなたかアイディアあれば教えてください。)
dialogモジュールのテスト時のモックも同じ理屈でできる
File -> Saveをクリックしたらdialogを表示する、みたいな操作は結構あると思います。 用意されているAPIのみでテストを書くとすると、現状ではSpectronでこのケースのE2Eテストを書くのは難しいです。
しかし同じ理屈で、preloadスクリプト内でモックオブジェクトをセットしてしまえば、dialogモジュールの処理のモックも可能です。
例えば、以下の処理はdialogモジュールのshowSaveDialogメソッドを置き換えています。
const nativeRequire = require; require = function(moduleName) { if (moduleName === 'electron') { const electron = nativeRequire('electron'); electron.dialog.showSaveDialog = mockShowSaveDialog; return electron; } return nativeRequire(moduleName); };
当然ですが、やればやるだけE2Eとは何だったのか...みたいなことになってどんどん信頼性が落ちていくので、用法用量は守りましょう。
まとめると
Rendererプロセスからの操作の方がテストは格段に楽です。
でもどうしてもMenuからの操作をテストしたい...という方は是非使ってみてください。
エンジニア立ち居振舞い: 発言にブレーキをかけない
僕自身が普段から意識的にやっているのは発言にブレーキをかけないことだ。
具体的にはslackのpublicな部屋で騒ぐ。 作業中にハマってしまっていかんともし難い時や、クソコードを見つけて怒りが湧いてきた時、などなど。
ハマってる内容を呟いておくと、以前にも同じような経験をした人が反応してくれて一瞬で解決に導かれるかもしれない。
クソコードに対して怒りをぶつけていると、同じように共感した人が現れてやっていきをお互い高められて、いい感じにタスク化できるかもしれない。クソコードは見ているだけでイライラしてくるので、吐き出すだけでも気持ちが宥められる効果がある。
時には実は騒いでいた対象はクソコードではなくて、僕の実力が足りていないがゆえにそう見えてしまっていただけのこともある。それはそれで良くて、一時僕は恥をかくかもしれないけど、引き換えによって得られる学びは比較にならない。
発言にブレーキをかけない、と言うと他人への配慮を欠く発言をしても良い、みたいな印象を受けてしまうかもしれないけど、それは全く違う。ここで大事なのは遠慮しないということと、publicな場で発言しにくい空気を作らないということだ。
人間のパターン認識力はめちゃめちゃ高度なので、内容だけ見ればある程度攻撃性を伴ってしまう発言でも、思いやりを含んだコンテキストや言葉尻であれば、受け手はそれを感じ取ることができる。 コードを憎んで人を憎まずというのと似ている。
人じゃなくてコードを憎みたい気持ちなんだけど、言い方と聞き方次第で人を攻撃してるみたいになりそうで言葉に出せない気持ちに行き場がない
— じょう (@joe_re) 2016年7月6日
じっさいのところ、プロダクションに入ってるコードは最低限レビューされてるはずだし、そのコードを書かなければならなかった当時の状況とか、既存のコードに引きづられたりすることもあるだろうし、書いた人は悪くない
— じょう (@joe_re) 2016年7月6日
という気持ちを持っていて、人は憎みたくないんだけど、悪いコードは悪いと言っていかないとお互いの成長は望めないと思う。
こういう自分の発言に反応があると嬉しいので、他の人の発言もなるべく反応するようにしている。なんでもかんでも反応していると作業時間がなくなってしまうので、なかなか線引きは難しい。自分がある程度近しいコンテキストを持てる内容ならば、という感じ。線引きを迷っているな??と自覚した場合には、ブレーキをかけずに発言するようにしている。分からなければ分からないと発言する。無視してしまうよりは良い。議論が盛り上がっていると、自然と人が集まってきて、そのうち有識者が現れるみたいなことも期待できる。
もしチームに入ったばかりの人が困惑しているようなら、詳しそうな人にメンションしたりもする。チームに入ったばかりの人は、誰がどこに詳しいかが分からないので、そういうフォローが大事だと思う。
こういうものはpublicな場でやることが大事だと思っていて、closedな場でやってしまうとせっかく得られたハマりポイントの知見やクソコードに対する怒りが共有されない。勿体無い。
とはいえTPOは大事で、サポートやセールスも含めた全体連絡用の部屋でクソコードの話題を振るのは違う。
いろいろなコンテキストごとに部屋があるとすごくやりやすくて、今の会社はslackの部屋を誰もが好きに作れるので恵まれてるなー、と思う。(部屋が大量に生まれすぎる問題もあるので、バランスは難しいですね。)
思いやりのあるマサカリを投げ合っていきましょう。