Node.js VexFlowでGruntを使う
Node.jsに戻る
概要
VexFlowがGruntというプロセスフロー管理システムを使っているようで、javascriptのプロジェクトを変更する都度、Gruntでビルドすることによって効率的なjavascriptの体系に変換してくれるそうなのです。スクリプトを変更する都度、手動で毎回やってると日が暮れるらしい。こういうツールは他にもあるらしいので、Gruntが特別抜きんでてるわけでもないのだそうですが、VexFlowが採用しているところをみるとかなり一般的な方法と言えると思います。javascript系のプロジェクトファイルにGruntfile.jsやPackage.jsonというファイルがあれば、Gruntというシステムを使っていると見て、ほぼ間違いないそうです。フェイクで配置していたりするのは例外でしょうけど。
MathJaxもそうだったけど、とても読みにくい形式にjavascriptが変換されているのはこういう仕組みをつかっているからだそうです。minify(ミニファイ)とかって呼んでるらしい。素人の自分にはそんな技術があることすら、よくわかっていなかったです。自分なりにVexFlowを改造して使いやすくしようかなって思ったことに端を発して、こんなものと付き合うことになりました。
導入
GitやNode.jsをインストールしたことを確認しておきます。Versionが古くなってるかもなとおもったら更新しておきましょう。いらぬ躓きになります。まず、VexFlowの一式を配置しているフォルダに移動します。Gruntfile.jsやPackage.jsonがあるフォルダです。そこで
Node.jsの更新
>npm install -g n >n latest
npmの更新
>npm install -g npm@latest
VexFlowに必要なGrunt用モジュールのインストールは以下のコマンドです。
>npm install
ってするだけで必要なノードパッケージモジュールがインストールされます。プロジェクトごとに必要なファイルやツールを分けれるのがGruntの特徴だそうです。なのでPackage.jsonに書かれている内容のモジュールがインストールされて、プロジェクトをダウンロードした人がすぐにプロジェクトに参加できるという、そんな感じの環境を提供できるので、とても便利なものらしいです。
それもこれも、知っていればのことですが
次に
>npm start
ってするだけでいいらしいのですが、自分の場合は初心者だったせいか以下のような設定をしないとダメでした。
>git init Initialized empty Git repository in (Path)/vexflow/.git/
さらに
>git config --global core.ignorecase false >git config --global core.autocrlf false >git config --global user.email (メールアドレス) >git config --global user.name (名前)
もう一度
>git init Reinitialized existing Git repository in (Path)/vexflow/.git/ >git add . >git commit -m "Initial commit" [master (root-commit) 72a8f42] Initial commit 271 files changed, 103207 insertions(+) create mode 100644 (ファイル名) … create mode 100644 (ファイル名) >git rev-parse HEAD 72a8f4254fd6e62d14e6a086cae49983cd0ea743
SHA-1ハッシュというものが取得できるようになりました。これでいいらしいです。
次に
>npm start
とするとGruntが動き始めます。もちろんビルドエラーになることもあり得ます。ここからは入手したプロジェクトとの闘いです。
ESMはECMAScriptModuleの略。Lintは繊維工業なんかで使われていた用語で糸から素材から異物を取り除くという作業のことで、それが転じて情報処理では不適切な状態を発見して修正する作業として使われるようになったDebugとはまた違う意味の是正の工程です。eslintはECMAScript向けのLint処理です。
ERROR in tests/accidental_tests.ts:28:5 TS2304: Cannot find name 'QUnit'.
ってなると
npm install qunit
ってするといいらしい。
ってなったってことは、Node.jsやnpmが古いってことなんで、最初からやり直しした方がいいです。ここまで来てそんなこと言うなよ!って思うでしょ。でも、ここまでこないとわからないよ。
Grunt動作
で、うまく行くと、以下のようになるようです。Gruntすげぇ。
>npm start
>> vexflow@4.2.2 start
>> grunt
Running "clean:build" (clean) task
>>> 1 path cleaned.
Running "webpack:prodAndDebug" (webpack) task
asset vexflow.js 969 KiB [emitted] [minimized] [big] (name: vexflow) 1 related asset
asset vexflow-bravura.js 557 KiB [emitted] [minimized] [big] (name: vexflow-bravura) 1 related asset
asset vexflow-petaluma.js 547 KiB [emitted] [minimized] [big] (name: vexflow-petaluma) 1 related asset
asset vexflow-gonville.js 492 KiB [emitted] [minimized] [big] (name: vexflow-gonville) 1 related asset
asset vexflow-leland.js 447 KiB [emitted] [minimized] [big] (name: vexflow-leland) 1 related asset
asset vexflow-core.js 347 KiB [emitted] [minimized] [big] (name: vexflow-core) 1 related asset
orphan modules 1.61 MiB [orphan] 110 modules
runtime modules 5.22 KiB 24 modules
cacheable modules 6.87 MiB
./entry/vexflow.ts + 110 modules 1.61 MiB [built] [code generated]
./entry/vexflow-bravura.ts + 103 modules 1.16 MiB [built] [code generated]
./entry/vexflow-gonville.ts + 103 modules 1.06 MiB [built] [code generated]
./entry/vexflow-leland.ts + 103 modules 1.02 MiB [built] [code generated]
./entry/vexflow-petaluma.ts + 103 modules 1.12 MiB [built] [code generated]
./entry/vexflow-core.ts + 98 modules 923 KiB [built] [code generated]
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
vexflow.js (969 KiB)
vexflow-bravura.js (557 KiB)
vexflow-gonville.js (492 KiB)
vexflow-leland.js (447 KiB)
vexflow-petaluma.js (547 KiB)
vexflow-core.js (347 KiB)
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
vexflow (969 KiB)
vexflow.js
vexflow-bravura (557 KiB)
vexflow-bravura.js
vexflow-gonville (492 KiB)
vexflow-gonville.js
vexflow-leland (447 KiB)
vexflow-leland.js
vexflow-petaluma (547 KiB)
vexflow-petaluma.js
vexflow-core (347 KiB)
vexflow-core.js
WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
webpack 5.81.0 compiled with 3 warnings in 133284 ms
asset vexflow-font-bravura.js 204 KiB [emitted] [minimized] (name: main) 1 related asset
orphan modules 254 KiB [orphan] 2 modules
runtime modules 396 bytes 2 modules
./entry/vexflow-font-bravura.ts + 2 modules 254 KiB [built] [code generated]
webpack 5.81.0 compiled successfully in 64070 ms
asset vexflow-font-leland.js 93.4 KiB [emitted] [minimized] (name: main) 1 related asset
orphan modules 114 KiB [orphan] 2 modules
runtime modules 396 bytes 2 modules
./entry/vexflow-font-leland.ts + 2 modules 114 KiB [built] [code generated]
webpack 5.81.0 compiled successfully in 63967 ms
asset vexflow-font-petaluma.js 193 KiB [emitted] [minimized] (name: main) 1 related asset
orphan modules 219 KiB [orphan] 2 modules
runtime modules 396 bytes 2 modules
./entry/vexflow-font-petaluma.ts + 2 modules 219 KiB [built] [code generated]
webpack 5.81.0 compiled successfully in 63912 ms
asset vexflow-font-gonville.js 139 KiB [emitted] [minimized] (name: main) 1 related asset
orphan modules 158 KiB [orphan] 2 modules
runtime modules 396 bytes 2 modules
./entry/vexflow-font-gonville.ts + 2 modules 159 KiB [built] [code generated]
webpack 5.81.0 compiled successfully in 65953 ms
asset vexflow-font-custom.js 12 KiB [emitted] [minimized] (name: main) 1 related asset
orphan modules 17.8 KiB [orphan] 2 modules
runtime modules 396 bytes 2 modules
./entry/vexflow-font-custom.ts + 2 modules 18 KiB [built] [code generated]
webpack 5.81.0 compiled successfully in 63731 ms
asset vexflow-debug-with-tests.js 2.68 MiB [emitted] (name: vexflow-debug-with-tests) 1 related asset
asset vexflow-debug.js 1.79 MiB [emitted] (name: vexflow-debug) 1 related asset
runtime modules 1.74 KiB 8 modules
modules by path ./src/ 1.61 MiB
modules by path ./src/*.ts 818 KiB 92 modules
modules by path ./src/fonts/*.ts 829 KiB
./src/fonts/load_all.ts 689 bytes [built] [code generated]
+ 17 modules
modules by path ./tests/*.ts 699 KiB
./tests/index.ts 3.01 KiB [built] [code generated]
./tests/accidental_tests.ts 44.2 KiB [built] [code generated]
./tests/annotation_tests.ts 19.2 KiB [built] [code generated]
./tests/articulation_tests.ts 24.4 KiB [built] [code generated]
+ 68 modules
modules by path ./entry/*.ts 1.84 KiB
./entry/vexflow-debug.ts 917 bytes [built] [code generated]
./entry/vexflow-debug-with-tests.ts 971 bytes [built] [code generated]
webpack 5.81.0 compiled successfully in 104788 ms
Running "build:esm" task
ESM: Building to ./build/esm/
TypeScript: Compiling 199 files...
Writing ESM version data to C:\(path)\vexflow\build\esm\src\version.js
Running "build:types" task
Types: Building *.d.ts files in build/types/
TypeScript: Compiling 122 files...
Running "build:docs" task
Done.
不具合・トラブル
- 5816 verbose stack Error: EPERM: operation not permitted, rename path -> dist
Windowの古いNode.jsだと発生することがあるみたいな感じです。いろいろ調べたけど結局、Node.jsのアンインストール・インストールでカタがつきましたよ。
VexFlow固有のGrunt動作について
この記事を書いた理由の一つがVexFlowの各クラスがコンストラクターにオブジェクトを引数で受け取る動作を期待しているにもかかわらず、GruntでMinify(圧縮)処理することによって、オブジェクトによる引数受け取りはできず、キー値を定義した順で引数をうけとることしかできなくなるという現象を知って、なぜこうなるのかということを知るためでした。
Minify化されたbuild/cjs/vexflow.jsの内容をEdgeのページ内右クリックで表示されるコンテキストメニューの[開発者ツールで調査する]のJavaScriptデバッガーを使うと、表示自体はbuild/cjs/vexflow.js.mapに格納されている元々のコード表示に従うことを知りましたし、build/cjs/vexflow.jsは一行にプログラムがまとめられていて、2行目の//# sourceMappingURL=vexflow.js.mapに指定されているmapファイルがデバッグの時に表示されるソースになる仕組みです。
その作業の中で、vexflow.jsの中身を更新してもスーパーリロードしなければ閲覧者には反映しないため新しくスクリプトを変更していくと閲覧者を置いてきぼりにする危険があることを知りました。閲覧者がスーパーリロードしないと、このページは更新されないなんて知る由もないわけで、なんて不親切なWebの世界なんだろうと感じました。なので開発者は更新する都度、ファイルパス名を変更してあたらしいバージョンのvexflow.jsを使い始めたということをアピールしないといけないのです。
VexFlowの公式のコンテンツ配信ネットワークはフォルダ名にバージョン番号をつけて公開していますから、変更する都度、あたらしいvexflow.jsを閲覧者がダウンロードすることになります。自分の場合は、ちょこっと修正しては、公開みたいに大きな変更もしないのですが、vexflow.jsのパス名を更新しないとだめなのです。そうするとバージョンアップするごとにディレクトリを変更していてはビルドファイルだらけになるので、変更して利用するファイルだけにvexflow@4.2.2.04.jsのように管理すればよいのだという結論にいたりました。マップファイルはvexflow@4.2.2.04.js.mapのように差し替えて、vexflow@4.2.2.04.jsの2行目を//# sourceMappingURL=vexflow@4.2.2.04.js.mapにするというメンドクササはありますが、これくらいの作業をして差し替えた方がいいということです。4.2.2を使っていると自分が自覚していればいいので、自分なりに変更して提供しているvexflowという意味でvexflow@1.0.04とかでも問題ないのでしょうけれど、このサイトのバージョンはいくつのモノを使っているのか知りたいという人向けにも4.2.2とかを差し込んだ方が親切なのかもしれません。そんな人いるとは思えませんけど。
ちなみに以下のようにVexFlowを変更するとオブジェクトを引数にできるようになります。GraceNoteGroupクラスをつかさどる、(vexflow_Root_Path)/src/gracenotegroup.tsの変更内容としては
//** `GraceNoteGroup` inherits from `Modifier` and is placed inside a `ModifierContext`. */
constructor(grace_notes: StemmableNote[], show_slur?: boolean) {
super();
//日本語コメント
if (typeof grace_notes === 'object' && grace_notes !== null && typeof grace_notes.notes !== 'undefined') {
this.grace_notes = grace_notes.notes; // オブジェクトかつtypeキーがある場合
if(typeof grace_notes.slur !== 'undefined') {
this.show_slur = grace_notes.slur;
}
}
else {
this.grace_notes = grace_notes; // オブジェクトでない場合、またはnotesキーがない場合
this.show_slur = show_slur;
}
this.position = Modifier.Position.LEFT;
this.width = 0;
this.slur = undefined;
という具合にif文を追加するといいみたいですが、TSC(TypScript Compiler)のチェックが厳しすぎて、自分にはどうやって変更していいのかがわからず、(vexflow_Root_Path)/Package.jsonファイルの中身の
"scripts": {
"start": "grunt --force",
のような変更してコンパイルの警告を無視する指定をしないと駄目でしたね。TypeScript Compilerについて勉強しないと駄目だ。高度すぎて、どうしろといっているのかわからない。そんじょそこらのjavascriptの参考書を読んでいるだけでは、警告のないjavascriptを書くのは不可能だと感じています。上記のjavascriptでもいいじゃんと思う人は多いと思うのですが、そこまで厳密なチェック必要なのかなって思います。ESLintはさらに厳しい。関数の定義する順序でさえも注意してきますからね。
まだ解決していませんが、以下のようなエラーを無視している状態です。
ERROR in ./src/accidental.ts:529:66
TS2339: Property 'type' does not exist on type 'never'.
527 | L('New accidental: ', type);//日本語コメント
528 |
> 529 | if (typeof type === 'object' && type !== null && typeof type.type !== 'undefined') {
| ^^^^
530 | this.type = type.type; // オブジェクトかつtypeキーがある場合
531 | }
532 | else {
ERROR in ./src/accidental.ts:530:24
TS2339: Property 'type' does not exist on type 'never'.
528 |
529 | if (typeof type === 'object' && type !== null && typeof type.type !== 'undefined') {
> 530 | this.type = type.type; // オブジェクトかつtypeキーがある場合
| ^^^^
531 | }
532 | else {
533 | this.type = type; // オブジェクトでない場合、またはtypeキーがない場合
ERROR in ./src/gracenotegroup.ts:101:87
TS2339: Property 'notes' does not exist on type 'StemmableNote[]'.
99 | super();
100 | //日本語コメント
> 101 | if (typeof grace_notes === 'object' && grace_notes !== null && typeof grace_notes.notes !== 'undefined') {
| ^^^^^
102 | this.grace_notes = grace_notes.notes; // オブジェクトかつtypeキーがある場合
103 | if(typeof grace_notes.slur !== 'undefined') {
104 | this.show_slur = grace_notes.slur;
ERROR in ./src/gracenotegroup.ts:102:38
TS2339: Property 'notes' does not exist on type 'StemmableNote[]'.
100 | //日本語コメント
101 | if (typeof grace_notes === 'object' && grace_notes !== null && typeof grace_notes.notes !== 'undefined') {
> 102 | this.grace_notes = grace_notes.notes; // オブジェクトかつtypeキーがある場合
| ^^^^^
103 | if(typeof grace_notes.slur !== 'undefined') {
104 | this.show_slur = grace_notes.slur;
105 | }
ERROR in ./src/gracenotegroup.ts:103:29
TS2339: Property 'slur' does not exist on type 'StemmableNote[]'.
101 | if (typeof grace_notes === 'object' && grace_notes !== null && typeof grace_notes.notes !== 'undefined') {
102 | this.grace_notes = grace_notes.notes; // オブジェクトかつtypeキーがある場合
> 103 | if(typeof grace_notes.slur !== 'undefined') {
| ^^^^
104 | this.show_slur = grace_notes.slur;
105 | }
106 | }
ERROR in ./src/gracenotegroup.ts:104:38
TS2339: Property 'slur' does not exist on type 'StemmableNote[]'.
102 | this.grace_notes = grace_notes.notes; // オブジェクトかつtypeキーがある場合
103 | if(typeof grace_notes.slur !== 'undefined') {
> 104 | this.show_slur = grace_notes.slur;
| ^^^^
105 | }
106 | }
107 | else {
ちなみにChatGPT、Gemini、Copilotですら上記エラーは修正できませんでした。わかる人は存在してるんだろうけど、ごく少数の優秀な人たちだけにわかるerrorだと思います。上記の生成AIが教えてくれた参考サイトでさえも解決法は示せていませんでした。動作自体にはなんら問題はありませんけどね。やっちゃいけないことをやってはいるらしい。NEVER型の可能性を排除してもエラーは消えませんでした。オプショナル演算子?をつけてもダメ、<string>type.typeをしてもダメだし、type.type:anyやtype.type as anyもだめだし、なかなか厳しい。引数が単純なstringか?objectか?の両方の場合があるというのがうまくjavascriptの厳しいチェックをしている場合にはできないのかもしれない。C++の場合は引数の型に合わせてコンストラクタが選択されるので、こんなことで悩まなくてもよかったけどな。javascript10級の管理人にはその答えが見いだせない。しょぼいのう。
最終的にはこのように記述すると良さそうだということの結論にたどり着きました。コンストラクタの引数に型をきちんと指定するという方法です。第一引数は、このように受け取るかもしれないという感じですね。オブジェクト{xx:xxx, xxx:xxxx}形式か単なる配列か?って感じですね。error TS2339: Property 'xxxx' does not exist on type 'xxxx'エラーは克服できた。けど、TypeScriptCompilerの激しいチェックに恐れをなす管理人なのでした。怖い。こんなこと守り切れるエンジニアどれくらいいるんだろう。
accidental.ts
constructor(type: string | { type: string }) {
super();
L('New accidental: ', type);//日本語コメント
if (typeof type === 'object' && type !== null && typeof type.type !== 'undefined') {
this.type = type.type; // オブジェクトかつtypeキーがある場合
}
else {
this.type = type.toString();//type; // オブジェクトでない場合、またはtypeキーがない場合
}
this.position = Modifier.Position.LEFT;
this.render_options = {
// Font size for glyphs
font_scale: Tables.NOTATION_FONT_SCALE,
// Padding between accidental and parentheses on each side
parenLeftPadding: 2,
parenRightPadding: 2,
};
this.accidental = Tables.accidentalCodes(this.type);
defined(this.accidental, 'ArgumentError', `Unknown accidental type: ${type}`);
// Cautionary accidentals have parentheses around them
this.cautionary = false;
this.reset();
}
gracenotegroup.ts
constructor(grace_notes: StemmableNote[] | {notes: StemmableNote[], slur?: boolean}, show_slur?: boolean) {
super();
if (Array.isArray(grace_notes)) {
this.grace_notes = grace_notes;
this.show_slur = show_slur;
}
else{
this.grace_notes = grace_notes.notes;
this.show_slur = grace_notes.slur;
}
this.position = Modifier.Position.LEFT;
this.width = 0;
this.slur = undefined;
this.voice = new Voice({
num_beats: 4,
beat_value: 4,
resolution: Tables.RESOLUTION,
}).setStrict(false);
this.render_options = {
slur_y_shift: 0,
};
this.beams = [];
this.voice.addTickables(this.grace_notes);
return this;
}
あ、あとGruntfile.jsの358行目を以下のように変更して無理やりvexflow.jsを出力しています。やり方がわからないことが多すぎる。
// return getConfig([VEX_DEBUG, VEX_DEBUG_TESTS], DEVELOPMENT_MODE, BANNER, 'Vex', watch);
return getConfig('vexflow', PRODUCTION_MODE, !BANNER, 'Vex', watch);
その変わりデバッグ版とデバッグテストが出力されませんので、一度は元の状態で出力したものをとっておいてマージしないとだめですね。たまにマージしたほうがいいです。両方出力するようにしたいな。あ、上記を元に戻して343行目を追加したらうまくいきましたね。
function fontConfigs(watch = false) {
return [
getConfig('vexflow', PRODUCTION_MODE, !BANNER, 'Vex', watch),
getConfig('vexflow-font-bravura', PRODUCTION_MODE, !BANNER, ['VexFlowFont', 'Bravura'], watch),
Node.jsに戻る