103 lines
6.6 KiB
Markdown
103 lines
6.6 KiB
Markdown
## 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`](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`](packages/scan/src/web/utils/helpers.ts) 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 `MutationObserver`s. 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`](tsconfig.json) `jsxImportSource: preact`).
|
|
- MUST: Combine static `className="btn"` with reactive `className={cn("btn", isActive.value && "active")}` via [`cn`](packages/scan/src/web/utils/helpers.ts).
|
|
- 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`](packages/scan/src/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+](https://viteplus.dev) (`vp lint`, `vp fmt`, `vp check`) and `turbo` for pipeline orchestration.
|
|
|
|
### Approved built dependencies
|
|
|
|
The root [`package.json`](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`](turbo.json).
|
|
|
|
### Playwright
|
|
|
|
`pnpm test:e2e` runs Playwright against the `kitchen-sink` Vite dev server on port 5173 (auto-started by [`playwright.config.ts`](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:
|
|
|
|
```bash
|
|
pnpm build
|
|
pnpm lint
|
|
pnpm format
|
|
pnpm typecheck
|
|
pnpm test:e2e
|
|
```
|