mizdra's blog

ぽよぐらみんぐ

思考実験: 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 の Personal Access Token (以下PAT) を使い、npm-cli を認証させる必要がある
    • npmレジストリの利用 - GitHub Docs
    • reporead:packages にチェックの入ったトークンが必要
    • package が public/private に限らず認証必須
  • 従って、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 https://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 できる機能が備わってます。

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

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