mizdra's blog

ぽよぐらみんぐ

JavaScript で print デバッグ時に変数名を出力する

数列の和を求めるプログラムを作成することになり、意気揚々と以下のようなプログラムを書いたという状況を想像して下さい。

function sum(nums, acc = 0) {
  if (nums.length === 0) return 0;
  if (nums.length === 1) return nums[0];
  return sum(nums.slice(1), acc + nums[0]);
}

const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(sum(nums)); // expected: 55

一見すると何も問題なさそうに見えるプログラムですが、実はバグがあります (皆さん分かりますか?) *1。実際に上記プログラムを実行すると 55 ではなく 10 が出力されます。

こうした場面に遭遇すると、自然と sum 関数が呼び出される度に numsacc がどう変化していくかを知りたくなってきます。この時取りうるデバッグ方法には様々なものがありますが、最も簡単なのは print デバッグでしょう。

function sum(nums, acc = 0) {
  console.log(nums);
  console.log(acc);
  if (nums.length === 0) return 0;
  if (nums.length === 1) return nums[0];
  return sum(nums.slice(1), acc + nums[0]);
}

これを Chrome devtools の Console パネルで実行すると以下のように出力されます。出力がどう整形 (色が付いたり、クリックで折りたたみを展開できたり) されるかは、その JavaScript 実行環境 (Chrome や Firefox、Node.js など) によりますが、大体どの実行環境でも似たような見た目で出力されるはずです。

f:id:mizdra:20210502225505p:plain

複数行に渡る出力を1行にまとめたい

実は console.log は複数の引数を渡せるようになっていて、以下のように書くこともできます。一度に複数の値を渡すと、それらの値は一緒の行にまとめられて出力されるので、1回の呼び出しで複数行出力されて見にくいという不満を解消できます。便利ですね。

function sum(nums, acc = 0) {
  console.log(nums, acc);
  if (nums.length === 0) return 0;
  if (nums.length === 1) return nums[0];
  return sum(nums.slice(1), acc + nums[0]);
}

f:id:mizdra:20210502230145p:plain

ここまではよくある tips ですね。

変数名も出力したい

出力する変数がだんだん増えてくると、出力の何番目がどの変数だったのか判断が難しくなります。当然変数名も一緒に出力して、どの値がどの変数のものかがひと目で分かるようにしたくなってくると思います。

そうした状況でオススメなのが、次のテクニックです。

function sum(nums, acc = 0) {
  console.log({ nums, acc });
  if (nums.length === 0) return 0;
  if (nums.length === 1) return nums[0];
  return sum(nums.slice(1), acc + nums[0]);
}

f:id:mizdra:20210502230518p:plain

console.log の引数リストを { ... } で囲うだけです。これだけで変数名も一緒に出力されるようになります。簡単ですね。

変数名が出力される仕組み

「何でこれで変数名が出力されるの?」という疑問を持った方も居るともうので、軽く解説します。先程のコードは Shorthand property names という、オブジェクトを初期化するためのシュガーシンタックスです。ES2015 で導入された比較的新しい *2 記法です。以下の2つのコードは等価になります。

console.log({ nums, acc });
console.log({
  nums: nums,
  acc: acc,
});

つまり先程のコードは、変数 nums の値を nums という名のプロパティに、変数 acc の値を acc という名のプロパティに持つオブジェクトを console.log で出力していた訳ですね。変数名を出力していたというよりは、たまたま変数名と同じ名前のプロパティ名が出力されていて、変数名が出力されているように見えていた、という感じです。

別解: degugger; を仕込む

結構忘れられがちなのですが、専用のデバッガを使うという手もあります。変数名の表示はもちろん、スタックトレースの表示やステップ実行ができたりと、print デバッグよりもずっと高級なデバッグ体験が得られます。もし高級なデバッグ体験が欲しければこちらを使うのが良いでしょう。

デバッガを利用する最も手軽な方法は、 debugger 文です。変数の値を覗き見したい行に debugger; と書いて Chrome などで実行すると、その行に到達した瞬間にデバッガが自動で起動して、実行が一時停止します *3。後はデバッガのメニューから変数の状態を見たり、ステップ実行してみたりと好き放題できます。

f:id:mizdra:20210502234958p:plain

デバッガは利用するための環境構築が大変なイメージがあり、敬遠されがちですが、便利なのでスキを見て使っていけると良いと思っています。最近の VSCode でもエディタ組み込みのデバッガの開発に力を入れていたりと、どんどんデバッガの利用が容易になっているように感じています。debugger; くらいなら簡単に始められるので、是非使ってみてはいかがでしょうか。

おまけ1

実行環境がChrome devtools、かつ関数呼び出しの入力を監視したい時限定で、変数名を出力出来るわけではないけど、monitor 関数も便利だよと教えて頂いた。便利!!!

blog.pastak.net

おまけ2

サムネオチって言葉久々に聞いた気がする。

*1:解: 3行目で acc を足し忘れている

*2:といっても標準化されてから約6年経過してますが。

*3:何となく分かると思いますが、debugger; のある行に breakpoint を仕掛けたかのような挙動になります。

思考実験: Wasm が普及した未来を考える

…という題でサークル仲間とディスカッションして盛り上がっていた。面白いアイデアが色々出てきたので、たまには正確性を無視して思い思いに書いてみる。裏取りは全くしていない。

  • もし Wasm が普及したら、.wasm に含まれる ホスト言語 (Wasm のコンパイル元の言語) のランタイムサイズが問題になって微妙な未来がやってきてしまうのでは
    • どういうことか:
      • アプリケーションというのは、ユーザコードとランタイムから構成される
      • 例えば Hello World を出力する C++プログラムがあったら、printf で Hello World を出力するコードがユーザコードで、printf の実装を提供したり *1、実行時例外の処理機構を提供するのがランタイム
      • アプリケーションを動作させるには、ユーザコード以外にも様々なコードが必要になる
      • 当然 Rust にもランタイムと呼ばれるものがあって、標準ライブラリや allocator 、async runtime などがそれに該当する *2
      • これが思いの外に大きくて *3、.wasm のサイズが許容できる範囲を超えてしまうのでは、という考え
    • React のランタイムサイズが大きくて困っている、みたいな話と同じ
    • 仮に React for Rust みたいなものが誕生したら、React のランタイムサイズと Rust のランタイムサイズの二重苦がやってくる、みたいな
    • Blazor はそんな感じだと聞いている (C# のランタイム + .NET のランタイム)
  • 何か解決策あるだろうか
    • ランタイムをブラウザに bundle させるとか *4
      • Rust ランタイムをブラウザに bundle させる、みたいな
      • 真の zero-runtime binary を実現できるが、Wasm は色んな言語に対して開かれるよう設計することになっているはず *5 なので、Rust だけ優遇することはなさそうな気もする…
      • Rust のランタイムそのまま bundle すると、ブラウザ側で Rust のランタイムの後方互換性を一生維持しないといけなくなる問題もある
        • ex: typeof null === 'object'
    • やるとしたら色々な言語のランタイムの共通部分をブラウザに bundle させる、みたいな形になりそう?
      • 分かりやすいのだと GC とか
      • 現に Wasm に GC 入れる proposal が出ている
      • まあでも切り出し可能な共通部分そんなにあるのかな…
        • コンパイラ基盤あんまり詳しくないので合ってるか知らないけど、exeption handing する runtime とか、async runtime とかはありそう
      • Reference Types proposal とかもそう
        • Wasm <=> JS のグルーコードを減らせるはず
    • あとはランタイムをある CDN から配信するようにして、色んなサイトで同じ CDN からランタイムを DL するようにするとか *6
      • CDN で配信された jQuery を import すれば色々なサイトでキャッシュを共有できて最高、みたいな取り組みに近い *7
      • CDN がパフォーマンス・可用性の観点で十分信頼できるならそういう形も良さそう?
  • 一方 JS はブラウザに全部ランタイムが bundle されていて、サーバからはユーザコードだけを配信すれば良いので、めちゃめちゃズルい
  • まあ Wasm が盛り上がるの少なくとも 5 年後だろうし、5 年も経てば世の中の通信環境やハードウェアのスペックが向上して、許容できるバイナリサイズも今とだいぶ変わってそう (本当に?) *8
    • あと Wasm はブラウザ側でのコンパイル速度や実行速度の観点で優れているので、そう簡単に比較できないという話もある
  • ところで今 Rust が Wasm をターゲットとする言語として注目されているけど、Rust はそもそもシステムプログラミング言語であって、JSer からしたら取っつきにくいという話がある
    • GC 無い / lifetime・借用辛い
    • もうちょっと JSer に馴染みがあって、GUI を書きやすくて、Wasm-first な言語がそのうち登場しても良さそう
    • こういう設計思想の言語があると良いんじゃないか
      • GC がある
        • 当然 stop the world になる時間が短くて、60fps 出せるようになっている
      • 最小限のランタイム
      • JS との binding が容易
        • JS の Object をその言語でも第一級で利用できる、とか
        • JS から触りやすい ABI が露出している、とか
        • Java <=> Scala が参考になったりしないかな
      • 宣言的 UI の構築が容易
        • 例えば JSX や algebraic effects が言語組み込みになっている、とか
        • algebraic effects を組み込みにしたら何が嬉しいのかは知らない
        • エラー時のスタックトレース見やすくなったり、他の言語機能と良い感じに連携取れたりするんじゃないかと思ってるけど、学術的な知識が無いので勘でものを言っている状態
        • 昔似たようなこと考えてた: https://twitter.com/mizdra/status/1347501080171565059
        • 宣言的 UI に bet して後悔しないかは考えもの
      • 標準ライブラリが WHATWG や W3C、ECMAScript で定義されている API の wrapper になっている
        • ブラウザに bundle されている API を利用するので、標準ライブラリのコードがランタイムを占める割合が少なくなる
        • 時刻 API が Temporal の wrapper になっている / ネットワークリクエストする API が fetch の wrapper になっている、とか
        • あと WASI
      • サーバサイドでも動く
        • クライアントサイドで GUI 記述するのに使い始めたら、絶対 SSR したいという需要が出てくるので
    • こういうの登場したら面白そう

*1:libc とか

*2:https://prev.rust-lang.org/ja-JP/faq.html#does-rust-have-a-runtime

*3:本当に思いの外大きくなるのかは知らない。仮にそうだとして、の話。

*4:これは僕ではなくサークル仲間のアイデア

*5:あってる?

*6:これは僕ではなくサークル仲間のアイデア

*7:懐かしい

*8:5年前と今を比較してどうだったか、を考えようとしたけど、5年前はネットワークの速度のことなんか考えていなかったので分からなかった

GitHub Packages を npm install するための手段あれこれ

概要

  • 社内向けの npm packages を publish するのに GitHub Packages が便利
    • GitHub 内で完結してお手軽 & Actions を使って自動リリースフローを作りやすい
  • しかし GitHub Packages に上がっている npm packages を npm install するには少々手間がかかる
  • 従って、GitHub Packages に上がっている npm packages を使うプロジェクトでは、npm install をするあらゆる場面で、PAT による認証を挟んでおかなければならない
    • local で docker-compose up する時
    • actions/jenkins 上で Node.js の CI を走らせる時
    • リリース用の docker image を CI 上で作る時
    • ...などなど
  • この時 PAT をどう管理するか、というのが意外と悩ましい (と個人的に思っている)
    • 何か楽な方法がないか色々考えてみる

案1: 社内の共有GitHubアカウントで発行した PAT をリポジトリの .npmrc に埋め込んでおく

共有トークンをリポジトリにベタ書きし、リポジトリに直接 commit してしまう、という案。

.npmrc:

//npm.pkg.github.com/:_authToken=<社内の共有GitHubアカウントで発行したトークンをここに貼る>
@example-corp:registry=https://npm.pkg.github.com/
  • メリット
    • npm は npm install 時に自動でカレントディレクトリにある .npmrc を見て認証してくれるので、これさえ書けば local だろうと actions だろうと jenkins だろうと、全ての場所で動く
  • デメリット
    • リポジトリにトークンが commit されてしまう

案2: プロジェクトを動かしたい人ごとに PAT を発行して、.npmrc に直書きしてもらう & GitHub Secrets などの仕組みを使う

共有トークンを commit するのを避けて、個人で PAT を発行して、それを使ってもらうという案。

.env などに個人で発行した PAT を埋め込んでおき、dotenv などと組み合わせて以下のように .npmrc から読み込む。

.npmrc:

//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
@example-corp:registry=https://npm.pkg.github.com/

これだけだと local でしか PAT が設定されず、actions や jenkins 上では認証ができないので、適時 GitHub Secrets や Jenkins の Credentials を使って PAT を環境変数 NPM_TOKEN に設定するようにしておく。

サードパーティツールの変数およびパスの設定 - Cloud 暗号化されたシークレット - GitHub Docs *1

  • メリット
    • トークンがリポジトリに commit されない
  • デメリット
    • プロジェクトを使いたい個人ごとにトークンを発行しないといけない
      • 一度セットアップすれば良いとはいえちょっと手間

package が公開可能な場合

社内向けだけど OSS にして良いとか、公開可能な場合は他にも取れる手段がある。

案3: npmjs.com に公開する

  • npmjs.com に public で package を publish する方法
  • デメリット
    • 公開可能な package でないと利用できない
    • npmjs.com のアカウントを作るのがちょっと手間
      • まあ一度作ってしまえば良いのだけど

案4: git 形式で npm install する

  • 予め GitHub リポジトリを公開しておき、 npm install git://github.com/example-corp/xxx.git#v1.0.0 でインストールする方法
  • デメリット
    • 公開可能な package でないと利用できない
    • 事前ビルドが必要な package との相性が悪い
      • というのも、通常 Git リポジトリにはビルドの成果物が commit されていない & git 形式の npm install は単に git に commit されているファイルをコピーして node_modules 配下に置くだけで、npm run build などはしてくれないため
    • npm update / yarn upgrade や renovate でアップデートできない *2
      • データソースが npm repository でないため、標準的なツールを使ったアップデートができない

おまけ: @example-corp:registry=<URL> の注意点

.npmrc に記載する @example-corp:registry=<URL>@example-corp をスコープとするパッケージ全てを <URL> から取得する使用になっています。つまり @example-corp/package-a, @example-corp/package-b, ... は全て <URL> から取得されます *3。その挙動の影響で「GitHub Packages にホストされている @example-corp スコープのパッケージ」と「npmjs.com にホストされている @example-corp スコープのパッケージ」を共存させることができないという既知の問題があります。public package は npmjs.com、private package は GitHub Packages、みたいな運用をしているとハマります。もしそういう使い方をする予定であれば、GitHub Packages or npmjs.com のどちらかに寄せる、という対応を別途検討したほうが良いでしょう *4


どの方式も一長一短あって難しい。皆さんはどうしてますか?

*1:Organization Secrets というやつがオススメです

*2:npm update / yarn upgrade は手元で試して確認した。renovate も https://github.com/mizdra-sandbox/git-npm-package-test/issues/1 で試して確認した。dependabot は試してないので知りません。

*3:URL に無かったら npmjs.com から取得するといった fallback も一切ないので、URL にないパッケージのインストールは失敗する。

*4:GitHub Packages と npmjs.com でスコープ名を変えるという素朴な workaround もある

Webpack における bundle size の変化を継続的に監視する

main ブランチとこのPRでどれだけ bundle size が変化したか比較したり、増加量がある閾値を超えていたら CI を fail させる、みたいなソリューションは結構紹介されているけど、bundle size の変化を継続的に監視する方法はあまり紹介されていないようだったので紹介します。

やり方

  1. webpack --mode production --json でビルド情報を JSON で取得
  2. JSON から chunk ごとの size に関する情報を抜き出す
  3. 好きなメトリクス監視サービスに2で手に入れたメトリクスを投げる

で、それを実装したのがこのPR。見れば分かるので見てください。

github.com

30行程度で実装できて簡単ですね。

VSCode で tsserver や ESLint が通知するエラーがおかしくなった時にやること

TypeScript を書いていると、tsconfig.json を変更したのに何故かその設定が tsserver に読み込まれないとか、vscode-eslint に変更が通知されずに古い型情報を使って lint し続けているとか、そういうことが多々ある。大体 VSCode を再起動すれば解決するのだけど、手間だし遅いし計算資源が勿体ない…

という訳でここでは僕が普段使っている再起動以外の workaround をいくつか紹介する。

F1 > Developer: Restart extension host

日本語だと「開発者: 拡張機能のホストを再起動」という項目。これを実行するとユーザがインストールした拡張機能や VSCode に bundle されている拡張機能 *1 を再起動することができる。全拡張機能の再起動するのでそこそこコストが掛かるけど、殆どのケースで問題が解決するし、VSCode 内で完結するので再起動よりはずっとお手軽。

F1 > TypeScript: Restart TS server

日本語だと「TypeScript: TS サーバーを再起動」。tsserver を再起動できる。VSCode 上に表示される TypeScript のエラーがおかしい時は、大体これを試せば解決する。1つ注意すべき点があって、TypeScript ファイルをカレントタブで開いていないと TypeScript: Restart TS server という項目が出てこない。もし F1 のメニューに TypeScript: Restart TS server が出てこなくて困っていたら、ちゃんとカレントタブで TypeScript ファイルを開いているか、をチェックすると良い。

tsconfig.json をカレントタブで開いて上書き保存する

tsconfig.json を保存すると tsserver が tsconfig.json を再読み込みしてくれるようになってるっぽくて、これだけで解決することもある。

.eslintrc をカレントタブで開いて上書き保存する

tsconfig.json の vscode-eslint 版。

*1:Git の拡張機能や tsserver client など

global install したパッケージを引き継ぎつつ Node.js のバージョンを上げる

時々 nodenv やら nodebrew やらで Node.js のバージョンを上げているのだけど、素朴にバージョンを上げるだけだと npm で global install したパッケージが引き継がれない。その結果、新しいバージョンで手作業で global install し直す作業を強いられることになる。

そうした作業を回避できるよう、大体どのパッケージマネージャーにも global install したパッケージを新しいバージョンの Node.js に移行するコマンドが用意されている。

が、そう高頻度でアップデートしないので、どういうコマンドだったのか忘れて毎回調べ直している… という訳で備忘録という形でいつも叩いているワンライナーを以下に貼っておく。今の所 nvm は使ってないので省略している。

nodenv

NEW_VERSION=15.11.0; OLD_VERSION=$(node -v | tr -d v); nodenv install $NEW_VERSION && nodenv migrate $OLD_VERSION $NEW_VERSION

nodebrew

NEW_VERSION=v15.11.0; OLD_VERSION=$(node -v); nodebrew install $NEW_VERSION && nodebrew use $NEW_VERSION && nodebrew migrate-package $OLD_VERSION

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.