Skip to content

Latest commit

 

History

History
102 lines (77 loc) · 6.6 KB

File metadata and controls

102 lines (77 loc) · 6.6 KB

General Rules

  • MUST: Use TypeScript interfaces over types.
  • MUST: Keep all types in the global scope.
  • MUST: Use arrow functions over function declarations.
  • MUST: Default to NO comments. Only add a comment when the user explicitly asks, or when the "why" is truly non-obvious - browser quirks, platform bugs, performance tradeoffs, fragile internal patching, or counter-intuitive design decisions. Never add comments that restate what the code does or what a well-named function/variable already conveys. When in doubt, leave the comment out.
    • If a hack is required (like a setTimeout that hides a race), prefix with // HACK: reason for hack.
    • Do not delete descriptive comments >3 lines without confirming with the user.
  • MUST: Use kebab-case for files.
  • MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names).
    • Example: for .map(), use innerNode instead of n.
    • Example: instead of moved use didPositionChange.
  • MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive.
  • MUST: Do not type cast (as) unless absolutely necessary.
  • MUST: Remove unused code and don't repeat yourself.
  • MUST: Always search the codebase, think of many solutions, then implement the most elegant solution.
  • MUST: Put all magic numbers in constants.ts using SCREAMING_SNAKE_CASE with unit suffixes (_MS, _PX).
  • MUST: Put small, focused utility functions in utils/ with one utility per file.
  • MUST: Use Boolean(x) over !!x.

Preact + @preact/signals Rules

React Scan's overlay UI runs as a Preact app mounted inside a Shadow DOM (see packages/scan/src/core/index.ts). Reactivity is driven by @preact/signals, not React state.

Signals

  • MUST: Read signals via .value inside JSX/effects: signalWidget.value not signalWidget().
  • MUST: Write signals atomically: signalWidget.value = { ...signalWidget.value, dimensions: ... }. One signal per logical slice.
  • MUST: Use useSignal(initial) for component-local reactive state, useComputed(() => ...) for derived values, useSignalEffect(() => ...) for subscriptions/DOM imperative work.
  • MUST: Wrap module-level signals exported from a tree-shaken bundle with /* @__PURE__ */ signal(...) so production builds can drop unused state.
  • SHOULD: Persist user-facing state via readLocalStorage/saveLocalStorage from ~web/utils/helpers and seed the signal with the persisted value.
  • NEVER: Mirror one signal into another inside useSignalEffect - find the single source of truth (compute on read).

Effects

Before reaching for useEffect/useSignalEffect, classify the work:

  • MUST: Use useComputed when the result is pure derived state from other signals. If no external system is touched, it is not an effect.
  • MUST: Use event handlers and direct action calls when work happens because the user clicked, dragged, or navigated. Do not watch a flag in an effect to trigger imperative logic.
  • MUST: Use useEffect(..., []) (or useSignalEffect with no deps) for one-time mount/cleanup of subscriptions, timers, listeners, and MutationObservers. Always return the cleanup.
  • MUST: Keep each effect single-purpose - one effect, one external bridge. Split mixed-responsibility effects.
  • NEVER: Use an effect just to copy one signal into another.
  • NEVER: Use an effect as an event bus (watching a trigger signal to run a command). Call the action directly from the event source.

Props

  • MUST: Access props via props.title, not destructuring, when the prop is read inside an event handler that fires later or inside an effect.
  • SHOULD: Destructure props at the top of the body for purely-rendered values (read once during render).
  • NEVER: Destructure a prop that is itself a signal-driven slice if you intend to re-read it after async work.

JSX & DOM

  • MUST: Use className (we render via Preact's React-compat JSX in tsconfig.json jsxImportSource: preact).
  • MUST: Combine static className="btn" with reactive className={cn("btn", isActive.value && "active")} via cn.
  • MUST: Read refs in useEffect or via the callback ref pattern - DOM refs are populated after render.
  • MUST: Mount overlay UI under the existing shadow root from initRootContainer() in core/index.ts. Do not append directly to document.body.
  • SHOULD: Use style={{ "--css-var": value }} for dynamic CSS variables; class toggles for boolean states.
  • SHOULD: Type refs as let element: HTMLElement | null = null with a guard.

Build & Toolchain

This is a pnpm 10 monorepo with packages/* (libraries, extension, website) and a top-level kitchen-sink/ (Playwright target). The toolchain is Vite+ (vp lint, vp fmt, vp check) and turbo for pipeline orchestration.

Approved built dependencies

The root package.json declares pnpm.onlyBuiltDependencies for @parcel/watcher, esbuild, sharp, spawn-sync, unrs-resolver. Without this list, pnpm install skips their native build steps and downstream packages fail.

Build before test

pnpm build must complete before pnpm test, pnpm test:e2e, or pnpm lint. After modifying source files, always rebuild before running tests. Turbo enforces this via dependsOn: ["^build"] in turbo.json.

Playwright

pnpm test:e2e runs Playwright against the kitchen-sink Vite dev server on port 5173 (auto-started by playwright.config.ts). Chromium must be installed: npx playwright install chromium --with-deps.

Key commands

Task Command
Install pnpm install
Build pnpm build
Dev watch pnpm dev (watches react-scan + kitchen-sink)
Unit tests pnpm test
E2E tests pnpm test:e2e
Lint pnpm lint (oxlint via vite-plus)
Lint + fix pnpm lint:fix
Format pnpm format (oxfmt via vite-plus)
Format check pnpm format:check
Typecheck pnpm typecheck
Combined pnpm check (lint + fmt check + typecheck)

Testing

Run checks always before committing with:

pnpm build
pnpm lint
pnpm format
pnpm typecheck
pnpm test:e2e