Skip to content

Rebuild irodr as a React 19 SPA on Vite+ - #127

Merged
azu merged 18 commits into
masterfrom
claude/inspiring-pasteur-57jyid
Sep 23, 2026
Merged

azu merged 18 commits into
masterfrom
claude/inspiring-pasteur-57jyid

Conversation

@azu

@azu azu commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

概要

irodr を作り直しました。Vite+(vp)でビルドする、ブラウザだけで動く React 19 のアプリです。これまでの Create React App + Almin + Office UI Fabric の構成を置き換えています。LDR 風の画面とキー操作(j/k/s/a など)は変えていません。

Inoreader と GitHub Notifications は、どちらも同じ Source インターフェースを実装する対等な「ソース」になりました。UI と Reader(状態管理の中心)は、どのソースのフィードかを知らずに動きます。新しいソースを足すときも、src/app と src/ui を変える必要はありません。

コードは関数ベースで immutable な書き方に統一し、Lint で強制しています。主なルールは次の3つです。

  • エラー以外のクラスは使わず、createX() という関数で作る。
  • 状態は store に置き、変えるときは丸ごと差し替える。
  • 状態の変え方は純粋関数にする。

技術スタック

以前 (master) この PR
ビルド・開発 Create React App (react-scripts 5) Vite+ 1.0.0-rc.0 (vp dev / vp build / vp preview)
UI React 18 + Almin + Office UI Fabric 7 React 19.3(関数コンポーネントと hooks)+ React Compiler(Oxc 版)
スタイル CSS + Office UI Fabric StyleX 0.19。UI フレームワークは使っていない
Lint・整形 Prettier 2 + lint-staged Oxlint + Oxfmt + 型チェック(すべて vp check)
テスト Jest (react-scripts test) Vitest 5 (vp test) + Playwright
実行時の依存数 48 4 (react, react-dom, @stylexjs/stylex, markdown-it)
対応ブラウザ 明記なし Baseline Widely available(Lint で検査)
パッケージ管理 pnpm pnpm workspace。バージョンはすべて catalog に固定
  • React Compiler は、Oxc による Rust 移植版を使っています(oxc-transform-react、@vitejs/plugin-react の react({ compiler: true }))。Babel は使っていません。
  • Lint には次のルールを入れています。型情報を使うルールも有効です。
    • React Compiler のルール: Oxlint に組み込みのもの(react/purity、react/refs など)。コンパイラが扱えないコンポーネントがあれば vp check が失敗します。
    • StyleX のルール(@stylexjs/eslint-plugin、JS プラグイン)
    • Baseline のルール(eslint-plugin-baseline-js、JS プラグイン)
    • immutable の書き方を強制するルール(詳しくは下の「コードの書き方」)
  • 依存のバージョンは pnpm-workspace.yaml の catalog: に集めて、すべて固定しています。catalogMode: strict なので、catalog にないバージョンは入りません。
  • コマンドは package.json の scripts にまとめ、AGENTS.md にも書きました。人も AI も同じ手順で使えます。
    • pnpm run check: 整形・Lint・型チェック
    • pnpm run fix: 自動修正
    • pnpm test: 単体テスト
    • pnpm run test:e2e: 結合テスト

ベンチマーク(1.x との比較)

旧版(master のビルド)とこの PR のビルドを、同じ条件で Playwright(Chromium)から動かして比べました。

  • データは、偽の Inoreader API に 40 フィード × 20 記事を入れたものです。記事は引用ブロック・画像・表・コードを含む HTML です。
  • API の応答には一律 100ms の遅延を入れました。外部への通信(旧版が読み込む CDN のフォントやアイコン)は、両方とも遮断しています。
  • 各アプリを 7 回ずつ、交互に計測しました。CPU は等速と 4 倍遅い条件の 2 通りです。数値は中央値で、s・j はキーを 70 回押した中央値です。
  • 共有のクラウド環境での相対比較です。実機での絶対値ではありません。
項目 CPU 等速: 1.x → この PR CPU 4 倍遅い: 1.x → この PR
フィード一覧が出るまで 376 → 228 ms(−39%) 1,188 → 570 ms(−52%)
FCP(最初の描画) 268 → 116 ms(−57%) 1,016 → 384 ms(−62%)
メインスレッドを長く止めた時間の合計(TBT) 71 → 0 ms 736 → 131 ms(−82%)
s で次のフィードの記事が出るまで(先読み済み) 66 → 23 ms(−65%) 215 → 111 ms(−48%)
s(先読みなしの初回) 213 → 148 ms(−31%) 524 → 305 ms(−42%)
j で次の記事へスクロールするまで 10.6 → 9.8 ms(同程度) 21.6 → 20.4 ms(同程度)
30 フィード読んだあとの JS メモリ 9.7 → 5.3 MB(−46%) 9.8 → 5.2 MB(−47%)
DOM ノード / イベントリスナー 1,030 / 306 → 963 / 243 同じ
1.x この PR
JS(圧縮前) 1,302 kB 398 kB
JS(gzip 後) 397 kB 136 kB
CSS(gzip 後) 30.9 kB 2.7 kB
本番ビルドの時間(1 回) 27 秒 0.7 秒
  • j は中央値では同程度です。ただ、CPU を 4 倍遅くした条件の上位 10%(p90)は 27 → 35 ms で、この PR のほうが少し遅くなっています。
  • 計測したのは Babel 版の React Compiler のビルドです。Oxc 版に切り替えたあとのビルドは、メモ化された箇所の数(27)が同じで、違いはモジュールの並び順だけでした。

構成

src/
  sources/   ソース。source.ts が共通の約束 (Source, Feed, Item, capabilities, settings)
    inoreader/  OAuth 2.0、購読一覧、記事の読み込み、mark-all-as-read
    github/     Notifications API、リポジトリ単位の既読化、IndexedDB キャッシュ
  app/       Reader(React に依存しない)
    reader-model.ts  immutable な状態と、その変え方の純粋関数
    reader-view.ts   画面に渡す形への変換(純粋関数)
    reader.ts        createReader: 通信・先読み・既読化・自動更新などの副作用
  ui/        React コンポーネント、キーボードショートカット、User Script API
  lib/       小さな共通処理(store、IndexedDB、サニタイザー、キーバインド)
  main.tsx   ソース・Reader・UI をつなぐ唯一の場所
e2e/
  fake-api/  偽の Inoreader / GitHub API(単体テストと結合テストの両方で使う)
lib/
  oxlint-plugin-immutable/  immutable の書き方を強制する Lint ルール(pnpm workspace のパッケージ)
  • 依存の向きは ui → app → sources → lib の一方向だけです。
  • ソースごとの違いは、各ソースの中で吸収しています。
    • 記事の読み込み方: Inoreader はフィードごとに必要なときに取得し、GitHub は未読の受信箱全体をキャッシュします。
    • 既読にする API も、ソースごとに違います。
  • 画面側には、次の2つだけを見せています。
    • capabilities (loadMore / unreadFilter / liveItems)
    • データで書いた設定フォーム (SourceSettings)。折りたたむ任意の項目や、外部リンクもデータで表現できます。
  • ソースの追加手順は docs/architecture.md の "Adding a source" に、各ソースの挙動は docs/source-adapters.md に書きました。

コードの書き方(関数ベース・immutable)

詳しくは docs/architecture.md の "Code style: functional and immutable" に書きました。

  • クラスは使わない: extends Error だけは、instanceof による判定とスタックトレースのために残しています。
  • 関数で作る: 振る舞いを持つモジュールは createX(options) で作ります。返すのはアロー関数を並べたオブジェクトなので、this はありません。
  • 状態は immutable にする: 状態は store に置き、変えるときは新しい値を作って丸ごと差し替えます。
  • 状態の変え方は純粋関数にする: (state, input) => state の形です。ソースやタイマーなしで単体テストできます。
  • 副作用は閉じ込める: 通信とタイマーは、factory 関数のクロージャ(関数が内側に保持する変数)の中だけで扱います。
  • Lint で強制する: lib/oxlint-plugin-immutable と Oxlint の組み込みルールで、次を禁止しています。
    • let
    • var
    • 引数への再代入
    • Object.assign
    • エラー以外のクラス
  • 対応ブラウザは Baseline Widely available: 主要ブラウザ(Chrome・Edge・Firefox・Safari)すべてで 2.5 年以上使えている機能だけを使います。
    • ビルドは Vite の既定の baseline-widely-available を対象にしています。
    • ただし、足りない組み込み関数を補う仕組み(polyfill)はありません。そのため、eslint-plugin-baseline-js が web-features のデータで組み込み関数と Web API を検査します。
    • 例外は Translator API だけです。Baseline ではありませんが、存在を確かめてから使う任意の機能なので許可しています。

1.x から引き継いだもの(互換性)

  • Inoreader のログイン状態 (localStorage["inoreader-token"])
  • GitHub の token とキャッシュ。IndexedDB の irodr-sources と irodr-source-credentials で、1.x の localforage と同じ形式です。
  • localStorage による設定の上書き (REACT_APP_CORS_PROXY, REACT_APP_INOREADER_BASE_URL)
  • User Script API
    • window.userScript と userscript-init イベント
    • SubscriptionContent::* イベント
    • registerKey が既定のキー割り当てを置き換える挙動
  • ユーザースクリプトが使うクラス名 (.SubscriptionContentsContainer-*, .SubscriptionListContainer-item, .ng-content)
  • 1.x で保存した設定。IndexedDB の AppRepository から、初回起動時に引き継ぎます。
  • 記事 ID の形式
  • 記事本文の見た目。1.x と同じくブラウザの既定のスタイルで描きます(引用ブロック・表・コードなど)。同じ記事で計算後のスタイルを比べて合わせました。
  • Netlify Edge Function の CORS プロキシ。変更していません。

挙動が変わったところ

  • 未読フィルタ
    • 1.x: 最終更新時刻より新しい記事を、未読数の件数だけ表示していました。
    • この PR: 読み込んだ時点で未読だった記事を、記事ごとに判定して表示します。既読のフィードを開き直したときは全件を表示します。
  • 既読にする範囲: フィードを離れると、読み込んだ中で最新の記事の時刻までを既読にします。
    • その後に届いた記事は未読のまま残ります。
    • 何も読み込んでいなければ、何も既読にしません。
  • 複数カテゴリのフィード: 1.x はすべてのカテゴリに重複して表示していました。この PR では最初のカテゴリにだけ表示します。
  • 自動更新
    • 1.x: 「Enable Auto Refresh Subscription」の設定が効いていませんでした。間隔を変えても、リロードするまで反映されませんでした。
    • この PR: 設定どおりに動き、変更はすぐ反映されます。
  • s の連打: 読み込み中に押すと、読み込み中のフィードからさらに先へ進みます。読み込み中のフィードは点線の枠で示します。
  • エラーメッセージ: 5 秒間は、通常のメッセージで上書きされません。
  • ショートカット: 物理キーで判定するので、英字以外のキーボード配列でも動きます。
  • Inoreader の認証
    • トークン更新のときにサーバー障害 (5xx) が起きても、ログアウトしません。
    • 別のタブが更新したトークンを再利用します。
  • Sources 画面
    • 自分の Inoreader アプリの Client ID / Secret は「Use your own Inoreader app」の中に折りたたみました。値を保存しているときは開いて表示します。
    • GitHub のトークン作成画面へのリンクを置きました。notifications スコープ(非公開リポジトリ用には repo も)を選んだ状態で開きます。
  • service worker: 1.x で登録を止めていたコードを削除しました。

テスト

  • 単体テスト 174 件 (vp test)
    • Reader: 状態の変え方の純粋関数、画面向けに変換した結果のオブジェクトが同じものかどうか、メモリ上のソースを使った挙動
    • Inoreader / GitHub のソース: 偽の API サーバーに対して実行
    • キー割り当て、設定、HTML 処理、store、emitter、Lint プラグイン
  • 結合テスト 35 件 (pnpm run test:e2e)
    • Playwright で動かします。vp build --mode e2e の成果物を vp preview で配信し、偽の Inoreader / GitHub API につなぎます。
    • 画面を操作して確かめている内容:
      • OAuth ログイン、キー操作、既読化、先読み
      • エラー時の挙動、設定、ユーザースクリプト API
      • s で移動したときに、現在のフィードがカテゴリ見出しの裏に隠れないこと
      • Sources 画面の折りたたみと、トークン作成画面へのリンク
  • 偽の API サーバー (e2e/fake-api/): 実際の API の挙動を真似ています。
    • Inoreader: OAuth、購読一覧、未読数、続きの読み込み (continuation)、mark-all-as-read
    • GitHub: Link ヘッダーでのページ送り、X-Poll-Interval、既読化の 205 / 202 応答
  • CI (.github/workflows/test.yml): 「Check, unit test and build」と「Integration tests」の 2 つのジョブで動きます。
    • 結合テストは Playwright の Docker イメージ(mcr.microsoft.com/playwright:v<バージョン>-noble)の中で動かします。
    • イメージのバージョンは、catalog の @playwright/test から読み出します。
  • pre-commit (.vite-hooks/pre-commit): ステージしたファイルを vp check --fix / vp fmt で直し、単体テストを実行します。pnpm install のときに vp config が入れます。

確認済みのこと

  • ✅ Netlify のデプロイプレビュー: https://deploy-preview-127--irodr.netlify.app/ で、次の2点を確かめました。
    • 新しいビルドが配信されていること
    • CORS プロキシ (/cors-proxy/) が Inoreader まで中継すること。認証なしのリクエストに、Inoreader へ直接送ったときと同じ 403 が返りました。
    • 出力先は build から dist に、ビルドコマンドは pnpm run build に変えています。
  • ✅ 本物の Inoreader / GitHub API での動作: ログインと既読化を確認しました。自動テストは偽の API サーバーに対して動かしています。
  • ✅ 開発サーバー (http://localhost:8888/) での OAuth ログイン: 既定の Inoreader アプリで確認しました。リダイレクト先は、1.x の開発時と同じ http://localhost:8888/ です。

Vite+ はまだリリース候補版 (1.0.0-rc.0) です。React Compiler の Oxc 版(oxc-transform-react)も、Oxc のドキュメントでは experimental の扱いです。

🤖 Generated with Claude Code

https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E

Rewrite the app from Create React App + Almin + Office UI Fabric to a small,
modern client-side stack:

- Vite+ (vp) for dev, build, test (Vitest), lint (Oxlint), format (Oxfmt),
  type checking, staged-file checks and the pre-commit hook
- React 19 function components and hooks; React Compiler with
  panicThreshold "all_errors" plus the React Compiler lint rules
- StyleX for styles, with @stylexjs/eslint-plugin rules in Oxlint
- Runtime dependencies reduced to react, react-dom, @stylexjs/stylex and
  markdown-it (removed almin, office-ui-fabric-react, localforage, lodash,
  date-fns, sanitize-html, combokeys, client-oauth2, and others)

Architecture: Inoreader and GitHub Notifications are peers behind one
`Source` contract (src/sources/source.ts). The framework-independent reader
(src/app/reader.ts) implements LDR navigation, auto mark-read on departure
through the newest loaded item, Shift+S skip, prefetch and filtering, and
the UI never branches on a specific source. Settings forms are described as
data so new sources need no UI changes.

Compatibility: the Inoreader token in localStorage, the GitHub cache and
token in IndexedDB, the localStorage CORS overrides, the keyboard shortcuts
and the window.userScript API keep working.

Tests: unit tests for the reader and both sources, and Playwright
integration tests that drive the production build against fake Inoreader
and GitHub APIs (e2e/fake-api). CI runs check, unit tests, build and the
integration tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
The shortcut guard checked the reader's panel state, which is updated by
the dialog's `close` event. A key pressed right after Escape could be
dropped on slower machines. Check the DOM for an open dialog instead, and
close the panel on `cancel` as well.

Also wait for the token refresh request in the e2e test instead of the
header message, which could already be shown before Refresh was clicked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
Copilot AI lite review requested due to automatic review settings September 23, 2026 02:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- Inoreader: keep the session when the token endpoint fails temporarily
  (5xx, proxy errors); only a rejected grant disconnects. Reuse a token
  another tab already refreshed instead of deleting it.
- Inoreader: mark nothing read when no item was loaded; guard missing
  `published` so an item cannot break rendering with an invalid date.
- Reader: ignore an older load that finishes after a newer revision,
  bound the item cache (LRU, 100 feeds), build category lists in O(n),
  and drop the unused view error field.
- GitHub: apply the "already read" filter inside the serialized cache
  write, so a sync cannot bring back notifications marked read meanwhile;
  group items in O(n).
- Keyboard: match the physical key on non-Latin layouts, and make
  userScript.registerKey replace default bindings like Combokeys did.
- Carry over irodr 1.x preferences from IndexedDB `AppRepository`.
- Refresh the relative-time clock when it restarts; show the action's own
  error in Sources; use StyleX longhands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
- Reader: keep a loaded feed when a newer load of it is still running,
  refresh the view after prefetch, show a source error only when this
  mark-read caused it, and show a feed the source dropped with 0 unread
  in both the list and the view.
- Inoreader: the local "read through" override expires after 5 minutes
  and is dropped once Inoreader reports 0, so items marked unread
  elsewhere show up again.
- GitHub: base last_read_at only on notifications with GitHub's own
  updated_at; keep repository order in a Set.
- IndexedDB store: resolve writes when the transaction commits and
  reopen a connection the browser closed.
- UI: close dialogs only on clicks that start and end on the backdrop,
  destroy a translator still being created when translate mode turns
  off, dispatch componentDidUpdate only on item or focus changes, let
  preference number fields be cleared while typing, set lazy loading
  before an image's src, and name the feed list class in CLASS.
- Share shallowEqual from src/lib/equal.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
@azu azu changed the title Rewrite irodr as a modern React 19 SPA with Vite Rebuild irodr as a React 19 SPA on Vite+ Sep 23, 2026
- Add a local Oxlint JS plugin, lib/oxlint-plugin-immutable (a pnpm
  workspace package), whose immutable/no-let rule bans `let`. Oxlint
  has no no-restricted-syntax, and prefer-const only flags a `let`
  that is never reassigned.
- Turn on no-var, no-param-reassign, and no-restricted-properties for
  Object.assign.
- Rewrite the existing code to satisfy them. Behavior is unchanged,
  verified by the unit and Playwright tests:
  - reassigned locals become const expressions or small helpers
  - counter loops become for...of over arrays and generators
  - state that changes over time lives in const holder objects
  - window.irodr gets a typed declaration instead of Object.assign
  - Translator is declared with `declare const`

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
Stage A of replacing classes with functional, immutable modules
(conventions in docs/architecture.md, "Code style: functional and
immutable").

- createStore, createEmitter, createKeyBindings and createTranslateMode
  replace the Store, Emitter, KeyBindings and TranslateMode classes,
  with the same semantics. New unit tests cover the store and emitter.
- createFakeInoreader and createFakeGitHub replace the fake API
  classes. Tests change them through configure(), markUnread() and
  reset() instead of assigning fields, and the request log is an
  immutable list returned by log(). Control endpoints validate their
  JSON bodies. HTTP responses are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
Stage B and C of replacing classes with functional, immutable modules.

- Reader: createReader replaces the class. The reading state is an
  immutable ReaderModel with pure transitions (reader-model.ts), and
  a pure projection builds ReaderState while keeping the identities of
  unchanged parts (reader-view.ts). reader.ts keeps the side effects:
  loads, prefetch, mark-read and auto refresh. The public API is
  unchanged. A comparison of 30 recorded scenarios (rapid s/a, racing
  opens, failures, dropped feeds) against the old class matched
  exactly.
- GitHub: createGitHubApi, createGitHubCache and createGitHubSource.
  Cache writes are pure (snapshot) => snapshot functions, run under the
  Web Lock after reloading the stored snapshot.
- Inoreader: createInoreaderOAuth and createInoreaderSource, with the
  feed projection and read-through overrides as pure helpers.
- Test doubles are factories too, and the new pure transitions have
  unit tests (170 unit tests now).
- Lint: immutable/no-class bans classes except errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
Moving with `s` scrolled the current feed to the top of the sidebar,
under the sticky category header, so it was hidden. The feed list now
has a scroll-padding-top of the header height (a new
sizes.categoryHeader token). The header stays on one line with an
ellipsis, so a long folder name can no longer make it taller than
that padding.

A Playwright test moves through 30 feeds in a short window with a long
folder name. At each step it checks that nothing covers the current
row. The test fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
The functional rewrite used Map.groupBy and toSorted in the feed list
projection and the GitHub source. build.target lowers syntax but adds
no polyfills. The reader therefore threw at startup, and showed a blank
page, in browsers that ran the previous code: Safari/iOS below 17.4,
Chrome below 117 and Firefox below 119. Both places now group and sort
with ES2022 built-ins and give the same output.

To keep it that way, tsconfig.app.json uses the ES2022 lib, and the
regression tests run startup code under withoutNewerBuiltIns, which
removes the newer built-ins. A final review compared the old and new
reader with about 8,800 random operation sequences and found no other
difference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
When Inoreader listed a subscription twice, both rows had the same feed
ID. Both showed as current, and `s` moved from the feed to its own copy
and stopped. irodr 1.x kept feeds in a repository keyed by ID, so the
copy disappeared there.

The Inoreader source now keeps the first entry for each stream ID,
which gives the first place and category. The reader drops repeated feed
IDs as well, so a source that lists a feed twice cannot stop
navigation. uniqueBy (src/lib/unique.ts) keeps the first item per key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
Browser support is now Baseline Widely available: features that
Chrome, Edge, Firefox and Safari have supported for at least 2.5
years. This replaces the ES2022 restriction from the previous fix.

- Build: use Vite's default target, baseline-widely-available, instead
  of es2024.
- Lint: eslint-plugin-baseline-js runs as an Oxlint JS plugin on src/
  (use-baseline, available: "widely", Web APIs and JS built-ins). It
  checks built-ins and Web APIs against web-features data, because
  nothing polyfills them. Tests and tools run in Node and are exempt.
- Map.groupBy and toSorted are Baseline Widely available (since
  2026-09-05 and 2026-01-04), so the feed list and the GitHub projection
  use them again, with the ES2024 lib. The ES2022 test helper is
  removed.
- Top-level await is only Baseline Newly available (Safari 27), so
  main.tsx starts through an async function.
- The Translator API is not Baseline. It is feature-detected in one
  helper, which is exempt from the rule, and translation stays optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
Empty Client ID and secret inputs looked like missing input, although
leaving them empty uses the default app. SourceSettings gains an
optional `advanced` group that the Sources dialog renders as a
collapsed <details>. The group is expanded when one of its fields has a
saved value. The Inoreader client fields move into "Use your own
Inoreader app".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
Blockquotes looked different from irodr 1.x. The rebuild had added its
own blockquote (left border, gray text, no indent), table, and code
styles, and used a 15px system-ui font. irodr 1.x rendered article HTML
with the browser defaults. Compared through computed styles of the same
content in both builds, article content now matches 1.x again:

- sans-serif at the `medium` keyword size (16px). The keyword keeps the
  default 13px for monospace text in code and pre.
- Browser-default blockquotes, tables and lists; 1.x's pre (line-height
  1).
- 24px titles, and title and "Original" links in 1.x's Fabric color
  (#0078d4) and font. The meta line is 16px and the footer 14px.
- The footer has 1.x's SubscriptionContentsContainer-contentFooter class
  again, for user scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
@azu azu added the Type: Refactoring A code change that neither fixes a bug nor adds a feature label Sep 23, 2026
…image

- All dependency versions live in the default catalog of
  pnpm-workspace.yaml, pinned to the installed versions. package.json
  refers to them with "catalog:", and catalogMode: strict rejects
  versions outside the catalog. The Vite+ overrides (the vite alias and
  vitest) use the catalog too.
- The e2e job runs in mcr.microsoft.com/playwright:v<version>-noble
  (--init --ipc=host --user=1001), which ships the browsers and their
  system dependencies, so CI no longer runs `playwright install
  --with-deps` on every run. A small job reads @playwright/test from the
  catalog with yq to pick the image, and fails if the version is not
  exact. Approach from https://tech.newmo.me/entry/playwright-ci-optimization

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
The Sources dialog links to GitHub's classic token form with the
notifications scope selected, and with notifications and repo for
private release bodies. SourceSettings gains `links` for such external
pages. The form URL comes from the configured GitHub web URL, so GitHub
Enterprise Server works too. GitHub documents pre-filled forms only for
fine-grained tokens, and the Notifications API does not accept those.
This classic URL is the one Composer builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
The React Compiler now runs through Oxc's Rust port: react({ compiler:
true }) in @vitejs/plugin-react with oxc-transform-react. Babel and
babel-plugin-react-compiler are no longer used. Oxlint's built-in React
Compiler rules (react/purity, react/refs, react/immutability, ...)
replace the eslint-plugin-react-hooks JS plugin. They reject components
the compiler would skip, which the Babel build's panicThreshold did
before. Oxlint has no rules for the compiler's config and gating
options, and this setup uses neither.

Removed: @babel/core, @rolldown/plugin-babel,
babel-plugin-react-compiler, eslint-plugin-react-hooks.
Added: oxc-transform-react.

The bundle keeps the same 27 memoized scopes. It differs from the Babel
build only in module order, and the production build takes 0.7 s
instead of 1.7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFAqs7EHEVBKFYc8cT6N6E
@azu
azu merged commit 4c00424 into master Sep 23, 2026
10 checks passed
@azu
azu deleted the claude/inspiring-pasteur-57jyid branch September 23, 2026 07:14
berlysia added a commit to berlysia/dotfiles that referenced this pull request Sep 27, 2026
Add a skill that defines how PR titles and bodies are written: a catalog
of sections with the condition for writing each, a size guideline, how to
state what was verified and by what means, and how to pick the language.
Point the always-loaded rules and /create-pr at it.

intent: make PR bodies answer the reviewer's questions with information the diff does not show, following azu/irodr#127
decision: skill holds the norms; rules get a one-line pointer so conversational "open a PR" requests also reach it
decision: only overview and verification sections are mandatory; other sections are written when their condition holds
decision: rename "Git Commit Standards" to "Git Commit & PR Standards" so the pointer is found by heading
rejected: per-repository pull_request_template.md - cannot reach other people's repositories and cannot vary sections by PR size
rejected: a mechanical hook checking PR bodies - only heading presence is machine-checkable, which does not measure whether the reviewer's questions are answered
learned: two runs on the same diff disagreed on optional sections until the catalog said where borderline items go; residual optional-section variance remains and is accepted
constraint: skill discovery from the rules line was verified with two sonnet runs that both invoked the skill without being told its name

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
berlysia added a commit to berlysia/dotfiles that referenced this pull request Sep 27, 2026
Two runs over the same diff chose different optional sections, and the
run that skipped a section also dropped the design reasons that would
have gone there. Unverified items almost never reached 確認したこと.

The skill now has the writer list the facts first (from every commit
message, its action lines, and the diff), assign each fact to one
section, and match every claimed behavior change against how it was
observed. A section appears only when a fact is assigned to it. Changes
seen only through automated tests are listed as 未確認 under
確認したこと. The テスト section is replaced by テスト基盤 (only for
changes to the test setup itself); added tests become a short note on
the verification line.

decision: measure by whether fixed required facts appear, not by whether headings match
decision: remove the テスト section; three rewordings of its rule could not stop test case enumeration, removing the place to put it did
rejected: making section conditions diff-derivable thresholds (keeps headings stable but not the reasons, and loses judgement like azu/irodr#127)
learned: over 4 runs per version on fb6f9e1 and dd44d27, all required facts 0-1/4 -> 4/4, 未確認 under 確認したこと 0/4 -> 4/4, no test enumeration 0/4 -> 3/4 (blind sonnet judge, quotes checked by hand)
constraint: both test commits have rich action lines; commits without them and small PRs were not measured

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCRFWZisrdLbxod73opuCA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Refactoring A code change that neither fixes a bug nor adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants