CLionが最強かもしれない
WebStorm大好きっ子な私ですが、WebStormはWEB系技術をターゲットとしていてC/C++のIDEサポートはありません。Go言語関連がGoLandに製品格上げなされた結果としてプラグイン提供がなくなってしまったのと同様にC/C++のプラグインもどうやらWebStormに提供されていない。素のWebStormでもかろうじてカラーリングやカッコ対応などの静的テキストファイルの範囲で頑張れるような機能はあったので、メモ帳よりはマシだと考えてましたがヘッダを見るのにもSpotlight検索かよ、と、気が短くなった中高年には無理無理。C++「も」気持ちよく書ける環境を模索して構築します。前提はTypeScriptで気持ちよく書けることで、さらに同じその上でC++。
CLion
信頼するJetBrainsの製品群を眺めてそれっぽいのをピックアップしました。ReSharper C++なるものはVisualStudioのプラグインだって書いてありますから初見除外すると、CLionとAppCodeというのになります。まずMac向けっぽい体裁のAppCodeをトライアルして見ましたがいきなりXCodeが無くて動かないぞエラーを出すので即アンインストールしました。私はXCodeを入れずにcommandline-toolsで凌いでいるし、今後もフルのXCodeを入れる予定は無い。またAppCodeはメインとしてSwiftとObjective-Cを対象としてMac向けに閉じていました。次いでCLionをトライアル。CLionはプラグイン設定で「NodeJS」と「Vue.js」それぞれのサポートプラグインをインストールできました。なぜか製品デフォルトでTypeScriptとTslintのサポート(当然上位のJavaScriptサポートやHTML5関連なども)は入ってたので私のWebStormの使い方にC++が加わったセットを容易に構成完了。既に動かす前からガッツポーズな気分。node/TypeScript/tslint/Jestは一通り動かして問題ない。
CMake
CLionでC++の新プロジェクトを作る場面でいきなり直面するのは、CMakeを用いた開発への強い推し姿勢です。IDEを一貫してCMake中心に考えてられているように見受けます。これまでCMakeは使ってなかったので手元環境には入れてなかったのですが、/Applications/CLion.app/Contentsを掘って確認すると、clang・cmake・gdb・lldbを含んでいました。CMakeはBrewだともっと新しいバージョンになりますが、CLionがサポートしているバージョンが一個古いものまでなので逆らわずに/usr/local/binへリンクを作って、IDE外部からの利用でも同じものを使うようにします。
$ cd /usr/local/bin $ sudo ln -s /Applications/CLion.app/Contents/bin/cmake/bin/cmake
CLionもその他のIDEでもnode-gypを綺麗にサポートしているものは無いと断じられる中、同じ効能をCMakeベースで構築し直したというCMake.jsがありました。これのチュートリアルをざっと流してみて(10分程度で完了します)。。。ちゃんとビルドできるし、node-gypで実現されていたみたいに自動コンフィギュアかつ自動ツールチェーンで動きます。よしよし。あくまで短時間での感触ですが、CLion-CMake-CMake.jsの組み合わせは筋が良さそうな気がする。
$ yarn add nan cmake-js bindings
WebStormが初年度$129.00/Yなのに対してCLionが初年度$199.00/Yなので、この値段差に柔軟性の違いがあるのでしょう。年末までWebStormのサブスクリプション残しているけど、CLion欲しいな。30日トライアルし尽くして気持ち変わらなければWebStormの半年分がもったいないけど切り替えよう。
とりあえずの設定
CMake.jsがNodeアドオンを作るために必要な環境の情報をかき集めてCMakeの設定を行ってくれるので、それをIDEにも教えてあげないといけない。
$ yarn cmake-js print-configure --debug
このコマンドで、CMake.jsがどんな情報収拾し、CMAKEに設定しているかがダンプされます。
cmake "/Users/masataka_k/Projects/clpoc" --no-warn-unused-cli -G"Unix Makefiles" -DCMAKE_JS_VERSION="3.7.3" -DCMAKE_BUILD_TYPE="Debug" -DCMAKE_LIBRARY_OUTPUT_DIRECTORY="/Users/masataka_k/Projects/clpoc/build/Debug" -DCMAKE_JS_INC="/Users/masataka_k/.cmake-js/node-x64/v10.1.0/include/node;/Users/masataka_k/Projects/clpoc/node_modules/nan" -DNODE_RUNTIME="node" -DNODE_RUNTIMEVERSION="10.1.0" -DNODE_ARCH="x64" -DCMAKE_CXX_FLAGS="-std=c++11 -D_DARWIN_USE_64_BIT_INODE=1 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DBUILDING_NODE_EXTENSION -w" -DCMAKE_SHARED_LINKER_FLAGS="-undefined dynamic_lookup"
ながーいコマンド行のうちのオプション部分をコピーしてCLionの設定画面に流し込む。
メニューでCLion > Preferences > Build, Execution, Deployment > CMAKE を選び、「CMake Options」にペースト。これでプロジェクトを背後でシンボルの構築が行われてIDEにおよそ期待する機能が動作し始めます。
このオプション群は、チュートリアルでコピペしたCMakeLists.txtに流し込まれてくる。
cmake_minimum_required(VERSION 2.8) # Name of the project (will be the name of the plugin) project(addon) # Build a shared library named after the project from the files in `src/` file(GLOB SOURCE_FILES "src/*.cc" "src/*.h") add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES}) # Gives our library file a .node extension without any "lib" prefix set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") # Essential include files to build a node addon, # You should add this line in every CMake.js based project target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC}) # Essential library files to link to a node addon # You should add this line in every CMake.js based project target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
これらの環境変数で穴開けてるところに当てはまるのをなぞると、だいたい何が行われているのかが想像できます。ここまでくるとあと必要なのはIDEではなく「そのもの」の知識ですね。チュートリアルはOKでも、CMakeの使い方の詳しいところはこれから習得しないといけない。
OSXの.frameworkを探す
# 〜省略 if(APPLE) find_library(CORETEXT CoreText) message("CORETEXT= ${CORETEXT}") find_library(FOUNDATION Foundation) message("FOUNDATION= ${FOUNDATION}") set(CMAKE_JS_LIB ${CMAKE_JS_LIB} ${CORETEXT} ${FOUNDATION}) message("CMAKE_JS_LIB= ${CMAKE_JS_LIB}") endif(APPLE) target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
練習としてOSXの.frameworkを探す定義を書いてみました。find_libraryの第1引数は変数名で第2引数以降に色々探す方法を書くのだけど、今回の場合はCoreText.frameworkの頭の部分だけ「CoreText」を書けば見つけてくれる。よってFoundation.frameworkは「Foundation」。チュートリアルでコピペしたCMakeLists.txtでは、CMAKE_JS_LIBが書かれてたけど実際は使われていなかった。もしかするとどっかから飛んでくることがあるのかもしれないけど、今のところは何も。見つけたライブラリを跡地利用としてsetを用いてCMAKE_JS_LIBに積んでみた。プラットフォームの切り分けは、if(APPLE) - endif()でできる。endifに引数渡さなくても問題ないと思うけど、引数「APPLE」を渡すこの書き方の方が読みやすいです。
ごちゃごちゃやってる後の対応
ごちゃごちゃ色々やってる後で、突然今までなんでもなかったところでコンパイルエラーをエディタが表示し始めることがあった。でもコマンドラインからcmake rebuildを実行すると何事もなくビルドが通る。これはIDEのキャッシュが疑わしい。実際にググって見つけたヘルプの通りにinvalidしてrestartしたら元に戻った。
C++でCoreText
NodeモジュールでまずはOSのフォント情報を取ろうと思います。Objective-CでやればサンプルもたくさんあるのだけどここはC++で書きたかった。
#include <ApplicationServices/ApplicationServices.h> #include <nan.h> using namespace v8;
ApplicationServices/ApplicationServices.hを取り込みます。この中にCoreFoundationやCoreTextやCoreGraphics等のOSXの中で特にC++で書けるライブラリのヘッダだけがまとまってます。他はObjective-CかSwiftじゃないと書かせてもらえないみたい。文字列はNSStringではなくCFString。
char* CFStringToUtf8String(CFStringRef str) { if (str == NULL) { return NULL; } CFIndex max = CFStringGetMaximumSizeForEncoding(CFStringGetLength(str), kCFStringEncodingUTF8) + 1; char* buffer = (char*)malloc(max); if (CFStringGetCString(str, buffer, max, kCFStringEncodingUTF8)) { return buffer; } free(buffer); return NULL; } char* getStringAttribute(CTFontDescriptorRef ref, CFStringRef attr) { return CFStringToUtf8String((CFStringRef)CTFontDescriptorCopyAttribute(ref, attr)); } char* getLocalizedAttribute(CTFontDescriptorRef ref, CFStringRef attr, CFStringRef *localized) { CFStringRef value = (CFStringRef)CTFontDescriptorCopyLocalizedAttribute(ref, attr, localized); if (value) { return CFStringToUtf8String(value); } return getStringAttribute(ref, attr); }
逆算的な説明ですが、とにかくCoreFoundationの文字列であるCFStringから、char*へ変換するのは上記の通り。この用意を踏まえて、フォントDescriptorをいじっていきます。キモはCFStringGetCStringで、CoreFoundationの提供する世界から外界たるC++へ値を取り出すのは他のAPIでもおよそこんな感じ。戻りがBoolで成功失敗で値はバッファをポインタ渡しで受け取ります。APIはだいたい汎用な作りになっていて細かにフラグを設定することが多いです。ここではエンコーディング種別がフラグになってます。
CTFontDescriptorCopyLocalizedAttributeでは可能な場合にログインロケールな文字列を戻します。すなわち日本語をメイン言語として利用している私は「ja」でロケールが探され、得られれば第3引数のポインタにjaを返します。多分このログイン言語環境を動的に変更できるAPIもあるんだろうけど、調べてません。とりあえず今の私は不要。
int getFontWeight(CTFontDescriptorRef ref) { CFDictionaryRef traits = (CFDictionaryRef)CTFontDescriptorCopyAttribute(ref, kCTFontTraitsAttribute); CFNumberRef value = (CFNumberRef)CFDictionaryGetValue(traits, kCTFontWeightTrait); CFRelease(traits); float weight = 0.0f; if (CFNumberGetValue(value, kCFNumberFloat32Type, &weight)) { if (weight <= -0.8f) { return 100; } else if (weight <= -0.6f) { return 200; } else if (weight <= -0.4f) { return 300; } else if (weight <= 0.0f) { return 400; } else if (weight <= 0.25f) { return 500; } else if (weight <= 0.35f) { return 600; } else if (weight <= 0.4f) { return 700; } else if (weight <= 0.6f) { return 800; } return 900; } return 0; } char* getSrc(CTFontDescriptorRef ref) { CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(ref, kCTFontURLAttribute); return CFStringToUtf8String((CFStringRef)CFURLCopyPath(url)); } Local<Object> toJSObject(CTFontDescriptorRef ref) { Nan::EscapableHandleScope scope; Local<Object> res = Nan::New<Object>(); CFStringRef localized = NULL; res->Set( Nan::New<String>("font-family").ToLocalChecked(), Nan::New<String>(getLocalizedAttribute(ref, kCTFontFamilyNameAttribute, &localized)).ToLocalChecked() ); res->Set( Nan::New<String>("font-style").ToLocalChecked(), Nan::New<String>(getStringAttribute(ref, kCTFontStyleNameAttribute)).ToLocalChecked() ); res->Set( Nan::New<String>("font-weight").ToLocalChecked(), Nan::New<Number>(getFontWeight(ref)) ); res->Set( Nan::New<String>("src").ToLocalChecked(), Nan::New<String>(getSrc(ref)).ToLocalChecked() ); if (localized) { res->Set( Nan::New<String>("localized").ToLocalChecked(), Nan::New<String>(CFStringToUtf8String(localized)).ToLocalChecked() ); } return scope.Escape(res); }
JSの世界へ持って来るために、Local<v8::Object>へ変換するのですが、いちいち面倒ですね。〜Refというのは全てポインタ型ですが、API毎に細かく定義されているのでCoreTextやCoreFoundationのヘッダファイルを見てあたりつけて、AppleのDeveloperサイトを検索するというのを繰り返す。。。同じCTFontDescriptorCopyAttributeでも、引数のフラグに応じて戻るポインタ型が違うので組み合わせの正解探しが続きましたが、ヘッダをよく見たらフラグのコメントに戻りの型がちゃんと書いてあった。
フォントWeightの変換ロジックは、font-managerからもらってきました。このfont-managerがメンテナンス止まってたので今回のコードを書いてます。ちょっと直すだけで使えたのだけど、繰り返しますが自分で書いてみたかったから。font-managerでのOSX版はObjective-Cを用いてます。
NAN_METHOD(getAllLocalFonts) { CTFontCollectionRef collection = CTFontCollectionCreateFromAvailableFonts(NULL); CFArrayRef results = CTFontCollectionCreateMatchingFontDescriptors(collection); CFRelease(collection); if (results) { CFIndex i, count = CFArrayGetCount(results); Local<Array> res = Nan::New<Array>(count); for (i = 0; i < count; i++) { res->Set(i, toJSObject((CTFontDescriptorRef)CFArrayGetValueAtIndex(results, i))); } CFRelease(results); info.GetReturnValue().Set(res); } else { info.GetReturnValue().Set(Nan::New<Array>(0)); } } NAN_MODULE_INIT(Init) { Nan::Export(target, "getAllLocalFonts", getAllLocalFonts); } NODE_MODULE(fontFinder, Init)
JSからアクセスされる関数はこちら。C++というよりボキャブラリを問われる書き方が過ぎて、新種のDSLみたいな。。。先日はAsyncの作りを調べてましたが、結局ローカルのAPIを叩くのは完全にSyncで十分だった。500以上のフォントが入っていてもすぐに値が返って来ます。
node-gypそしてnanへ。おまけでTypeScript
nodeのアドオンを調べてたら本もなければ情報も少なく、この土日ずっと家族には仕事が忙しくてヤバいという体裁を装い篭ってたので、せめて記録ぐらいは残そうとサンプルを書きました。サンプルは超シンプルになりましたがこれは理解してからシンプルに書き直した結果で、元は何倍ものゴミクズです。
承前
- nodeだけではできないことをやる。
- 動画や画像の出力や解析はゼロから書くのは無理ゲーだけど良いライブラリが既にある
- OSにインストールされているフォントを取得して使う。更に出力サイズはきっちり計る
- パフォーマンス
- 極重な処理をハイパーにこなす
既存のコマンドをシェルで叩き標準入出力やファイルシステムでツールをチェーンするのは手軽に実現できます。これは手元環境だけでなく、AWS Lambdaとかのサーバレス環境も要はDockerなのでこの方法は有効です。魔法を見て驚いて中身見たらexecやspawnかよとズッコケることたまにある。
無い物はシェルコマンドをGoやSwiftででも作れば良いのでしょう。そのほうがUNIX的な思考だし再利用もしやすい。でも、なんかズッコケるんだよね。燃えない。TypeScriptで楽に書きたいのに、nodeで無理なところ探してアドオン書こうというのは、やってみたかったという初めから答えあってのことです。手間を考えたらあんまり正解じゃなかった気もするけど、ギリギリ日曜の夜にはできるようになったからOK。
nodeのアドオンを作る
nodeのアドオンはnode-gypを使って作るのが便利なようです。これはマルチプラットフォーム開発環境を提供してくれて、プラットフォーム毎にMakefile等を調整してくれる。しかもnpmと組み合わせて利用者の手元でOSとnodeのバージョンに応じてコンパイルすることができるので配布が柔軟になります。あらかじめよくあるOSとnodeの組み合わせについてバイナリを用意しておくためには、node-pre-gypというのもあります。これはAWSに提供側ビルドをホストしておくことがすぐできる。
nodeのアドオンというのは結局はそのJSインタープリタであるV8の拡張なのですが、こいつが後方互換軽視なアグレッシブな姿勢で作られているらしく、つられてnodeもアグレッシブにならざるを得ない様子なので、過去からの互換性を維持するための抽象層がマクロとC++テンプレートで提供されています。それがNan。nodeアドオンの作りかたでググったら全く違ういろんな書き方が出てきます。これは記事が書かれた時期によるV8のAPI変化であり、書き手それぞれのNanの適用範囲の差であり、中にはNanを使っていない例も多いためでした。よってスマートかつモダンにnode-gypでV8かつフルNanなプログラミングをモノにしましょうという算段です。
ざっくりとした手順
実際はサンプル見てもらうことにして、ざっくりと手順をまとめると以下の感じ。
- nodeのプロジェクトを作る。フォルダにpackage.jsonがあることが最低。
- nodo-gyp、nanを用意する。node-gypはグローバルに入れるのが大勢のように思われる。
- binding.gypを書く。これはnode-gypの唯一設定ファイル。
- Nanの利用はおきまりのフレーズをここに書く。
- 驚いたのはnodeのシェル実行でrequireを用いていた。
node -e "require('nan')"
- C++のソースコードを書く。V8とNanを利用。
- package.scriptsに、"{ install": "node-gyp rebuild }" を追加する。
- 開発時のビルドはこれをnpmやyarnで実行する
- 公開後は利用者がnpm installないしyarn addしたと同時にビルドが走る
- ちなみにnode-gyp rebuildは
node-gyp clean && node-gyp configure && node-gyp build
- マルチプラットフォームはbinding.gypにうまく書ける仕組みがある。サンプルでは利用してない。
- 既存ライブラリのリンクはbinding.gypにうまく書ける仕組みがある。サンプルでは利用してない。
Nan::AsyncWorker
サンプルでは、TypeScriptコードから文字列の引数とコールバックをアドオンに送る->アドオンは文字列の引数を保存して作業を行い(サンプルでは何も作業していないけど)、結果がOKだったら非同期にコールバックに返す -> コールバックのTypeScriptコードが実行される、という段取りになります。
ポイントはNan::AsyncWorkerがlibuvをラップしていて別プロセスを立ち上げていることです。ハイパーな処理はメインに迷惑かけずにここでやってくれという。サンプルのような軽い処理だったら本当は同プロセス同期実行でいいですね。その上でさらなるポイントは非同期に入る前に文字列を保存しているところです。V8の環境では常にガベージコレクションが走っていて、メモリの解放はAPIの中で強力にサポートされているので、別プロセス非同期実行だと解放された後にメモリアクセスしてしまいます。ここが私の週末の最後の壁で何度もクラッシュさせてました。今はstrcpyでコピーしているけど多分V8のAPIの中にうまい書き方が隠れていそう。 *1
Nan::New<String>("Hello ").ToLocalChecked()
頻出イディオムとして上記のようなコードを書くことになりますが、Nan::Newでv8::MaybeLocalというクラスを作り、ToLocalCheckedでガベージコレクションの対象となるように登録します。
TypeScript
ここまでの論点でTypeScriptは全く関係ないです。全編C++。利用のコツとしては、bindings
なるライブラリを使うことです。const binding = require('bindings')(<binding.gypのtarget_name>);
bindingsはバイナリの位置を探します。
const binding = require('bindings')('helloLib'); export const greetingPromise = (name: string): Promise<string> => { return new Promise<string>((resolve) => { binding.nativeHello(name, (msg: string) => { resolve(msg); }); }); };
こんな感じで、最近流行りのPromise化も可能。
import { greeting, greetingPromise } from '../hello'; test('greetingPromise', async () => { console.warn(await greetingPromise('Promise World!')); });
こんな感じにasync-awaitで書けます。素敵。
気づき
- V8はガベージコレクションをやるんだなということを思い出せばかなり光が射してくる
- コールバックの有無と同一プロセス/別プロセスは違うこと
- Nanは常に何かを楽にしてくれようとしてる。彼の親切心が何処にあるのかを探せば真実が見える
25年ぶりのC++、そして新たな問題
対象言語としてC++ 11が必須となっています。で、25年ぶりだから遠い記憶のそれは当然C++ 11じゃないんですよ。私の経験にあるのは学生の時に16bitのVC++でした。その後すぐにDelphiで更にはJava時代がきちゃう。初見はどこまでがマクロでどこから何のAPIで、言語仕様的なものはどこなのかわかりませんでした。結局Nanマクロが黒魔術であり、私が単にC++を忘れていただけでC++ 11的な何かというのは少なかったのですが。。。
ところでそもそも何でこんなことをしているかというと、まずはC/C++で書かれた魔法的ライブラリ達を取り込むためですが、次いではOSにアクセスするためです。しかしそのOSとは私の場合はOSXなのです。これが問題。OSXは基本的にObjective-Cであり、近年Swiftで代替されてつつあるわけです。C++からSwiftをイージーに呼べればアドオンのベースだけC++で書いてあとはSwiftで良かったのだけど、結構調べましたがC++からSwift(SwiftからC++じゃなく)をうまく使う方法がわからなかったので「おいおい、この年でObjective-Cかよ。俺はもう45才だぞ」と暗い気持ちになりました。Objective-CはiPhoneの初期のころにいくらかやりましたが、なんかしっくり来なかったのでちゃんと習得しなかった。老い先短いので、同じく老い先短いObjective-Cは避けたい。その点でC++はいつまでも老いずに良いね。
更に調べると、CoreGraphicsやCoreTextといった一部のフレームワークはC++でOKなよう。でもC++版のガイドやサンプルは当然皆無。OSXはOSSじゃないからヘッダの先はブラックボックスなので、ヘッダを読んでObjective-C版のドキュメントやサンプルを参考にするしかない。ま、OSXの範囲でやりたいことは幸いにCoreGraphicsやCoreTextに集約されているようなのでボチボチやります。
*1:隠れてた。AsyncWorkerのSaveToPersistentとGetFromPersistentを使う
node v10.1.0は回復
node v10.1.0がリリースされていたので、nで切り替えて一通りテストやビルドを動かしてみました。
(node:26203) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
ほぼ全てと言っていいぐらいに頻繁にBufferコンストラクタのワーニングが出ますけど、私の手元ではv10.0.0の時が嘘のように全てきちんと動きます。10.1.0でいいですね。
node-canvasの型定義備忘
node-canvasの型定義を使うところだけ書いてみたけど目的に敵わなかったので、定義だけ備忘。いつか使うかも。
declare module 'canvas' { import * as Stream from 'stream'; interface PNGOptions { palette: Uint8ClampedArray; backgroundIndex?: number; } interface JPEGOptions { bufsize?: number; quality?: number; progressive?: boolean; disableChromaSubsampling?: boolean; } interface Canvas extends HTMLCanvasElement { inspect(): string; pngStream(options?: PNGOptions): Stream.Readable; jpegStream(options?: JPEGOptions): Stream.Readable; // pdfStream(): Stream.Readable; } interface Image extends HTMLImageElement { inspect(): string; } interface FontFace { family: string; weight?: string; style?: string; } function registerFont(src: string, fontFace: FontFace): any; function createCanvas(width: number, height: number, type?: any): Canvas; // function loadImage(src: string | Buffer): Promise<Image>; }
v2.0.0-alpha12でregisterFontしてcreateCanvasで作ったヘッドレスCanvasに描画してpngStream/jpegStreamで書き出す一連の流れは問題ないが、loadImageで爆死。pdfStreamも不調。
@types/nodeは治った
@types/nodeの10.0.4が出てたので試しにupgradeしてみたらTypeScriptのコアLibとぶつかる問題は解決していました。nにまだ10.0.0より後は来てないので本体入れ替えないから定義だけupgradeの意味はないけど、害もなし。
node v10.0.0は地雷
node v10.0.0は地雷バージョンでした。すでに@types/nodeがv10.0.0でTypeScriptのコアLibとURLなどいくつかの型定義が被って爆死という案件がありましたが、今回はts-jestの環境で以下のテストが沈黙してしまうという問題を抱えています。非TypeScriptな素のjest環境なら大丈夫なので辛い。Babel挟むのは試してません。
describe('silence', () => { test('error', () => { fail('silent'); }); });
これだけでテストランナーが沈黙のまま吹っ飛ぶ。これを $sudo n 9.11.1
と前のバージョンに切り替えるとts-jestでもしっかり動きます。v10.0.0はTypeScriptクラスタは回避で。APIがPromise多用で近代的に書き換わったらしいが、普通にSPAやWEBサーバ書いてる限りには関係ないし。
偶数バージョンで根源的な不都合が出ると戸惑っちゃう。。。
この構成が正義。不用意に$ brew upgrade
でnodeのバージョンが変わらないし、nで適宜バージョンを変更して試せる。