Skip to content

Add @amika/reviews — code review library with diffs/trees UI - #194

Draft
dbmikus wants to merge 14 commits into
mainfrom
js-reviews-package
Draft

Add @amika/reviews — code review library with diffs/trees UI#194
dbmikus wants to merge 14 commits into
mainfrom
js-reviews-package

Conversation

@dbmikus

@dbmikus dbmikus commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Bootstraps a pnpm workspace at the repo root (first JS package in amika) and adds @amika/reviews at js/reviews: a TypeScript/React library for reviewing code with .diff / .patch upload, file tree, per-file diff viewer, threaded comments at line / file / patch / series scope, deep linking, opt-in localStorage persistence, and a ReviewExportV1 JSON export.
  • Built on @pierre/diffs/react and @pierre/trees/react; state is plain React (useReducer + split contexts + useSyncExternalStore) — no Jotai. Same surface is also exported headlessly as @amika/reviews/headless.
  • Adds @amika/reviews-example at js/reviews-example: a real Vite + React SPA demonstrating the package with checked-in fixture patches and a Playwright E2E suite (12 scenarios covering load / comment / reply / file-comments / persistence / deep-link / export).
  • Plan is checked in at plans/2026-05-15-amika-reviews-package.md.

Built across 13 commits so reviewers can read it incrementally.

Test plan

  • pnpm install succeeds at the repo root
  • pnpm --filter @amika/reviews run typecheck is green
  • pnpm --filter @amika/reviews run lint is green
  • pnpm --filter @amika/reviews run formatcheck is green
  • pnpm --filter @amika/reviews run test — 84 vitest tests pass (reducer, store, parser, location serializer, annotations, plus RTL coverage of CommentForm / CommentThread / FileCommentPanel / ItemCommentPanel / SeriesCommentPanel / FileTreePanel / ReviewProvider hooks / CodeReview / UploadDropzone / ExportButton / CopyLinkButton)
  • pnpm --filter @amika/reviews-example run typecheck build succeeds
  • pnpm --filter @amika/reviews-example exec playwright install --with-deps chromium then pnpm --filter @amika/reviews-example run test:e2e passes (browsers need system libs — sandbox in this run did not have them; verified test file shape only)
  • Manual smoke: pnpm --filter @amika/reviews-example run dev, open http://localhost:5173/?fixtures=1, drop a patch, leave a comment at each scope, hit Export, check the JSON

Notable choices baked in

  • Plain React, not Jotai. Comment volume is small; diff renderer dominates render cost. Store layer is framework-agnostic so swapping in a different state library later is local.
  • Each uploaded .diff / .patch file = one ReviewItem (kind patch); the in-memory collection = the series. Documented in the README.
  • Five ReviewItem kinds map 1:1 to the five @pierre/diffs/react components (PatchDiff, MultiFileDiff, FileDiff, File, UnresolvedFile). Upload always produces patch; the other kinds are for programmatic callers.
  • Pierre packages render into shadow DOM and ship their own styling — our chrome (comment threads, sidebar, dropzone) is lightly styled via data-amika="..." selectors so hosts can replace or remove styles.css freely.
  • Deep linking uses flat search params (?item=...&file=...&line=...&side=... or ?comment=...). linkSync prop binds to window.location; pass an adapter for Next.js / React Router.
  • Stable item IDs default to a FNV-1a 12-hex-char content hash so shareable links survive reloads. Callers can override (e.g. with a commit SHA) by constructing ReviewItems directly.

dbmikus and others added 14 commits June 9, 2026 18:47
Plan for a TypeScript/React code review library at js/reviews.
Bootstraps a pnpm workspace at the repo root, mirrors conventions
from amika-mono/js/components, and breaks the work into 12 commits
covering parser, store, React UI, deep linking, an example SPA,
and a Playwright E2E suite.
Adds repo-root pnpm workspace (pnpm@10.18.2, packages: js/*) and the
first JS package, @amika/reviews, with conventions copied from
amika-mono/js/components: TS strict, ESLint flat config + typescript-eslint,
Prettier, Vitest with jsdom + @testing-library/jest-dom setup.

Empty src/index.ts, src/headless.ts, and src/styles.css will be filled
in subsequent commits. All four scripts (typecheck, lint, formatcheck,
test) pass on the empty package.
Introduces the public data model (ReviewItem, ParsedPatch, Hunk, etc.) and
two entry points for loading patches: loadFromFiles for upload events and
loadFromText for raw strings. parsePatch wraps parse-diff and normalizes
to a library-agnostic shape so we can swap parsers later.

Stable IDs come from a synchronous FNV-1a hash of the patch text
(util/hashItemId), so shareable deep-links survive reloads. Bumps
tsconfig target to ES2020 to allow BigInt literals.

11 unit tests over five fixtures (single, multi-file, rename, binary,
malformed) all pass.
Pure reducer covers add/edit/delete/reply/resolve, idempotency on
SET_RESOLVED, recursive descendant removal on DELETE_COMMENT, and
import/load resets selection. createReviewStore is framework-agnostic
(getState/subscribe + full ReviewAPI), takes injectable now() and
newCommentId() for deterministic tests, and accepts a pluggable
PersistenceAdapter (localStorage adapter included; key-scoped, ignores
quota/SSR errors, listens for storage events).

ReviewExportV1 is flat: items + comments arrays plus schemaVersion and
exportedAt. import() validates the shape before dispatching.

The package index now re-exports the full headless surface; @amika/reviews
and @amika/reviews/headless deliver the same TS API today (UI lands in
later commits).

Coverage: 35 tests across reducer, store, parser. All green.
ReviewProvider creates (or accepts) a store and exposes it via a single
context. State subscription is via useSyncExternalStore so re-renders are
driven directly by the store's listener set.

Hooks: useReview (the API, stable identity), useReviewState (full state),
usePatches, useComments (memoized by filter slices), useSelectedFile
(resolves selection against current items, returns null when stale).

Index re-exports the React surface alongside the headless API, so
consumers can `import { ReviewProvider, useReview } from "@amika/reviews"`
without pulling pierre deps until they reach for a UI component.

8 RTL tests cover stable identity, mount-only construction, reactive
updates, filtered comment queries, and stale-selection resolution. Total
43 tests across the package, all green.
FileTreePanel reads the unique set of file paths across all loaded items,
renders them via useFileTree, and routes selection back to the store (or
to an optional onSelectFile prop). Comment counts at file or line scope
appear as row decorations on each file.

Path collisions across items resolve to the most recently loaded item so
a single tree view stays coherent when patches in a series touch the same
file. (A future revision could namespace paths per item.)

Two RTL smoke tests confirm the panel mounts in both loaded and empty
states; deeper interaction lives in the Playwright suite since the
tree's interior is rendered inside a shadow root.
ItemView dispatches by ReviewItem kind to the right @pierre/diffs/react
component (PatchDiff, MultiFileDiff, FileDiff, File, UnresolvedFile),
forwarding line-scope comments as DiffLineAnnotation / LineAnnotation.
Side mapping: amika's "old" / "new" maps to pierre's "deletions" /
"additions" at the annotation boundary so the diff renderer puts comments
on the correct gutter.

CommentForm is a small headless composer (textarea + submit + cancel).
CommentThread renders root + replies with edit / delete / resolve actions
and an inline reply form. FileCommentPanel composes the thread + a new-
comment form scoped to a single file.

tsconfig wires Vitest cleanup so each component test gets a fresh DOM
(@testing-library/react auto-cleanup isn't detected under vitest).

57 tests pass: reducer, store, parser, annotations, plus full RTL
coverage of CommentForm, CommentThread, FileCommentPanel, FileTreePanel
mount, and ReviewProvider hooks.
Two sidebar panels mirroring FileCommentPanel at coarser scopes:
ItemCommentPanel for a single uploaded patch/commit, SeriesCommentPanel
for the entire review (all items together). Both reuse CommentThread for
reply/edit/delete/resolve.

Documents the convention in the panel chrome: one uploaded .diff/.patch
file is one ReviewItem ("patch comments"); the in-memory collection of
items is the series ("review comments").

4 new RTL tests; 61 total in the package.
CodeReview is the top-level composition: three-pane layout (file tree
left, diff middle, comment sidebar right) with a header bar holding the
upload dropzone and export button. Provides initialItems, persistKey,
author, store, and headerExtras props; default-selects the first file
when items arrive.

UploadDropzone accepts .diff / .patch files via drag-drop or native
picker and routes them through store.loadFromFiles. ExportButton
serializes ReviewExportV1 and triggers a browser download (or hands JSON
to a custom onExport handler).

styles.css ships minimal selectors against the data-amika attributes our
components emit; hosts can drop it in once or skip it entirely. Pierre
internals stay self-styled via their shadow DOM.

CodeReview tests stub @pierre/diffs/react at vi.mock level since jsdom
lacks CSSStyleSheet.replaceSync; full diff rendering is covered in the
Playwright suite (commit 11). 65 vitest tests still green.
ReviewLocation describes what's focused (none / item / file / line /
comment). Serializes to flat URL search params (?item=...&file=...
&line=...&side=...) or just ?comment=<id>; the comment form self-resolves
to its stored scope.

Store gains navigate(), getLocation(), locationToSearch/locationFromSearch,
and an onScrollRequest emitter so the UI can scroll the diff viewer to a
target line/comment. Comment-link navigation walks the comment's scope,
selects the right item+file, and fires the scroll request.

ReviewProvider gets a linkSync prop (boolean or {read, write, subscribe}
adapter). When true, the provider binds to window.location.search: hydrates
on mount, pushes on selection change, listens for popstate. Adapter form
lets Next.js / React Router callers route through their own history.

useReviewLocation hook + CopyLinkButton (writes baseHref + ?search to
clipboard) round out the public surface.

19 new tests (store.navigate, location serialize/parse round-trip,
CopyLinkButton). Total: 84 vitest tests, all green.
A real Vite + React app at js/reviews-example demonstrating the full
@amika/reviews surface. Wires <CodeReview> inside a <ReviewProvider> with
linkSync enabled and a small toolbar for loading fixture patches or
resetting state.

URL flags supported by the demo:
- ?persistKey=<name> — persist state to localStorage
- ?author=<name>     — default comment author
- ?fixtures=1        — auto-load fixtures on mount (used by Playwright)

Fixtures (simple.patch, multi-file.patch, rename.patch) live in
public/fixtures and are copies of the unit-test fixtures so behavior is
identical between layers.

`pnpm --filter @amika/reviews-example run typecheck build` succeeds.

Also surfaces the React API from @amika/reviews's main entry point — the
package now exports every component, hook, and provider directly (the
headless entry continues to expose only the framework-agnostic pieces).
Twelve scenarios covering the full flow: auto-loading fixtures, adding
comments at each scope, replying, file-level commenting, persistence
across reloads via persistKey, deep linking by item/file (linkSync
roundtrips through window.location), and Export JSON download with the
ReviewExportV1 schema check.

Tests target the data-amika="..." hooks the components emit so they
remain stable across CSS or layout tweaks and don't reach inside pierre's
shadow DOM.

Run via: pnpm --filter @amika/reviews-example run test:e2e

In environments where the chromium binary needs glib/libnss3/etc, run
`pnpm exec playwright install --with-deps chromium` (or install the system
packages: libglib2.0-0 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2
libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxrandr2 libgbm1
libasound2) before running. Browser launch was verified locally; not yet
verified in this build environment because system libs are unavailable.
README covers install, two quick-start flows (drop-in <CodeReview> vs.
composed primitives), the imperative loading API, the five ReviewItem
kinds and which pierre component each maps to, the public ReviewStore
TypeScript shape, comment scopes + replies, the ReviewExportV1 schema,
opt-in localStorage persistence, deep linking with linkSync + ReviewLocation,
headless usage from @amika/reviews/headless, styling/theming pointers
into pierre's CSS variables, and how to run the example SPA + Playwright
suite. Closes the package build-out: every commit from the plan is in.
- Add PatchNavigator to the example toolbar: ← label (N/M) → buttons
  that step through loaded items, auto-selecting the first file of each
  patch so the diff viewer shows content immediately.
- Thread the outer ReviewProvider store into CodeReview so the toolbar
  and diff viewer share a single store instead of operating on two
  independent ones.
- Fix ItemView to extract a single-file slice from multi-file patches
  before passing to PierrePatchDiff, which requires exactly 1 file diff.
  Uses the selectedPath when available, falls back to the first parsed
  file to avoid the race before useDefaultSelection fires.
- Extract patch subject from git-format Subject: header for navigator
  label; falls back to truncated content hash.
@dbmikus
dbmikus force-pushed the js-reviews-package branch from 523ac3d to c09770d Compare June 9, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant