Nasu Stack v2 を公開しました
部品・フック・契約の各層で選択肢を増やした v2.0.0 から、実アプリで責任境界を確かめる dogfooding を始めた v2.1.0、セキュリティパッチの v2.1.1 までの更新をまとめました。
Nasu Stack v2 を公開しました
こんにちは、なすです。
8月27日に Nasu Stack v2.0.0 をリリースし、その後 v2.0.1、v2.1.0、v2.1.1 をリリースしました。この記事では v2.1.1 までの変更をまとめます。
Nasu Stack そのものについては v1.0.0 の公開記事 を読んでください。
| バージョン | 日付 | 内容 |
|---|---|---|
| v2.0.0 | 8月27日 | 2つ目の安定版。配布物を11個追加 |
| v2.0.1 | 8月30日 | 外部レビューで見つかった不具合の修正と、リリース工程の強化 |
| v2.1.0 | 9月1日 | 実アプリ「Repository Pulse」を雛型として追加 |
| v2.1.1 | 9月28日 | Astro 周りのセキュリティパッチ |
v2.0.0:なぜメジャーバージョンを上げたのか
結論から言えば、意図的な挙動の変更が1つあるからです。
useAction は、VALIDATION と HTTP 422 のエラーを自動でリトライしなくなりました。同じ不正な入力を何度送り直しても結果は変わらず、ユーザーへのフィードバックが遅れるだけだからです。
それ以外はすべて追加です。v1 の配布物の名前、公開している export、セマンティックトークンは1つも削除していません。
何もしなくても壊れない
Nasu Stack は shadcn 方式で、コードを利用者のリポジトリにコピーします。コピーされたコードは利用者のものなので、v2 が出たからといって裏で書き換わることはありません。v1 で作ったアプリは、そのまま使い続けられます。
v2 の挙動が欲しい部品だけを、1つずつ更新します。
npx [email protected] add Nasu726/Nasu-Stack/async-form --dry-run
npx [email protected] add Nasu726/Nasu-Stack/async-form --diff
npx [email protected] add Nasu726/Nasu-Stack/async-form --overwrite
--dry-run でファイルを書かずに中身を確認し、--diff で差分を見て、手を入れていないコピーだけを --overwrite で上書きします。自分で書き換えたコピーには、必要な変更を手でマージしてください。最後のコマンドは既存のファイルを置き換えるので、差分を確認するまで実行しないでください。
見えない場所で更新する仕組みは、意図的に用意していません。手順は 移行ガイド にまとめています。
増えた部品
v2.0.0 で配布物が11個増え、合計51個になりました。
validation— ライブラリに依存しないバリデーション結果の契約use-interaction-guard— 二重発火だけを防ぐ軽量なフックuse-autosave— 自動保存のキューuse-copy/copy-button— クリップボードへのコピーerror-boundary— 描画の失敗を受け止めるfield-array— 行を増やしたり減らしたりできるフォーム項目paginator— ページ送りpopover— ポップオーバーの土台load-more-list— カーソル方式の「もっと読み込む」search-list— 検索付きリストのレシピ
ほかに、配布物を増やさずに layout へ Switcher と SidebarLayout を足しました。コンテナの幅に応じて並び方が切り替わるレイアウトです。
AsyncForm もできることが増えました。
- ライブラリに依存しないバリデータを走らせられる
- 変換済みのデータを action に渡せる
- 最初のエラー項目にフォーカスを移す
- ネストした項目のエラーを消せる
降りる道も増えた
v1 から、足りなくなったら「乗り換えではなく降りる」をエスケープハッチの形にしています。v2 では、レイアウト・バリデーション・クリップボード・ポップオーバーの位置決め・カーソル方式のページングにも、コンポーネント → フック → 契約 と降りる道を用意しました。
たとえば load-more-list を入れると、下位の CursorPage / CursorLoader / useCursorList も一緒に入ります。コンポーネントの既定の見た目が合わなければ、フックや契約まで降りて自分で組めます。
責任境界は変わらない
v2 は境界のところに契約やアダプタを足しましたが、バックエンドのフレームワークになったわけではありません。次の項目は、引き続きアプリ・サーバの責任です。
- 認証・認可
- サーバ側の正規のバリデーション
- レート制限・冪等性・トランザクション
- 下書きの永続保存
- データベース上のカーソルの正しさ
新しい部品も、同じ考え方で境界を引いています。
validationはクライアントを正規の判定場所にしません。 同じバリデータをクライアントとサーバで共有すれば重複は減らせますが、それでもサーバは独立して必ず判定します。権限・一意性・在庫を確かめるのはサーバとデータベースの仕事です。useAutosaveは値をlocalStorageに保存しません。 引き受けるのは、debounce、進行中1件と待機中の最新値1件の管理、古いレスポンスの破棄、リトライ、アンマウント時の中断通知といった、少しの実装差で壊れやすいクライアント側のキューだけです。任意のフォームデータを永続化すると、プライバシーや保持期間、マイグレーションの判断までUIの部品が背負うことになるためです。cancel()は取り消しではありません。 サーバがすでに書き込みを確定させていることもあります。hook の状態に結果を戻さないだけで、データベースやメッセージ、決済は巻き戻りません。
リリース前の検査は、v1 の29段階から33段階に増えました。生成したプロジェクトへの112項目の検査に加えて、51個すべての配布物を本物の shadcn CLI で入れて確かめています。
v2.0.1:リリースの工程を固める
外部レビューで再現条件がはっきりした不具合を、まとめて直しました。公開している契約は変えていません。いくつか挙げると、
FieldArrayの最低行数を親が増やしたあとにフォームをリセットすると、最低行数を下回っていた- エラーレスポンスの
fieldsを、React に渡す前に実行時に正規化するようにした。知らない項目のエラーしか無いときは、一般エラーとして表示する createSubmitが JSON のシリアライズの失敗を、ネットワークや CORS の失敗と区別して報告するようにした
もう1つ大きいのが、リリース工程の強化です。
- GitHub の Immutable Releases を有効にした。これ以降のリリースは、公開後にアセットを差し替えられない(v2.0.0 は有効にする前に公開したので対象外です)
- リリースのジョブは、タグの指すコミットが
main上にあり、かつ同じコミットで Pages のワークフローが成功していないと動かない - 検証用のハーネス自体にも、わざと失敗させるチェックを入れた。テストが何も見ていないのに緑になっていないかを確かめるためです
v2.1.0:実アプリで確かめる
ここからは、部品を机の上で増やすのをやめました。Nasu Stack で実際にアプリを作り、そこで繰り返した判断から次の改善を決める「dogfooding」を始めています。
性質の違う3本を作る計画です。
- Repository Pulse — 読み込み中心の Vite アプリ
- Weather Planner — 状態の多い Vite アプリ
- Service Intake — 書き込み中心の Astro + React アプリ
v2.1.0 では、その1本目の Repository Pulse を正式な雛型にしました。
Repository Pulse
公開リポジトリの概要、Issue / Pull Request の検索、明示的な追加読み込み、狭い画面ではカードに組み替わるリリース表を持つアプリです。
- 実物は こちら
create-nasu-stack で repository-pulse を選ぶと、このアプリと同じものが手に入ります。配布用に別のコピーを用意するのではなく、このアプリを唯一の原本にして、ビルド時に雛型を生成しています。中に入っている Nasu Stack のコードも、利用者と同じく公開している shadcn レジストリ経由で入れたものです。
ここでも境界をはっきりさせています。
VITE_*の値はビルド後の JavaScript に埋め込まれ、誰でも読めます。なので GitHub のトークンは置きません- 認証なしの GitHub API はレート制限が低いですが、それは受け入れています。private なリポジトリを見たい、上限を上げたい、という場合は、認証・認可・レート制限・ログ・シークレット管理を引き受けるサーバ側のプロキシを別に用意してください
- 必須の環境変数が無かったり不正だったりすると、真っ白な画面にせず、直すべき変数名を画面に出します
- CI では HTTP を固定のデータに差し替えています。外部 API が落ちているかどうかを、リリースの判定に混ぜないためです
雛型に昇格させる条件
アプリを作っただけでは雛型にしません。次の条件をすべて満たしたものだけを昇格させます。
- 空の環境から、実際の利用手順を1回以上完走した
- 外部サービスが落ちても偽の成功を表示せず、失敗と再試行が分かる
- install / typecheck / build / serve / ブラウザでの検査を自動化できる
- 320 / 375 / 414 / 768 / 1024 / 1920px で主要な経路が崩れない
- キーボードだけで主要な操作を完了でき、loading / empty / error / success が読める
.env.example、設定手順、責任外の運用要件が英語と日本語で分かる- レジストリのコードを手で複製せず、実際のレジストリ経路で入れたコードを使う
もう1つ決めているのが、1本のアプリで同じ実装が出てきただけでは公開APIを増やさないことです。2本以上で繰り返し出てきて、かつ Nasu Stack が引き受ける責任の範囲内だと説明できるものだけを、次の検討対象にします。
dogfooding の前に直したこと
アプリ側に回避策を埋め込む前に、既存の契約の不整合を先に直しました。
AsyncFormは、返ってきた項目エラーの「一部」ではなく「すべて」に表示先があるときだけ、一般エラーを抑えるようにしたAsyncSelectをAsyncFormの項目の契約に参加させた。送信中の無効化、サーバエラー、ARIA、最初のエラーへのフォーカスなどが、他の項目と揃ったEndpointSpecとcreateSubmitの JSON シリアライズを共通化し、BigIntや循環参照をSERIALIZATIONとして扱うようにした
v2.1.1:セキュリティパッチ
毎週走らせている本番の依存関係の監査が、新しく公開されたセキュリティアドバイザリで落ちたので、パッチを出しました。
- Astro の下限を 7.2.2 から 7.2.10 へ
- Sharp を 0.35.3 から 0.35.4 へ
- Astro / Blog 雛型のロックファイルを更新し、high 以上の既知の脆弱性をゼロに
2本目の Weather Planner は v2.2.0 向けに main へマージしていましたが、v2.1.1 には含めず、依存関係の更新だけをリリースしています。
使い方
npx https://github.com/Nasu726/Nasu-Stack/releases/download/v2.1.1/create-nasu-stack-2.1.1.tgz my-site
--lang、--template、--yes は v1 と同じように使えます。
npm の create-nasu-stack という名前は取得していません。npx create-nasu-stack は実行しないでください。 無関係な第三者のコードが実行されます。
これから
残りの Weather Planner と Service Intake を順に作って検証し、条件を満たしたら雛型にします。3本を作り終えたら、繰り返し書いた実装、邪魔だった既定値、文書の不足、エスケープハッチで降りた場所を横断して記録し、何を採用して何を採用しないかを決める予定です。
最後まで読んでくださりありがとうございました。
