UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【Turborepo】Code Generatorについて

TurborepoのCode Generator について調べた

turbo.build

まず、generator configuration fileを用意する

基本的にpromptsとactionsを定義するだけで良い

import type { PlopTypes } from "@turbo/gen";
 
export default function generator(plop: PlopTypes.NodePlopAPI): void {
  // create a generator
  plop.setGenerator("Generator name", {
    description: "Generator description",
    // gather information from the user
    prompts: [
      ...
    ],
    // perform actions based on the prompts
    actions: [
      ...
    ],
  });
}

promptsはhygenのようにユーザーに入力させたいものを定義する

actionsは入力値に応じてどのようなことをするかを定義する

    actions: [
      {
        type: "add",
        path: "src/{{kebabCase name}}.tsx",
        templateFile: "templates/component.hbs",
      },
      {
        type: "append",
        path: "package.json",
        pattern: /"exports": {(?<insertion>)/g,
        template: '"./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
      },
    ],

ここでのaddappendbuilt-in Plop actionsというのを使っており、他に以下のようなtypeが指定できる

  • Add:プロジェクトにファイルを追加する
  • AddMany:1 回のアクションで複数のファイルをプロジェクトに追加できる
  • Modify:patternでマッチしたものを置換したり、transformでファイルの中身を変更したりできる
  • Append:ファイルの特定の場所にデータを追加する

もしくは、完全に自分でaction functionをカスタムする方法もある

https://plopjs.com/documentation/#functionsignature-custom-action

これまでhygenを使っていたが、turborepo使うならこの機能を利用した方が良さそう

【Turborepo】turbo.jsonのオプションについて

Turborepoのチュートリアルで出てくるturbo.jsonのオプションについて調べた

チュートリアルは以下
Getting Started with Turborepo – Turborepo

オプションについては以下
Configuration – Turborepo

globalDependencies

グローバルハッシュの生成に影響あるファイルをリストにする

例えば.envtsconfig.jsonファイルといったルートディレクトリに配置するファイルを指定する

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    // ... omitted for brevity
  },
 
  "globalDependencies": [
    ".env", // contents will impact hashes of all tasks
    "tsconfig.json" // contents will impact hashes of all tasks
  ]
}

pipeline

プロジェクト内のタスクの出力を適切にスケジューリング、実行、キャッシュするための設定

オブジェクト内の各キーは、実行するタスクの名前

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

例えば以下のようにコマンドを入力すると各リポジトリのビルドが始まる

$ turbo run build
  • "dependsOn"はタスクを実行する前に実行する必要があるタスク依存関係を指定している
  • "outputs"はキャッシュするファイル群を指定する
  • "cache"スクリプトの結果をキャッシュしないようにTurborepo に指示している
  • "pesistentは長時間実行される開発サーバーであることを知らせるための設定で、他のタスクが依存しないようにできる(ないと他のタスクが始まらない)

一部だけ実行したい場合は--filterを使う

$ turbo dev --filter docs

モノレポのrootのpackage.jsonのscriptをturboから実行するには以下のように//#f<tasks>を設定する

{
  ...
  "pipeline": {
    ...
    "//#format": {
      "dependsOn": [],
      "outputs": ["dist/**/*"],
      "inputs": ["version.txt"]
    }
  }
}

これでrootのpackage.jsonで以下のように書かれたスクリプトを実行することができる

  "scripts": {
    ...
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },

任意のワークスペースのタスクを実行したい時、かつ、任意のタスクを先に実行したい時は<workspace>#<task>を使う

例えば、webワークスペースのbuildを実行する前にuiワークスペースのlintを実行する場合、以下のように定義する

{
  ...
  "pipeline": {
    ...
    "web#build": {
      "dependsOn": ["@repo/ui#lint"]
    }
  }
}

この時、webはuiに依存しているのだが、その場合は@repo/をつける必要があった

TIPS

ドキュメントにtypecheckの高速化のTIPSがあった

dependencies-outside-of-a-task

例えば、webがuiに依存している場合で uiに変更があった場合、以下のようなタスクを定義してしまうと uiに変更があったにも関わらずwebはキャッシュを利用して成功してしまう

{
  ...
  "pipeline": {
    ...
    "typecheck": {}
  }
}

なので、webのtypecheckタスクの前にuiのtypecheckタスクを走らせたい

とはいえ、以下のようにしたとすると

{
  ...
  "pipeline": {
    ...
    "typecheck": {
      "dependsOn": ["^typecheck"]
    }
  }
}

並行実行されず、uiのtypecheckが終わるまでwebのtypecheckが開始されないため遅くなる

そこで、並行実行した上で、webがキャッシュミスを検知したらuiのtypecehckを開始するような設定にしたい

その場合は、以下のように設定すると良い

{
  ...
  "pipeline": {
    ...
    "topo": {
      "dependsOn": ["^topo"]
    }
    "typecheck": {
      "dependsOn": ["topo"]
    }
  }
}

typecheckが並列に実行される前に、topoが実行される

topoはダミーのタスクで、topo^がついているので依存関係の変更を確認しながら実行される

この時、uiに変更があるのでuiとwebのキャッシュは更新されるが、ダミーなので何もせずにすぐに終了する

topoが終わったら、次にtypecheckが実行されるがwebのtypecheck時にはキャッシュと差分があるので、キャッシュを使わずに再実行される

これで、依存関係の変更を検知しつつ並列的にtypecheckを実行することができる

【サイト紹介】Googleが出しているコードレビューのガイドラインを読んだ

googleが出しているコードレビューガイドラインを知った

コードレビューをやる場合はこれを見ながらやるとよさそう

fujiharuka.github.io

現場のコードレビュー時に以下のようなトレードオフが存在する

  • どんな変更に対してもレビュアーがいちいち難色を示して変更を取り入れなかったら開発者の改善意欲が削がれる
  • 一方で、レビュアーはコードベースの健康状態が悪化しないようにきちんと確認が必要である

この問題に対して、何かしらのガイドラインがあった方が良いということで、このガイドラインを作られたとのこと

コードレビューの全ガイドラインで最上位の原則は以下

一般に、変更が完璧でなくても、その変更がシステムのコードの全体的な健康状態を改善すると確実にわかれば、レビュアーは変更を積極的に承認すべきである

「完璧さ」は求めておらず、「継続的な改善」を原則としているのが注目ポイント

コメントはためらわず残すべきであるが、重要ではないものには「Nit: 」(あら探しや細かい指摘という意味)のようなプレフィックスを付けて無視してもらっても構わない作成者に知らせるべきだと述べられている

レビューする上でのその他の原則

  • 自分の意見や好みを捨て、技術的な事実とデータでレビューする
  • スタイルガイドが絶対的な権威であるが、ない場合は前例に従い、前例もなければ作者のスタイルを受け入れるべき
  • ソフトウェア設計において複数の方法が同等に有効であると証明できる場合は作者の選択を受け入れるが、証明できない場合はの標準的な原則に従うべき
  • 上記以外ではコードベースを悪化させない限りは一貫性を維持するように求めて良い

意見が対立した場合

  • 対面でのミーティングや電話会議
  • 三者を巻き込む

コードレビューの観点

コードレビューの観点も記載されている
コードレビューの観点 | google-eng-practices-ja

要約を引用する

コードレビューをする際には、次のことを確認する

  • コードがうまく設計されている
  • 機能性がコードのユーザーにとって適切である
  • UI の変更がある場合、よく考えられていて見た目も適切である
  • 並行処理がある場合、安全に行われている
  • コードが必要以上に複雑でない
  • 開発者は将来必要になるかもしれないものではなく、現在必要だとわかっているものを実装している
  • コードには適切なユニットテストがある
  • テストがうまく設計されている
  • 開発者はあらゆるものに明確な名前を使った
  • コメントは明確で有意義なもので、「何」ではなく「なぜ」を説明している
  • コードは適切にドキュメント化されている
  • コードはスタイルガイドに準拠している

レビューを依頼されたコードを一行ずつレビューすること、コンテキストを確認すること、コードの健康状態を改善しているかを見極めること、開発者が良いことをしたらそれを褒めることを忘れない

複数ファイルあって大変な時

複数ファイルあって大変な時の手順の要約
レビューで CL を閲覧する | google-eng-practices-ja

  • ステップ 1: 変更を広く眺める
  • ステップ 2: 変更の主要部分を調べる
    • 変更の「メイン」の部分になっているファイルを見つける
    • 変更があまりに巨大でどの部分が主要部分なのか判別つかない場合は開発者にどこを最初に見るべきか質問するか、変更を小さくしてもらう
  • ステップ 3: 変更箇所の残りを適切な順序で見る
    • 主要なファイルを確認し終えたら、普通はコードレビューツールが表示してくれる順序で各ファイルを調べると楽
    • 主要なコードを読む前にテストをまず読むのが効果的な場合もある

コードレビューのスピード

コードレビューのスピードについて
コードレビューのスピード | google-eng-practices-ja

  • コードレビューの依頼が来たらすぐに着手(最長の時間は一営業日)
  • コードを書くような集中的に取り組むべきタスクの最中には、自分のタスクを中断してコードレビューしてはいけない
  • 時間が取れなそうであれば全体のレビューが完了をする前に、部分的にでも素早いリアクションをとる(「このコードは私達の基準を満たしている」という意味だと言えるくらいに素早くレビューする)
  • 以下の場合は指摘を残しつつ承認しても良いが、どちらの意図なのかをはっきりさせる
    • 開発者がレビュアーの残したコメントに後で着実に取り組んでくれるとレビュアーが信頼できるとき
    • 先送りにした変更がさほど重要でなく、開発者本人が必ずしも行う必要のないとき
  • コードレビューの基準に妥協しない、スピードを上げてもコードの品質の改善に妥協しない

コメントの書き方

レビューコメントの書き方 | google-eng-practices-ja

上のページを要約すると

  • コメントは丁重に
  • 理由を説明する
  • 問題の指摘に加えて明確な方向性を示すことと、開発者本人に決定を委ねることをバランス良く行う
  • 複雑なコードを見つけたらそれを説明してもらうだけで終わらせず、コードをシンプルにしてもらうとかコードにコメントを追加するよう開発者に勧める

取り下げるか否か迷うとき

  • 開発者の提案が受け入れられない時、開発者のほうが正しいのではないかということを最初に少し考慮しておく
  • その指摘によってさらに作業が発生するものの、それに見合うだけコードの品質が改善すると見込まれれば、粘り強く変更を勧めるべき
  • 指摘によって開発者が傷つくのではないかと思う人もいるが、もし傷ついてもそれは一時的なことで、時が経つとコードの品質改善に感謝する
  • 開発者が仕事を完了させたいがために「後でやる」と言って指摘取り下げを要求してくることがあるが、大体はだんだんと忘れ去られるため、「今片付ける」ように促す
    • 問題を顕在化しているのに開発者がすぐにその問題に取り組めないときには、問題解決のためにバグを整理保存し、忘れないように自身をアサインし、TODOコメントを残す

コードレビュー中の取り下げに対応する | google-eng-practices-ja

【サイト紹介】v0というテキストプロンプトと画像からReactコンポーネントを作るサービス

Vercelのv0.devというサービスを知ったので調べた

v0.dev

v0とは

AIを活用したVercelのジェネレーディブUIシステム

単純なテキストプロンプトと画像から shadcn/ui を使用して UI(Reactコンポーネント) を生成するツール

早速やってみる

まず、入力フォームに「Date picker」と入力してみる

すると、画面が切り替わって数秒後に画面下に3つのUIが生成された

ちょっと3つの違いがわからなかったが、もう少しいい感じのプロンプトにするとわかるのかもしれない

右側には「Code」のボタンが付いていて、押すと以下の3ファイルが修正できるようになっている

  • component.jsx
  • styles.css
  • layout.jsx

スタイリングには tailwindを使っていた

Next.jsへの取り込みは、tailwindのインストールとv0のセットアップが必要

セットアップが完了したら、先ほどの「Code」ボタンをクリックしたときに表示される以下のようなコマンドを実行するともろもろインストールされる

$ npx v0 add 1RuL03GXGsX

自分はtailwindを使ったことないので導入には抵抗があるが、これならReactコンポーネントの雛形が簡単に作れるので、0->1フェーズにはいいのかもと思った

ちなみにログイン時のポップアップの 「同意する」をクリックするか、プロンプトに入力するか、製品を使用することでプレリリース契約に同意することになるので、使う場合はよく読んだほうがよさそう

参考

言葉でUIを生成する。VercelのジェネレーティブUI「v0」を触ってみた | DevelopersIO

【Vercel】EdgeレンダリングをNode.jsのレンダリングに戻したという話

VercelがEdgeレンダリングをNode.jsのレンダリングに戻したという話が話題になった

理由は

  • SSRするランタイムはDBのすぐ近くに置いた方が良かった(わかってたけど)
    • エッジにデータを置くソリューションもあるが、ほとんどのデータはエッジにレプリケートできない
    • Vercel自体はCloudflareと違ってエッジ上でデータを管理ソリューションがない
  • DBの近くで実行する場合だったらいいかというとそうでもなく、v0でテストしたらNode.js ランタイムを使用した方が起動が速くなった
    • Cloudflare Workersで使えるCPUのパフォーマンスが低いのかも
  • SSR + ストリーミング よりも TTFBが約 50%程度速くなった

以前に調べたことがあって、VercelのEdgeレンダリングは期待感があったのだが少し残念

uga-box.hatenablog.com

v0を知らなかったので後日調べる

【技術本まとめ】実践Next.jsを読んでのメモ - Server Action とパフォーマンス

takepepeさんの新しい本『実践Next.js』を読んだので、Server Action あたりのメモ

読んでいて思ったのは、噂通りApp Routerではキャッシュの仕組みや、設計が大切だと思った

キャッシュを使って以下に早くユーザーに情報を届けるかが重要であるが、キャッシュ更新漏れによって、意図しない画面が気づかず出てしまうことが往々にしてありそう

なので設計時からキャッシュを意識した図やドキュメントが必要だなと感じた

最後の10章は各キャッシュの挙動が整理されていてすごく勉強になるので、これを使って実プロジェクトでも整理していきたい

以下はメモ

  • 9.1 Server Action とは、これまでFormによるデータ更新の実装をAPI Clientを介したAPIの呼び出しによって行ってきたが、これをFormから直接関数を呼び出せるようにする仕組み
    • action属性に非同期関数を渡す仕組みは「Progressive Enhancement」と呼ぶ
    • これにより、API Clientが不要になるのと、ハイドレーションが完了を待たなくても実行できる利点がある
    • 使うためには"use server"ディレクティブをファイルの上位か、関数のスコープの先頭で宣言する必要がある
    • それを普通にClient Componentや Server Componentからimportして使う
  • 9.2 On-demand Revalidationとは任意のタイミングでキャッシュを無効化するプロセス
  • 9.4 useFormStateはFormの状態を保持するHookで、Client Componentでのみ使用可能
    • const [ state, formAction] = useFormSate(fn, initialFormState);のように定義し、formActionをformのaction属性に指定する
    • initialFormStateはFormState型と合わせて使うのが良い(FormStateのプロパティにはerrorMessageなどをもつ)
    • fnは第一引数を FormState | null 型にし、関数がFormState型に適合するオブジェクトを返すようにPromiseを型注釈する決まりがある
  • 9.6 Web APIリクエストが成功する前提で、先んじてUIを更新する手法をOptimistic Update(楽観的更新)と呼ぶ
    • const [ optimisticState, addOptimistic] = useOptimistic(state, updateFn);のように定義する
    • stateにはuseStateなどで保持した状態を、updateFnには楽観的に更新した状態をマージして返す関数を渡す
    • updateFnは、第一引数に更新前の状態と、第二引数に更新する値をとる
    • addOptimisticは楽観的更新をするための関数
  • 9.7 Formのバリデーションにはaction属性に指定するformDispatch関数とは別に、onSubmitイベントにも関数を渡してClientバリデーションを行うようにする
  • 9.8 RevalidateのrevalidateTagは、タグ名が抽象的であれば無効化が楽だが、更新の必要ないページにも影響してしまうためデータソースアクセス効率が良くない。その一方で、タグ名が具体的であれば無効化が手間だが、更新のあったページだけ無効化できるのでデータソースアクセス効率が良い
  • 10.1 コンポーネントが必要とするデータをそのコンポーネント自身で取得することを「コロケーション」と呼ぶ
  • 10.2 fetch関数が動的関数とみなされた場合でも、強制的にキャッシュしたいば場合はfetch(url, { cache: 'force-cache', ... })を指定するとできる
    動的なページで部分的にキャッシュしたい時に利用する
  • 10.3 getServerSessionでは動的Routeになるので、useSessionを使って静的Routeにする方法もある
  • 10.4 SSGでは一つの静的Routeに対し3種のファイル「html, meta, rsc」が出力されており、これらをFull Routeキャッシュと呼ぶ
    • SSG Routeを実装するにはgenerateStaticParams関数を使用する
    • generateStaticParamsは非同期関数での定義が可能なので、DBからとってきた値を使うこともできる
    • ?page=2のようにsearchParamsを参照すると「動的Route」になってしまうので/categories/flower/2のようにURLを設計する
    • SSG Routeで生成された画面は一定期間キャッシュされ、キャッシュ期間はfetchに渡すrevalidate変数で指定ができる(Time-based Revalidation)
      ※page.tsx内でexport const revalidate = 10;としても、同様にキャッシュ期間が指定できる
  • 10.6 next/Imageを使用する際、LCP要素にpriorityPropsを追加すると画像に特別な優先度をつけることができる
  • next.config.mjsremotePatternsに信頼できる画像ホストを指定するとセキュアである
  • 画像キャッシュ期間はnext.config.mjsminimumCacheTTLに指定する(デフォルトでは画像に変化がない限り永続的)
    • minimumCacheTTLとサーバーmax-ageのうち、大きい方が採用される(s-maxageとmax-age両方がある場合はs-maxageが優先される)
    • 開発環境では.next/cache/imagesフォルダを削除するとキャッシュを無効にできる
    • キャッシュ状態はx-nextjs-cacheレスポンスヘッダーを参照すると判断できる

【React】キャプチャフェーズのイベント

onClickCaptureを使えば、イベント伝搬のうちのキャプチャリングフェーズで実行される関数を指定できることを知った

ja.react.dev

キャプチャリングフェーズとは、ルート要素からクリックされた要素に向かってイベントが伝播し、それぞれの要素にイベントリスナ・イベントハンドラが登録されているかどうかを調べるフェーズで、登録されていれば実行される

ちなみに、キャプチャリングフェーズの他に、クリックされた要素に到達した時のターゲットフェーズ、クリックされた要素からルートに向かって伝搬するフェーズのバブリングフェーズがある

参考:React・JavaScriptのイベント伝播について今更ながらに理解したのでまとめる

つまり、onClickなどで指定したイベントハンドラを実行する前に実行したいイベントハンドラは、onClickCaptureで指定するのが良い

ちなみに、Reactではv16 -> v17の時に、ドキュメントルート要素からクリックされた要素に向かってイベントが伝播していたものから、ReactDOM.render(<App />, rootNode);のrootNodeに当たる要素からの伝搬に変わったみたい

イベントデリゲーションに関する変更