mizdra's blog

ぽよぐらみんぐ

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

rule ごとに高速に eslint --fix できるツールを作った

大量のエラーに対して rule ごとに高速に eslint --fix できるツール 「eslint-interactive」 を作ったので、その紹介です。

動機

ESLint のデフォルトの出力はエラーの発生源や修正のためのヒントなど、開発者に役立つ多くの情報を含みます。これは多く場合機能しますが、膨大な量のエラーが報告される状況ではあまり機能しません (例えばプロジェクトに ESLint を導入する時や、プロジェクトの .eslintrc に大幅な変更を加える時など)。そうした状況では ESLint の出力が膨大になってしまい、開発者による出力の分析が困難になってしまいます。また、多くの種類のエラーがごちゃまぜになって出力されているため、エラーを修正するのも困難です。

そのため、このような多くのエラー発生する状況では、以下の 2 つの事柄が重要であると考えています。

  • 全てのエラーをまとめた概要を表示し、簡単に全体を把握できること
    • エラーごとの詳細を表示しても、かえって開発者を混乱させてしまいます
  • 大量のエラーを効率よく修正する手段が提供されていること
    • eslint --fix はエラーを効率よく修正する優れた手段の 1 つですが、一括で全ての rule のエラーを auto-fix してしまいます
    • rule によってはコードの挙動に影響を与える auto-fix を行うため、auto-fix は慎重に実施する必要があります
    • そのため eslint --fix よりも小さい単位で auto-fix できる手段が提供されていることが望ましいです

eslint-interactive の紹介

そこで eslint-interactive という ESLint を wrap したツールを作成しました。このツールでは全てのエラーを rule ごとにグルーピングし、rule あたりのエラー数を整形して出力します。rule ごとの warningerror の内訳に加え、fixable なエラーの数など、開発者がエラーを修正するためのヒントも出力されます。また、いくつかの rule を指定して、生の ESLint のエラーメッセージを表示したり、auto-fix を実行することができます。

github.com

こんな感じに rule ごとのエラー総数を眺めつつ、rule ごとに auto-fix していけます。

www.youtube.com

インストール方法や使い方は README を見てもらえれば分かると思うので、先程貼ったリポジトリから辿って下さい。

eslint-nibble との違い

eslint-interactive とよく似たツールに eslint-nibble があります。どちらも同じ問題を解決するツールですが、eslint-interactive には eslint-nibble にないいくつかの機能があります。例えばeslint-interactive はruleごとのfixableなエラー数を出力しますが、eslint-nibble は出力しません。また、eslint-interactive はruleごとのauto-fixのサイクルを高速で回すための様々な工夫 *1 がありますが、eslint-nibble は一度 auto-fix を行うと毎回プロセスが終了するため、eslint-interactive ほど高速にauto-fixのサイクルを回すことはできません。

どちらも同じ問題を解決できるツールではあるのですが、これらの機能が自分にとって非常に重要だったので、新規にeslint-interactive として実装してみました。

おわりに

元々とあるプロジェクトの.eslintrc を大改修する機会があって、その作業を円滑に進めるために用意したツールでした。実際に eslint-interactive を何度か使って作業してみていますが、最初に semi などコードの挙動を変えない rule の auto-fix を適用 => 次に import/order などコードの挙動を変えうる rule の auto-fix を適用、といったように段階的に安全に作業を進めることができて、快適に過ごせてます。皆さんも同じような状況に遭遇したらぜひ eslint-interactive を試してみて下さい。

*1:Lint結果をキャッシュする機能や、プロセスを維持したまま連続で auto-fix できる機能が備わってます。

ブラウザにおけるメモリリークを解決するために読んでおけると良い資料

最近趣味や仕事の Web アプリケーションでメモリリークに遭遇して、頑張ってメモリリークの原因を突き止めて修正する、ということがあった。その過程でメモリリークについて色々調べて知見が溜まったので、学習資料の紹介という形でアウトプットしてみる *1

前置き

  • 紹介する記事がかなり偏っていることに注意
    • 冒頭で触れたメモリリークを解決するために読んだ記事をまとめただけなので、内容にそれなりの偏りがある
    • 例えば id:mizdra が遭遇したメモリリークは全てブラウザ上で発生していたものだったので、これから紹介する内容も主にブラウザにおけるメモリリークに焦点を当てたものになる
  • GC がどうメモリをどう解放しているか、何故メモリリークが発生するのかは全てカット
    • 調べれば色々な記事が出てくるので、必要に応じて読んでください

基本的な知識を抑える

  • まずメモリリークとメモリ撹拌の違いを学ぼう
    • どちらも同じメモリに起因する問題だけど、アプリケーションの性質によって深刻度が異なったり、対処方法も変わるのでちゃんと区別しておけると良い
    • こういうのは世の中の事例を見ると分かりやすい
    • https://www.html5rocks.com/ja/tutorials/memory/effectivemanagement/
      • Gmail におけるメモリリークの事例
      • 内容は結構古いけど、書いてあることのほとんどは現代でも通用する
    • https://www.html5rocks.com/ja/tutorials/speed/static-mem-pools/
      • メモリ撹拌 (メモリリークとは違う!) の事例
      • メモリ撹拌自体は GC が実行されればメモリ使用量は元に戻る
      • 頻繁に GC が発生することで GC によるメインスレッドの停止が発生するので、常に 60fps 維持したいアプリケーション(FPS ゲームとか)などでは問題になる
      • 「メモリ撹拌にはオブジェクトプールが有効だよ」みたいな話題も書かれている
        • 逆にここからメモリリークに対してはオブジェクトプールはそれほど有効ではないことも分かるはず
  • Chrome devtools を使ったメモリ問題の特定方法を学ぼう

ここまで読めば、一通りメモリの問題に対処できる力がつくはず。

その他メモリにまつわる技術資料

  • https://v8.dev/features/weak-references
    • WeakRef / FinalizationRegistry を使うと特定のメモリリークを解決できるケースがあるよ、という話
  • https://web.dev/monitor-total-page-memory-usage/
    • メモリ使用量を測定する API にperformance.memoryというものがあるけど、API 実行直前に GC が発生していたか、そうでないかによって測定結果が大きく変わって使い物にならない
      • GC 発生直後ならメモリ使用量が少なくなり、発生から十分時間が経過している場合はゴミが残っているのでメモリ使用量が大きくなってしまうはず
    • それを受け、この記事ではperformance.memoryの代わりに事前に GC を実行してからメモリ使用量を測定してくれるperformance.measureMemoryを使いましょう、という話が紹介されている
  • GC の挙動をもっと知りたい場合
  • GC の停止時間がアプリケーションの価値に直結する場合 (FPS ゲームなど) は GC の挙動も知っておけると良い
  • V8 チームが出している記事を読むのがオススメ
    • まず GC の基礎を説明して、それから Chrome 固有の実装の詳細について説明するというスタイルになっていて、比較的読みやすい
    • Chrome が現代のモダンブラウザの中では一番イケているはずなので、とりあえず Chrome だけ抑えておければ良い
    • いわゆる「リファレンス実装」として利用する
  • https://v8.dev/blog/trash-talk
  • https://v8.dev/blog/concurrent-marking
    • Concurrent Marking に関する詳細
  • https://v8.dev/blog/high-performance-cpp-gc
    • Blink(Chrome のレンダリングエンジン)で利用されている GC「Oilpan」の仕組みの話
    • DOM などは Orinoco ではなく Oilpan によって管理されるので、こっちも読んでおけると良いはず

*1:元々社内向けに書いた資料で、それを公開できる形に調整した。

スナップショットテストの向き不向きについて考えてみる

ふとスナップショットテストってなんだろう、どういう場面で向いていて、どういう場面には向いていないんだろうと考える機会があって色々調べてました。丁寧な記事にしようとしたのですが、上手くまとまらなくて挫折してしまった… とはいえこのまま手元に置き続けておくのも勿体ないので、下書き段階のものを公開して供養します。

スナップショットテストとは

スナップショットテストとは、あるプログラムの出力を以前の出力と比較し、両者に差分があるかをテストする手法のことです。予め以前のバージョンのプログラムの出力 (スナップショット) のどこかに保存しておき、新しいバージョンのプログラムの出力と比較し、差分があったら fail させます。これにより、プログラムの出力内容が予期せぬうちに変わってしまっていた場合に気づくことができます。

例: React コンポーネントのテストへの適用

代表的な利用例が Jest を使った React コンポーネントのテストです。

// Jestのドキュメントから引用
// from: https://deltice.github.io/jest/docs/ja/snapshot-testing.html
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

it('renders correctly', () => {
  const tree = renderer.create(<Link page="http://www.facebook.com">Facebook</Link>).toJSON();
  expect(tree).toMatchSnapshot();
});

この例では、<Link>コンポーネントの render 結果をシリアライズし (const tree = renderer.create(...))、その結果をスナップショットとして保存・比較するよう Jest に指示しています (expect(tree).toMatchSnapshot())。このテストを初めて実行すると、ファイルシステムに以下のようなスナップショットファイルが作成されます。

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

2 回目以降テストを実行する際は、このスナップショットファイルとの差分が計算されます。もし<Link>コンポーネントの実装にミスがあり、予期せぬうちに render 結果が変わってしまった場合はスナップショットに差分が発生し、テストが fail するようになります。

スナップショットテストに対する一般的な理解

これは個人的な意見ですが、スナップショットテストが React コンポーネント以外にどんな場面へ適用可能なのか、というのはあまり知られていないように思います (僕も含めて)。スナップショットテストは非常に優れた性質を持っており、様々な場面へ適用できる有用なテスト手法です。しかしスナップショットテストがどういった特性を持っていて、どういう向き不向きがあるのか、といったことはほとんど知られていません。ここぞという時に使えるよう、使い時くらいは分かっていると便利ですよね。

どういう場面では向いているか

という訳でここからは、スナップショットテストがどういった場面で有用なのかが分かるよう、スナップショットテストの向き不向きについて考えてみます。ざっとスナップショットテストが向いている場面について書き出すと、こんな感じでしょうか。

  • 出力に差分が出たことを検知したい時
    • 差分があった時に fail して警告ほしい、どういう差分が出たのか教えてほしい、みたいなケースを想定
  • 有意な出力を出せる時
    • もうちょっと噛み砕くと、出力にテストしたいことが埋め込まれていて、かつ人間の目に読みやすい形になっている時
    • 例えばコンパイラが出力するコンパイルエラーのメッセージにて、エラーの発生行数が期待通りに印字されているかをテストしたい、みたいなケースを考えます
      • この場合はターミナルに出力されたエラーメッセージに発生行数がerror: index.ts:10:3みたいな形式で埋め込まれているので、ターミナルへ出力されたテキストををそのままスナップショットとして取れれば良いです
      • 逆にエラーメッセージが何件出たかをこれでテストするのは難しいです
        • 沢山エラーメッセージが出ると巨大なスナップショットが生成されてしまいます
        • 人間の目で数えることになって大変
    • 素朴にexpect(typeCheck(program).errors.length).toEqual(100)のような assertion を書くくらいで十分なはず
  • テスト対象がテスタブルなインターフェイスを公開してくれていない時
    • Virtual DOM など
      • <TodoList items={items} />が render された結果に items がitems.length個含まれているかをテストしたい、みたいなケースを想定
      • jsdom をインストールして DOM API を使ってテストしようと思えばできなくはないです (expect(document.querySelectorAll('.item').length).toEqual(5);)
      • けど Virtual DOM をそのままシリアライズしてそのスナップショットを取ったほうがずっと簡単にテストできます
    • スナップショットテストであれば出力さえ取れればテストできるので、テスタブルなインターフェイスが無くても何とかなる可能性があります
  • テストがない朽ちたコードを触る時
    • 触ろうにもテストが無いのでまずはテストを追加したい / けどテストを追加しようにもユニットテストを差し込めるようなインターフェイスになっていない…みたいな状況を想像してください
    • こういう時スナップショットテストが役に立ちます
    • ロガーを用意して、そのロガーを使ってアプリケーションのあらゆる場所でデバッグ出力をして、その出力をスナップショットテストする、というもの
      • 何かの拍子でログ出力の順番が変わったり、出力の内容が変わった時に気づくことができます
      • アプリケーションコードにはログ出力するコードを差し込むだけで、インターフェイスを全く変更する必要が無いので安全かつ簡単に導入できます
    • Golden Master と呼ばれている手法らしいです

どういう場面では向いていないか

逆にどういう場面では向いていないかというと…

  • 上記のメリットを殆ど享受できない場合、スナップショットテストは向いてません
    • そんなの当たり前では、と思うかもしれないけど、この観点は意外と重要だったりします
    • というのもこういうケースの場合、他のテスト手法を採用したほうが適切な場合が多いため
    • 例えば 2 つの整数を受け取ってその和を返すadd関数のテストがしたくて、以下のようなテストを書いたとします
    • さて何が問題でしょうか
    • まず答えが何になるべきなのかスナップショットテストからは分かりません
      • パッと見では3になるべきですが、正確なことはスナップショットファイルを見に行かなければ分かりません
    • また、スナップショットテストにしてしまうと、正解のデータを書き換えるには一度テストを走らせる必要があります
      • expect(add(1, 2)).toEqual(3)ならexpect(add(1, 3)).toEqual(4)と書き換えるだけで良いはずでした
    • ...というようにテストとしては動いているけど、なんか過剰な感じがしてしまう
    • こういう場合は素朴に数値として assert したほうが、テストが落ちた時にデバッグしやすいし、テストが読みやすいし、スナップショットファイルも要らなくなってシンプルになります
    • 小学校レベルの知識で解けるのであれば、大学レベルの知識を使って解くのではなくてそっちを使って解きましょう、みたいな
    • ゴブリンを倒すのであれば大量に MP を消費する大魔法を使わずとも、錆びた剣を振りかざすので十分なはず
    • 乱用することなく、状況に応じて道具を使い分けるという話
  • 出力の差分が巨大になる場合、スナップショットテストは向いてません
    • 例: 2 京行の差分が出る時
    • 例: Visual Regression Test で、5GB の画像が出力される時
    • テストフレームワークが処理するのに時間が掛かるし、最悪の場合クラッシュします
  • 出力の差分を見て、その妥当性を判断するのが困難な場合、スナップショットテストは向いていません
    • 例: 5 万行の差分が出るテスト
    • 1 分で 50 行見たとしても見終わるのに 1000 分掛かります
      • 9 時に出社して差分を見始めて、お昼過ぎてもまだまだで、定時過ぎてもまだまだで、終電が無くなった頃にようやく見終わる、みたいなスケール
      • 機械より人間が頑張るスタイル
    • ただし差分の内容を見ずに変わったことだけ分かれば良い場合や、最初の 100 行だけ見て妥当ですねと判断できる場合であればこの限りではないです
  • 出力の変化が激しい場合
    • 例: 現在時刻を表示するクロックコンポーネントのテスト
      • テストを実行する時刻によって出力が変わって毎回テストが fail します
      • 毎回人の目でチェックしないといけません
      • 機械より人間が頑張るスタイル 2
      • ただし何らかの手段で出力を固定できれば、問題を回避できる場合があります
        • クロックコンポーネントの例では時間を司る API をモックして時刻を固定すれば良い
        • Visual Regression Test なら変化する部分を黒塗りしてから比較すれば良い
        • このような出力の固定化が困難な場合はスナップショットテスト以外のテスト手法を検討しましょう
    • 例: アプリケーションのソースコードそのもののスナップショットテスト
      • ソースコードを 1 文字書き換えたらテストが fail する、みたいな
      • 誰もそんなテスト書かないと思いますが…
      • 良い具体例が思いつかなかった!本当はちょっとどこかを弄ったら毎回 fail するのは良くないよね、みたいなことを言いたかった。

事例

今までで見つけた利用例などを書いてみます。

その他使えそうなところ

以上の情報を元に他にどういう場面で使えそうか、というのを考えてみます。

  • nginx が返すレスポンスのテスト
    • ある URL にリクエストを飛ばした時に、期待されるヘッダや body から構成されるレスポンスが返ってくるかをテストしたい、みたいな
    • ちゃんと期待するヘッダが付いてるレスポンスが返ってくるかなどをテストできるはず

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

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