<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>Web Scratch</title>
 <link href="https://efcl.info/feed/" rel="self"/>
 <link href="https://efcl.info"/>
 <updated>2025-05-02T17:53:46+09:00</updated>
 <id>https://efcl.info/</id>
 
 <author>
   <name>azu</name>
   
 </author>
 

 
 <entry>
   <title>JavaScript PrimerのES2025対応を手伝ってくれるContributorとSponsorを募集しています</title>
   <link href="https://efcl.info/2025/04/25/jsprimer-es2025-proposal/"/>
   <updated>2025-04-25T12:00:00+09:00</updated>
   <id>https://efcl.info/2025/04/25/jsprimer-es2025-proposal</id>
   <content type="html"><![CDATA[ <p>JavaScript Primer (<a href="https://jsprimer.net/">https://jsprimer.net/</a>) では、毎年ECMAScriptの新しい仕様への追従を行っています。</p>
<p>ES2025はもうすでにリリース候補が公開されていて、2025年6月に正式リリースされる予定です。</p>
<ul>
<li><a href="https://github.com/tc39/ecma262/releases/tag/es2025-candidate-2025-04-23">Release ES2025 Candidate April 23rd 2025 · tc39/ecma262</a></li>
</ul>
<p>今年もES2025で追加される機能についての対応Issueを作成しました。</p>
<p>これらのIssueを一緒に進めてくれるContributorと、JavaScript Primerの活動を支援してくれるSponsorを募集しています。</p>
<h2>ES2025対応のIssue</h2>
<p>ES2025のMeta Issueとして次のIssueがあります</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/issues/1778">ES2025の対応 · Issue #1778 · asciidwango/js-primer</a></li>
</ul>
<p>具体的に対応するものとして次のIssueを作成しています。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/issues/1783">ES2025: import attributes · Issue #1783 · asciidwango/js-primer</a>
<ul>
<li>見積もり: 1 or 2 point</li>
</ul>
</li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1781">ES2025: <code>RegExp.escape</code> · Issue #1781 · asciidwango/js-primer</a>
<ul>
<li>見積もり: 2 or 3 point</li>
</ul>
</li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1784">ES2025: Set Methods for JavaScript · Issue #1784 · asciidwango/js-primer</a>
<ul>
<li>見積もり: 3 or 5 point</li>
</ul>
</li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1782">ES2025: Iterator Helpers · Issue #1782 · asciidwango/js-primer</a>
<ul>
<li>見積もり: 8+ point の可能性あり</li>
</ul>
</li>
</ul>
<p>各Issueには、作業量の見積もりとして<code>point</code>を付与しています。(これは感覚値なのであんまり正確ではないです。実際にやってみたら変わる可能性もあります)
この<code>point</code>は、作業の難易度や必要な調査量などを考慮して設定していて、後述するOpen Collectiveでの報酬計算にも利用します。</p>
<p><code>point</code>の目安は以下の通りです。これは作業時間ではなく、タスクの複雑さや規模を表す指標です。
例えば2 pointは「1日あれば終わるかな」という感覚値に近いものです。</p>
<table>
<thead>
<tr>
<th>Point</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>些細な変更 (typo修正など)</td>
</tr>
<tr>
<td>1</td>
<td>2 よりは簡単</td>
</tr>
<tr>
<td>2</td>
<td>大体1日分の作業量で終わる想定</td>
</tr>
<tr>
<td>3</td>
<td>2 よりは難しい</td>
</tr>
<tr>
<td>5</td>
<td>かなり難しい、調査や広範な変更が必要</td>
</tr>
<tr>
<td>8</td>
<td>難易度がとても高く、できる人が限られるレベル</td>
</tr>
</tbody>
</table>
<p>ES2025に対応するマイルストーンは、次のページで公開しています。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/milestone/6">v7(ES2025) Milestone</a></li>
</ul>
<p>ES2025は毎年6月末ぐらいに公開される予定なので、7月ぐらいには完成させる予定です。</p>
<h2>Contributorを募集しています</h2>
<p>JavaScript Primerの執筆、レビュー、サンプルコード作成、仕様調査などに興味がある方を募集しています。</p>
<p>今年のIssueは、1人 1Issueで綺麗に分けられると思うので、それぞれのIssueに興味がある人を募集しています。</p>
<ul>
<li>募集しているDiscussion: <a href="https://github.com/asciidwango/js-primer/discussions/1789">ES2025に対応するIssueへのContributorを募集しています · asciidwango/js-primer · Discussion #1789</a></li>
</ul>
<p>去年のES2024対応 (<a href="https://efcl.info/2024/03/21/jsprimer-es2024-proposal/">JavaScript PrimerのES2024対応を手伝ってくれるContributorとSponsorを募集しています | Web Scratch</a>) の反省を踏まえ、今年はもう少しオンボーディングやコミュニケーションを丁寧に行いたいと考えています。
IssueにAssignはすると思うのですが、どういうイメージで書いていくかをやり取りしながら進めていければいいかなと思っています。
(幸いLLMが充実してきているので、下書きレベルならざっくりと出せると思うので、具体に近いものを見ながら進めていけるといいのではないかなと思ってます。)</p>
<p>特に、<a href="https://github.com/asciidwango/js-primer/issues/1782">Iterator Helpers</a>のように難易度が高いものは、事前にオンラインミーティングなどで進め方や認識合わせをする機会を設けたいと思っています。
Iterator Helpersは正直かなり難易度が高いので、実際に手を動かせるレベルになるまではもうちょっとかかりそうな気がします。
(一応募集はしていますが、これは結構な気合いが必要なIssueだと思っています。)</p>
<p>Contributeしたい人は、次のDiscussionに参加してみてください。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/discussions/1789">ES2025に対応するIssueへのContributorを募集しています · asciidwango/js-primer · Discussion #1789</a></li>
</ul>
<h3>Open Collectiveによる報酬</h3>
<p>JavaScript Primerは<a href="https://opencollective.com/jsprimer">Open Collective</a>を通じて、活動資金の支援を受け付けています。
Contributorとして参加していただいた方には、この予算から<a href="https://github.com/asciidwango/js-primer/blob/master/CONTRIBUTING_EXPENSE.md">Contributing Expenses Policy</a>に基づき、作業量に応じた報酬を請求できます。</p>
<p>報酬額は、Issueごとに設定された<code>point</code>に基づいて計算されます。
現時点での年間予算は約$421で、これを元に計算すると <strong>1 pointあたり約$7</strong> となります。</p>
<p><code>point</code>の目安は以下の通りです。これは作業時間ではなく、タスクの複雑さや規模を表す指標です。</p>
<table>
<thead>
<tr>
<th>Point</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>些細な変更 (typo修正など)</td>
</tr>
<tr>
<td>1</td>
<td>2 よりは簡単</td>
</tr>
<tr>
<td>2</td>
<td>大体1日分の作業量で終わる想定</td>
</tr>
<tr>
<td>3</td>
<td>2 よりは難しい</td>
</tr>
<tr>
<td>5</td>
<td>かなり難しい、調査や広範な変更が必要</td>
</tr>
<tr>
<td>8</td>
<td>難易度がとても高く、できる人が限られるレベル</td>
</tr>
</tbody>
</table>
<p>過去のIssueに対応するpointの参考値やOpen Collectiveの利用方法については次のページを参照してください。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/blob/master/CONTRIBUTING_EXPENSE.md">Contributing Expenses Policy</a></li>
</ul>
<h3>書き方について</h3>
<p>JavaScript Primerは技術書であるため、次の点に気をつけて書いていきます。</p>
<ul>
<li><strong>正確性</strong>: 仕様やMDN、信頼できる情報源を元に、矛盾のない正確な記述をします。</li>
<li><strong>読みやすさ</strong>: 読者が理解しやすいように、平易な言葉遣いや構成を意識しますが、<a href="https://textlint.org/">textlint</a>のチェックがあるのである程度強制されます。
<ul>
<li>LLMの利用自体は問題ありませんが、最終的な品質は人間が読みやすかどうかで判断します</li>
</ul>
</li>
<li><strong>サンプルコード</strong>: ユースケースに基づいた、実践的で理解しやすいサンプルコードを扱います。なぜそのコードが必要なのか、どのような場面で役立つのかが伝わるように意識します。
<ul>
<li>実際に使われているパターンなどをもとにサンプルコードを書きます</li>
</ul>
</li>
<li><strong>目的意識</strong>: jsprimerには<a href="https://jsprimer.net/intro/">はじめに · JavaScript Primer #jsprimer</a>に書いているように、本書の目的と目的ではないことが書かれています
<ul>
<li>毎年悩むのは「どこまで書くか」ということですが、悩んだ時は本書の目的に立ち返って判断します</li>
</ul>
</li>
</ul>
<p>これもLLMを使って一部書いていますが、やや小難しくなりがちです。</p>
<p>実際に書籍を書くときには、<a href="https://textlint.org/">textlint</a>による文章のチェックやレビューやサンプルコードに対するテストの仕組みなどもあるので、文章ですがコードを書くような感覚で書いていくのが良いと思います。</p>
<p>詳しい書き方やルールについては、次のドキュメントを参照してください。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/blob/master/CONTRIBUTING.md">Contribution Guide</a></li>
</ul>
<h2>参加方法</h2>
<p>Contributorとして参加してみたい方は、次のDiscussionにコメントしてみてください。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/discussions/1789">ES2025に対応するIssueへのContributorを募集しています · asciidwango/js-primer · Discussion #1789</a></li>
</ul>
<p>ご興味のある方、ぜひ参加してみてください！</p>
<h2>Sponsorを募集しています</h2>
<p>JavaScript Primerの活動は、個人や企業のSponsorからの支援によって支えられています。
書籍の継続的なメンテナンスや改善活動を支援してくださるSponsorを随時募集しています！</p>
<p>今年の更新に関する費用は、次の方々の支援によって成り立っています。
ご支援ありがとうございます！</p>
<p><strong>Gold Sponsors</strong></p>
<p><a href="https://kokuchou.net/" title="株式会社コクチョウ"><img src="https://images.opencollective.com/kokuchou/4241e96/logo/256.png?height=166" height="166" alt="" loading="lazy"></a></p>
<ul>
<li><a href="https://kokuchou.net/">株式会社コクチョウ</a></li>
</ul>
<p><strong>Supporters</strong></p>
<p><a href="https://opencollective.com/jsprimer#backers"><img src="https://opencollective.com/jsprimer/backers.svg?width=890&amp;avatarHeight=40" alt="jsprimer backers" /></a></p>
<p>jsprimerの更新を金銭的にサポートしたいという方は、是非検討してみてください！</p>
<p>詳細は<a href="https://jsprimer.net/intro/sponsors/">JavaScript Primerスポンサー · JavaScript Primer #jsprimer</a>をご覧ください。</p>
<ul>
<li><a href="https://opencollective.com/jsprimer">JavaScript Primer - Open Collective</a></li>
<li><a href="https://jsprimer.net/intro/sponsors/">JavaScript Primerスポンサー · JavaScript Primer #jsprimer</a></li>
</ul>
<h2>オマケ: TSKaigi 2025での発表</h2>
<p><a href="https://2025.tskaigi.org/">TSKaigi 2025</a>の2日目に、JavaScript Primerの取り組みについて発表する予定です。</p>
<blockquote>
<p>技術書をソフトウェア開発する - jsprimerの10年から学ぶ継続的メンテナンスの技術
<a href="https://2025.tskaigi.org/talks?day=2">https://2025.tskaigi.org/talks?day=2</a></p>
</blockquote>
<p>JavaScript Primerという書籍が作られ、どのようにメンテナンスされているかといった内容について話す予定です。
発表は一ヶ月後なので、その頃にはES2025対応もある程度進んでいる（または始めている）状態になっていると思います。</p>
<p>興味がある人は、ぜひ見にきてください！</p>
<h3>参考: 前回の募集</h3>
<ul>
<li><a href="https://efcl.info/2024/03/21/jsprimer-es2024-proposal/">JavaScript PrimerのES2024対応を手伝ってくれるContributorとSponsorを募集しています | Web Scratch</a></li>
<li><a href="https://github.com/asciidwango/js-primer/discussions/1727">ES2024に対応するIssueへのContributorを募集しています · asciidwango/js-primer · Discussion #1727</a></li>
</ul>
 ]]></content>
 </entry>
 
 <entry>
   <title>DJControl Mix Ultraを買った</title>
   <link href="https://efcl.info/2025/04/19/DJControlMixUltra/"/>
   <updated>2025-04-19T20:12:00+09:00</updated>
   <id>https://efcl.info/2025/04/19/DJControlMixUltra</id>
   <content type="html"><![CDATA[ <p>DJControl Mix Ultraという、モバイル向けのDJコントローラーを買って遊んでいた。</p>
<ul>
<li><a href="https://dirigent.jp/hercules/djcontrol-mix-ultra">DJControl Mix Ultra - Dirigent | ディリゲント</a></li>
<li><a href="https://www.hercules.com/en/product/djcontrol-mix-ultra/">DJControl Mix Ultra - Hercules</a></li>
</ul>
<p><video src="/wp-content/uploads/2025/04/DJControl_Mix_Ultra_optimized.mp4" autoplay loop muted playsinline></video></p>
<h2>DJControl Mix Ultraとは</h2>
<p><a href="https://www.hercules.com/en/product/djcontrol-mix-ultra/">DJControl Mix Ultra - Hercules</a>は、普通のDJコントローラーと違って、iOSやAndroidのスマホやタブレットとBluetoothで接続して使う目的で作られているデバイス。
なので、LINE INやLINE OUTもないし、スマホとの接続はBluetooth LEでのみ行うような作りになってる。</p>
<p>一方で、USB Cで充電できてバッテリー駆動なので、Bluetoothでスマホと繋いで動作するので、ワイヤレスで動く。</p>
<p><img src="/wp-content/uploads/2025/04/19-1745061533.png" alt="DJControl Mix Ultra" /></p>
<p>そのため、この見た目通りの構成で動く。</p>
<p>音の出力は、スマホのスピーカー または スマホに繋いでスピーカーから鳴らすので、本格的な用途じゃなくて遊びとか外で使う向けに作られている。
基本的に利用できるDJアプリも、<a href="https://www.algoriddim.com/apps">DJ Apps | Algoriddim</a>のみ限定されている。</p>
<p>エフェクトとかミックスとかはソフトウェア的な処理を行うので、ハードウェアは本当にコントローラーというだけの役割になっている。</p>
<p>デバイス自体も1kgぐらいで、ケースもちゃんとしていて、デザインも好きなので、ガジェットとしてかなり良くできてた。</p>
<p><img src="/wp-content/uploads/2025/04/ultra-case.jpeg" alt="ケース" /></p>
<p>ケースが、スマホのスタンドにもなるというデザインも良い。
Macbook Airより一回り小さくて軽いぐらいな感じで、持ち運びもかなり安定してて良い。</p>
<h2><a href="https://www.algoriddim.com/apps">djay Pro</a></h2>
<p>DJControl Mix Ultraは、djay Proというアプリと接続して使う専用コントローラー的なもの。
djay Proは、iOS/Android/Mac/Windowsで動くDJアプリで、かなりちゃんと作られているアプリなのでできは良いと思う。</p>
<p>djay Proは有料ではあるけど、DJControl Mix Ultraと接続するとHardware modeが有効になって、必要な機能が自由に使えるようになる。
ハードウェアのデバイスごとにこういったUnlock機能が実装されているらしく、デバイスごとに有効になる機能はバラバラだけど、大体デバイスで操作できるものは全部有効になるイメージ。</p>
<p>DJControl Mix Ultraなら、Hot Cue/Loop/Fx/Neural Mixとか遊ぶのに必要な機能は大体使えるようになる。
(Auto Mixとかそういう、ソフトウェアのPro的な機能は使えないけど、DJコントローラーとしての機能は全部使える)
なので、デバイスだけで、アプリは特に課金しなくても普通に使える。</p>
<ul>
<li><a href="https://community.algoriddim.com/t/hardware-unlock-table/26854">Hardware Unlock Table - Suggestions - Algoriddim Community Forums</a></li>
</ul>
<h3>音源</h3>
<p>音源は、Apple Music/djay Msuic/SoundCloud/Beatport/Beatsourceなどの音源をストリーミングで流せる。
Apple Musicはサブスクに入ってる必要があるけど、基本的にApple Musicの曲をそのまま流せる。</p>
<p>注意点として、Apple MusicはAppleの規約でNeural Mix機能が使えない(FxやEQなどは使えるので、Neural Mix以外は使える)。
djay Msuicは、djay Proがやっているストリーミングなので、Neural Mixも使える。</p>
<p>djay Proの出来もかなりよくて、流している音源のBPMに合わせた曲も探せたりするので、適当に流してても結構面白い。
(この辺を完全に自動でmixするAuto Mixという有料の機能もある)</p>
<p><img src="/wp-content/uploads/2025/04/IMG_7376_optimized.jpg" alt="djay Pro" /></p>
<h3>iOS版のdjay Pro</h3>
<ul>
<li><a href="https://www.algoriddim.com/djay-ios">DJ App for iPad and iPhone - djay by Algoriddim</a></li>
</ul>
<p>iPhoneのdjay Proを起動して、DJControl Mix Ultra をBluetoothで接続すると、Hardware modeが有効になって、DJControl Mix Ultraのボタンやノブで操作できるようになる。
有料機能である2つのデッキを並べて表示もできて、DJControl Mix Ultraでこれを操作できる。</p>
<p><img src="/wp-content/uploads/2025/04/IMG_7377_optimized.jpg" alt="djay Pro" /></p>
<p><img src="/wp-content/uploads/2025/04/djcontrol-mix-ultra-2.jpg" alt="DJControl Mix Ultra" /></p>
<p>ブラウズのノブで曲も選べるので、基本的にDJControl Mix Ultraだけで、曲を流し続けられる。</p>
<p>Androidも大体同じように使える</p>
<h3>macOS版のdjay Pro</h3>
<p>公式サイトだとモバイル専用のDJコントローラーみたいな扱いだけど、macOSとUSB接続して使うこともできる。
特に期待はしてなかったけど、実はmacOS版のdjay ProでもHardware modeが有効になって、有料の機能がUSB接続時は使えるようになってる。</p>
<p><img src="/wp-content/uploads/2025/04/19-1745062209.png" alt="DJControl Mix Ultra macOS" /></p>
<p><img src="/wp-content/uploads/2025/04/19-1745063096.png" alt="djay Pro macOS" /></p>
<p><img src="/wp-content/uploads/2025/04/DJControl_Mix_Ultra_mac.jpg" alt="djay Pro macOS" /></p>
<p>そのため、macOSでもDJControl Mix Ultraと接続するだけで、必要な機能が全部無料で使えるようになってる。</p>
<h2>遅延</h2>
<p>基本的にモバイル端末との接続はBluetooth LEで行うけど、実際にデバイス触ってみて遅延を感じることがなかった。
公式によると大体7ms以下の遅延で、ほとんどわからなかった。
USBでmacOSと繋いでる場合と、BluetoothでiPhone繋いでる場合はほぼ同じ感覚ではあった。</p>
<blockquote>
<p>Bluetooth LE adds less than 7ms of latency (similar to a USB controller’s latency)
<a href="https://support.hercules.com/en/kb/1806-en/">DJControl Mix Ultra – Technical FAQs – Hercules</a></p>
</blockquote>
<p>さらに面白いのが、DJControl Mix Ultra  → iPhone → AirPods Pro2に流してる場合でもあんまり遅延を感じなかった。
AirPods Pro2はBluetooth LE/LE Audioを使ってるらしいので、ここの遅延もかなり小さいんだなーと思った。
(他のBluetoothスピーカーだと、明らかにわかるレベルの遅延は発生する)</p>
<p>ここまでワイヤレスで遅延を感じないのはすごいなーと思った。</p>
<h2>まとめ</h2>
<p>DJControl Mix Ultraは定価が$199で、日本でも3万円ぐらいで買える割には、デバイス自体がよくできててよかった。
こういう、いろんな機能を削ぎ落としてニッチに振り切ったガジェットは触ってると結構楽しい。
また、djay Proもかなりよくできてるアプリだったので、触ってて楽しかった。</p>
<p>公式のチュートリアル動画とかもあるので、遊んでみると楽しいと思う。</p>
<ul>
<li><a href="https://www.youtube.com/watch?v=wPbNcCWuji4">DJControl Mix Ultra | Get started | English - YouTube</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLvBPf5gwVxRN3uIn-hcG8qH_4hJUwvpC3">DJControl Mix Ultra - YouTube</a> playlist</li>
</ul>
 ]]></content>
 </entry>
 
 <entry>
   <title>オープンソース活動の振り返り/GitHub Sponsorsの収入まとめ @ 2024</title>
   <link href="https://efcl.info/2024/12/31/open-source-in-2024/"/>
   <updated>2024-12-31T21:27:00+09:00</updated>
   <id>https://efcl.info/2024/12/31/open-source-in-2024</id>
   <content type="html"><![CDATA[ <p>2024 年のオープンソース活動の振り返りと GitHub Sponsors での収入をまとめた記事です。
いつもは別々に書いていましたが、内容的にどちらもオープンソースの話が被っているのでまとめました。</p>
<h2>今までの振り返り</h2>
<ul>
<li>2014 年: <a href="https://efcl.info/2014/12/31/oss-in-2014/">https://efcl.info/2014/12/31/oss-in-2014/</a></li>
<li>2015 年: <a href="https://efcl.info/2015/12/31/oss-in-2015/">https://efcl.info/2015/12/31/oss-in-2015/</a></li>
<li>2016 年: <a href="https://efcl.info/2016/12/31/oss-in-2016/">https://efcl.info/2016/12/31/oss-in-2016/</a></li>
<li>2017 年: <a href="https://efcl.info/2017/12/30/oss-in-2017/">https://efcl.info/2017/12/30/oss-in-2017/</a></li>
<li>2018 年: <a href="https://efcl.info/2018/12/31/oss-in-2018/">https://efcl.info/2018/12/31/oss-in-2018/</a></li>
<li>2019 年: <a href="https://efcl.info/2019/12/31/oss-in-2019/">https://efcl.info/2019/12/31/oss-in-2019/</a></li>
<li>2020 年: <a href="https://efcl.info/2020/12/31/open-source-in-2020/">https://efcl.info/2020/12/31/open-source-in-2020/</a></li>
<li>2021 年: <a href="https://efcl.info/2021/12/31/open-source-in-2021/">https://efcl.info/2021/12/31/open-source-in-2021/</a></li>
<li>2022 年: <a href="https://efcl.info/2022/12/31/open-source-in-2022/">https://efcl.info/2022/12/31/open-source-in-2022/</a></li>
<li>2023 年: <a href="https://efcl.info/2023/12/31/open-source-in-2023/">https://efcl.info/2023/12/31/open-source-in-2023/</a></li>
</ul>
<h2>オープンソース活動の種類</h2>
<p>自分のオープンソース活動の種類として、次のようなものがあると思います。</p>
<ul>
<li>ソフトウェア開発: <a href="https://github.com/textlint/textlint">textlint</a>、<a href="https://github.com/secretlint/secretlint">Secretlint</a>、<a href="https://github.com/HonKit/HonKit">HonKit</a>などの開発やメンテナンス</li>
<li>ウェブサービス開発: <a href="https://philan.net/">philan.net</a>、<a href="https://komesan.pages.dev/">Komesan</a>、<a href="https://irodr.netlify.app/">Irodr</a>などの開発やメンテナンス</li>
<li>ブログ: <a href="https://jser.info/">JSer.info</a>や<a href="https://ecmascript-daily.github.io/">ECMAScript Daily</a>のブログの投稿</li>
<li>書籍: <a href="https://jsprimer.net/">JavaScript Primer</a>や<a href="https://azu.github.io/promises-book/">JavaScript Promise の本</a>の執筆</li>
</ul>
<h2>オープンソース関係の収入の種類</h2>
<p>自分のオープンソース関係の収入<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup>は、ほぼ GitHub Sponsors 経由となっています。</p>
<ul>
<li><a href="https://github.com/sponsors/azu">@azu の GitHub Sponsors</a></li>
</ul>
<p>GitHub Sponsors では Monthly と One-Time でのサポートができます。
自分の場合は、GitHub Sponsors の Monthly での支援がベースとなっています。</p>
<p>また、今年から<a href="https://jsprimer.net/">JavaScript Primer</a>の Open Collective を開始したので、来年はここからもいくらか経費申請をする予定です。(jsprimer への Contributor への支払いものこの Open Collective の予算から行なっています)</p>
<ul>
<li><a href="https://opencollective.com/jsprimer">JavaScript Primer - Open Collective</a></li>
</ul>
<p>今年は自分は経費申請してないので、今年のオープンソース活動に関する収入は GitHub Sponsors のみです。</p>
<h2>オープンソース関係の収入の合計金額</h2>
<p>2024 年の GitHub Sponsors の収入は 約 259 万円となりました。</p>
<p><a href="/wp-content/uploads/2024/12/github-sponsors-per-year.png"><img src="/wp-content/uploads/2024/12/github-sponsors-per-year.png" alt="GitHub Sponsorsの収入 year" /></a></p>
<p>月毎で見ると次のようになります。</p>
<p><a href="/wp-content/uploads/2024/12/github-sponsors-per-month.png"><img src="/wp-content/uploads/2024/12/github-sponsors-per-month.png" alt="GitHub Sponsorsの収入 month" /></a></p>
<p>テーブルにすると次のようになります。</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>金額(円)</th>
</tr>
</thead>
<tbody>
<tr>
<td>2024/01</td>
<td>148,267</td>
</tr>
<tr>
<td>2024/02</td>
<td>232,138</td>
</tr>
<tr>
<td>2024/03</td>
<td>170,552</td>
</tr>
<tr>
<td>2024/04</td>
<td>187,441</td>
</tr>
<tr>
<td>2024/05</td>
<td>211,506</td>
</tr>
<tr>
<td>2024/06</td>
<td>194,666</td>
</tr>
<tr>
<td>2024/09</td>
<td>899,839</td>
</tr>
<tr>
<td>2024/10</td>
<td>179,029</td>
</tr>
<tr>
<td>2024/11</td>
<td>186,958</td>
</tr>
<tr>
<td>2024/12</td>
<td>184,130</td>
</tr>
</tbody>
</table>
<p>7月~8月がなくて 9月にまとまってるのは、GitHub Sponsors の設定を更新し忘れていて一時的に積んでいたのが原因です。</p>
<p>また、6月に<a href="https://www.toyokumo.co.jp/">トヨクモ株式会社</a>さんの<a href="https://oss.toyokumo.co.jp/">Thanks OSS Award presented by Toyokumo</a>の 2024 年度(1月~6月)に選ばれたので、9月はその分増えています。</p>
<blockquote>
<p>Thanks OSS Award とは私達の製品開発で使用しているライブラリ/ツールなどにおいて、 年度単位で選出した開発者の方々にその開発の継続を金銭的に支援しましょうという活動です。</p>
</blockquote>
<ul>
<li><a href="https://tech.toyokumo.co.jp/entry/oss_donation">OSS への寄付活動について - Toyokumo Tech Blog</a></li>
<li><a href="https://tech.toyokumo.co.jp/entry/thanks_oss_award">Thanks OSS Award というサイトを公開しました - Toyokumo Tech Blog</a></li>
</ul>
<p>この記事は 2024 年での収入なのでまだ含まれていませんが、<a href="https://oss.toyokumo.co.jp/">Thanks OSS Award presented by Toyokumo</a>の 2024 年度(7月~12月)にも選ばれたので、重ね重ねありがとうございます！</p>
<p>GitHub Sponsorsは、2019年10月から始めています。
今までのGitHub Sponsorsでの収入の推移は、次のとおりです。</p>
<p><a href="/wp-content/uploads/2024/12/github-sponsors-history.png"><img src="/wp-content/uploads/2024/12/github-sponsors-history.png" alt="GitHub Sponsorsの収入 history" /></a></p>
<p>GitHub Sponsors を始めた経緯については、次の記事にまとめてあります。</p>
<ul>
<li><a href="https://efcl.info/2021/10/01/github-sponsors/">GitHub Sponsors の募集を始めてから 2 年が経ったので振り返る | Web Scratch</a></li>
</ul>
<p>今までのまとめは、次のページで公開しています。</p>
<ul>
<li><a href="https://efcl.info/2023/12/25/github-sponsors-report/">GitHub Sponsors の収入 @ 2023 | Web Scratch</a></li>
<li><a href="https://efcl.info/2022/12/22/github-sponsors-report/">GitHub Sponsors の収入 @ 2022 | Web Scratch</a></li>
</ul>
<h2>継続的なサポートをしてくれている人の数</h2>
<p>GitHub Sponsors で継続的にサポートしていくれている人の推移は次のとおりです。</p>
<p><a href="/wp-content/uploads/2024/12/sponsors_count.svg"><img src="/wp-content/uploads/2024/12/sponsors_count.svg" alt="GitHub Sponsors Count" /></a></p>
<p>GitHub の API でいい感じに表現するのが難しいため、この人数は現在も継続している人数だけを出しています。
そのため、one-time の場合や今は中断している方は含まれていません。</p>
<p>Monthly Estimated Income(単位は$)は次のようになっています。</p>
<p><a href="/wp-content/uploads/2024/12/sponsors_count.svg"><img src="/wp-content/uploads/2024/12/sponsors_count.svg" alt="GitHub Sponsors Count" /></a></p>
<h2>GitHub Sponsorsに感謝</h2>
<p>GitHub Sponsors でご支援いただきありがとうございます！</p>
<ul>
<li><a href="https://github.com/sponsors/azu">Sponsor @azu on GitHub Sponsors</a></li>
</ul>
<p><a href="https://github.com/sponsors/azu"><img src="/wp-content/uploads/2024/12/github-sponsors.png" alt="GitHub Sponsors" /></a></p>
<blockquote>
<p>画像は<a href="https://github.com/antfu-collective/sponsorkit">SponsorKit</a>を使って作成しています</p>
</blockquote>
<p>2024 年にご支援いただいた企業さんもありがとうございました！</p>
<ul>
<li><a href="https://cybozu.co.jp/">サイボウズ株式会社</a></li>
<li><a href="https://www.velc.co.jp/">ヴェルク株式会社</a></li>
<li><a href="https://smarthr.co.jp/">株式会社SmartHR</a></li>
<li><a href="https://www.toyokumo.co.jp/">トヨクモ株式会社</a></li>
</ul>
<p>最近では<a href="https://opensourcepledge.com/">Open Source Pledge</a>という <code>開発者数 * $2000 per year</code> をオープンソースに支払うイニシアチブに参加を宣言する企業も増えてきています。(金額自体は購買力が地域で異なるので適正な値かという議論もあります。<a href="https://github.com/opensourcepledge/opensourcepledge.com/issues/36">Account for purchasing power disparities · Issue #36 · opensourcepledge/opensourcepledge.com</a>)</p>
<p>こういった議論は増える傾向にあると思いますが、実際に行動へと移している方々に感謝しています。</p>
<h2>オープンソース活動の振り返り</h2>
<p>次は、2024年のオープンソース活動の振り返りです。</p>
<h3>Contributions/Issues/PR</h3>
<ul>
<li>Contributions: 10157</li>
<li>作成したリポジトリ: 59</li>
<li>作成した Issue: 58</li>
<li>作成した Pull Request: 214</li>
</ul>
<p>今年はそこまで新しいものは作ってなくて、メンテナンスだったりバグレポートとかを色々していた気がします。
あと、個人的にも Issue 管理に<a href="https://linear.app/">Linear</a>を使うようになったので、Issue の数が少なめです。</p>
<ul>
<li><a href="https://github.com/azu/type-safe-env">azu/type-safe-env: Type Safe /Environment Variables snippet for Node.js</a>
<ul>
<li><a href="https://efcl.info/2024/08/24/type-safe-env/">Node.js で型安全な環境変数を扱うスニペット | Web Scratch</a></li>
<li>ENV の読み込みを TypeScript で型安全にするためのスニペット的なものです</li>
<li>個人的には ENV に型変換は求めてない(boolean とか)ので、かなり小さなスニペットだけど実用的な感じがします</li>
<li><code>NODE_OPTIONS='--import ./env.local.js'</code> と allowJs で、型安全に環境変数の設定までできるところまで行けたのは結構面白いポイント</li>
<li>来年ぐらいには、Node.js が.ts を普通にロードぐらいまではいけると思うので、<code>NODE_OPTIONS='--import ./env.local.ts'</code>とできる気はします</li>
</ul>
</li>
<li><a href="https://github.com/azu/octocov-viewer">azu/octocov-viewer</a>
<ul>
<li><a href="https://github.com/k1LoW/octocov">octocov</a>という CodeCoverage ツールの結果を GitHub の Pull Request の Diff にカバレッジ情報を表示するブラウザ拡張</li>
<li>動くところまで書いて、メンテナンスどうしようかなーというところで止まっている</li>
<li>仕組み的に必要なものとかはわかっているので、カバレッジに熱意にある人に任せたい感じがしていて止まっている</li>
<li>目的としては、<a href="https://about.codecov.io/">Codecov</a>のような外部 Saas/またはセルフホストはしないで GitHub 上だけでカバレッジの可視化ができるとレビューしやすいよねってのを実現させるためのもの</li>
</ul>
</li>
<li><a href="https://github.com/azu/nextjs-trace-to-tree">azu/nextjs-trace-to-tree: Next.js trace-to-tree CLI</a>
<ul>
<li>謎の秘伝スクリプトをコピって実行するのは面倒だったので、CLI 化した</li>
<li><a href="https://github.com/vercel/next.js/blob/canary/scripts/trace-to-tree.mjs">https://github.com/vercel/next.js/blob/canary/scripts/trace-to-tree.mjs</a></li>
<li>これを使うと、Next.js の Trace ファイルを Tree 表示できるようになる</li>
<li>なんで作ったかは思い出せないけど、<a href="https://zenn.dev/s_takashi/scraps/d13e6300993233">Next.js で trace の解析結果を表示する</a>をみて、この手順をできる気がしないと思って CLI を書いた気がする</li>
</ul>
</li>
<li><a href="https://github.com/azu/jsconf-jp-2024-schedule">azu/jsconf-jp-2024-schedule: Schedule Tool for JSConf JP 2024</a>
<ul>
<li><a href="https://jsconf-jp-2024-schedule.vercel.app/">JSConf JP 2024 Scheduler</a></li>
<li>JSConf JP のスケジュール組むのが難しすぎて<a href="https://v0.dev/">v0 by Vercel</a>を使って作って調整した</li>
<li>こういうワンタイムウェブサイトが楽にできて便利だった</li>
</ul>
</li>
<li><a href="https://github.com/azu/ts-to-json">azu/ts-to-json</a>
<ul>
<li>昔からスキーマをスキーマとして書くのが好きではないので、TypeScript の型定義からいい感じにしたいという発想がある</li>
<li><a href="https://efcl.info/2021/03/26/create-validator-ts/">TypeScript の型定義からバリデーションコードを生成するツールを書いた | Web Scratch</a> とかもそうだった</li>
<li>値の指定自体は手動だけど、その指定は型安全にしたいというケースは、TypeScript の型定義だけがあれば良いはずという仮説がある</li>
<li>たとえば、ENV の指定とかルーティングの指定とかは、値自体は自動生成じゃなくて手動で指定してるはず</li>
<li>とはいえ、デバッグ時には値自体も欲しいことがあったりするので、その辺をいい感じにしようとした時に依存を最小限にして実現する方法を考えてた</li>
<li>よくよく考える <code>typescript</code> はあらゆるところに入っていて、<code>typescript</code> パッケージ自体にパーサがついてるので、これベースで型定義からコード生成すればいいじゃんという発想のサンプル</li>
<li><a href="https://github.com/dsherret/ts-morph">ts-morph</a>とかはあるけど、これを使うと結構依存として大きくなる気がしたので、<code>typescript</code>だけで手軽にできるパターンを考えていて書いたサンプル</li>
<li>まだそこまで出来てないけど、もう何回かか書けばデバッグ用途ならこれで十分みたいな感じのことができるような気がしている
<ul>
<li>デバッグ用途なのでコード生成ツールみたいな厳密なパースとか生成を省いていいみたいな前提でいい感じにしたい</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>全体的にライブラリを作るというより、最小限のものをどうやって作るかというのを考えていた気がします。</p>
<h3>JavaScript Primer</h3>
<p>今年の<a href="https://jsprimer.net/">JavaScript Primer</a>では、ES2024 に対応した<a href="https://github.com/asciidwango/js-primer/releases/tag/v6.0.0">v6.0.0</a>をリリースしました。</p>
<ul>
<li><a href="https://efcl.info/2024/09/02/jsprimer-v6/">JavaScript Primer v6.0.0 リリース: ES2024 の対応と Node.js のユースケースを刷新 | Web Scratch</a></li>
</ul>
<p>大きな変更としては、Node.js のユースケース周りの書き直しと<a href="https://opencollective.com/jsprimer">JavaScript Primer - Open Collective</a>の開始があります。</p>
<p>最近の Node.js は Deno や Bun の登場もあってコア部分への変更も多く入って、Node.js 本体だけでできることがかなり増えています。
jsprimer もそこへの対応を追従していて、<code>node:util</code>の<code>parseArgs</code>関数に対応したり、テストを<code>node:test</code>で書くように変更したりしています。</p>
<p>最近も Node.js 本体(実際には SWC を利用している)で TypeScript の実行をサポートする変更も入ってきています。</p>
<ul>
<li><a href="https://gist.github.com/azu/ac5dafbf211ef8b5ecf386930ac75250">Node.js の TypeScript サポートについて</a></li>
</ul>
<p>そのため、この辺はどうやって対応していくかは考えていく必要がありそうです。</p>
<p>また、ECMAScript 自体も大きなアップデートが来る予定で、ES2025 では Iterator Helpers など基礎文法と読んでいた部分にも変更が大きく入りそうです。jsprimer では、こういった変更に対応する仕組みを作りたくて、<a href="https://opencollective.com/jsprimer">JavaScript Primer - Open Collective</a>を始めました。</p>
<p>Open Collective は GitHub Sponsors と似たオープンソースを支援する仕組みですが、Collective というプロジェクト単位での金銭的な支援ができます。また、Collective への支援金額をそのプロジェクトの Contributors に経費精算という形で支払うことができます。</p>
<p>今年の ES2024 の対応でも手伝っていただいた Contributors の方には、Open Collective から Contribution に応じてお支払いしています。
この支払いルールは<a href="https://github.com/asciidwango/js-primer/blob/master/CONTRIBUTING_EXPENSE.md">Contributing Expenses Policy</a>という形でまとめています。(繰り越した予算については深くは考えずに設計したので、来年もルールは更新していくとは思います。)</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/blob/master/CONTRIBUTING_EXPENSE.md">Contributing Expenses Policy</a></li>
</ul>
<p>次の記事は今年の ES2024 の対応に関するものですが、そろそろ ES2025 の内容も確定するので ES2025 の対応を進めていく必要があります。</p>
<ul>
<li><a href="https://efcl.info/2024/03/21/jsprimer-es2024-proposal/">JavaScript Primer の ES2024 対応を手伝ってくれる Contributor と Sponsor を募集しています | Web Scratch</a></li>
</ul>
<p>まだ特に Issue とかは切ってないですが、このへんやっていきたいとかあったら気軽に Discussions のスレッドを作ってもらえると助かります。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/discussions">https://github.com/asciidwango/js-primer/discussions</a></li>
</ul>
<p>時間的な制約もあるので、レビューの仕組みをちゃんと作って回るようにしていきたいです。</p>
<h2>secretlint</h2>
<p>今年の<a href="https://github.com/secretlint/secretlint">Secretlint</a>では、Secretlint を単一のバイナリファイルとして配布できるようにしました。
以前から Docker 対応はしましたが、ただのバイナリの方が使いやすい場面もあったので、<a href="https://bun.sh/">Bun</a>を使って Single-file executable binary として配布するようにしました。</p>
<p>次の記事では詳しく解説していますが、Node.js 向けに書いたツールでもバイナリとして配布ができて普通に動くので、CI で動かす時とかに便利になりました。</p>
<ul>
<li><a href="https://efcl.info/2024/10/06/bun-single-file-executable-binary/">Bun で Node.js のツールを Single-file executable binary にしてバイナリを配布する | Web Scratch</a></li>
</ul>
<p><a href="https://github.com/secretlint/secretlint">Secretlint</a>や<a href="https://github.com/textlint/textlint">textlint</a>は動的に Node.js のパッケージを読み込むプラグインの仕組みを持っていますが、このようなツールもバイナリにできたのでほとんどのツールは動かせる仕組みだと思います。</p>
<h2>textlint</h2>
<p>今年の<a href="https://github.com/textlint/textlint">textlint</a>も結構大きな変更を入れていました。
textlint v14 で、Old API を非推奨にしました。</p>
<table>
<thead>
<tr>
<th>Old API</th>
<th>New API</th>
</tr>
</thead>
<tbody>
<tr>
<td>textlint</td>
<td>use @textlint/legacy-textlint-core or @textlint/kernel</td>
</tr>
<tr>
<td>TextLintCore</td>
<td>use @textlint/legacy-textlint-core or @textlint/kernel</td>
</tr>
<tr>
<td>TextLintEngine</td>
<td>use createLinter and loadTextlintrc</td>
</tr>
<tr>
<td>TextFixEngine</td>
<td>use createLinter and loadTextlintrc</td>
</tr>
</tbody>
</table>
<ul>
<li><a href="https://textlint.github.io/blog/2024/02/03/textlint-14.html">textlint v14.0.0 · textlint</a></li>
<li><a href="https://efcl.info/2024/02/04/textlint-v14/">textlint v14 をリリースしました | Web Scratch</a></li>
</ul>
<p>次のメジャーアップデートで非推奨となった API は削除していく予定です。</p>
<p>また、textlint は Secretlint での学習を取り込んでいくことが多いのですが、Secretlint で実装した Single-file executable binary にするための仕組みも textlint にも実装してあります。</p>
<ul>
<li><a href="https://github.com/textlint/textlint/pull/1432">feat(resolver): use @textlint/resolver instead of try-resolve by azu · Pull Request #1432 · textlint/textlint</a></li>
</ul>
<p>そのため、textlint も Bun で単体のバイナリとして作ることはできると思います。</p>
<p>他の課題としては、<a href="https://github.com/textlint/editor">@textlint/editor</a>や<a href="https://github.com/taichi/vscode-textlint">vscode-textlint</a>などのエディタ周りもののメンテナンスの問題がありそうです。</p>
<ul>
<li><a href="https://github.com/orgs/textlint/discussions/2">Transferring vscode-textlint to community-based maintenance · textlint · Discussion #2</a></li>
</ul>
<p>エディタ固有に作っていくのはやっぱりコストが高い部分もあるので、<a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a>(LSP)のような汎用的なものに乗れるといいんですが、LSP 自体も大きいため大変そうです。
エディタとの連携が簡単にできて、ブラウザでも簡単に動かせるような仕組みがあるいい感じの落とし所があるといいのですが、なかなか進んでいないのが現状です。</p>
<p>この辺は何かいいアイデアが欲しいです。</p>
<h2>JSer.info</h2>
<p><a href="https://jser.info/">JSer.info</a>は相変わらず更新しています。</p>
<p>情報の収集方法は<a href="https://efcl.info/2024/02/08/javascript-info/">私の JavaScript の情報収集法 2024 年版 | Web Scratch</a>あたりから特に変わった感じはしないですが、最近ちょっとまとめて作業してる感じが出てきているので、もっと細かく情報を集められる仕組みが必要そうな気がしています。</p>
<p>なんだかんだ更新はずっと PC でやっているので、そろそろスマホだけで回るような状態は作った方がいいのかなという気もしました。
(実際の作業自体はスマホでもできるが、あくまでできるレベルなので、効率的にできるようにしたい)</p>
<p>情報見る時に一つ変わったこととしては<a href="https://kagi.com/">Kagi Search</a>をメインの検索エンジンとして使うようになったことです。</p>
<ul>
<li><a href="https://efcl.info/2024/03/15/kagi-search/">Kagi Search をメインの検索エンジンとして使っている | Web Scratch</a></li>
</ul>
<p>1 年ぐらい使ってみて、検索してみてアレ?って感じのことはかなり少ないので、多分このまま使うと思います。
高度な機能を使ってるわけじゃないですが、ちゃんと検索できてその検索結果をちゃんとコントロールできるのが良いです。
<kbd>q</kbd>キーを押して Quick Answer で、検索結果のサマライズを見たい時に見れるというのも意外と便利です。
(AI 機能が欲しいんじゃなくて、検索機能があってそこにサマライズ機能が付属してるぐらいの感覚です。)</p>
<h2>まとめ</h2>
<p>今年は今までの中でもかなり忙しくて、ペース配分は結構難しかったです。
それでも<a href="https://jser.info/">JSer.info</a>とかは、そこまで大きなズレはなく更新し続けられていたので、「更新コストを小さくして継続できる形を作る」というのは大事だなーと思いました。
「更新コストを小さくして継続できる形を作る」については、次の記事では書いています。</p>
<ul>
<li><a href="https://jser.info/2021/01/16/jser-10th/">JSer.info 10周年: JavaScript情報の集め方、書き方、まとめ方 - JSer.info</a></li>
</ul>
<p><a href="https://jsprimer.net/">JavaScript Primer</a>も、ある種サイクルがあるもの(ECMAScriptは毎年更新される)なので、この更新コストをどうやって分散していくかというのは課題になっています。時間的な分散か人的な分散かの方法は色々ありますが、色々組み合わせてやる方法を考えていきたいです。</p>
<p>特に結論はないですが、来年も引き続きオープンソース活動を続けていきたいと思います。</p>
<p>こういったまとめ記事を書いてるのは、振り返りの意味もありますが、GitHub Sponsorsに関する情報はやっぱり少ないので、公開することで他の人の参考になるかなと思っているためです。
実際に去年の記事を書いた後に、GitHub Sponsorsに関する情報をまとめてる人もいたみたいでした。</p>
<ul>
<li><a href="https://dev.to/szabgab/github-sponsors-a-series-on-giving-an-receiving-4fmb">GitHub Sponsors - A series on giving an receiving 💰 - DEV Community</a></li>
</ul>
<section class="footnotes">
<ol>
<li id="fn1">
<p>📝 <a href="https://ja.wikipedia.org/wiki/%E5%A0%B1%E9%85%AC">報酬</a>にしなかったのは定義が難しいのと、見返りを設定してるわけでないため。“所得”は税を引いた後の金額で、GitHub Sponsorsの振込額には源泉徴収はなくて<a href="https://github.com/azu/github-sponsors-tax">確定申告</a>するまで実際の金額が決まらないので不適当そう。<a href="https://english.chicken168.com/uriage/">IncomeやRevenue</a>が妥当な表現と思ったため”収入”と表記している。 <a href="#fnref1" class="footnote-backref">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
 </entry>
 
 <entry>
   <title>BunでNode.jsのツールをSingle-file executable binaryにしてバイナリを配布する</title>
   <link href="https://efcl.info/2024/10/06/bun-single-file-executable-binary/"/>
   <updated>2024-10-06T20:25:00+09:00</updated>
   <id>https://efcl.info/2024/10/06/bun-single-file-executable-binary</id>
   <content type="html"><![CDATA[ <p><a href="https://github.com/secretlint/secretlint">Secretlint</a> v8.3で、単体のバイナリファイルとして<code>secretlint</code>コマンドを配布するようにしました。</p>
<ul>
<li><a href="https://github.com/secretlint/secretlint/releases/tag/v8.3.3">Release v8.3.3 · secretlint/secretlint</a></li>
</ul>
<p>どういうことができるようになるかと、Node.jsをインストールしなくても<code>secretlint</code>コマンドを使えるようになります。
次のようにCurlでダウンロードして実行するだけで、機密情報の検出ができるようになります。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash"><span class="c">#!/usr/bin/env bash</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail
<span class="nv">SECRETLINT_VERSION</span><span class="o">=</span><span class="s2">"8.3.3"</span> <span class="c"># secretlintのバージョン</span>
<span class="nv">ARCH</span><span class="o">=</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-m</span><span class="si">)</span>
<span class="nv">OS</span><span class="o">=</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-s</span> | <span class="nb">tr</span> <span class="s1">'[:upper:]'</span> <span class="s1">'[:lower:]'</span><span class="si">)</span>
<span class="c"># Map architecture to the expected format</span>
<span class="k">case</span> <span class="s2">"</span><span class="nv">$ARCH</span><span class="s2">"</span> <span class="k">in
    </span>x86_64<span class="p">)</span>
        <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"x64"</span>
        <span class="p">;;</span>
    aarch64<span class="p">)</span>
        <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"arm64"</span>
        <span class="p">;;</span>
    arm64<span class="p">)</span>
        <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"arm64"</span>
        <span class="p">;;</span>
    <span class="k">*</span><span class="p">)</span>
        <span class="nb">echo</span> <span class="s2">"Unsupported architecture: </span><span class="nv">$ARCH</span><span class="s2">"</span>
        <span class="nb">exit </span>1
        <span class="p">;;</span>
<span class="k">esac</span>

<span class="c"># Download the binary</span>
curl <span class="nt">-sSL</span> <span class="s2">"https://github.com/secretlint/secretlint/releases/download/</span><span class="si">$(</span>SECRETLINT_VERSION<span class="si">)</span><span class="s2">/secretlint-</span><span class="si">$(</span>SECRETLINT_VERSION<span class="si">)</span><span class="s2">-</span><span class="si">$(</span>OS<span class="si">)</span><span class="s2">-</span><span class="si">$(</span>ARCH<span class="si">)</span><span class="s2">"</span> <span class="nt">-o</span> secretlint
<span class="nb">chmod</span> +x ./secretlint
<span class="c"># Create .secretlintrc.json</span>
./secretlint <span class="nt">--init</span>
<span class="c"># Run secretlint</span>
./secretlint <span class="s2">"**/*"</span>
</code></pre></div></div>
<p><a href="https://github.com/secretlint/secretlint">Secretlint</a>は<a href="https://github.com/secretlint/secretlint#using-docker">Docker</a>も対応していますが、コマンドラインツールの場合は単体のバイナリとして使えると何かと便利です。</p>
<p>npm経由と比べて、Node.jsを必要としないのとダウンロードが1つのファイルだけなのでシンプルです。
Docker経由に比べて、Dockerを必要としないのと実行までの時間が早いはずです。
デメリットとしては、単体のバイナリはルールも同梱しないと配布できないので、ルールの追加が難しい点です。
(一応 node_modules 以下にルールがあれば参照できますが、それは実質npmに依存してるので素直にnpm使ったほうが良いです)</p>
<p>この <code>secretlint</code> バイナリは、<a href="https://bun.sh/">Bun</a>を使ってランタイムも含んだSingle-file executable binaryとして配布しています。
この記事では、Bunを使ってNode.jsのツールのSingle-file executable binaryを配布する方法を紹介します。</p>
<h2>Bunとは</h2>
<p><a href="https://bun.sh/">Bun</a>は、Node.js互換のランタイムです。
<a href="https://bun.sh/blog/bun-v1.0">Bun 1.0</a>が出てから1年ほど経ちますが、Node.jsのコードは結構そのまま動きます。
Secretlintは、特別なBunの対応はすることなく、そのままBunでも実行できました。</p>
<h2>BunでSingle-file executable binaryを作る</h2>
<p>Bunには、<code>bun build --compile</code>というコードから、Single-file executable binaryを作る機能があります。</p>
<ul>
<li><a href="https://bun.sh/docs/bundler/executables">https://bun.sh/docs/bundler/executables</a></li>
</ul>
<p>次のように、<code>bun build --compile</code>を実行すると、<code>secretlint</code>のSingle-file executable binaryが作成されます。
<a href="https://bun.sh/blog/bun-v1.1.5">Bun v1.1.5</a>で、Cross Compileも対応したため、macOSでLinuxのバイナリも作成できます。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash">bun build <span class="nt">--compile</span> <span class="nt">--target</span><span class="o">=</span>bun-linux-x64 ./index.ts <span class="nt">--outfile</span> myapp
</code></pre></div></div>
<p>Secretlintでは、<code>secretlint</code>からCLIのインターフェースを公開しているので、次のようにCLIのインターフェースをラップするだけの<code>index.ts</code>を作成しました。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="k">import</span> <span class="p">{</span> <span class="nx">cli</span><span class="p">,</span> <span class="nx">run</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">secretlint/cli</span><span class="dl">"</span><span class="p">;</span>
<span class="nf">run</span><span class="p">(</span><span class="nx">cli</span><span class="p">.</span><span class="nx">input</span><span class="p">,</span> <span class="nx">cli</span><span class="p">.</span><span class="nx">flags</span><span class="p">).</span><span class="nf">then</span><span class="p">(</span>
    <span class="p">({</span> <span class="nx">exitStatus</span><span class="p">,</span> <span class="nx">stderr</span><span class="p">,</span> <span class="nx">stdout</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nf">if </span><span class="p">(</span><span class="nx">stdout</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">stdout</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nf">if </span><span class="p">(</span><span class="nx">stderr</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nx">stderr</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nx">process</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="nx">exitStatus</span><span class="p">);</span>
    <span class="p">},</span>
    <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
        <span class="nx">process</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>
<p>あとは、これを<code>bun build --compile</code>でビルドするだけ、<code>secretlint</code>のSingle-file executable binaryが作成できます。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash">bun build <span class="nt">--compile</span> <span class="nt">--target</span><span class="o">=</span>bun-linux-x64 ./index.ts <span class="nt">--outfile</span> dist/secretlint
</code></pre></div></div>
<p>しかし、Secretlintはデフォルトではルールを同梱してないので、バイナリにルールを含める必要があります。
ルールは、基本的に<code>.secretlintrc.json</code>に定義したものを、動的にロード(Dynamic Import)することで読み込むことで利用できます。
ただ、<code>bun build --compile</code>は、外からパッケージ名を指定してロードするようなものをはそのままではバイナリに含めることができません。</p>
<p>そのため、SecretlintやESLint、textlintなどの動的なパッケージロードを含むツールをSingle-file executable binaryにするには工夫が必要です。</p>
<h2>動的なパッケージロードを含むツールをSingle-file executable binaryにする</h2>
<p>Secretlintには、<a href="https://github.com/secretlint/secretlint/tree/master/packages/%40secretlint/secretlint-rule-preset-recommend">@secretlint/secretlint-rule-preset-recommend</a>という推奨ルールセットがあります。
多くのケースでは、このプリセットルールがあればある程度使えるので、単体のバイナリに含めることにしました。</p>
<p>しかし、<code>bun build --compile</code>は、<code>import(name)</code>のようなDynamic Importの引数が変数の場合には対応していません。</p>
<p>具体的には、次のようなコードがあった場合、<code>loadDynamicModule(&quot;rambda&quot;)</code>のようなコードはビルド済みのバイナリでは実行に失敗します。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="k">import</span> <span class="dl">"</span><span class="s2">rambda</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// ← これはOK</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">loadDynamicModuleHard</span> <span class="o">=</span> <span class="nf">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">head</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="dl">"</span><span class="s2">rambda</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// ← これはOK</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nf">head</span><span class="p">(</span><span class="dl">"</span><span class="s2">XLL</span><span class="dl">"</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">loadDynamicModule</span> <span class="o">=</span> <span class="nf">async </span><span class="p">(</span><span class="nx">packageName</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">head</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="nx">packageName</span><span class="p">);</span> <span class="c1">// ← これはNG</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nf">head</span><span class="p">(</span><span class="dl">"</span><span class="s2">XLL</span><span class="dl">"</span><span class="p">));</span>
<span class="p">}</span>
<span class="nf">loadDynamicModule</span><span class="p">(</span><span class="dl">"</span><span class="s2">rambda</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>
<p>📝 Deno Compileの場合は、パッケージ名でキャッシュされているので、事前にimportしておくだけで<code>loadDynamicModule(packageName)</code>のようなコードは動作します。</p>
<ul>
<li><a href="https://zenn.dev/kn1cht/articles/deno-textlint">https://zenn.dev/kn1cht/articles/deno-textlint</a></li>
</ul>
<p>この問題を回避するには、事前にstatic import(import文)で必要なパッケージをimportしておき、Dynamic ImportをHookしてモックするような処理が必要です。
Secretlintでは、<a href="https://github.com/secretlint/secretlint/tree/master/packages/%40secretlint/resolver"><code>@secretlint/resolver</code></a>という内部的なresolver/import hooksを実装しました。</p>
<p><code>@secretlint/resolver</code>は、<code>require.resolve(specifier)</code>と<code>import(specifier)</code>をラップした関数を提供しています。
それと合わせて、<code>registerResolveHook</code>と<code>registerImportHook</code>という関数を提供して、resolveやimportするときの処理に対してHookを登録できるようにしています。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="k">import</span> <span class="p">{</span> <span class="nx">createRequire</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:module</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">url</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:url</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:path</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">require</span> <span class="o">=</span> <span class="nf">createRequire</span><span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">ResolverContext</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">parentImportMeta</span><span class="p">:</span> <span class="nx">ImportMeta</span><span class="p">;</span>
    <span class="nl">parentModule</span><span class="p">:</span> <span class="dl">"</span><span class="s2">config-loader</span><span class="dl">"</span> <span class="o">|</span> <span class="dl">"</span><span class="s2">formatter</span><span class="dl">"</span><span class="p">;</span>
<span class="p">};</span>
<span class="kd">type</span> <span class="nx">ResolverSkipResult</span> <span class="o">=</span> <span class="kc">undefined</span><span class="p">;</span>
<span class="cm">/**
 * Resolve Hook
 */</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">ResolveHook</span> <span class="o">=</span> <span class="p">(</span>
    <span class="nx">specifier</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="nx">context</span><span class="p">:</span> <span class="nx">ResolverContext</span>
<span class="p">)</span> <span class="o">=&gt;</span>
    <span class="o">|</span> <span class="p">{</span>
          <span class="na">url</span><span class="p">:</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="o">|</span> <span class="nx">ResolverSkipResult</span><span class="p">;</span>
<span class="cm">/**
 * dynamic import() hook
 */</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">ImportHook</span> <span class="o">=</span> <span class="p">(</span>
    <span class="nx">specifier</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="nx">context</span><span class="p">:</span> <span class="nx">ResolverContext</span>
<span class="p">)</span> <span class="o">=&gt;</span> <span class="nb">Promise</span><span class="o">&lt;</span>
    <span class="o">|</span> <span class="p">{</span>
          <span class="na">exports</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nx">unknown</span><span class="o">&gt;</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="o">|</span> <span class="nx">ResolverSkipResult</span>
<span class="o">&gt;</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">resolveHooks</span><span class="p">:</span> <span class="nx">ResolveHook</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">const</span> <span class="nx">importHooks</span><span class="p">:</span> <span class="nx">ImportHook</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[];</span>
<span class="cm">/**
 * Register Resolver Hook
 * Hook can return resolved URL
 * if hooks pass through, it should return `undefined` instead of object
 * @param hook
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">registerResolveHook</span> <span class="o">=</span> <span class="p">(</span><span class="nx">hook</span><span class="p">:</span> <span class="nx">ResolveHook</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">resolveHooks</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">hook</span><span class="p">);</span>
<span class="p">};</span>

<span class="cm">/**
 * Try to resolve package name
 * if `packageName` is found, return resolved absolute path.
 * if `packageName` is not found, return `undefined`
 * @param packageName
 * @param context
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">tryResolve</span> <span class="o">=</span> <span class="p">(</span><span class="nx">packageName</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">context</span><span class="p">:</span> <span class="nx">ResolverContext</span><span class="p">):</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="nf">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">hook</span> <span class="k">of</span> <span class="nx">resolveHooks</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">hook</span><span class="p">(</span><span class="nx">packageName</span><span class="p">,</span> <span class="nx">context</span><span class="p">);</span>
            <span class="c1">// Skip if hook return undefined from hook</span>
            <span class="nf">if </span><span class="p">(</span><span class="o">!</span><span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">continue</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="nf">if </span><span class="p">(</span><span class="nx">result</span><span class="p">?.</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nx">result</span><span class="p">.</span><span class="nx">url</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="c1">// TODO: import.meta.resolve is not supported in Node.js 18</span>
        <span class="c1">// We will change to import.meta.resolve(packageName)</span>
        <span class="k">return</span> <span class="nx">require</span><span class="p">.</span><span class="nf">resolve</span><span class="p">(</span><span class="nx">packageName</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
        <span class="k">return</span> <span class="kc">undefined</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="cm">/**
 * Register Import Hook
 * @param hook
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">registerImportHook</span> <span class="o">=</span> <span class="p">(</span><span class="nx">hook</span><span class="p">:</span> <span class="nx">ImportHook</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">importHooks</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">hook</span><span class="p">);</span>
<span class="p">};</span>

<span class="c1">// Windows's path require to convert file://</span>
<span class="c1">// https://github.com/secretlint/secretlint/issues/205</span>
<span class="kd">const</span> <span class="nx">convertToFileUrl</span> <span class="o">=</span> <span class="p">(</span><span class="nx">filePath</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">if </span><span class="p">(</span><span class="nx">filePath</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">file://</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">filePath</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">url</span><span class="p">.</span><span class="nf">pathToFileURL</span><span class="p">(</span><span class="nx">filePath</span><span class="p">).</span><span class="nx">href</span><span class="p">;</span>
<span class="p">};</span>
<span class="cm">/**
 * dynamic import() with hooks
 * @param specifier file path or package name
 * @param context
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">dynamicImport</span> <span class="o">=</span> <span class="nf">async </span><span class="p">(</span>
    <span class="nx">specifier</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="nx">context</span><span class="p">:</span> <span class="nx">ResolverContext</span>
<span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="p">{</span>
    <span class="na">exports</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nx">unknown</span><span class="o">&gt;</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">;</span>
<span class="p">}</span><span class="o">&gt;</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">hook</span> <span class="k">of</span> <span class="nx">importHooks</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">hook</span><span class="p">(</span><span class="nx">specifier</span><span class="p">,</span> <span class="nx">context</span><span class="p">);</span>
        <span class="nf">if </span><span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="c1">// if the `specifier` is not absolute path, it should be package name</span>
    <span class="nf">if </span><span class="p">(</span><span class="o">!</span><span class="nx">path</span><span class="p">.</span><span class="nf">isAbsolute</span><span class="p">(</span><span class="nx">specifier</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="na">exports</span><span class="p">:</span> <span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="nx">specifier</span><span class="p">),</span>
        <span class="p">};</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="na">exports</span><span class="p">:</span> <span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="nf">convertToFileUrl</span><span class="p">(</span><span class="nx">specifier</span><span class="p">)),</span>
    <span class="p">};</span>
<span class="p">};</span>
</code></pre></div></div>
<p><code>bun build --compile</code>するコードに、<code>@secretlint/resolver</code>を使って、本来は動的にロードされる<code>@secretlint/secretlint-rule-preset-recommend</code>を静的にロードするようなHookを登録しています。
これによって、本来は<code>import(packageName)</code>というコードで動的にロードされるパッケージを、<code>import</code>文で静的にロードしておくことで、バイナリに動的なパッケージを含めています。</p>
<ul>
<li>コード: <a href="https://github.com/secretlint/secretlint/blob/8698cc263acc28a38e4bb2f3f4f27b5086669509/publish/binary-compiler/src/secretlint-resolver-hooks.ts">https://github.com/secretlint/secretlint/blob/8698cc263acc28a38e4bb2f3f4f27b5086669509/publish/binary-compiler/src/secretlint-resolver-hooks.ts</a></li>
</ul>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">preset</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@secretlint/secretlint-rule-preset-recommend</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">registerImportHook</span><span class="p">,</span> <span class="nx">registerResolveHook</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@secretlint/resolver</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">mocks</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">@secretlint/secretlint-rule-preset-recommend</span><span class="dl">"</span><span class="p">:</span> <span class="nx">preset</span><span class="p">,</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">mockNames</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nf">keys</span><span class="p">(</span><span class="nx">mocks</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">mockNameRegex</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RegExp</span><span class="p">(</span><span class="s2">`^(?:</span><span class="p">${</span><span class="nx">mockNames</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">|</span><span class="dl">"</span><span class="p">)}</span><span class="s2">)$`</span><span class="p">);</span>
<span class="c1">// preserve mocks module name</span>
<span class="c1">// "@secretlint/secretlint-rule-preset-recommend" -&gt; "@secretlint/secretlint-rule-preset-recommend"</span>
<span class="c1">// By default, secretlint will resolve it into absolute path, but we want to keep it as is.</span>
<span class="nf">registerResolveHook</span><span class="p">((</span><span class="nx">moduleName</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">if </span><span class="p">(</span><span class="nx">mockNames</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="nx">moduleName</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="na">url</span><span class="p">:</span> <span class="nx">moduleName</span>
        <span class="p">};</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kc">undefined</span><span class="p">;</span> <span class="c1">// pass through</span>
<span class="p">});</span>
<span class="c1">// import mock module</span>
<span class="c1">// "@secretlint/secretlint-rule-preset-recommend" -&gt; { exports: preset }</span>
<span class="nf">registerImportHook</span><span class="p">(</span><span class="nf">async </span><span class="p">(</span><span class="nx">moduleName</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">if </span><span class="p">(</span><span class="nx">mockNameRegex</span><span class="p">.</span><span class="nf">test</span><span class="p">(</span><span class="nx">moduleName</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="na">exports</span><span class="p">:</span> <span class="nx">mocks</span><span class="p">[</span><span class="nx">moduleName</span> <span class="k">as</span> <span class="kr">keyof</span> <span class="k">typeof</span> <span class="nx">mocks</span><span class="p">]</span>
        <span class="p">};</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kc">undefined</span><span class="p">;</span> <span class="c1">// pass through</span>
<span class="p">});</span>
</code></pre></div></div>
<p>あとは、これらのHookを含むコードを<code>bun build --compile</code>でビルドするだけで、Single-file executable binaryが作成できます。</p>
<ul>
<li>Binary Compiler: <a href="https://github.com/secretlint/secretlint/tree/master/publish/binary-compiler">https://github.com/secretlint/secretlint/tree/master/publish/binary-compiler</a></li>
</ul>
<h2>他の実装方法</h2>
<p><a href="https://github.com/secretlint/secretlint">Secretlint</a>では、Bunを使ってSingle-file executable binaryを作成しましたが、他にもいくつかの方法があります。
<a href="https://deno.com/">Deno</a>にも<a href="https://docs.deno.com/runtime/reference/cli/compiler/"><code>deno compile</code></a>というコマンドがあり、Denoのコードをバイナリに変換できます。</p>
<h3>Denoでの実装</h3>
<p><a href="https://docs.deno.com/runtime/reference/cli/compiler/"><code>deno compile</code></a>の場合は、事前に動的にロードされるパッケージをimportしておくだけで、バイナリに含めることができます。</p>
<p>具体的には次のようなコードを書くだけです。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="c1">// preload for embedded binary</span>
<span class="c1">// TODO: use local file path instead of npm registry</span>
<span class="c1">// https://github.com/denoland/deno/issues/18474</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">npm:@secretlint/secretlint-rule-preset-recommend</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">cli</span><span class="p">,</span> <span class="nx">run</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">npm:secretlint/cli</span><span class="dl">"</span><span class="p">;</span>
<span class="nf">run</span><span class="p">(</span><span class="nx">cli</span><span class="p">.</span><span class="nx">input</span><span class="p">,</span> <span class="nx">cli</span><span class="p">.</span><span class="nx">flags</span><span class="p">).</span><span class="nf">then</span><span class="p">(</span>
    <span class="p">({</span> <span class="nx">exitStatus</span><span class="p">,</span> <span class="nx">stderr</span><span class="p">,</span> <span class="nx">stdout</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nf">if </span><span class="p">(</span><span class="nx">stdout</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">stdout</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nf">if </span><span class="p">(</span><span class="nx">stderr</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nx">stderr</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nx">Deno</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="nx">exitStatus</span><span class="p">);</span>
    <span class="p">},</span>
    <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
        <span class="nx">Deno</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>
<p>あとは、<code>deno compile</code>でビルドするだけで、Single-file executable binaryが作成できます。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash">deno compile <span class="nt">--target</span> <span class="s2">"x86_64-unknown-linux-gnu"</span> <span class="nt">--output</span> dist/secretlint src/entry.ts
</code></pre></div></div>
<ul>
<li><code>deno compile</code>での実装: <a href="https://github.com/secretlint/secretlint/tree/d23e72dec453dbbdb9a7e0fd58ca56e91af30dcd/publish/binary-compiler">https://github.com/secretlint/secretlint/tree/d23e72dec453dbbdb9a7e0fd58ca56e91af30dcd/publish/binary-compiler</a></li>
</ul>
<p>最初はDenoで実装していましたが、この方法にはデメリットがあります。
<code>npm:</code> というspecifierを指定してることからもわかるように、npm registryからパッケージをダウンロードしたものをバイナリにしています。</p>
<p>そのため、CIでバイナリをビルドしてそのままテストするには、ローカルのNode.js向けのコードを参照してバイナリにする方法が必要です。
<code>deno compile</code>で、ローカルのNode.js向けのパッケージを参照して、それをバイナリにする方法を試してみましたがよくわかりませんでした。</p>
<p>Denoはnpmのworkspacesもサポートしているので、これを利用するとローカルのNode.js向けのパッケージをDenoで使うことはできます。</p>
<ul>
<li><a href="https://docs.deno.com/runtime/fundamentals/workspaces/#migrating-from-npm-workspaces">https://docs.deno.com/runtime/fundamentals/workspaces/#migrating-from-npm-workspaces</a></li>
</ul>
<p>しかし、<code>deno compile</code>したバイナリには、このローカルのNode.js向けのパッケージを含めることができませんでした。</p>
<ul>
<li>サンプル: <a href="https://github.com/azu/deno-node-workspace-example">https://github.com/azu/deno-node-workspace-example</a></li>
</ul>
<p>また、Denoには必要なAPIがまだ実装されてなかったのもあり、今回はBunを使うことにしました。</p>
<ul>
<li><a href="https://github.com/denoland/deno/issues/25034">Implement <code>PerformanceObserver.observe</code> from <code>node:perf_hooks</code> · Issue #25034 · denoland/deno</a></li>
</ul>
<h3>Bun Plugin APIでの実装(失敗)</h3>
<p>Secretlintの実装では、<code>@secretlint/resolver</code>というHookするパッケージを実装しています。
これは内部的なコードだから対応できましたが、外部のパッケージが同じような動的ロードをしている場合は、Hookすることが難しいです。</p>
<p>これを回避するためにBunのPlugin APIを使って、<code>require.resolve</code>や<code>import(name)</code>をHookする方法を試しましたが、うまくいきませんでした。</p>
<ul>
<li><a href="https://bun.sh/docs/runtime/plugins">Plugins – Runtime | Bun Docs</a></li>
</ul>
<p><code>onResolve</code>で<code>require.resolve</code>のパスを書き換えできれば、<code>@secretlint/resolver</code>のようなHookを実装できるかと思いましたが、うまくいきませんでした。
Bun Plugin APIの<code>onResolve</code>は、拡張子がない識別子を<code>require.resolve</code>するときに呼ばれないことがあったり、挙動がイマイチよくわからない感じでした。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="k">import</span> <span class="p">{</span> <span class="nx">plugin</span><span class="p">,</span> <span class="kd">type</span> <span class="nx">PluginBuilder</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">bun</span><span class="dl">"</span><span class="p">;</span>

<span class="k">await</span> <span class="nf">plugin</span><span class="p">({</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">bun-test</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">target</span><span class="p">:</span> <span class="dl">"</span><span class="s2">bun</span><span class="dl">"</span><span class="p">,</span>
  <span class="nf">setup</span><span class="p">(</span><span class="na">build</span><span class="p">:</span> <span class="nx">PluginBuilder</span><span class="p">):</span> <span class="k">void</span> <span class="o">|</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nx">build</span><span class="p">.</span><span class="nf">onResolve</span><span class="p">({</span> <span class="na">filter</span><span class="p">:</span> <span class="sr">/.*/</span> <span class="p">},</span> <span class="nf">async </span><span class="p">(</span><span class="nx">args</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">onResolve</span><span class="dl">"</span><span class="p">,</span> <span class="nx">args</span><span class="p">);</span>
      <span class="k">return</span> <span class="nx">args</span>
    <span class="p">});</span>
    <span class="nx">build</span><span class="p">.</span><span class="nf">onLoad</span><span class="p">({</span> <span class="na">filter</span><span class="p">:</span> <span class="sr">/.*/</span> <span class="p">},</span> <span class="nf">async </span><span class="p">(</span><span class="nx">args</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">onload</span><span class="dl">"</span><span class="p">,</span> <span class="nx">args</span><span class="p">);</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">contents</span><span class="p">:</span> <span class="dl">"</span><span class="s2">console.log('Hello, world!')</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">loader</span><span class="p">:</span> <span class="dl">"</span><span class="s2">ts</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">};</span>
    <span class="p">});</span>
    <span class="nx">build</span><span class="p">.</span><span class="nf">module</span><span class="p">(</span><span class="dl">"</span><span class="s2">rambda</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">module load</span><span class="dl">"</span><span class="p">);</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">exports</span><span class="p">:</span> <span class="p">{},</span>
        <span class="na">loader</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span>
      <span class="p">}</span>
    <span class="p">})</span>
  <span class="p">}</span>
<span class="p">});</span>

<span class="c1">// resolve existing module</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">resolve - existing</span><span class="dl">"</span><span class="p">)</span>
<span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nf">resolve</span><span class="p">(</span><span class="dl">"</span><span class="s2">rambda</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">groupEnd</span><span class="p">()</span>

<span class="c1">// import existing module</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">import - existing</span><span class="dl">"</span><span class="p">)</span>
<span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="dl">"</span><span class="s2">rambda</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">groupEnd</span><span class="p">()</span>

<span class="c1">// resolve unknown extension module</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">resolve - unknown extension</span><span class="dl">"</span><span class="p">)</span>
<span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nf">resolve</span><span class="p">(</span><span class="dl">"</span><span class="s2">rambda.ext</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">groupEnd</span><span class="p">()</span>

<span class="c1">// import unknown extension module</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">import - unknown extension</span><span class="dl">"</span><span class="p">)</span>
<span class="k">try</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="dl">"</span><span class="s2">rambda.ext</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">groupEnd</span><span class="p">()</span>

<span class="c1">// resolve file path</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">resolve - file path</span><span class="dl">"</span><span class="p">)</span>
<span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nf">resolve</span><span class="p">(</span><span class="dl">"</span><span class="s2">./file-path.js</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">groupEnd</span><span class="p">()</span>

<span class="c1">// import file path</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">import - file path</span><span class="dl">"</span><span class="p">)</span>
<span class="k">try</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="dl">"</span><span class="s2">./file-path.js</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">groupEnd</span><span class="p">()</span>
</code></pre></div></div>
<p>実行結果は次のようになります。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; bun run entry.ts

resolve - existing
undefined
import - existing
module load
undefined
resolve - unknown extension
onResolve {
  path: "rambda.ext",
  importer: "~/bun-plugin-resolver-test/entry.ts",
}
undefined
import - unknown extension
onResolve {
  path: "rambda.ext",
  importer: "~_bun-plugin-resolver-test/entry.ts",
}
undefined
resolve - file path
undefined
import - file path
onResolve {
  path: "./file-path.js",
  importer: "~/bun-plugin-resolver-test/entry.ts",
}
</code></pre></div></div>
<p>Bun内部の<a href="https://bun.sh/docs/bundler/loaders">Loaders</a>もこの仕組みで実装しているそうですが、挙動がイマイチよくわからない感じでした。すでに登録されているものがあると、呼ばれないとかそういうことがあったりするのかなと思いました。</p>
<p>ファイルパスではなく、パッケージのみを対象にする場合は、<a href="https://bun.sh/docs/runtime/plugins#virtual-modules">build.module</a>という<a href="https://bun.sh/docs/runtime/plugins#virtual-modules">Virtual Modules</a>を扱うAPIを使うことで対応できます。
Secretlintだと<code>require.resolve</code>したパスをimportしていたため、この方式は諦めました。</p>
<h2>まとめ</h2>
<p>Node.jsのツールをSingle-file executable binaryにする方法を紹介しました。
Secretlintでは、Bunを使ってSingle-file executable binaryを作成しています。</p>
<p>CIでSecretlintを実行する場合、起動までの時間が短いとCIのコストも抑えられます。
npmを使ってインストールする方式に比べて、<code>secretlint</code>のバイナリ1つで済むので、CIのセットアップ時間が短くなります。
いくつかのプロジェクトで試していた感じでは、CIのトータル時間は大体半分ぐらいになりました(setup時間の方が基本的に長いです)。</p>
<p>動的なパッケージロードするようなツールはちょっと工夫が必要でしたが、それ以外は特に問題なくバイナリにして動作させることができました。</p>
<ul>
<li>実装: <a href="https://github.com/secretlint/secretlint/tree/master/publish/binary-compiler">@secretlint/binary-compiler</a></li>
</ul>
<p><a href="https://github.com/azu/bluenotiondb">bluenotiondb</a>でも似た方式でバイナリにして使ってたりするので、Node.jsのツールをバイナリにする選択肢が増えて便利になったと思います。</p>
<ul>
<li><a href="https://efcl.info/2023/09/09/bluenotiondb/">Bluesky/GitHub/Calendar/RSSをNotionに同期するbluenotiondbを作った | Web Scratch</a></li>
</ul>
 ]]></content>
 </entry>
 
 <entry>
   <title>JavaScript Primer v6.0.0リリース: ES2024の対応とNode.jsのユースケースを刷新</title>
   <link href="https://efcl.info/2024/09/02/jsprimer-v6/"/>
   <updated>2024-09-02T09:00:00+09:00</updated>
   <id>https://efcl.info/2024/09/02/jsprimer-v6</id>
   <content type="html"><![CDATA[ <p>JavaScript Primer v6.0.0をリリースしました 🎉</p>
<p>JavaScript Primer v6.0.0では、ECMAScript 2024の対応とNode.jsのユースケースを新たに増えた<code>node:util</code>の<code>parseArgs</code>関数や<code>node:test</code>を使うように書き直しています。</p>
<h1>JavaScript Primer Sponsors</h1>
<p>JavaScript Primer(jsprimer)では、Open Collectiveを通じてプロジェクトの更新に関わる資金を募っています。</p>
<ul>
<li><a href="https://opencollective.com/jsprimer">JavaScript Primer - Open Collective</a></li>
</ul>
<p>今回のv6リリースにおいては、次の方々にご支援いただきました！</p>
<p><img src="https://github.com/user-attachments/assets/3b80e74f-917a-450b-9bfc-f4e3659d3015" alt="JavaScript Primerスポンサー" /></p>
<p><a href="https://kokuchou.net/">株式会社コクチョウ</a>さんは、Yearly Gold Sponsorとしてご支援いただいています。</p>
<p>ご支援ありがとうございます！</p>
<p>JavaScript Primerスポンサーについては、次のページを参照してください。</p>
<ul>
<li><a href="https://jsprimer.net/intro/sponsors/">JavaScript Primerスポンサー · JavaScript Primer #jsprimer</a></li>
</ul>
<p>また、ご支援いただいた資金は、jsprimerにcontributionしてくれた方へ還元できるような仕組みを設計しました。
次回からは、この仕組みを回していけるようにしたいです。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/blob/master/CONTRIBUTING_EXPENSE.md">Contributing Expenses Policy</a></li>
</ul>
<h1>JavaScript  Primer v6.0.0の変更点</h1>
<p>JavaScript  Primer v6.0.0の変更点について紹介します。</p>
<p>リリースノートは次のページからも確認できます。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/releases/tag/v6.0.0">Release v6.0.0: ES2024の対応/Node.jsの大幅更新 · asciidwango/js-primer</a></li>
</ul>
<p>更新をメールで受け取りたい方は、次のフォームから登録できます。</p>
<!-- Begin Mailchimp Signup Form -->
<link href="//cdn-images.mailchimp.com/embedcode/classic-071822.css" rel="stylesheet" type="text/css">
<style type="text/css">
	#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; padding: 4px; max-width:600px;}
	/* Add your own Mailchimp form style overrides in your site stylesheet or in this style block.
	   We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
</style>
<div id="mc_embed_signup">
    <form action="https://jsprimer.us13.list-manage.com/subscribe/post?u=fc41e11a2b9dc6f05350e0de0&amp;id=7ab1594ae8&amp;f_id=00f796e2f0" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
        <div id="mc_embed_signup_scroll">
        <h2>JavaScript Primerの更新情報を購読</h2>
        <div class="indicates-required"><span class="asterisk">*</span> indicates required</div>
<div class="mc-field-group">
	<label for="mce-EMAIL">Email Address  <span class="asterisk">*</span>
</label>
	<input type="email" value="" name="EMAIL" class="required email" id="mce-EMAIL" required>
	<span id="mce-EMAIL-HELPERTEXT" class="helper_text"></span>
</div>
	<div id="mce-responses" class="clear foot">
		<div class="response" id="mce-error-response" style="display:none"></div>
		<div class="response" id="mce-success-response" style="display:none"></div>
	</div>    <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
    <div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_fc41e11a2b9dc6f05350e0de0_7ab1594ae8" tabindex="-1" value=""></div>
        <div class="optionalParent">
            <div class="clear foot">
                <input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button">
                <p class="brandingLogo"><a href="http://eepurl.com/h44CST" title="Mailchimp - email marketing made easy and fun"><img src="https://eep.io/mc-cdn-images/template_images/branding_logo_text_dark_dtp.svg"></a></p>
            </div>
        </div>
    </div>
</form>
</div>
<script type='text/javascript' src='//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js'></script><script type='text/javascript'>(function($) {window.fnames = new Array(); window.ftypes = new Array();fnames[0]='EMAIL';ftypes[0]='email';}(jQuery));var $mcj = jQuery.noConflict(true);</script>
<!--End mc_embed_signup-->
<p>フォームが表示されない人は <a href="https://github.us13.list-manage.com/subscribe/post?u=fc41e11a2b9dc6f05350e0de0&amp;id=7ab1594ae8">https://github.us13.list-manage.com/subscribe/post?u=fc41e11a2b9dc6f05350e0de0&amp;id=7ab1594ae8</a> から登録できます。</p>
<h2>変更のサマリ</h2>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/pull/1732">ErrorCause への対応</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1755">改訂 2 版の電子版へのリンクを追加</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1751">Map.groupBy 静的メソッドの追加</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1749">Object.groupBy 静的メソッドを追加</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1743">Stage 2.7 を追加</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1757">fix(nodecli): commanderパッケージ を <code>node:util</code> の <code>parseArg</code> に変更</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1737">mocha を node:test に変更する</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1760">update to marked@14</a></li>
</ul>
<h1><a href="https://github.com/asciidwango/js-primer/pull/1732">ErrorCauseへの対応 by himanoa · Pull Request #1732</a></h1>
<h2>概要</h2>
<p>ES2022で追加された<code>Error</code>の<code>cause</code>オプションは、エラーのスタックトレースを改善するために追加されました。
<code>cause</code>オプションを使うことで、エラーを再度投げ直す場合に、元のエラーのスタックトレースを維持しながら新しいエラーを投げることができます。</p>
<h2>変更されたページ</h2>
<ul>
<li><a href="https://jsprimer.net/basic/error-try-catch/">例外処理 · JavaScript Primer #jsprimer</a></li>
</ul>
<h2>変更内容</h2>
<ul>
<li><code>ErrorCause</code>オブジェクトの説明を追加</li>
<li>ウェブ版のREPLをErrorCauseに対応</li>
</ul>
<h1><a href="https://github.com/asciidwango/js-primer/pull/1755">改訂2版の電子版へのリンクを追加 by azu · Pull Request #1755</a></h1>
<p>電子版の改訂2版が公開されたので、リンクを追加しました。</p>
<ul>
<li><a href="https://tatsu-zine.com/books/javascript-primer-2ed">JavaScript Primer 改訂2版【委託】 - 達人出版会</a></li>
</ul>
<h1><a href="https://github.com/asciidwango/js-primer/pull/1751"><code>Map.groupBy</code>静的メソッドの追加 by azu · Pull Request #1751</a></h1>
<h2>概要</h2>
<p>ES2024では、配列の要素をグループ分けしたマップを作成する<code>Map.groupBy</code>静的メソッドが追加されています。</p>
<p><code>Map.groupBy</code>静的メソッドは<code>Object.groupBy</code>静的メソッドとよく似たメソッドです。
<code>Object.groupBy</code>静的メソッドは配列からオブジェクトを作成するのに対して、<code>Map.groupBy</code>静的メソッドは配列からマップを作成します。</p>
<p><code>Map.groupBy</code>静的メソッドも元々は、<code>Array.prototype.groupToMap</code>メソッドという配列のメソッドとして提案されていましたが、<code>Object.groupBy</code>静的メソッドに合わせる形で<code>Map.groupBy</code>静的メソッドに変更されました。</p>
<h2>変更されたページ</h2>
<ul>
<li><a href="https://jsprimer.net/basic/map-and-set/">Map/Set · JavaScript Primer #jsprimer</a></li>
</ul>
<h2>変更内容</h2>
<ul>
<li><code>Map.groupBy</code>静的メソッドの説明を追加</li>
</ul>
<h2>関連</h2>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/pull/1716">https://github.com/asciidwango/js-primer/pull/1716</a></li>
<li><a href="https://github.com/asciidwango/js-primer/pull/1749">https://github.com/asciidwango/js-primer/pull/1749</a></li>
</ul>
<h1><a href="https://github.com/asciidwango/js-primer/pull/1749"><code>Object.groupBy</code> 静的メソッドを追加 by azu · Pull Request #1749</a></h1>
<h2>概要</h2>
<p>ES2024では、配列の要素をグループ分けしたオブジェクトを作成する<code>Object.groupBy</code>静的メソッドが追加されています。
配列の要素をグループ化を簡潔に書けるようになっています。</p>
<p><code>Array.prototype.groupBy</code>のようなArrayのメソッドではないのは、既存のprototype拡張をしていたライブラリとの競合を避けるためです。
最初は、<code>Array.prototype.groupBy</code>メソッドとして提案されていましたが、既存のウェブサイトとの互換性の問題が見つかったため、<code>Object.groupBy</code>静的メソッドに変更されました。</p>
<h2>変更されたページ</h2>
<ul>
<li><a href="https://jsprimer.net/basic/array/">配列 · JavaScript Primer #jsprimer</a></li>
</ul>
<h2>変更内容</h2>
<ul>
<li><code>Object.groupBy</code>静的メソッドの説明を追加</li>
</ul>
<h2>関連</h2>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/pull/1716">https://github.com/asciidwango/js-primer/pull/1716</a></li>
</ul>
<h1><a href="https://github.com/asciidwango/js-primer/pull/1743">Stage 2.7を追加 by azu · Pull Request #1743</a></h1>
<h2>概要</h2>
<p>ECMAScript ProposalのStage 2.7を追加しました</p>
<p>ブラウザが実装しはじめて見つかる問題などによりステージ2と3を行き来してしまうことがあったためです。
そのため、実装の前のテストと検証のためのステージとして<code>2.7</code>が追加されました。</p>
<h2>変更されたページ</h2>
<ul>
<li><a href="https://jsprimer.net/basic/ecmascript/">ECMAScript · JavaScript Primer #jsprimer</a></li>
</ul>
<h2>関連</h2>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/pull/1713">https://github.com/asciidwango/js-primer/pull/1713</a></li>
</ul>
<h1>Node.js CLIのアップデート</h1>
<p>Node.jsの標準パッケージがカバーする範囲が広がったため、Node.jsのユースケースを大幅に書き換えました。</p>
<h2><a href="https://github.com/asciidwango/js-primer/pull/1757">commanderパッケージ を <code>node:util</code> の <code>parseArg</code> に変更 by azu · Pull Request #1757</a></h2>
<h3>概要</h3>
<p><code>commander</code>パッケージは使わずに、Node.jsの標準モジュールである<code>node:util</code>の<code>parseArg</code>関数を使うように変更しています。</p>
<h3>変更されたページ</h3>
<ul>
<li><a href="https://jsprimer.net/use-case/nodecli/">Node.jsでCLIアプリ · JavaScript Primer #jsprimer</a></li>
</ul>
<h3>変更内容</h3>
<ul>
<li><a href="https://jsprimer.net/use-case/nodecli/helloworld/">Node.jsでHello World</a>
<ul>
<li>“Node.jsプロジェクトのセットアップ “を追加 (<code>package.json</code>の作成)</li>
</ul>
</li>
<li><a href="https://jsprimer.net/use-case/nodecli/argument-parse/">コマンドライン引数を処理する</a>
<ul>
<li><code>commander</code> を <code>node:util</code> の <code>parseArgs</code>関数に変更</li>
<li><code>デフォルト設定を定義する {#declare-default}</code>セクションは削除(defaultオプションでいいため)</li>
<li><code>--gfm</code>の呼び方をフラグに統一(オプションと呼んでる場所があった)</li>
</ul>
</li>
<li><a href="https://jsprimer.net/use-case/nodecli/md-to-html/">MarkdownをHTMLに変換する</a>
<ul>
<li><code>npm install</code> の説明を移動</li>
<li><a href="https://jsprimer.net/use-case/nodecli/md-to-html/#node-prefix">[コラム] node: prefix </a>を追加
<ul>
<li>本文は <code>node:</code> に統一したので、逆に本文にはいらなくなった</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>関連</h3>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/pull/1698">https://github.com/asciidwango/js-primer/pull/1698</a></li>
</ul>
<h2><a href="https://github.com/asciidwango/js-primer/pull/1737">mochaを<code>node:test</code>に変更する by windchime-yk · Pull Request #1737</a></h2>
<h3>概要</h3>
<p>テストを<a href="https://mochajs.org/">Mocha</a>から標準モジュールの<code>node:test</code>に変更しました</p>
<h3>変更されたページ</h3>
<ul>
<li><a href="https://jsprimer.net/use-case/nodecli/refactor-and-unittest/">ユニットテストを記述する · JavaScript Primer #jsprimer</a></li>
</ul>
<h3>変更内容</h3>
<ul>
<li><a href="https://mochajs.org/">Mocha</a>から<code>node:test</code>に変更</li>
<li><a href="https://nodejs.org/api/test.html">Test runner</a>では、<a href="https://nodejs.org/api/test.html#running-tests-from-the-command-line">デフォルトで<code>test</code>フォルダが指定されている</a>ことも加味して<code>node --test</code>でテストを実行するように変更</li>
</ul>
<h2><a href="https://github.com/asciidwango/js-primer/pull/1760">update to marked@14 by azu · Pull Request #1760</a></h2>
<h3>概要</h3>
<p><code>marked</code>パッケージを<code>14</code>にアップデートしました。</p>
<h3>変更されたページ</h3>
<ul>
<li><a href="https://jsprimer.net/use-case/nodecli/md-to-html/">MarkdownをHTMLに変換する · JavaScript Primer #jsprimer</a></li>
</ul>
<h3>変更内容</h3>
<ul>
<li><code>marked@4</code>から<code>marked@14</code>にアップデート</li>
<li><code>&lt;h1 id=xxx&gt;</code> は付与されなくなったので出力から削除
<ul>
<li>auto linkについては言及してるが、idについては言及してないのでコードのみの変</li>
</ul>
</li>
</ul>
<h2>おわりに</h2>
<p>jsprimerでは毎年更新していけるような仕組み作りの一貫として<a href="https://opencollective.com/jsprimer">JavaScript Primer - Open Collective</a>での支援を募集しはじめました。サイト上へのロゴの掲載やリリースノートへのロゴの掲載などの特典を含んでいます。</p>
<ul>
<li><a href="https://jsprimer.net/intro/sponsors/">JavaScript Primerスポンサー · JavaScript Primer #jsprimer</a></li>
</ul>
<p>また、文章の修正やコード的なコミットはいつでも歓迎しています！</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/blob/master/CONTRIBUTING.md">Contribution Guide</a></li>
<li><a href="https://jsprimer.net/intro/feedback/">文章の間違いに気づいたら · JavaScript Primer #jsprimer</a></li>
</ul>
<p>大体毎年の1月ぐらいには、次のECMAScriptのリリースに合わせた計画を立て始めています。
この時期になると次のリリースに向けてのIssueが立ち始めるので、興味がある人はリポジトリをWatchしてください！</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer">asciidwango/js-primer: :book: JavaScript Primer - 迷わないための入門書</a></li>
<li><a href="https://efcl.info/2024/03/21/jsprimer-es2024-proposal/">JavaScript PrimerのES2024対応を手伝ってくれるContributorとSponsorを募集しています | Web Scratch</a></li>
</ul>
 ]]></content>
 </entry>
 
 <entry>
   <title>Node.jsで型安全な環境変数を扱うスニペット</title>
   <link href="https://efcl.info/2024/08/24/type-safe-env/"/>
   <updated>2024-08-24T13:58:00+09:00</updated>
   <id>https://efcl.info/2024/08/24/type-safe-env</id>
   <content type="html"><![CDATA[ <p>Node.js で型安全な環境変数を扱うスニペットを作りました。</p>
<p><code>next dev</code>のようなアプリケーションの起動、Playwright でのテストなどコマンドごとに渡したい環境変数のセットが異なるケースがあります。
この場合に環境変数をまとめたものを定義して、それをコマンドごとに読み込むセットを変えたいことがあります。</p>
<p>次のようにベタ書きしてもいいのですが、渡したい環境変数が増えると管理が大変になります。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash"><span class="nv">NEXT_PUBLIC_LOCALHOST_URL</span><span class="o">=</span>http://localhost:3000 <span class="nv">NEXT_PUBLIC_API_URL</span><span class="o">=</span>http://localhost:3001 <span class="nv">NEXT_PUBLIC_IS_TEST_MODE</span><span class="o">=</span><span class="nb">false </span><span class="nv">FOO</span><span class="o">=</span><span class="s2">"bar"</span> next dev
</code></pre></div></div>
<p>そのため、<code>.env</code>のような環境変数をまとめたファイルを使いたくなります。
Node.js は<code>--env-file</code>フラグで<code>.env</code>ファイルを読み込むことができますが、<code>.env</code>ファイルは型安全ではありません。</p>
<ul>
<li><a href="https://nodejs.org/api/cli.html#--env-fileconfig"><code>--env-file</code></a></li>
<li><a href="https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs">Node.js — How to read environment variables from Node.js</a></li>
</ul>
<p>環境変数名の Typoや必須の環境変数が設定されていないなどの問題が発生する可能性があります。
そのため環境変数をの定義自体も TypeScript で型安全に定義したいです。</p>
<p>これをやるための 50 行ほどのスニペットを書いたので、使い方を紹介します。</p>
<ul>
<li>📝 主な用途はローカルやCIでの開発用で、実際にデプロイするサーバなどには利用しない想定です</li>
<li>📝 ライブラリとかにしてないのは、この仕組み自体が外部パッケージの依存もなく短いスニペットだからです
<ul>
<li>JSからTSを参照する都合上、ライブラリにはちょっとしにくい気がします</li>
</ul>
</li>
</ul>
<h2>サンプルリポジトリ</h2>
<p>次の場所にサンプルリポジトリがあります。</p>
<ul>
<li><a href="https://github.com/azu/type-safe-env">azu/type-safe-env: Type Safe /Environment Variables snippet for Node.js</a></li>
</ul>
<h2>使い方</h2>
<p>大きく分けて、環境変数の型を定義する<code>defineEnv</code>関数と、環境変数をセットする<code>setEnv</code>関数があります。</p>
<h3>環境変数の型を定義する</h3>
<ul>
<li><a href="https://github.com/azu/type-safe-env/blob/main/src/env/defineEnv.ts">defineEnv.ts</a>: 環境変数の型を定義する Utility</li>
</ul>
<p><code>defineEnv</code>関数を次のような受け取りたい環境変数の型定義をするUtilityです。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="k">export</span> <span class="kd">type</span> <span class="nx">BaseEnvRecord</span> <span class="o">=</span> <span class="nb">Record</span><span class="o">&lt;</span>
    <span class="kr">string</span><span class="p">,</span>
    <span class="p">{</span>
        <span class="na">value</span><span class="p">:</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">;</span>
        <span class="nl">required</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
        <span class="nl">defaultValue</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="p">}</span>
<span class="o">&gt;</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">ReturnTypeOfCreateEnv</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">BaseEnvRecord</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1">// If the value is required, it should be a string, otherwise it should be a string or undefined</span>
    <span class="p">[</span><span class="nx">K</span> <span class="k">in</span> <span class="kr">keyof</span> <span class="nx">T</span><span class="p">]:</span> <span class="nx">T</span><span class="p">[</span><span class="nx">K</span><span class="p">][</span><span class="dl">"</span><span class="s2">required</span><span class="dl">"</span><span class="p">]</span> <span class="kd">extends</span> <span class="kc">true</span> <span class="p">?</span> <span class="kr">string</span> <span class="p">:</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">;</span>
<span class="p">};</span>
<span class="cm">/**
 * Define environment variables and create them
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">defineEnv</span> <span class="o">=</span> <span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">BaseEnvRecord</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">envs</span><span class="p">:</span> <span class="nx">T</span><span class="p">):</span> <span class="nx">ReturnTypeOfCreateEnv</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span><span class="o">&gt;</span><span class="p">();</span>
    <span class="nb">Object</span><span class="p">.</span><span class="nf">entries</span><span class="p">(</span><span class="nx">envs</span><span class="p">).</span><span class="nf">forEach</span><span class="p">(([</span><span class="nx">key</span><span class="p">,</span> <span class="p">{</span> <span class="nx">value</span><span class="p">,</span> <span class="nx">required</span><span class="p">,</span> <span class="nx">defaultValue</span> <span class="p">}])</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nf">if </span><span class="p">(</span><span class="nx">required</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">value</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">defaultValue</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span>
                <span class="s2">`Missing required environment variable: </span><span class="p">${</span><span class="nx">key</span><span class="p">}</span><span class="s2">, value: </span><span class="p">${</span><span class="nx">value</span> <span class="o">===</span> <span class="kc">undefined</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span> <span class="p">:</span> <span class="s2">`"</span><span class="p">${</span><span class="nx">value</span><span class="p">}</span><span class="s2">"`</span><span class="p">}</span><span class="s2">`</span>
            <span class="p">);</span>
        <span class="p">}</span>
        <span class="nx">result</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span> <span class="o">||</span> <span class="nx">defaultValue</span><span class="p">);</span>
    <span class="p">});</span>
    <span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nf">fromEntries</span><span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="k">as</span> <span class="nx">ReturnTypeOfCreateEnv</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<ul>
<li><a href="https://github.com/azu/type-safe-env/blob/main/src/env/env.ts">env.ts</a>: アプリケーション用の環境変数を定義する</li>
</ul>
<p><code>env.ts</code>では、<code>defineEnv</code>関数を使ってアプリケーションで受け取りたい環境変数の型を定義します。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="k">import</span> <span class="p">{</span> <span class="nx">defineEnv</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./defineEnv</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">env</span> <span class="o">=</span> <span class="nf">defineEnv</span><span class="p">({</span>
  <span class="cm">/**
   * Localhost URL
   */</span>
  <span class="na">LOCALHOST_URL</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">value</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="dl">"</span><span class="s2">LOCALHOST_URL</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">required</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">defaultValue</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:3000</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="cm">/**
   * Is test mode?
   */</span>
  <span class="na">IS_TEST_MODE</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">value</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="dl">"</span><span class="s2">IS_TEST_MODE</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">required</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">defaultValue</span><span class="p">:</span> <span class="dl">"</span><span class="s2">false</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="cm">/**
   * Optional value
   */</span>
  <span class="na">OPTIONAL_VALUE</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">value</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="dl">"</span><span class="s2">OPTIONAL_VALUE</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">required</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>
<p>このときに、Node.js なら<code>process.env</code>を使って環境変数を取得します。
値ごとに<code>required</code>で必須かどうかの指定や、<code>defaultValue</code>でデフォルト値を指定できます。
具体的には、<code>required: true</code> で <code>defaultValue</code>が指定されていなくて、<code>process.env.*</code>の値がない場合はエラーになります。</p>
<p>この <code>defineEnv</code>関数で定義した<code>env</code>はアプリケーションが使う環境変数をまとめたものです。</p>
<p>次のようにアプリケーションからは<code>env</code>を import して使います。
<code>defineEnv</code>関数で定義した環境変数を型安全に使うことができます。</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="ts"><span class="c1">// use env</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">env</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../env/env</span><span class="dl">"</span><span class="p">;</span>
<span class="c1">// type-safe</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">localhost url</span><span class="dl">"</span><span class="p">,</span> <span class="nx">env</span><span class="p">.</span><span class="nx">LOCALHOST_URL</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Is Test Mode</span><span class="dl">"</span><span class="p">,</span> <span class="nx">env</span><span class="p">.</span><span class="nx">IS_TEST_MODE</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">OPTIONAL_VALUE</span><span class="dl">"</span><span class="p">,</span> <span class="nx">env</span><span class="p">.</span><span class="nx">OPTIONAL_VALUE</span><span class="p">);</span> <span class="c1">// string or undefined</span>
</code></pre></div></div>
<p>一方で、<code>process.env</code>に設定する環境変数自体も型安全に設定したいです。</p>
<h3>環境変数をセットする</h3>
<p>次の<code>setEnv</code>関数を使って、プロセスに対して環境変数をセットします。</p>
<ul>
<li><a href="./src/env/setEnv.js">setEnv.js</a>: 環境変数をセットする Utility</li>
</ul>
<p>例として、次のような<code>env.local.js</code>と<code>env.test.js</code>のような環境変数をまとめたファイルを作ります。</p>
<ul>
<li><a href="./env.local.js">env.local.js</a>: ローカル開発用の環境変数をセットする</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="js"><span class="k">import</span> <span class="p">{</span> <span class="nx">setEnv</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./src/env/setEnv</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// local環境用の環境変数をセット</span>
<span class="nf">setEnv</span><span class="p">({</span>
  <span class="na">LOCALHOST_URL</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:3500</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">IS_TEST_MODE</span><span class="p">:</span> <span class="dl">"</span><span class="s2">false</span><span class="dl">"</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>
<ul>
<li><a href="./env.test.js">env.test.js</a>: テスト用の環境変数をセットする</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="js"><span class="k">import</span> <span class="p">{</span> <span class="nx">setEnv</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./src/env/setEnv</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// test環境用の環境変数をセット</span>
<span class="nf">setEnv</span><span class="p">({</span>
  <span class="na">LOCALHOST_URL</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:3500</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">IS_TEST_MODE</span><span class="p">:</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>
<p>あとは、この<code>env.*.js</code>を<code>NODE_OPTIONS</code>を使って読み込むことで、環境変数をセットできます。</p>
<p>例えば、<code>env.local.js</code>を使って開発サーバーを起動する場合は次のようにします。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash"><span class="nv">NODE_OPTIONS</span><span class="o">=</span><span class="s1">'--import ./env.local.js'</span> npm run dev
</code></pre></div></div>
<p>テストを実行する場合は、<code>env.test.js</code>を使って次のようにします。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash"><span class="nv">NODE_OPTIONS</span><span class="o">=</span><span class="s1">'--import ./env.test.js'</span> npm <span class="nb">test</span>
</code></pre></div></div>
<p>これで、<code>npm run dev</code>や<code>npm test</code>などのコマンドごとに異なる環境変数をセットできるようになります。</p>
<h2>仕組み</h2>
<p><code>defineEnv.ts</code>の方はただのTypeScriptなのであまり問題ないと思います。</p>
<p><code>setEnv.js</code>の方は、TypeScriptではなくJavaScriptで書いていますが、<a href="https://www.typescriptlang.org/tsconfig/#checkJs">checkJs</a>を使って型チェックを行っています。</p>
<p>具体的には、次のような<code>tsconfig.json</code>を使って、<code>env.*.js</code>を型チェックしています。
<code>allowJs</code>を有効化していますが、通常はなんでも<code>.js</code>を<code>tsc</code>では扱いたくないので、<code>env.*.js</code>だけを<code>includes</code>に指定しています</p>
<div class="language-json5 highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="json5">{
  "compilerOptions": {
    // ....
    // Type Check for env.*.js
    "allowJs": true,
    "checkJs": true
  },
  "include": [
    "**/*.ts",
    "**/*.tsx",
    // allow to check js files
    "./src/env/setEnv.js",
    "env.*.js"
  ],
  "exclude": [
    ".git",
    "node_modules"
  ]
}
</code></pre></div></div>
<p><code>setEnv.js</code>の方は、JSDocを使って受け取れる環境変数の型を定義しているのでcheckJsが有効になっていると型チェックが行われます。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="js"><span class="k">import</span> <span class="nx">process</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:process</span><span class="dl">"</span><span class="p">;</span>

<span class="cm">/**
 * set env util
 * @param {Partial&lt;typeof import("./env").env&gt;} env
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">setEnv</span> <span class="o">=</span> <span class="p">(</span><span class="nx">env</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">if </span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="dl">"</span><span class="s2">PRINT_ENV</span><span class="dl">"</span><span class="p">]</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">table</span><span class="p">(</span><span class="nx">env</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nb">Object</span><span class="p">.</span><span class="nf">entries</span><span class="p">(</span><span class="nx">env</span><span class="p">).</span><span class="nf">forEach</span><span class="p">(([</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">value</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<h3>FAQ</h3>
<h4><code>.env</code>の代わりに<code>env.*.js</code>を使う理由</h4>
<p><code>.env</code>ファイルは型安全ではないです。
<code>env.*.js</code>はTypeScriptの<code>checkJs</code>機能で型チェックされるため型安全です。</p>
<p>Node.js は<code>--env-file</code>フラグで<code>.env</code>ファイルを読み込むことができます。</p>
<ul>
<li><a href="https://nodejs.org/api/cli.html#--env-fileconfig"><code>--env-file</code></a></li>
<li><a href="https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs">Node.js — How to read environment variables from Node.js</a></li>
</ul>
<p>しかし、<code>NODE_OPTIONS=&quot;--env-file=.env&quot;</code>は許可されていません。</p>
<ul>
<li><a href="https://github.com/cypress-io/github-action/issues/1096">Node 20.6+ <code>--env-file</code> flag in <code>NODE_OPTIONS</code> is not allowed · Issue #1096 · cypress-io/github-action</a></li>
</ul>
<h4><code>env.*.ts</code>の代わりに<code>env.*.js</code>を使う理由</h4>
<p>Node.js の<code>--experimental-strip-types</code>はまだ実験的な機能です。</p>
<p>ts-nodeやtsxなどを使えば<code>.ts</code>でも書けますが、あえてTypeScriptの変換を入れるほどでもないので<code>env.*.js</code>を使っています。</p>
<h4>直接<code>--import</code>フラグを使わずに<code>NODE_OPTIONS</code>を使う理由</h4>
<p><code>pnpm</code>のようなパッケージマネージャはパッケージのbinをシェルスクリプトとしてインストールします。</p>
<p>例えば、<code>node_modules/.bin/vite</code>はシェルスクリプトとしてインストールされます。</p>
<p>そのため、<code>node</code>コマンドを使って<code>vite</code>コマンドを実行できません。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash">node <span class="nt">--import</span><span class="o">=</span>./env.local.js node_modules/.bin/vite
<span class="c"># エラーになる</span>
</code></pre></div></div>
<p><a href="https://nodejs.org/docs/latest/api/cli.html#node_optionsoptions">NODE_OPTIONS=options</a>を使うことで、Node.jsプロセスにオプションを渡すことができます。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="bash"><span class="nv">NODE_OPTIONS</span><span class="o">=</span><span class="s1">'--import ./env.local.js'</span> node_modules/.bin/vite
<span class="c"># これなら動く</span>
</code></pre></div></div>
<h2>まとめ</h2>
<p>Node.js で型安全な環境変数を扱うスニペットを作りました。</p>
<ul>
<li><code>defineEnv</code>関数で環境変数の型を定義</li>
<li><code>setEnv</code>関数で環境変数をセット</li>
<li><code>NODE_OPTIONS</code>で<code>env.*.js</code>を読み込むことで、コマンドごとに異なる環境変数をセット</li>
</ul>
<p>50行ほどのスニペットですが、環境変数を型安全に扱うことができるので結構便利でした。</p>
<h2>参考</h2>
<ul>
<li><a href="https://github.com/azu/type-safe-env">azu/type-safe-env: Type Safe /Environment Variables snippet for Node.js</a></li>
</ul>
 ]]></content>
 </entry>
 
 <entry>
   <title>モバイル端末でのウェブアプリのデバッグ方法、Safari on iOS/Chrome on Android</title>
   <link href="https://efcl.info/2024/06/29/debug-safari-chrome-on-mobile/"/>
   <updated>2024-06-29T19:52:00+09:00</updated>
   <id>https://efcl.info/2024/06/29/debug-safari-chrome-on-mobile</id>
   <content type="html"><![CDATA[ <p>iOSのSafari、AndroidのChromeはそれぞれPCと連携してウェブアプリケーションをデバッグできます。
PCのSafariのWeb Inspector、ChromeのDevToolsと連携すれば、コンソールログやネットワーク、デバッガーなども利用できます。</p>
<p>スマホと繋いでWebサイトをデバッグする方法をまとめたページが見つけられなかったので、メモです。
ネットワークで繋いでデバッグもできたりするので、結構便利です。</p>
<h2>Mobile Safari on iOS</h2>
<p>iOSのSafariは、macOSのSafariと接続してログやネットワークの通信などを見るデバッグが可能です</p>
<ul>
<li>ドキュメント: <a href="https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios">Inspecting iOS and iPadOS | Apple Developer Documentation</a></li>
</ul>
<h3>iOSのSafariとmacOSのSafariを接続する</h3>
<p>初回は次の設定が必要です</p>
<p><strong>iOS</strong></p>
<ol>
<li>設定アプリを開く</li>
<li><strong>Safari</strong> の設定を開く</li>
<li><strong>詳細</strong>を開く</li>
<li><strong>Webインスペクタ</strong> をONにする</li>
</ol>
<img src="https://efcl.info/wp-content/uploads/2024/06/29-1719658594.jpg" alt="iOS Settings" width="300" />
<p><strong>macOS</strong></p>
<ol>
<li>Safariの<strong>設定</strong>を開く</li>
<li><strong>詳細</strong>タブを開く</li>
<li>Webデベロッパ用の機能を表示にチェックを入れる</li>
</ol>
<p><img src="https://efcl.info/wp-content/uploads/2024/06/29-1719658617.jpg" alt="macOS Safari" /></p>
<h3>iOSのSafariとmacOSを接続してWeb Inspectorを表示する</h3>
<ol>
<li>iPhoneとmacをUSBで接続します
<ul>
<li>初回はデバイスを信頼しますか? というダイアログが出てきます</li>
</ul>
</li>
<li>iOSのSafariでデバッグしたいページを開く</li>
<li>macOSのSafariの開発メニューから、Safariで開いているページを選択する</li>
</ol>
<p><img src="https://efcl.info/wp-content/uploads/2024/06/29-1719658649.jpg" alt="mmaOS Safari" /></p>
<p>これでmacOSのWeb InspectでiOSのSafariのデバッグでできます。</p>
<p>📝 初回はUSBで接続する必要があります。</p>
<p>ネットワークで接続したい場合は、”ネットワーク経由で接続”にチェックを入れてみてください。
(すでに入ってる場合は、一度外してから入れ直すなどすると、つながると思います。)</p>
<p>同じネットワークにいる場合は、USBを接続しなくても、iOSで開いているページをmacOSでデバッグできるようになります。
端末やタブが出てこない場合は、iOSのSafariを一回閉じたりすると出てくることがあります。</p>
<h3>コンソールログを取る方法</h3>
<ol>
<li>Web Inspectorのコンソールタブを開く</li>
<li>選択した項目をコピー または 選択部分を保存 でログをコピーできます</li>
</ol>
<aside>
💡 コンソールログはWeb Inspectorを開いた情報じゃないと正しく取れないことがあるので、Web Inspectorを開いだ状態でリロードをしてから取得すると良いです
</aside>
<p><img src="https://efcl.info/wp-content/uploads/2024/06/29-1719658732.jpg" alt="iOS Safari Console" /></p>
<h3>ネットワークログを取る方法</h3>
<ol>
<li>Web Inspectorの<strong>ネットワーク</strong>タブを開く</li>
<li><strong>HARを書き出す</strong>を選択</li>
</ol>
<p>これでHARファイルを保存できます。</p>
<aside>
💡 Web Inspectorを開くまでに行われたネットワークの通信は入ってないことがあるので、Web Inspectorを開いてからリロードして通信したものを利用するのが確実です
</aside>
<p><img src="https://efcl.info/wp-content/uploads/2024/06/29-1719658748.jpg" alt="iOS Safari Network" /></p>
<p>HARファイルはリクエストとレスポンスが全て保存されているファイルになります。
ChromeのDevToolsや<a href="https://www.charlesproxy.com/">Charles</a>/<a href="https://proxyman.io/">Proxyman</a>などのProxyツール、<a href="https://playwright.dev/docs/mock#mocking-with-har-files">Playwright</a>などのテストツールなども読み込みに対応しています。</p>
<p>そのサイトで行った通信が全て入ってるため、Cookieなどの認証情報も全て含まれています。
そのため、本番環境の通信内容を含むHARファイルを渡すことは避けてください。また信頼できない人へHARファイルを渡すことも避けてください。</p>
<ul>
<li>Oktaでの不正アクセスにもHARファイルが悪用されました
<ul>
<li><a href="https://www.okta.com/jp/blog/2023/11/harfiles/">Oktaのサポートケース管理システムへの不正アクセス： 根本原因と改善策 | Okta</a></li>
<li><a href="https://piyolog.hatenadiary.jp/entry/2023/10/24/010831">サポートケース管理システムへの不正アクセスから派生して行われたサイバー攻撃についてまとめてみた - piyolog</a></li>
</ul>
</li>
</ul>
<p>本番環境の通信を含むHARファイルを渡す場合は、<strong>HAR File Sanitizer</strong>などで余計な情報を削除してから渡すことが推奨されます。</p>
<ul>
<li><a href="https://har-sanitizer.pages.dev/">HAR Sanitizer</a></li>
</ul>
<h2>Chrome on Android</h2>
<h3>Android開発者オプションを有効にする</h3>
<p>設定 &gt; デバイス情報 &gt; ビルド番号連打すると、開発者向けオプションが有効された旨のメッセージが表示されます。</p>
<ul>
<li><a href="https://developer.android.com/studio/debug/dev-options?hl=ja">デバイスの開発者向けオプションを設定する  |  Android Studio  |  Android Developers</a></li>
</ul>
<h3>デバイスで USB デバッグを有効にする</h3>
<p>Android端末をUSBでPCと繋ぐと「USBデバッグを許可しますか？」というダイアログが出た場合は、信頼するを選択してください。</p>
<p>手動で対応する場合は、Androidのバージョンによって設定が異なります。</p>
<ul>
<li>Android 9（API レベル 28）以上: [設定] &gt; [システム] &gt; [詳細設定] &gt; [開発者向けオプション] &gt; [USB デバッグ]</li>
<li>Android 8.0.0（API レベル 26）および Android 8.1.0（API レベル 27）: [設定] &gt; [システム] &gt; [開発者向けオプション] &gt; [USB デバッグ]</li>
<li>Android 7.1（API レベル 25）以下: [設定] &gt; [開発者向けオプション] &gt; [USB デバッグ]</li>
</ul>
<h3>AndroidのChromeとPCのChromeを接続する</h3>
<ol>
<li>AndroidとPCをUSBで接続します</li>
<li>AndroidのChromeでデバッグしたいページを開く</li>
<li>PCのchromeのロケーションバーより <code>chrome://inspect/#devices</code> にアクセス</li>
<li>Androidで開いているタブが表示されるので、デバッグタブの”Inspect”ボタンをクリックします</li>
<li>PCでDeveloper Toolsが起動します</li>
</ol>
<p>詳細: <a href="https://developer.chrome.com/docs/devtools/remote-debugging?hl=ja">Android デバイスのリモート デバッグ  |  Chrome DevTools  |  Chrome for Developers</a></p>
<h3>コンソールログを取る方法</h3>
<ol>
<li>Developer ToolsのConsoleタブを開く</li>
<li>ログをコピーする</li>
</ol>
<p>詳細: <a href="https://developer.chrome.com/docs/devtools/console/log?hl=ja">コンソールでメッセージをログに記録する  |  Chrome DevTools  |  Chrome for Developers</a></p>
<h3>ネットワークログを取る方法</h3>
<ol>
<li>Developer ToolsのNetworkタブを開く</li>
<li>ダウンロードボタン↓(またはコンテキストメニューのSave all contents as HAR)を選択し、HARファイルを保存</li>
</ol>
<p>詳細: <a href="https://developer.chrome.com/docs/devtools/network/reference?hl=ja">ネットワーク機能のリファレンス  |  Chrome DevTools  |  Chrome for Developers</a></p>
 ]]></content>
 </entry>
 
 <entry>
   <title>JavaScript PrimerのES2024対応を手伝ってくれるContributorとSponsorを募集しています</title>
   <link href="https://efcl.info/2024/03/21/jsprimer-es2024-proposal/"/>
   <updated>2024-03-21T10:00:00+09:00</updated>
   <id>https://efcl.info/2024/03/21/jsprimer-es2024-proposal</id>
   <content type="html"><![CDATA[ <p><a href="https://jsprimer.net/">JavaScript Primer</a>のES2024の対応を進めていく予定なので、
対応を手伝ってくれるContributorとjsprimerというプロジェクトを支援してくれるSponsorを募集しています。</p>
<p>追記(2024-03-22): Contributorを希望する方は集まりました。ありがとうございます！</p>
<p><a href="https://jsprimer.net/intro/sponsors/">JavaScript Primerスポンサー</a>は引き続き募集しています！</p>
<p><strong>Gold Sponsors</strong></p>
<p><a href="https://opencollective.com/jsprimer#sponsors"><img src="https://opencollective.com/jsprimer/sponsors.svg?width=890&amp;avatarHeight=80" alt="jsprimer sponsors" /></a></p>
<p><strong>Supporters</strong></p>
<p><a href="https://opencollective.com/jsprimer#backers"><img src="https://opencollective.com/jsprimer/backers.svg?width=890&amp;avatarHeight=40" alt="jsprimer backers" /></a></p>
<h2>3行サマリー</h2>
<ul>
<li>ES2024の対応を6月末までにやる<a href="https://github.com/asciidwango/js-primer/milestone/5">マイルストーン</a>を切りました</li>
<li>ES2024の対応を手伝ってくれるContributorを募集しています</li>
<li><a href="https://opencollective.com/jsprimer">Open Collective</a>を始めたので、プロジェクトを支援してくれるSponsorを募集しています</li>
</ul>
<h2>jsprimerのES2024対応</h2>
<p>jsprimerは毎年のECMAScriptの仕様改定にあわせてメジャーアップデートを行なっています。
次の更新は、2024年6月末ごろにリリースされる予定のECMAScript 2024に対応する予定です。</p>
<p>そのため、ES2024の対応とユースケースであるNode.jsに関するアップデートをしていくマイルストーンを切りました。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/milestone/5">v6(ES2024) Milestone</a></li>
</ul>
<p><a href="https://github.com/asciidwango/js-primer/milestone/5">ES2024に対応するマイルストーン</a>をざっとみると、大体ざっくり12-20日分ぐらいのタスクがあります。</p>
<p><img src="/wp-content/uploads/2024/03/20-1710920867.png" alt="v6(ES2024) Milestone" /></p>
<p>大きく分けて次の3つのグループに分かれています。</p>
<ul>
<li>ES2024の対応</li>
<li>Stage 2.7の追加</li>
<li>Node.jsのユースケースの更新</li>
</ul>
<h3>ES2024の対応</h3>
<p>ES2024ではいくつか機能が増えているので、それに対応するアップデートを行います。
<a href="https://github.com/asciidwango/js-primer/issues/1706">ECMAScript 2024の対応 · Issue #1706</a>というIssueに、ES2024の対応についてのタスクをまとめています。</p>
<p>具体的に対応したいIssueは次の3つになっています。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/issues/1716">ES2024: array-grouping · Issue #1716</a></li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1715">ES2024: Well-Formed Unicode Strings · Issue #1715</a></li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1714">ES2022: Error Cause · Issue #1714</a></li>
</ul>
<p>Array Groupingは恐らく対応が必須ですが、他の2つは必要なら対応する形になると思います。
実際に内容や入れる場所を考えてみて、それが読む人にとってほんとに必要な情報なのかどうかを考えて判断する形になると思います。</p>
<p>jsprimerでは、必ずしも新しい機能を網羅的に解説はしていません。
これはjsprimerでは、リファレンスを作ることは、<a href="https://jsprimer.net/intro/#do-not">目的ではないこと</a>だからです。</p>
<h3>Stgae 2.7の追加</h3>
<p>ECMAScriptの仕様策定プロセス自体が変更されているので、それを解説してる<a href="https://jsprimer.net/basic/ecmascript/">ECMAScript · JavaScript Primer #jsprimer</a>を更新する予定です。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/issues/1713">Stage 2.7を追加する · Issue #1713</a></li>
</ul>
<p>多分これは自分がやるような気もしますが、やりたい人がいればコメントください。</p>
<h3>Node.jsのユースケースの更新</h3>
<p><a href="https://jsprimer.net/use-case/nodecli/">Node.jsでCLIアプリ · JavaScript Primer #jsprimer</a>の章を全体的にアップデートする予定です。</p>
<p><a href="https://github.com/asciidwango/js-primer/issues/1719">Meta: Node.jsのユースケースの更新 · Issue #1719</a>というMeta Issueに依存関係をまとめています。
最近は、今まではnpmパッケージを使わないといけなかったものが、Node.jsの標準機能になったりしているので、それに対応する形のリファクタリングになります。</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/issues/1718">Node.js 20 LTSへアップデートする · Issue #1718</a></li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1717">mochaを <code>node:test</code> に変更する · Issue #1717</a></li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1698">utils.parseArgsの利用法を追記 · Issue #1698</a></li>
<li><a href="https://github.com/asciidwango/js-primer/issues/1694">markedjsのアップデート · Issue #1694</a></li>
</ul>
<p>こちらは、サンプルコードのアップデートやそれにあわせた文章の変更が中心になります。
そのため、基本的に文章の意味合いを変えるような変更は少なくて、リファクタリング的な変更が中心になります。</p>
<h2>jsprimerのContributorを募集しています</h2>
<p>今回のjsprimerのES2024の対応を手伝ってくれるContributorを募集しています！</p>
<p>先ほども書いていましたが、おおよそざっくり12-20日ぐらいのタスクがあります。
jsprimerは文章のプロジェクトですが、<a href="https://textlint.github.io/">textlint</a>でのチェックやテストも多いので、必要以上に恐れずに手伝ってくれると嬉しいです。</p>
<p>Node.jsのユースケースの更新は、Node.jsの新しい機能を使うように変更するというのが主なので、そこに興味がある方は特に歓迎です。
こちらは、文章の意味合いはあまり変える必要はないので(コードが変わるのでそれに合わせた変更や流れの調整は必要です)、比較的やりやすいかと思います。</p>
<p>ただし、Node.jsはタスク間の依存関係がちょっとややこしいところが複数人だとちょっとやりにくいかもれないです。
(文章を自然に読めるようにするために未知のことに依存させないという方針なので、文章の並び替えが必要になります)</p>
<ul>
<li><a href="https://github.com/asciidwango/js-primer/issues/1719">Meta: Node.jsのユースケースの更新 · Issue #1719 · asciidwango/js-primer</a></li>
</ul>
<p>もし、やってみたい方とかがいれば、一度認識合わせのためにMTGしたり、レビューとかは当然やるのでコメントなどでお知らせください。
募集スレッドとして次のDiscussionを立てていますので、そちらでコメントしてもらえると嬉しいです。</p>
<ul>
<li><del><a href="https://github.com/asciidwango/js-primer/discussions/1727">ES2024に対応するIssueへのContributorを募集しています · asciidwango/js-primer · Discussion #1727</a></del></li>
</ul>
<p>皮算用になるため、まだやり方は決めていませんが、後述するOpen Collectiveの機能を使ってContributeに対して返せる仕組みも作っていく予定です。</p>
<h2>jsprimerのSponsorを募集しています</h2>
<p>jsprimerでは、jsprimer自体が変化をし続け、読んだ人が「変化に対応できる基礎を身につけること」を目的としています。
そのためjsprimerは、毎年のECMAScriptの更新に合わせてアップデートしています。 継続的にアップデートするには、継続的なサポートが重要だと考えています。</p>
<p>jsprimerでは、Open Collectiveを使ってSponsorを募集しています。
次のページから、毎年や毎月ごとに一定の金額を支援することができます。</p>
<ul>
<li><a href="https://opencollective.com/jsprimer">JavaScript Primer - Open Collective</a></li>
</ul>
<p>Open CollectiveはGitHub Sponsorsと似たサービスですが、特定の個人ではなくjsprimerというプロジェクトに対して支援できます。
そして、Open Collectiveは支援された金額を、Contributorに対して分配することができます。
このやりとりをオープンに透明性を持ってできるのが、Open Collectiveの特徴です。</p>
<p>また、企業向けのスポンサーの特典として、次の特典を用意しています。</p>
<ul>
<li><a href="https://jsprimer.net">https://jsprimer.net</a>のトップページへのロゴの掲載</li>
<li>リリースノートでのロゴの掲載</li>
<li>好きなページへのアイコンの掲載/ページスポンサー（任意）</li>
</ul>
<p>参考の情報として、<a href="https://jsprimer.net">https://jsprimer.net</a>のアクセス数などの情報も公開しています。
毎月、ページビューは5-6万程度で、アクティブユーザーは2-3万程度いて、そのうち「読者」(3ページ以上読んだ人)は10%程度の2000人ぐらいになっています。
書籍なので、何度も見にくる人も多いという特性があります。</p>
<iframe class="iframe-looker" width="840" height="472" src="https://lookerstudio.google.com/embed/reporting/5079dfdf-681c-4db7-a216-77c842fdae45/page/p_ajx9imd6zc" frameborder="0" style="border:0" allowfullscreen sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>
<!-- モバイルではiframeを非表示 -->
<style>
@media (max-width: 840px) {
    .iframe-looker {
        display: none;
    }
}
</style>
<ul>
<li><a href="https://lookerstudio.google.com/u/0/reporting/5079dfdf-681c-4db7-a216-77c842fdae45/page/p_ajx9imd6zc">JavaScript Primer Dashboard</a></li>
</ul>
<p>詳細は<a href="https://jsprimer.net/intro/sponsors/">JavaScript Primerスポンサー · JavaScript Primer #jsprimer</a>も参照してください。</p>
<p>元々jsprimerは、新しく会社に入った人などに「これ読んでおいて!」と渡せる書籍を作るという目的で始まりました。
色々なところで、そのような目的でjsprimerを使ってるという話は聞くようになったので、是非会社としての支援なども検討してみてください！</p>
<ul>
<li><a href="https://efcl.info/2020/04/27/jsprimer/">JavaScript Primerを出版しました！/JavaScript Primerはなぜ書かれたのか？ | Web Scratch</a></li>
</ul>
<p>jsprimerは、<a href="https://github.com/asciidwango/js-primer/tree/master/meetings/2015-12-17">2015年12月17日</a>に開始したプロジェクトで、<a href="https://github.com/azu">@azu</a>と<a href="https://github.com/lacolaco">@lacolaco</a>でおおよそ8年ぐらいメンテナンスを続けています。</p>
<p>今回Open Collectiveで支援を募集するのは、次の2つの目的があります。</p>
<ul>
<li>jsprimerの継続的な更新にかかっているコストを見える化する</li>
<li>新しいContributorを増やしてプロジェクトを継続的に更新していく形を模索する</li>
</ul>
<p>最適な方法はまだわかりませんが、継続的にアップデートする形を模索していくためにも、支援を募集しています！</p>
<h2>まとめ</h2>
<ul>
<li>jsprimerをES2024対応する<a href="https://github.com/asciidwango/js-primer/milestone/5">マイルストーン</a>を切りました</li>
<li>ES2024の対応などを手伝ってくれる<a href="https://github.com/asciidwango/js-primer/discussions/1727">Contributorを募集</a>しています！</li>
<li><a href="https://opencollective.com/jsprimer">Open Collective</a>を始めたので、jsprimerを支援してくれる<a href="https://jsprimer.net/intro/sponsors/">Sponsor</a>を募集しています！</li>
</ul>
 ]]></content>
 </entry>
 
 <entry>
   <title>Kagi Searchをメインの検索エンジンとして使っている</title>
   <link href="https://efcl.info/2024/03/15/kagi-search/"/>
   <updated>2024-03-15T00:25:00+09:00</updated>
   <id>https://efcl.info/2024/03/15/kagi-search</id>
   <content type="html"><![CDATA[ <p>最近はGoogleではなく<a href="https://kagi.com/">Kagi Search</a>をメインの検索エンジンとして使っています。</p>
<p><a href="https://kagi.com/">Kagi Search</a>は$108/year($10/month)の有料の検索エンジンです。
広告モデルではない検索エンジンなので、有料のサブスクリプションモデルとなっています。</p>
<ul>
<li><a href="https://help.kagi.com/kagi/plans/plan-types.html">Plan Types | Kagi’s Docs</a>
<ul>
<li>いくつかプランがあり、検索し放題のProfessionalプランが$10/monthです</li>
<li><a href="https://help.kagi.com/kagi/plans/ultimate-plan.html">Ultimate Plan </a>($25/month)だと外部のOpenAIのGPT 4とかClaude 3との連携とかも入ってきます</li>
</ul>
</li>
</ul>
<p>月に1-2万回ぐらいは検索することを考えると、 <code>(108 / (10000 * 12)) * 150</code> で大体1検索が0.1円ぐらいのイメージですが、こちらもKagiのLLM機能は利用できるので、実質もう少しコスパは良いと思います。</p>
<p>検索ソースにはGoogle, Yandex, Mojeek, Braveなどのリソースを使っているので、検索結果自体はGoogleとそこまで変わらないと思います。</p>
<ul>
<li><a href="https://help.kagi.com/kagi/search-details/search-sources.html">Search Sources | Kagi’s Docs</a></li>
</ul>
<p>基本的な使い方ではGoogleで見つかったものがKagiで見つからなかったという経験はありません。もちろん、検索の量でGoogleより優れたものはないと思うので、量にフォーカスしたいときはGoogleを使えば良いと思います(逆に今は飽和的になって検索が逆に難しかったり、検索されてないところにコンテンツがあったりして、量でのカバーは難しい感じはします)</p>
<p>自分がKagiを使う一番理由は検索体験にあると思っています。</p>
<h2>いいところ</h2>
<ul>
<li>Googleより良いと感じる検索結果が出しやすい(人による)
<ul>
<li>日本語の検索結果はそこまで変わらない感じもする(後述する漢字だけ検索した時の問題はまだある)
<ul>
<li>フィルター系がビルトインであるので、絞り込みはしやすく感じる</li>
</ul>
</li>
<li>英語で検索した時に英語のリソースにマッチさせやすいので良い結果と感じることが多い
<ul>
<li>たとえば、Googleだと英語で検索して”英語のページを検索”を選択にしても、日本語のサイトが出てくる(おそらくRegionが日本だと、日本語訳があったらそっちを出すみたいな感じになったりして、これが嫌なこと多い)</li>
<li><a href="https://www.google.com/search?q=javascript&amp;lr=lang_en">https://www.google.com/search?q=javascript&amp;lr=lang_en</a></li>
<li>Kagiの場合、こういうケースでは検索結果の画面でregionを”International” や “US” に切り替えると基本的に素直に感じる結果が出てくる</li>
<li><a href="https://kagi.com/search?q=javascript&amp;r=no_region">https://kagi.com/search?q=javascript&amp;r=no_region</a> や <a href="https://kagi.com/search?q=javascript&amp;r=us">https://kagi.com/search?q=javascript&amp;r=us</a></li>
<li>GoogleではRegionの切り替えが設定画面になって遠い</li>
</ul>
</li>
<li><a href="https://github.com/iorate/ublacklist">uBlacklist</a>を使ってなくても、いかがでしたか?状態にはならない
<ul>
<li>スパムっぽいサイトとかを表示する回数は減った感覚がある</li>
<li>後述するBlockやQuickAnswerを活用しているのもある</li>
</ul>
</li>
<li>また、ビルトインでBlockや検索結果の優先度を変更する機能が入ってる
<ul>
<li><a href="https://help.kagi.com/kagi/features/website-info-personalized-results.html">Website Info &amp; Personalized Results | Kagi’s Docs</a></li>
<li><a href="https://kagi.com/stats?stat=leaderboard">Kagi Search Stats</a>で統計データも公開されている</li>
<li>pinterestのような表示したくないサイトをブロックしたり、逆にMDNのようなサイトを優先度を上げるといったことができる</li>
</ul>
</li>
<li><a href="https://help.kagi.com/kagi/features/redirects.html">Redirects (URL Rewrites) | Kagi’s Docs</a> という機能で、検索結果のURLを書き換える機能も持っている</li>
</ul>
</li>
<li><code>?</code> を末尾につけると、LLMを使った検索結果のサマリを出してくれるQuick Answerが便利
<ul>
<li><img src="https://efcl.info/wp-content/uploads/2024/03/15-1710429999.png" alt="Kagi Quick Answer" /></li>
<li>あまり詳しくないことや検索結果から欲しい部分を抽出してくれるので手間が省ける(検索結果を元にするので書かれてないことはでない)</li>
<li>同じようにページのサマリ表示する機能がある
<ul>
<li>kagiのブラウザ拡張を使うと見ているページのサマリも表示できる(自分はそこまで使ってない)</li>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/kagi-search-for-firefox/">https://addons.mozilla.org/en-US/firefox/addon/kagi-search-for-firefox/</a></li>
<li><a href="https://chrome.google.com/webstore/detail/cdglnehniifkbagbbombnjghhcihifij">https://chrome.google.com/webstore/detail/cdglnehniifkbagbbombnjghhcihifij</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="https://help.kagi.com/kagi/features/lenses.html">Lenses</a>で特定のサイトからの検索結果だけにフィルターできるのが便利
<ul>
<li>プログラミング関係(GitHubやStackoverflowなど)のサイトだけに絞ったりが、1 clickで切り替えできる</li>
<li>日本の主要なブログだけを検索するLens
<ul>
<li><code>*.hatenablog.com, *.hatenablog.jp, *.hateblo.jp, *.hatenadiary.com, *.hatenadiary.jp, note.com, ameblo.jp, sizu.me, zenn.com, qiita.com</code></li>
</ul>
</li>
<li><a href="https://kagi.com/lenses/0Q9bHFmidnH3TfNAR3OYQKb0gyqDEzM7">https://kagi.com/lenses/0Q9bHFmidnH3TfNAR3OYQKb0gyqDEzM7</a></li>
<li>レビューとか検索したいときに個人のブログを検索したい といった感じの用途でよく使う</li>
</ul>
</li>
<li>検索結果のOrder ByとTimeが素直な感じ
<ul>
<li><a href="https://help.kagi.com/kagi/features/filtering-results.html">Filtering Results | Kagi’s Docs</a></li>
<li>Order By: recentで検索のソート順を新しいもの順にできる</li>
<li>Timeで24時間以内の結果にできる(ここはGoogleでもできる)</li>
<li><strong>Verbatim mode</strong>を使うと、クエリの文字列が含まれているサイトだけが表示される</li>
<li>全体的に素直な検索結果を扱える</li>
</ul>
</li>
</ul>
<h2>だめなところ</h2>
<ul>
<li><del>漢字だけのクエリだと中国語の結果が混ざることがある</del> 起きなくなった気がする
<ul>
<li><a href="https://kagifeedback.org/d/2118-japanese-chinese/7">Japanese / Chinese - Kagi Feedback</a> をVoteしてください</li>
<li>RegionとLanguageの区別が部分的にしか実装されてないので、漢字だけだと両方出てくることがある</li>
<li>そこまで漢字の単語 一つだけ検索することは少ないので、数百回に1回ぐらいのイメージ</li>
<li>漢字1つだけのケースは辞書的な検索な気はしてて、Wikipedia(<code>!w</code> bang)を指定したり、Googleに行ったり、もう少し明確なクエリにすることで回避してる</li>
<li>この問題をもう少し広く捉えると、ここを自動でやるのは検索的にかなり面倒そうな問題な気はしている。たとえば、ca-enとus-enでは求めてるものが違う、ca-frとca-enがあるとか</li>
<li>自動でやり切るにはエッジケースが結構多そう(Googleはそれをやってるイメージだけど、丸まりすぎてしまう)</li>
<li>人によって最適だと思う結果が結構違うので、クエリでその人の意図を表現できるような方向だと良い気はする</li>
<li>Ideaがある人はここに投稿すると良いと思う</li>
<li><a href="https://kagifeedback.org/d/3022-ideas-for-improving-localinternationalenglish-search-modalities">Ideas for improving local/international/english search modalities - Kagi Feedback</a></li>
</ul>
</li>
<li>検索結果がGoogleより遅い
<ul>
<li>大体1秒弱ぐらいかかるイメージ</li>
<li>データセンターがHong Kongになってるので、日本にサーバがあればもうちょっとマシになると思う
<ul>
<li><a href="https://help.kagi.com/kagi/search-details/search-speed.html#data-center-locations">Search Speed | Kagi’s Docs</a></li>
</ul>
</li>
<li><a href="https://kagifeedback.org/d/183-kagi-slow-post-here/91">Kagi slow? Post here! - Kagi Feedback</a>
<ul>
<li>ここにポストすると対応してくれそう</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>その他のTips</h2>
<ul>
<li><a href="https://apps.apple.com/jp/app/kagi-for-safari/id1622835804">「Kagi for Safari」</a>を入れるとiOSでもKagiを使える
<ul>
<li>Google検索とかの代わりにKagiを表示してくれるSafari拡張になってる</li>
<li>ブロックの共有とかもされてるので、uBlacklistみたいな拡張をわざわざ入れなくてもPCと同じ検索体験になる</li>
</ul>
</li>
<li><a href="https://help.kagi.com/kagi/features/bangs.html#bangs">Bangs | Kagi’s Docs</a> という機能で <code>!g</code> でGoogleにジャンプしたりできる
<ul>
<li>自分はショートカットでGoogleに行きたかったので別の方法使ってる</li>
<li><a href="https://gist.github.com/azu/2f0d47d4d800fb02fda0ba64c2f076dd">Surfingkeys shortcut: kagi.com to google</a></li>
<li>検索結果がダメだったら <code>g</code> をおして、Google検索をする</li>
</ul>
</li>
</ul>
<h2>ユースケース</h2>
<p>Kagi Searchの機能を使ったユースケースの紹介をいくつか書いています。
他のサービスでもブラウザ拡張などを使えば実現はできると思いますが、Kagi Search側の機能なのでPC/モバイル問わずどこからでも利用できるのが利点です。</p>
<h3>AmazonのURLをシンプルする</h3>
<p><a href="https://help.kagi.com/kagi/features/redirects.html">Redirects (URL Rewrites) </a>を使って、検索結果のAmazonのURLをシンプルにする。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>^https://www.amazon.co.jp/([^/]+)/dp/(.*)|https://www.amazon.co.jp/dp/$2
</code></pre></div></div>
<h3>npm docsを常に最新のバージョンにする</h3>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>^https://docs.npmjs.com/cli/(v[^/]+)/(.*)|https://docs.npmjs.com/cli/$2
</code></pre></div></div>
<h3>日本の主要なブログだけを検索する</h3>
<p>次の<a href="https://help.kagi.com/kagi/features/lenses.html">Lenses</a>を使って、日本のブログだけに絞り込める</p>
<ul>
<li><a href="https://kagi.com/lenses/0Q9bHFmidnH3TfNAR3OYQKb0gyqDEzM7">https://kagi.com/lenses/0Q9bHFmidnH3TfNAR3OYQKb0gyqDEzM7</a></li>
</ul>
<h2>感想</h2>
<p><a href="https://kagi.com/">Kagi Search</a>は有料の検索エンジンです。</p>
<p>万人におすすめできるというわけではないですが、プログラミングなどする人にとってはGoogleより扱いやすい検索エンジンだと感じる作りになってると思います。</p>
<p>Googleは賢いので、検索クエリに対してそれっぽい検索結果を出してしまうことがあります。1単語で検索した場合とかはこれが便利ですが、複数のクエリを組み合わせたような検索結果は求めてない結果が混ざることが多いです。</p>
<p>一方でKagiは素直な感じの検索エンジンなので、素直に書いたクエリの通りの検索結果になるというイメージです。
検索のデータとしてGoogleのものなども使っているので、Kagiでは見つからなかったけどGoogleでは見つかるというケースもあまりない印象です（特に思いつかなかった）。</p>
<ul>
<li><a href="https://help.kagi.com/kagi/search-details/search-quality.html">Search Quality | Kagi’s Docs</a></li>
<li><a href="https://help.kagi.com/kagi/search-details/search-sources.html">Search Sources | Kagi’s Docs</a></li>
</ul>
<p>これに加えて、LensやSort、Verbatimなども絞り込み系がアクセスしやすいので、扱いやすく感じます。
また、Kagiは <a href="https://kagi.com/settings?p=user_ranked">https://kagi.com/settings?p=user_ranked</a> どのドメインを出すか出さないかというパーソナライズも自分でちゃんと管理できます。</p>
<p>Googleは検索をちゃんとやろうとするほど余計な結果(もしかして)が混ざってくるのが不便でした。Kagiでは素直な検索が返ってくるので、検索したいことをちゃんと検索しやすいです。
また、曖昧な検索結果が欲しい時(何を調べればいいのかもまだわかってないことを調べる時)は<a href="https://help.kagi.com/kagi/ai/quick-answer.html">Quick Answer</a>を使うといった使い分けをしています。</p>
<p>使っていると、曖昧な検索と正確な検索?(絞り込んでいくような検索)を意識的に使い分けやすい作りになってるのかなと思いました。
前者はGoogleの方が考えずに使えると思いますが、後者になるとKagiの方が扱いやすい部分が多いと感じました。</p>
<p>Kagi Searchを使おうと思った理由としては</p>
<ul>
<li>Google検索で絞り込んで行ったときに、意図してない結果が出てくるケースが最近多かったのでどうにかしたかった</li>
<li>使ってみて、よくある懸念の「Googleで見つかるけどKagiでは見つからない」というケースはほとんどないと思った</li>
<li><a href="https://blog.kagi.com/kagi-features">Kagi search features | Kagi Blog</a>とか基礎機能が細かくできてて良いと思った(設定画面をみるとわかる)
<ul>
<li>過去にGoogleにあった機能もあるけど、Googleは機能を消してしまうので</li>
<li>LensとかBlockとかこういう機能は検索エンジン側に実装された方が管理が楽なので</li>
</ul>
</li>
<li><a href="https://blog.kagi.com/blog">Kagi Blog</a>を読んでて方向性が結構面白いと思った</li>
<li>試用期間(100回/month)だと機能を使いきれてなかったけど、まあ悪くなさそうと思って課金した
<ul>
<li>最悪Googleとかに戻ればいいだけだし</li>
</ul>
</li>
</ul>
<p>最近では<a href="https://www.wolframalpha.com/">Wolfram</a>のFounderの人がKagiに入ったり、検索のフロントエンドとしてのKagiは結構面白いと思うので、それに期待して使ってる部分もあります。</p>
<ul>
<li><a href="https://blog.kagi.com/kagi-wolfram">Kagi + Wolfram | Kagi Blog</a></li>
</ul>
<p>一応、月100回までは無料で検索できるので試してみると良いと思いますが、正直100回ぐらいでいいかどうかはわからない感じはします。</p>
<ul>
<li><a href="https://blog.kagi.com/update-kagi-search-pricing">Update to Kagi Search pricing | Kagi Blog</a></li>
</ul>
<p>機能というより体験的な部分に依存すると思うので、定常的に使わないと評価が難しいサービスだと思いました。</p>
 ]]></content>
 </entry>
 
 <entry>
   <title>Twitter/Blueskyの自己ポストの全文検索サービスをNext.js App Router(RSC)で書きなおした方法/設計/感想</title>
   <link href="https://efcl.info/2024/02/26/mytweets-next-rsc/"/>
   <updated>2024-02-26T16:18:00+09:00</updated>
   <id>https://efcl.info/2024/02/26/mytweets-next-rsc</id>
   <content type="html"><![CDATA[ <p><a href="https://github.com/azu/mytweets">mytweets</a>という自分の Twitter/Bluesky の自己ポストの全部検索サービスを<a href="https://nextjs.org/docs/app">Next.js App Router(RSC)</a>で書きなおしました。</p>
<p>mytweets は Twitter のアーカイブや Bluesky の API を使って自分のポストを S3 に保存しておき、
<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/selecting-content-from-objects.html">S3 Select</a>を使って全文検索ができる自分専用の Twilog のようなサービスです。</p>
<ul>
<li><a href="https://efcl.info/2021/06/18/mytweets-delete-tweets/">自分の Tweets をインクリメンタル検索できるサービス作成キット と Tweets をまとめて削除するツールを書いた | Web Scratch</a></li>
<li><a href="https://efcl.info/2023/07/03/mytweets-bluesky/">過去の Tweets を全文検索できる mytweets を Bluesky に対応した。自分用 Twilog みたいなもの | Web Scratch</a></li>
</ul>
<p>最初は CloudFront + Lambda@Edge + Next.js Pages Router で動かしていました。
その後、Next.js App Router が Stable になったので、App Router + React Server Components(RSC)で書きなおしました。</p>
<p>この記事では、Next.js Pages Router から Next.js App Router(RSC)に書きなおした話を紹介します。
ただし、この記事は発散的な内容になっているのと、あまり正確性が保証されてないので、個人的なメモ書きとして読んでください。</p>
<p>あまりにも長くなったので、あんまり読みやすくは書けませんでした。</p>
<p>📝 <a href="https://nextjs.org/docs/app/building-your-application/caching">キャッシュとメモ化</a>については特に<a href="https://twitter.com/azu_re/status/1762404180990492867">触れていません</a>。</p>
<h2>mytweets の動作</h2>
<p><a href="https://github.com/azu/mytweets">mytweets </a> は、次のような動作をします。</p>
<ul>
<li>クエリがない場合は、S3 Select から最新のポストを取得して表示</li>
<li>クエリを入力したら、S3 Select の API を使って全文検索を行い、結果を表示</li>
</ul>
<p><video src="/wp-content/uploads/2024/02/mytweets_optimized.mp4" controls muted loop playsinline width="100%"></video></p>
<p>この動画は、App Router + React Server Components(RSC)で動かしてるものを録画したものです。
表示的にファーストビューが出てからローディングが走って、結果を取得してポストを表示するという動作をしているので一般的な<a href="https://developer.mozilla.org/en-US/docs/Glossary/SPA">SPA (Single-page application)</a>っぽく見えます。</p>
<p>実際のコードベース上では、クライアント側には Fetch API などは書いていません。
初期化のロード表示は、RSC + <a href="https://react.dev/reference/react/Suspense"><code>&lt;Suspense&gt;</code></a> + <a href="https://react.dev/reference/react/use">use</a>で実現しています。（静的な部分は SSR されているので、TTFB(Time to First Byte)が短いです。)
検索時の更新のロード表示は、Next.js の<code>router.push</code>と<a href="https://react.dev/reference/react/useTransition">useTransition</a>で実現しています。</p>
<h2>App Router への移行のメモ</h2>
<p>どのように移行したかを簡単に振り返ってみます。
メモ書きのようなものなので、かなり乱雑に書かれています。
具体的な変更だけ見たい人は、次の Pull Request を見てください。</p>
<ul>
<li><a href="https://github.com/azu/mytweets/pull/5">recactor(web): migrate to App Router by azu · Pull Request #5 · azu/mytweets</a></li>
<li><a href="https://github.com/azu/mytweets/pull/6">refactor(web): Suspense + Streaming by azu · Pull Request #6 · azu/mytweets</a></li>
</ul>
<p>大きく 3 つのステップで移行しました。</p>
<ol>
<li>App Router に移行</li>
<li>RSC を使うように変更</li>
<li>Suspense を使うように変更</li>
</ol>
<h3>1. App Router に移行</h3>
<p>元々 mytweets は Next.js Pages Router で動いていました。</p>
<ul>
<li>Source: <a href="https://github.com/azu/mytweets/tree/3235481d12f88de7e763439e705322499496d255/web">https://github.com/azu/mytweets/tree/3235481d12f88de7e763439e705322499496d255/web</a></li>
</ul>
<p>サーバ側の処理は、<a href="https://nextjs.org/docs/pages/building-your-application/routing/api-routes">API Routes</a>で S3 Select を叩く API を用意してるぐらいで、他はほぼクライアントの処理でした。
次の<code>index.tsx</code>という一つのファイルに全部書いてあるような単純なページでした。</p>
<ul>
<li><a href="https://github.com/azu/mytweets/blob/3235481d12f88de7e763439e705322499496d255/web/pages/index.tsx">https://github.com/azu/mytweets/blob/3235481d12f88de7e763439e705322499496d255/web/pages/index.tsx</a></li>
</ul>
<p>そのため、この<code>index.tsx</code>に”use client”をつけて Client Component として移行すれば App Router でも動きます。</p>
<p><code>/pages/api</code>に定義する<a href="https://nextjs.org/docs/pages/building-your-application/routing/api-routes">API Routes</a>は、App Router でも動くのでサーバ側の処理はそのままに<code>index.tsx</code>を<code>pages/</code>から<code>app/</code>に移動して、<code>use client</code>をつけた Client Component に変更しました。</p>
<p>これで一旦 App Router で動くようになりました。
特に App Router の機能は使ってないですが、段階的に移行する際にはこのようなアプローチが利用できます。</p>
<p>参考:</p>
<ul>
<li><a href="https://speakerdeck.com/cyberagentdevelopers/puraibetokuraudonokonsoruhua-mian-wonext-dot-jsnoapp-routerdehururipureisusitahua">プライベートクラウドのコンソール画面を Next.js の App Router でフルリプレイスした話 - Speaker Deck</a></li>
</ul>
<h2>2. RSC を使うように変更</h2>
<p>このままでは、App Router の機能を使っていないので、RSC を使うように変更しました。
RSC をちゃんと使うために、コンポーネントが Client Component なのか RSC なのかが明確になっている必要があります。</p>
<p>これは、Client Component は RSC をインポートできないが、RSC は Client Component をインポートできるという不可逆性があるためです。
そのため、コンポーネントの境界を明確にする必要があります。</p>
<table>
<thead>
<tr>
<th>子＼親</th>
<th>Client</th>
<th>RSC</th>
<th>Server Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Client</td>
<td>インポートできる</td>
<td>インポートできない</td>
<td>呼べる(通信が発生)</td>
</tr>
<tr>
<td>RSC</td>
<td>インポートできる</td>
<td>インポートできる</td>
<td>呼べる(関数コール)</td>
</tr>
</tbody>
</table>
<p>RSC は<code>useState</code>や<code>useEffect</code>などは使えません。
インタラクティブな部分は、Client Component で行い、RSC はデータを受け取って表示するという形になります。
RSC は、サーバ側で処理されるので、そこで moment や marked のようなライブラリを使っても、クライアント側にはライブラリは含まれません。
(あくまで、処理結果だけがクライアントに渡される)</p>
<p>この境界を見極めるのが結構難しいですが、最悪 Client Component のままでも動作的には問題ないです。
そのため、mytweets で RSC を使う部分は、次のような目的を設定して進めていきました。</p>
<ul>
<li>基本コンセプトはクライアントサイドのサイズを削る目的</li>
</ul>
<p>Client Component と RSC がツリーに混在することはありますが、基本的にはどちらかが上にいる形になります。</p>
<p>Client Component で RSC を包むような形は、<a href="https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns">Composition Patterns</a>を使うとかけます。</p>
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns">Rendering: Composition Patterns | Next.js</a></li>
</ul>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="dl">"</span><span class="s2">use client</span><span class="dl">"</span><span class="p">;</span>
<span class="c1">// children(RSCもOK) として ReactNode を受け取る</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nc">ClientComponent</span><span class="p">({</span> <span class="nx">children</span> <span class="p">}:</span> <span class="nx">ReactNode</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">children</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>この書き方のユースケースとしては、枠を Client Component で作って、その中に RSC を入れてロード中は opacity を下げるというような使い方ができます。</p>
<p>mytweets でも入力して検索中の opacity を下げることでロード中を表現しています。</p>
<p><video src="/wp-content/uploads/2024/02/mytweets_optimized.mp4" controls muted loop playsinline width="100%"></video></p>
<ul>
<li><a href="https://github.com/azu/mytweets/blob/efd1281ea3936a5794712d39d8be81bc04284a96/web/app/page.tsx#L80-L93">https://github.com/azu/mytweets/blob/efd1281ea3936a5794712d39d8be81bc04284a96/web/app/page.tsx#L80-L93</a> で使っている</li>
</ul>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="c1">// Composition Patternを使う</span>
<span class="c1">// https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns</span>
<span class="p">&lt;</span><span class="nc">ClientComponent</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">RSC</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">ClientComponent</span><span class="p">&gt;</span>
</code></pre></div></div>
<p>逆に RSC の中に Client Component を入れるには単純にインポートして使うだけです。</p>
<p>多くの場合は、この形になって大枠を RSC に書いていき、部分的に Client Component を使うという形になります。
原理的には<a href="https://www.patterns.dev/vanilla/islands-architecture/">Islands Architecture</a>と同じで、大枠は静的な表示(RSC)にして、インタラクティブな部分(Client Component)を小さくしていくという形になります。</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="c1">// RSCはClient Componentをインポートできる</span>
<span class="p">&lt;</span><span class="nc">RSC</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">ClientComponent</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">RSC</span><span class="p">&gt;</span>
</code></pre></div></div>
<p>Client Component と RSC のコンポーネントが横並びになる場合がかなり難しいです。</p>
<p>基本的には RSC をツリーの上に持ってきて、Client Compoent はツリーの下に持ってくると書きやすいと思います。
これ言い換えると、インタラクションがあるボタンやフォームなどは、範囲を限定しておくという形になります。
(再描画の範囲も小さくなるように書くというのが意識としては近いと思います)</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="c1">// RSCが上にある形は、RSCからClient Componentをインポートできるので問題ない</span>
<span class="p">&lt;</span><span class="nc">RSC</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">ClientComponentX</span> <span class="p">/&gt;</span>
  <span class="p">&lt;</span><span class="nc">RSC_Y</span> <span class="p">/&gt;</span>
  <span class="p">&lt;</span><span class="nc">ClientComponentZ</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">RSC</span><span class="p">&gt;</span>
</code></pre></div></div>
<p>次の Client Component が上にある混在の仕方はかなり難しいので、基本的に避けた方が良さそうです。</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="c1">// Composition Patternもやりにくいので基本的に避けたい</span>
<span class="p">&lt;</span><span class="nc">ClientComponent</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">RSC_Y</span> <span class="p">/&gt;</span>
  <span class="p">&lt;</span><span class="nc">ClientComponentX</span> <span class="p">/&gt;</span>
  <span class="p">&lt;</span><span class="nc">RSC_K</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">ClientComponent</span><span class="p">&gt;</span>
</code></pre></div></div>
<p>この辺を考えながえら、クライアントに不要なものを RSC に移行していくと、
最終的には、<code>useState</code>や<code>useEffect</code>が必要ない部分が全部 RSC になりました。</p>
<p>擬似的なアプリの構造は、次のようになりました。</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="jsx"><span class="k">export</span> <span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">searchParams</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">searchResults</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetchS3Select</span><span class="p">(</span><span class="nx">searchParams</span><span class="p">.</span><span class="nx">query</span><span class="p">);</span>
    <span class="k">return</span> <span class="p">&lt;</span><span class="nc">TransitionContextProvider</span><span class="p">&gt;</span> <span class="si">{</span><span class="cm">/* Client */</span><span class="si">}</span>
        <span class="p">&lt;</span><span class="nc">SearchBox</span> <span class="p">/&gt;</span>  <span class="si">{</span><span class="cm">/* Client */</span><span class="si">}</span>
        <span class="p">&lt;</span><span class="nc">SearchResultContentWrapper</span><span class="p">&gt;</span>  <span class="si">{</span><span class="cm">/* Client */</span><span class="si">}</span>
                <span class="p">&lt;</span><span class="nc">SearchResultContent</span> <span class="na">searchResults</span><span class="p">=</span><span class="si">{</span><span class="nx">searchResults</span><span class="si">}</span> <span class="p">/&gt;</span>  <span class="si">{</span><span class="cm">/* Server */</span><span class="si">}</span>
        <span class="p">&lt;/</span><span class="nc">SearchResultContentWrapper</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">TransitionContextProvider</span><span class="p">&gt;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>これに合わせて、データのリロード方法も RSC ベースに変更しています。
RSC はルーティングを移動すれば、もう一度 RSC の処理がよばれるので、ルーティングを移動することでデータのリロードができます。</p>
<table>
<thead>
<tr>
<th>内容</th>
<th>変更前</th>
<th>変更後</th>
</tr>
</thead>
<tbody>
<tr>
<td>ベースの仕組み</td>
<td>Pages Router + api/</td>
<td>App Router</td>
</tr>
<tr>
<td>API サーバ</td>
<td>api/ で Rest API を作り、クライントから呼び出す</td>
<td>RSC から関数としてサーバの処理を書いて呼ぶだけ</td>
</tr>
<tr>
<td>S3 Select の取得</td>
<td>api/ で S3 Select を叩いて、Stream として返して、クライアントから Fetch with Stream で取得しながら表示</td>
<td>pages.tsx で、S3 Select から取得して props で各コンポーネントに配るだけ</td>
</tr>
<tr>
<td>更新処理</td>
<td>入力欄が変更されたら、useEffect で Fetch して取得 → State を更新して描画し直す</td>
<td>入力欄が変更されたら、 <code>router.push(&quot;/?q={検索]&quot;)</code> へ移動するだけ(取得は pages.tsx に書かれてる仕組みがそのまま使われる)</td>
</tr>
<tr>
<td>初期ロード中の表示</td>
<td>Client 側で取得する。取得中は、isLoading の state(useState)を更新して、取得が終わったら state を更新する</td>
<td>pages.tsx で、S3 Select から取得してし終わったらレンダリングするので、初期ロードはなし(ただし、S3 から取得できるまでページが表示されない)</td>
</tr>
<tr>
<td>更新中の表示</td>
<td>(初期ロードと同じ) Client 側で取得する。取得中は、isLoading の state(useState)を更新して、取得が終わったら state を更新する</td>
<td><code>startTransition(() =&gt; router.push(...))</code> で更新中かの状態(state)を得て、更新中の表示を行う。この state を Context を通して、Client Component 間で共有して、いろいろな場所のローディング表示を行う。</td>
</tr>
<tr>
<td>URL</td>
<td>特に変化しない</td>
<td>入力に合わせて <code>?q=&lt;クエリ&gt;</code> を更新していく</td>
</tr>
</tbody>
</table>
<p>実際の Pull Request は、次の URL から見れます。</p>
<ul>
<li><a href="https://github.com/azu/mytweets/pull/5">recactor(web): migrate to App Router by azu · Pull Request #5 · azu/mytweets</a></li>
</ul>
<p>この時点でパフォーマンスは良くなり、クライアントからも<code>useEffect</code>と Fetch でデータ取得をしていた複雑な部分がなくなりました。
コンポーネントの境界を考えたり、RSC と Client Component の組み合わせのためのコンポネーントは増えたりしますが、ロジック自体はかなりシンプルになりました。</p>
<ul>
<li>FCP: 0.6s → 0.3s</li>
<li>LCP: 0.6s → 0.3s</li>
<li>Speed Index: 1.8s → 0.5s</li>
</ul>
<p><img src="/wp-content/uploads/2024/02/26-1708958667.png" alt="perf App Router" /></p>
<h3>3. Suspense を使うように変更</h3>
<p>ここまでで、App Router + RSC で動くようになりました。
一方で、S3 Select の検索が終わるまで、ページが表示されないという問題があります。
S3 Select はファイルの上から下まで全文検索するので、ヒットしない場合は時間がかかります。</p>
<p>その間、ページが表示されないのは体験として良くないので、検索中もページが表示されるようにするために、Suspense を使うように変更しました。
Next.js のドキュメントだと Streaming という言葉が使われていますが、React の Suspense でロード中はプレースホルダーを表示する仕組みのことです。
(Fetch with Stream とは異なるものです)</p>
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming">Routing: Loading UI and Streaming | Next.js</a></li>
</ul>
<p>RSC は、props として Promise を渡せるようになっています。</p>
<ul>
<li><a href="https://react.dev/reference/react/use-client#serializable-types">‘use client’ directive – React</a></li>
</ul>
<p>そのため、コンポーネントの Props として Promise を受け取り、そのコンポーネントを Suspense でラップすることで、ローディング中の表示を行うことができます。
受け取った Promise を unwrap するには、<a href="https://react.dev/reference/react/use">use</a>を利用します。</p>
<p>先ほどのコードでは、<code>await</code>していたので検索が終わるまでページが表示されない形になっていました。
次のように<code>await</code>を外して Promise として、その Promise を検索結果を表示するコンポーネントに props として渡すだけです。</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="diff"><span class="gd">- const searchResults = await fetchS3Select(searchParams.query);
</span><span class="gi">+ const searchResultsPromise = fetchS3Select(searchParams.query);
</span></code></pre></div></div>
<p>今までのコンポーネントの中で<code>use</code>を使って Promise を unwrap(resolve した値を取得)してもいいのですが、promise を受け取るコンポーネントが気持ち悪いので、それ用のラッパーコンポーネントを定義しました。</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="c1">// useでunwrapして渡すだけのコンポーネント</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">SearchResultContentStream</span> <span class="o">=</span> <span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="p">{</span>
  <span class="nl">retPromise</span><span class="p">:</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">FetchS3SelectResult</span><span class="o">&gt;</span><span class="p">;</span>
  <span class="nl">screenName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">retPromise</span><span class="p">,</span> <span class="p">...</span><span class="nx">other</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">props</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">ret</span> <span class="o">=</span> <span class="nf">use</span><span class="p">(</span><span class="nx">retPromise</span><span class="p">);</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nc">SearchResultContent</span> <span class="na">ret</span><span class="p">=</span><span class="si">{</span><span class="nx">ret</span><span class="si">}</span> <span class="si">{</span><span class="p">...</span><span class="nx">other</span><span class="si">}</span> <span class="p">/&gt;;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>もしくは、サーバ側のみで動くコンポーネントなら、<code>async</code>/<code>await</code>が利用できるので次のように書いても良いかもしれません。</p>
<ul>
<li>📝 <code>use</code>はClient Component/RSCどちらでも動くが、Async/AwaitはRSCのみで動く</li>
<li><a href="https://github.com/reactjs/rfcs/pull/229">RFC: First class support for promises and async/await by acdlite · Pull Request #229 · reactjs/rfcs</a></li>
</ul>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="k">export</span> <span class="kd">const</span> <span class="nx">SearchResultContentStream</span> <span class="o">=</span> <span class="nf">async </span><span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="p">{</span>
  <span class="nl">retPromise</span><span class="p">:</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">FetchS3SelectResult</span><span class="o">&gt;</span><span class="p">;</span>
  <span class="nl">screenName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">ret</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">props</span><span class="p">.</span><span class="nx">retPromise</span><span class="p">;</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nc">SearchResultContent</span> <span class="na">ret</span><span class="p">=</span><span class="si">{</span><span class="nx">ret</span><span class="si">}</span> <span class="si">{</span><span class="p">...</span><span class="nx">other</span><span class="si">}</span> <span class="p">/&gt;;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>この Suspense と use を使ったラッパーコンポーネントを使った擬似的なアプリの構造は、次のようになりました。</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="k">export</span> <span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">searchParams</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">searchResults</span> <span class="o">=</span> <span class="nf">fetchS3Select</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span> <span class="c1">// waitしないでpromiseのまま扱う</span>
  <span class="p">{</span><span class="cm">/* Client */</span><span class="p">}</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nc">TransitionContextProvider</span><span class="p">&gt;</span>
    <span class="si">{</span><span class="cm">/* Client */</span><span class="si">}</span>
    <span class="p">&lt;</span><span class="nc">SearchBox</span> <span class="p">/&gt;</span>
    <span class="p">&lt;</span><span class="nc">SearchResultContentWrapper</span><span class="p">&gt;</span>
      <span class="si">{</span><span class="cm">/* Client */</span><span class="si">}</span>
      <span class="p">&lt;</span><span class="nc">Suspense</span> <span class="na">fallback</span><span class="p">=</span><span class="si">{</span><span class="dl">"</span><span class="s2">Loading ...</span><span class="dl">"</span><span class="si">}</span><span class="p">&gt;</span>
        <span class="si">{</span><span class="cm">/* Server 中で use を使う*/</span><span class="si">}</span>
        <span class="p">&lt;</span><span class="nc">SearchResultContentStream</span> <span class="na">retPromise</span><span class="p">=</span><span class="si">{</span><span class="nx">searchResults</span><span class="si">}</span> <span class="p">/&gt;</span>
      <span class="p">&lt;/</span><span class="nc">Suspense</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">SearchResultContentWrapper</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nc">TransitionContextProvider</span><span class="p">&gt;;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>これで、検索中もページが表示されるようになりました。
この変更により、検索が遅い場合でもページ自体は安定してすぐに表示されるようになりました。</p>
<p>📝 最良パターンは2より若干悪くなる(CLSもあるため)。最悪パターンは2よりかなり改善される</p>
<table>
<thead>
<tr>
<th>内容</th>
<th>変更前</th>
<th>変更後</th>
</tr>
</thead>
<tbody>
<tr>
<td>ベースの仕組み</td>
<td>Pages Router + api/</td>
<td>App Router</td>
</tr>
<tr>
<td>APIサーバ</td>
<td>api/ でRest APIを作り、クライントから呼び出す</td>
<td>RSCから関数としてサーバの処理を書いて呼ぶだけ</td>
</tr>
<tr>
<td>S3 Selectの取得</td>
<td>api/ でS3 Selectを叩いて、Streamとして返して、クライアントからFetch with Streamで取得しながら表示</td>
<td>pages.tsx で、S3 Selectから取得してpropsで各コンポーネントに配るだけ</td>
</tr>
<tr>
<td>更新処理</td>
<td>入力欄が変更されたら、useEffectでFetchして取得 → Stateを更新して描画し直す</td>
<td>入力欄が変更されたら、 router.push(”/?q={検索]”) へ移動するだけ(取得は pages.tsx に書かれてる仕組みがそのまま使われる)</td>
</tr>
<tr>
<td>初期ロード中の表示</td>
<td>Client側で取得する。取得中は、isLoadingのstate(useState)を更新して、取得が終わったらstateを更新する</td>
<td><strong>更新点</strong>: <a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components#streaming-with-suspense"><code>&lt;Suspense&gt;</code></a> を使う。<code>&lt;Suspense&gt;</code> と use を使うことで、部分的にローディング表示を組み込む。S3からの取得が完了する前からページは表示される。</td>
</tr>
<tr>
<td>更新中の表示</td>
<td>(初期ロードと同じ) Client側で取得する。取得中は、isLoadingのstate(useState)を更新して、取得が終わったらstateを更新する</td>
<td><code>startTransition(() =&gt; router.push(...))</code> で更新中かの状態(state)を得て、更新中の表示を行う。このstateを Contextを通して、Client Component間で共有して、いろいろな場所のローディング表示を行う。</td>
</tr>
<tr>
<td>URL</td>
<td>特に変化しない</td>
<td>入力に合わせて <code>?q=&lt;クエリ&gt;</code> を更新していく</td>
</tr>
</tbody>
</table>
<p>実際の Pull Request は、次の URL から見れます。</p>
<ul>
<li><a href="https://github.com/azu/mytweets/pull/6">refactor(web): Suspense + Streaming by azu · Pull Request #6 · azu/mytweets</a></li>
</ul>
<h2>感想</h2>
<p>よく作られたサイトは クライアントサイドレンダリング だけでもほぼ RSC と同じことはできるけど、RSC はコンポーネントを分割する一種の規約なのでそれが強制される。
これは言い換えると、サイトが複雑化してきたときでもパフォーマンスが急激には悪くなりにくいという形になってる。
一般的に、クライアントサイドレンダリングだけだと開発が進んで複雑化してきたときに、同じパフォーマンスを維持するのはかなり難しくなる。</p>
<p>具体的には API が増えたときにどうするか、コンポーネントが増えた時にここは遅延ロードしないといけないとか、細かいことを色々考える必要がある。
これまでは GraphQL で必要なものだけを取得したり、初期表示に必要ないコンポーネントを遅延ロードするなどの対応をしてきている。</p>
<p>RSC だと</p>
<ul>
<li>必要なものだけ読み込む: RSC は必要なものだけを持ったコンポーネントをシリアライズしてクライアントに渡す仕組みになってる
<ul>
<li>Note: RSC は GraphQL を使わなくても GraphQL のようなことができる Alternative という考え方</li>
<li><a href="https://www.youtube.com/watch?v=TQQPAU21ZUw&amp;t=2449s">Data Fetching with React Server Components - YouTube</a></li>
</ul>
</li>
<li>遅延ロード: Suspense を使って<a href="https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming">Streaming</a>でコンポーネントをロードする</li>
</ul>
<p>パフォーマンスの悪化を避ける方法として、いらない処理を別のところに逃すというのは良くあることで、
RSC だと、この逃す場所として RSC と Server Action が増えたという感じがする。
必要になるまで読み込まないという考え方をなんでも取り込んでるのは<a href="https://qwik.dev/">Qwik</a>で、RSC の場合は<a href="https://react.dev/reference/react/use-client#serializable-types">シリアライズできる範囲</a>としてコンポーネントと Promise ぐらいになっている。</p>
<p>一方で、Server Action は何も規約がないので、Web API を作る意識なくやってしまうと無法地帯となる可能性がある。
これは Next.js が柔軟性のためにフレームワークをしてない部分なので、この辺はもうちょっとフレームワークとしての仕組みが必要そう。</p>
<ul>
<li><a href="https://nextjs.org/blog/security-nextjs-server-components-actions">How to Think About Security in Next.js | Next.js</a></li>
<li><a href="https://zenn.dev/moozaru/articles/c3bfd1a7e3c004">Server Actions にユーザ操作されたくないデータは渡さない</a></li>
</ul>
<p>Next.js の App Router を見たときに、Client と Server で話を分けたくなるけど、実際には React Client Components/React Server Components/React Server Actions の 3 つに分かれる。</p>
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/rendering/client-components">React Client Components</a>: React クライアントのコンポーネント</li>
<li><a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">React Server Components</a>: React サーバのコンポーネント</li>
<li><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations">React Server Actions</a>: React サーバの API
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers">Route Handlers</a>もレイヤー的には同じ</li>
</ul>
</li>
</ul>
<p><a href="https://github.com/reactwg/server-components/discussions/4">Why do Client Components get SSR’d to HTML? · reactwg/server-components · Discussion #4</a>を見ると、クライアントとサーバというのは物理的なクライアントサーバの話ではないのがわかる。
React の元々あった Tree のことを Client Tree と呼んで、React Server Component と一緒にできたのを Server Tree と呼んでる。 (HTML を生成するものを”Client”と呼んで、“Client”にシリアライズしたデータを処理して渡すやつを”Server”と呼んでるだけ)
そのため、“React の Client Component”ではなく”React Client の Component”という感じの意味合いになってる。</p>
<p>また、Client Component は RSC をインポートできないというルールを思い出すと、それぞれが扱うデータの範囲が異なるという感じがする。</p>
<table>
<thead>
<tr>
<th>名前</th>
<th>ユーザー入力</th>
<th>サーバーデータ</th>
</tr>
</thead>
<tbody>
<tr>
<td>React Client Components</td>
<td>受け取る</td>
<td>indirect read-only</td>
</tr>
<tr>
<td>React Server Components</td>
<td>受け取らない</td>
<td>direct read-only</td>
</tr>
<tr>
<td>React Server Actions</td>
<td>受け取る</td>
<td>direct read/write</td>
</tr>
</tbody>
</table>
<p>📝 React Server Componets は searchParams でユーザー入力は受け取れるので全部ではない。またサーバーデータも読み書きできてしまうが、GET で Write は基本避けるので原則的な話。</p>
<p>RSC から Server Actions を呼ぶこともできるけど、その Server Actions を Cient Component から使い回すというやり方をすると事故る可能性がある。
これは、RSC がユーザー入力を基本的には受け取らない(searchParams はあるけど)けど、Client Component は受け取るという違いがある。
Server Actions から見るとどちらも同じ引数として渡ってくるので、この引数が安全なのかは基本的にわからない。</p>
<p>Server Actions は、クライアントとのインタラクティブ性がある API だったり、ユーザーに紐づかないデータ処理をサーバに逃すのに適している。
たとえば、郵便番号の検索して住所を返す処理とかフォームのバリデーションのような処理。</p>
<p>一方で、データを実際に Write するような処理は気をつけないといけないので、その辺は<a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers">Route Handlers</a>の方が API として扱うには安全な感じがする。
もしくは、Server Actions で一旦受けてから、別のサーバの関数にバリデーションしてから渡すような形にするとか。
この辺が、結構あいまいになりやすい気がするので、ここはもうちょっと整理されるといいと思いました。</p>
<p>App Router は全体的に、需要を満たすための柔軟な機能を多めに入れている感じはします。
Page Router の場合は、最初はそこまでなんでもできるというものじゃなかった気はしますが、App Router は最初から Pages Router の superset として作られている感じはします。おそらくここが、複雑に感じる部分で、この辺が整理されるともっと使いやすくなると思う。</p>
<p>何が opt-in で何が opt-out なのかがわかりにくいのも、難しく感じる部分なのかもしれません。</p>
<table>
<thead>
<tr>
<th>opt-in</th>
<th>opt-out</th>
</tr>
</thead>
<tbody>
<tr>
<td>App Router</td>
<td>キャッシュ</td>
</tr>
<tr>
<td>React Client Component(”use client”)</td>
<td></td>
</tr>
<tr>
<td>React Server Action(”use server”)</td>
<td></td>
</tr>
</tbody>
</table>
<p>これは適当なテーブルなのでどこかにドキュメントが欲しい。</p>
<h3>感想のサマリ</h3>
<ul>
<li>Next.js App Routerをちゃんと使うとパフォーマンスが落ちにくいサイトを作れるフレームワークになっている</li>
<li>一方で、ただ乗れば作れるという感じではなく、ちゃんと設計する必要はある</li>
<li>現状だと、フレームワークがフレームワークしてない部分もあるので、この辺は考えて扱う必要がある</li>
</ul>
<hr />
<p>こっからはメモ書き成分が多いです。
作りながら書いてたメモをコピペしてます。</p>
<h2>技術的なメモ書き</h2>
<p>実際に動かさないとやり方がわからなかった部分をメモ書きとして残しておきます。</p>
<h3>Client Component 間のデータのやり取り</h3>
<p><a href="https://www.patterns.dev/vanilla/islands-architecture/">Islands Architecture</a>と同じ話ですが、Client ComponentとRSCの境界を切っていくと、Client Component同士が離れた位置にあるけど、状態は同期したいというケースが出てきます。
入力中の表示を別の場所に出すとか、ロード中は色々なところにあるボタンをdisabledにしたとか、大枠をまたいで状態を共有したいというケースです。</p>
<p>この場合は、Client Component間で状態を共有する方法が必要です。</p>
<p>やったこと</p>
<ul>
<li>Context と引数を持たない Provider のラッパーを作る</li>
<li>Source: <a href="https://github.com/azu/mytweets/blob/efd1281ea3936a5794712d39d8be81bc04284a96/web/app/client/TransitionContext.tsx">https://github.com/azu/mytweets/blob/efd1281ea3936a5794712d39d8be81bc04284a96/web/app/client/TransitionContext.tsx</a></li>
<li>RSC の中で Client Component を呼べるが、RSC の中で useState などは使えない</li>
<li><code>&lt;Provider value={ setState } /&gt;</code> みたいな技は使えない</li>
<li>代わりにこれをラップした Provider の Client コンポーネントを作って使うことで、任意の初期値を入れた Context を RSC の中でも埋め込める</li>
</ul>
<p>具体的には次のような<code>TransitionContextProvider</code>という Provider のラッパーコンポーネントを用意してる。
このコンポーネントは RSC からもインポートして埋め込むことができる。</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="dl">"</span><span class="s2">use client</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createContext</span><span class="p">,</span> <span class="nx">ReactNode</span><span class="p">,</span> <span class="nx">useContext</span><span class="p">,</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">type</span> <span class="nx">TransitionContext</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">isLoadingTimeline</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
  <span class="nl">setIsLoadingTimeline</span><span class="p">:</span> <span class="p">(</span><span class="na">isLoading</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">TransitionContext</span> <span class="o">=</span> <span class="nx">createContext</span><span class="o">&lt;</span><span class="nx">TransitionContext</span><span class="o">&gt;</span><span class="p">({</span>
  <span class="na">isLoadingTimeline</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
  <span class="na">setIsLoadingTimeline</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{},</span>
<span class="p">});</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">TransitionContextProvider</span> <span class="o">=</span> <span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="p">{</span> <span class="nl">children</span><span class="p">:</span> <span class="nx">ReactNode</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">isLoadingTimeline</span><span class="p">,</span> <span class="nx">setIsLoadingTimeline</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
  <span class="nf">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">TransitionContext</span><span class="p">.</span><span class="nc">Provider</span>
      <span class="na">value</span><span class="p">=</span>
    <span class="p">&gt;</span>
      <span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">children</span><span class="si">}</span>
    <span class="p">&lt;/</span><span class="nc">TransitionContext</span><span class="p">.</span><span class="nc">Provider</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">useTransitionContext</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="nf">useContext</span><span class="p">(</span><span class="nx">TransitionContext</span><span class="p">);</span>
  <span class="nf">if </span><span class="p">(</span><span class="o">!</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">useTransitionContext must be used within a TransitionContextProvider</span><span class="dl">"</span>
    <span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">context</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<ul>
<li>Server では引数に setState を渡すということができないので、初期値を持たない Context Provider を作るにはラッパーが必要となる</li>
<li>RSC では、Client Component の境界のためにこういったラッパーコンポーネントを作るケースが結構ある</li>
<li>ここではContextを作っているけど、state管理のライブラリを使う場合も大体似た話になります</li>
</ul>
<p>参考</p>
<ul>
<li><a href="https://future-architect.github.io/articles/20231214a/">React Server Component でも Context で状態を共有する | フューチャー技術ブログ</a></li>
<li><a href="https://blog.stin.ink/articles/do-not-export-react-context">React Context を export するのはアンチパターンではないかと考える | stin’s Blog</a></li>
</ul>
<h3>ルーティングの移動中の判定</h3>
<p><code>router.push</code>で移動中の表示をしたいというケース。
たとえば、移動中はローディング表示をしたいとか、ボタンクリックでロード中はボタンを disable にしたいというケース。</p>
<ul>
<li><code>useTransition</code> を使うとできる</li>
<li>router.push と <code>const [isPending, startTransition] = useTransition();</code> を組み合わせる</li>
</ul>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="tsx"><span class="c1">// 移動中はisLoadingがtrueになる</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">isLoading</span><span class="p">,</span> <span class="nx">startTransition</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useTransition</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">handlers</span> <span class="o">=</span> <span class="nf">useMemo</span><span class="p">(</span>
  <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
    <span class="na">search</span><span class="p">:</span> <span class="p">(</span><span class="na">query</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nf">startTransition</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">router</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="s2">`/?q=</span><span class="p">${</span><span class="nx">query</span><span class="p">}</span><span class="s2">`</span><span class="p">));</span>
    <span class="p">},</span>
  <span class="p">}),</span>
  <span class="p">[]</span>
<span class="p">);</span>
</code></pre></div></div>
<ul>
<li><a href="https://github.com/azu/mytweets/blob/efd1281ea3936a5794712d39d8be81bc04284a96/web/app/client/SearchBox.tsx#L26-L41">https://github.com/azu/mytweets/blob/efd1281ea3936a5794712d39d8be81bc04284a96/web/app/client/SearchBox.tsx#L26-L41</a></li>
<li><a href="https://github.com/vercel/next.js/discussions/49810">The new Router doesn’t return a Promise · vercel/next.js · Discussion #49810</a></li>
<li><a href="https://ja.react.dev/reference/react/useTransition#building-a-suspense-enabled-router">useTransition – React</a></li>
</ul>
<p>これは、Server Action を呼ぶときにも利用できる。</p>
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#non-form-elements">Data Fetching: Server Actions and Mutations | Next.js</a></li>
</ul>
<p>この辺が、はっきりと Next.js のドキュメントには書かれてなくてかなりわかりにくいと思った。</p>
<h2>問題</h2>
<h3>フォーカス管理とルーティング</h3>
<ul>
<li>input の状態とルーティングと同期できていない
<ul>
<li>戻るで戻ったときにinputの値が残ったままになる</li>
</ul>
</li>
<li>Vercel の公式サンプルもルーティングと input の同期するために <code>&lt;input key={key}/&gt;</code>という感じで key を変えて破棄している
<ul>
<li><a href="https://demo.vercel.store/search">Search | Acme Store</a></li>
<li>検索するとinputのフォーカスを失っているのがわかる</li>
</ul>
</li>
<li><code>key</code> で破棄すると input のフォーカスも無くなるので、体験が悪い</li>
</ul>
<h3>Failed to load response data: No data found for resource with given identifier</h3>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Next.js RSCのpayloadが&quot;Failed to load response data: No data found for resource with given identifier&quot;のエラーで見えないの、<br><br>Chrome DevToolsでChrome DevToolsにdebuggerを入れてみると、<a href="https://t.co/U9fdVQBLNs">https://t.co/U9fdVQBLNs</a><a href="https://t.co/UZB1AsycaT">https://t.co/UZB1AsycaT</a><br>ここでエラーとなってる。 <a href="https://t.co/Jmh2J2Fwkk">pic.twitter.com/Jmh2J2Fwkk</a></p>&mdash; azu (@azu_re) <a href="https://twitter.com/azu_re/status/1761243872712380890?ref_src=twsrc%5Etfw">February 24, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 
<ul>
<li>RSC PayloadがChrome DevToolsで読み込めない</li>
<li>実際には読み込めているが表示されない問題</li>
<li>おそらくはChromeのバグを踏んでいるのだと思うけど、Next.js側とかで何かワークアラウンドで回避してほしい
<ul>
<li><a href="https://github.com/cyrus-and/chrome-remote-interface/issues/260">Error: No data found for resource with given identifier · Issue #260 · cyrus-and/chrome-remote-interface</a></li>
<li><a href="https://issues.chromium.org/issues/41367044">Network.getResponseBody fails sporadically [41367044] - Chromium</a></li>
<li><a href="https://issues.chromium.org/issues/40267158">Fail to load response data: No resource with given identifier found [40267158] - Chromium</a></li>
<li><a href="https://issues.chromium.org/issues/40254754">DevTools: XHR (and other resources) content not available after navigation. [40254754] - Chromium</a></li>
</ul>
</li>
<li><a href="https://chromewebstore.google.com/detail/rsc-devtools/jcejahepddjnppkhomnidalpnnnemomn">RSC Devtools</a>を使って見るというのもなんか微妙な話</li>
</ul>
<h2>参考</h2>
<ul>
<li><a href="https://vercel.com/blog/understanding-react-server-components">Understanding React Server Components – Vercel</a>
<ul>
<li>RSC が何をしてるか</li>
</ul>
</li>
<li><a href="https://speakerdeck.com/player/28c9e46adaaf46cba9001926612bacde?title=false&amp;skipResize=true">控えめな App Router と持続可能な開発 - PWA Night vol.59 - Speaker Deck</a>
<ul>
<li><a href="https://zenn.dev/overflow_offers/articles/20240112-using-nextjs-app-router-sparingly">Next.js App Router と控えめにお付き合いして普通の Web アプリを配信する | Offers Tech Blog</a></li>
<li>RSC を<code>getServerSideProps</code> の代用(page.tsx のみ)として使い、そのほかは client component として使う方法</li>
<li>デフォルトが RSC なのを、opt-in で RSC を使うアプローチ</li>
</ul>
</li>
<li><a href="https://speakerdeck.com/player/eb9e90fafbb64052a8c6a519894baea5?title=false&amp;skipResize=true">プライベートクラウドのコンソール画面を Next.js の App Router でフルリプレイスした話 - Speaker Deck</a>
<ul>
<li>i18n の問題</li>
</ul>
</li>
<li><a href="https://tech.aisaac.jp/entry/2023/09/26/130758">【Next.js の新機能】App Router を早速本番環境で使ってみた - aisaac 技術ブログ</a>
<ul>
<li>移行してのサイズがどれぐらいかわったか</li>
</ul>
</li>
<li><a href="https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional">File Conventions: page.js | Next.js</a>
<ul>
<li>searchParams は RSC の Props で受け取れる</li>
<li>URLSearchParams ではないことに注意</li>
</ul>
</li>
<li><a href="https://cam-inc.co.jp/p/techblog/859745503506595841">Next.js v14 で Form validation を server 側で行う | 株式会社 CAM</a>
<ul>
<li>redirect は例外を投げる</li>
<li>フォームバリデーション</li>
<li><a href="https://azukiazusa.dev/blog/use-form-state-to-display-error-messages-in-server-actions-forms/">Server Actions のフォームバリデーションにおいて useFormState でエラーメッセージを表示する</a></li>
<li><a href="https://zenn.dev/ikenohi/scraps/86618e830636e6">form x Server Actions x useFormState の探求</a></li>
</ul>
</li>
<li><a href="https://www.youtube.com/watch?v=VIwWgV3Lc6s">React Server Components: A Comprehensive Breakdown - YouTube</a>
<ul>
<li><a href="https://www.joshwcomeau.com/react/server-components/">Making Sense of React Server Components</a></li>
<li>RSC の仕組み</li>
</ul>
</li>
<li><a href="https://github.com/calcom/cal.com/issues/9923">https://github.com/calcom/cal.com/issues/9923</a>
<ul>
<li>page → app router への移行 calcom</li>
</ul>
</li>
<li><a href="https://addyosmani.com/blog/react-server-components-app-router/">AddyOsmani.com - React Server Components, Next.js App Router and examples</a>
<ul>
<li>色々な example</li>
</ul>
</li>
<li><a href="https://speakerdeck.com/uhyo/app-routershi-dai-nodetaqu-de-akitekutiya">App Router 時代のデータ取得アーキテクチャ - Speaker Deck</a>
<ul>
<li>SSR の便利レイヤーだよねという話</li>
</ul>
</li>
<li><a href="https://speakerdeck.com/mugi_uno/next-dot-js-app-router-deno-mpa-hurontoendoshua-xin?slide=29">https://speakerdeck.com/mugi_uno/next-dot-js-app-router-deno-mpa-hurontoendoshua-xin?slide=29</a>
<ul>
<li>キャッシュを無効にする方法</li>
</ul>
</li>
<li><a href="https://quramy.medium.com/react-server-component-%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%81%A8-container-presentation-separation-7da455d66576">React Server Component のテストと Container / Presentation Separation | by Yosuke Kurami | Medium</a></li>
<li><a href="https://nextjs.org/docs/app/building-your-application/configuring/typescript#statically-typed-links">https://nextjs.org/docs/app/building-your-application/configuring/typescript#statically-typed-links</a>
<ul>
<li><code>experimental.typedRoutes</code> で router.push も型安全になる</li>
<li><code>--turbo</code> は <code>experimental.typedRoutes</code> に対応してない</li>
<li>ただし、パラメータの型安全はないため<a href="https://github.com/azu/mytweets/blob/efd1281ea3936a5794712d39d8be81bc04284a96/web/app/lib/useTypeUrlSearchParams.ts">useTypeUrlSearchParams</a>を使ってる</li>
</ul>
</li>
<li>“failed to load response data: No data found for resource with given identifier”
<ul>
<li>このエラーは謎い</li>
<li><a href="https://windowsreport.com/chrome-failed-to-load-response-data/">How to Fix Chrome’s Failed to Load Response Data Error</a></li>
<li>proxyman では見える、Chromium 側で起きるエラーメッセージがでてる</li>
<li><a href="https://twitter.com/azu_re/status/1761243872712380890">https://twitter.com/azu_re/status/1761243872712380890</a></li>
</ul>
</li>
<li><a href="https://zenn.dev/cybozu_frontend/articles/next-rsc-testing">Next.js で Server Components がちょっとだけテストできるようになってた</a></li>
<li><a href="https://quramy.medium.com/react-server-component-%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%81%A8-container-presentation-separation-7da455d66576">React Server Component のテストと Container / Presentation Separation | by Yosuke Kurami | Medium</a>
<ul>
<li>RSC のテスト</li>
<li>Unit Test はロジックのテストだけしたいので、
<ul>
<li>RSC → ロジックだけを関数に切り出してテスト
<ul>
<li>これは Node.js のテストとして普通に書く</li>
</ul>
</li>
<li>Client Component → Custom Hooks をテスト
<ul>
<li>Hooks のテストとして書く</li>
<li>ack とか使わないといけないのが微妙</li>
</ul>
</li>
<li>Component のテスト
<ul>
<li>これが要求されるのはライブラリの品質を求めるコンポーネントなので、</li>
<li>それは Storybook とか持つようなコンポーネントな気がするので、</li>
<li>アプリケーションレイヤーのコンポーネントに向けて書くのは厳しい気がする</li>
</ul>
</li>
<li>E2E → ブラウザテスト</li>
</ul>
</li>
<li>という感じなのではという気はする。</li>
<li>E2E は重たいし安定させるのはとても難しい。
<ul>
<li>ここを真面目にやった方がいい</li>
<li>変にモックで頑張ると崩壊した時の方が大変</li>
<li>Netflix のアプローチは全モックで全部コントロールという感じだけど、アップデートがたいへんになりそう</li>
<li><a href="https://github.com/kolodny/safetest?tab=readme-ov-file">kolodny/safetest</a>
<ul>
<li><a href="https://netflixtechblog.com/introducing-safetest-a-novel-approach-to-front-end-testing-37f9f88c152d">Introducing SafeTest: A Novel Approach to Front End Testing | by Netflix Technology Blog | Feb, 2024 | Netflix TechBlog</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><a href="https://zenn.dev/suzu_4/articles/2e6dbb25c12ee5">Next.js から学ぶ Web レンダリング <del>React 誕生以前から App Router with RSC までの流れ</del></a></li>
<li><a href="https://github.com/reactwg/react-18/discussions/94">When to use Suspense vs startTransition? · reactwg/react-18 · Discussion #94</a>
<ul>
<li>初期ロードと更新でロードの表示方法が異なるので、両方使う場面がある</li>
<li>けど意図的に簡略化するためにどちらかに寄せるというのを選べるといい気はする。</li>
<li>Suspense に寄せる場合は、soft navigation をやめて常に <code>&lt;a&gt;</code> で移動させるとかなのかな</li>
</ul>
</li>
<li><a href="https://react.dev/reference/react/use#usage">use – React</a>
<ul>
<li>Suspese やるときに Promise<T>を props に受け取らないで、T を受け取るコンポーネントを維持する方法ってないのかな? コンポーネントをわざわざ Suspense ようにラップしたりしないといけないのが微妙</li>
<li>次みたいに書かないといけないけど、Promise を受け取るコンポーネントってめっちゃ使いにくい気がする</li>
</ul>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="markdown">const C = (props: {a:Promise<span class="nt">&lt;A&gt;</span>}) =&gt; {
const a = use(props.a);
...
}
</code></pre></div></div>
<ul>
<li>これやるぐらいなら、コンポーネントそのものが Suspense 向けというのを明示した方がいい気がする</li>
</ul>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="jsx"><span class="kd">const</span> <span class="nx">C_for_Suspense</span> <span class="o">=</span> <span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">CProps</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">props</span> <span class="o">=</span> <span class="nf">use</span><span class="p">(</span><span class="nx">props</span><span class="p">);</span>
  <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li>最終的には Promise を受けるコンポーネントラッパーを書いている
<ul>
<li>複数の props があると <code>const { a, ...other } = props;</code> みたいなことをしないと props のバケツリレー漏れが起きるのでできれば避けたい</li>
</ul>
</li>
</ul>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code data-lang="jsx"><span class="kd">const</span> <span class="nx">C</span> <span class="o">=</span> <span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="nx">A</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="p">...</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">CStream</span> <span class="o">=</span> <span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">A</span><span class="o">&gt;</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="nf">use</span><span class="p">(</span><span class="nx">props</span><span class="p">.</span><span class="nx">a</span><span class="p">);</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nc">C</span> <span class="na">a</span><span class="p">=</span><span class="si">{</span><span class="nx">a</span><span class="si">}</span> <span class="p">/&gt;</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li>Edit: Ref <a href="https://twitter.com/koichik/status/1762403998609678773">https://twitter.com/koichik/status/1762403998609678773</a></li>
</ul>
</li>
<li><a href="https://zenn.dev/ojin/articles/8b383b0ac98eb9">Client Components で Suspense+use 使用時のエラー(Not implemented)の解決方法</a></li>
<li><a href="https://react.dev/reference/react/use-server">‘use server’ directive – React</a>
<ul>
<li>Client Component から <code>&quot;use server&quot;</code> でマークされたサーバの関数を RPC 的に呼ぶのは、Server Action と言うらしい</li>
<li>React のドキュメントが <code>&lt;form action={fn}&gt;</code>だけじゃなくて、Client → Server function も Server Action と呼んでる</li>
</ul>
</li>
<li><a href="https://zenn.dev/cybozu_frontend/articles/react-taint-apis">Next.js 14 で導入された React Taint APIs を試してみた</a>
<ul>
<li>Server Action とかはこれを使ってマークした方がいい気はするが、大体忘れそうなのでもっと包含的な middlewa の仕組みが必要そう a</li>
</ul>
</li>
<li><a href="https://zenn.dev/moozaru/articles/c3bfd1a7e3c004">Server Actionsにユーザ操作されたくないデータは渡さない</a>
<ul>
<li>クライアントとサーバが同じ言語だと結構起きやすい感じはある</li>
<li>セッションやDBから引くべきものをクライアントからそのまま渡してしまうと起きる問題</li>
<li>Server → ClientはTaint APIが対策として利用できるが</li>
<li>Client → Serverは設計的な対応が必要になる</li>
</ul>
</li>
<li><a href="https://nextjs.org/blog/security-nextjs-server-components-actions">How to Think About Security in Next.js | Next.js</a>
<ul>
<li>Server Actionsは通常のAPIと同じように考える必要がある</li>
</ul>
</li>
</ul>
 ]]></content>
 </entry>
 

</feed>
