# Tooling web reference

> Extends: `base`. Apply base stack first.

## Overview

The web layer covers web-universal tooling shared across Vite + React, Astro, and any future web stack. It ships golden configs for ESLint, Vitest, Playwright, Tailwind v4, screenshots, VS Code integration, and CI. Framework-specific wiring (Vite plugins, Astro islands, Next.js config) lives in per-stack adapter folders that extend this layer.

## What ships as golden configs

Golden config files live in `tooling/web/configs/` and are copied into the target on `aitk tooling sync web . --write`, replacing whatever sits at those paths. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.

- `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
- `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, a route, and its own viewport, and the loop below writes `screenshots/<section>/<theme>.png`. Per-project cases extend the one record. A route's themes sit together under its section folder, so the filename carries the theme alone.
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
- `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
- `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
- `scripts/worktree-port.sh`: prints a base port plus this working directory's offset. Called with no argument it prints the offset alone. It refuses a folder left under the worktrees directory after its worktree was removed, rather than printing a port for it.

## What stays in per-stack adapters

Framework glue lives in `tooling/vite-react/configs/` or `tooling/astro/configs/` because the merge helpers and config shapes differ:

- `vite.config.ts` or `astro.config.mjs`: the framework's config.
- `vitest.config.ts`: uses `mergeConfig` in Vite stacks, `getViteConfig` from `astro/config` in Astro.
- `playwright.config.ts`: `webServer` command differs per stack.
- `tsconfig.json`: `extends` target differs. Astro uses `astro/tsconfigs/strict`. Vite projects use scaffold defaults.

## File layout

- `src/` for app code, `e2e/` for Playwright, `scripts/` for shell, `src/test/setup.ts` for Vitest globals.
- Path alias `@` maps to `./src` in both tsconfig and the framework's build config.
- Tsconfig is unified at root with `noEmit: true` in Vite stacks. Astro uses the scaffold default from `@astrojs/check`.

## Ports

Two worktrees of one repository run the same stack, so a fixed port makes the second one attach to the first.

- Derive every served port from `scripts/worktree-port.sh`. Never write a port literal into a script string.
- Read `WORKTREE_PORT_OFFSET` in a config and add it to the stack's default port. Unset yields the default, so a plain clone keeps the port it has always served on.
- Draw the offset from a band of 50, hashed from the worktree folder name. Two worktrees can hash to one offset, so set `WORKTREE_PORT_OFFSET` by hand to break a tie.
- Expect a non-zero exit and no port from a folder left under `.claude/worktrees/` once its worktree is gone. Git reports the parent repository from inside one, so the helper cannot read it as a worktree and every base it serves would land on the main checkout's port. Both shapes refuse, and they reach differently. A folder whose own `.git` was deleted refuses only under that directory, since location is the only thing separating it from an ordinary subdirectory the base port is correct for. A folder whose `.git` names a pruned administrative directory refuses wherever it sits, because a pointer to nothing is broken regardless of where the folder is.
- Call it as `VAR=$(bash scripts/worktree-port.sh) && export VAR && <server>`, never as the shorter `VAR=$(bash scripts/worktree-port.sh) <server>`. An assignment prefix discards the exit status of its own substitution, so the shorter form starts the server with `VAR` set to the empty string, every config reads that back as an offset of zero, and the refusal lands on the port it was raised to protect. The assignment alone carries the status, which is what the `&&` reads.
- Set `WORKTREE_PORT_OFFSET` by hand to serve from such a folder anyway. That is the one override, and it is checked before any directory test.
- Force-replace `dev` and `preview` through `[scripts.override]`. Both stacks' scaffolds define those keys, and a plain `[scripts]` entry never replaces a key the scaffold already wrote.
- Set `strictPort` on every dev and preview server. A server that walks to the next free port serves where nothing is looking for it.
- Set Playwright `reuseExistingServer: false`. Reuse attaches to whatever answers on the port, which reports a pass against another branch's code and prints nothing to say so.

## Anti-patterns

Sticky negative knowledge. Do not relearn.

- Do NOT use `tsc -b` in a Vite project. Composite mode emits `.js` next to `.ts` and ESLint lints the emitted files. Use `tsc --noEmit`.
- Do NOT accept `eslint@^10` alongside `typescript-eslint@^8`. Chain breaks with `TypeError: Class extends value undefined` from `LegacyESLint`. Pin `eslint@^9` until `typescript-eslint@^9` with ESLint 10 support ships.
- Do NOT rely on bare-folder exclude globs like `exclude: ["e2e"]`. Use `"e2e/**/*"`.
- Do NOT ship Vitest with no-tests-fail. Fresh scaffolds have zero tests. Use `passWithNoTests: true` or equivalent until the project has at least one test.
- Do NOT put Playwright `trace` at the top level of `defineConfig`. It lives under `use`.
- Do NOT skip `skipLibCheck: true` in Vite + React tsconfig. `@testing-library/jest-dom` and Vite's `module-runner` produce type conflicts otherwise.
- Do NOT run `bunx shadcn@latest init` over a pre-existing `src/styles/global.css`. Shadcn silently overwrites it. Back up first or run shadcn init before writing custom global styles. Use flags `-t <template> -b <base> -p <primary> -y` to avoid interactive prompts.

## Tool pairing

- Unit tests: Vitest with jsdom, globals on, setup file at `src/test/setup.ts`, `@testing-library/react`, `@testing-library/user-event`.
- E2E: Playwright in `e2e/`. Chromium-only for Chrome extensions, all browsers for web apps.
- Tailwind: v4 via `@tailwindcss/vite`. Never the v3 integration.
- Prettier: `prettier-plugin-tailwindcss` last in plugins array. Astro also adds `prettier-plugin-astro` first.

## Prettier (extend)

- Base ships `.prettierrc` with `semi: false`, `singleQuote: true`. Web layer adds `jsxSingleQuote: true` and `prettier-plugin-tailwindcss` via the manifest.
- Per-stack overrides go in the stack's configs (Astro adds `prettier-plugin-astro` and the `.astro` parser override).

## lint-staged (extend)

- Add `**/*.{js,jsx,ts,tsx}` glob running `eslint --fix --max-warnings 0`, `prettier --write`, `cspell --no-must-find-files`.
- Extend the prettier glob to include `css`: `**/*.{json,css,md,mdc}`.
- Each file type runs cspell once via its own glob. No standalone cspell glob.

## CI docs (extend)

Extend `.claude/context/ci.md` so the `## Checks` table reflects the web jobs.

Append rows:

| Typecheck | `bun run typecheck` | framework-specific typecheck passes |
| Lint | `bun run lint` | ESLint passes with zero warnings |
| Tests | `bun run test:coverage` | Vitest passes with coverage thresholds |
| Build | `bun run build` | Production build succeeds |
| E2E | `bun run test:e2e` | Playwright passes against the built preview |

Under `## Running CI locally`, document that `bun run check:full` runs verify plus `test:e2e`.

## Development docs (extend)

Extend the `development` context entry under `.claude/context/` so the `## Scripts` table lists every web script. Stack adapters add their `dev`, `build`, `preview`, `typecheck` rows.

Append rows:

| `bun run lint` | Run ESLint with zero warnings allowed. |
| `bun run lint:fix` | Auto-fix ESLint issues. |
| `bun run test` | Run Vitest in watch mode. |
| `bun run test:run` | Run Vitest once with verbose reporter. |
| `bun run test:coverage` | Run Vitest with coverage. |
| `bun run test:e2e` | Run Playwright E2E tests. |
| `bun run test:e2e:changed` | Run Playwright E2E tests for specs the import graph reaches from the current diff. |
| `bun run screenshot` | Build, preview, then capture screenshots. |

`aitk tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.

`governance/rules/ui/440-surface-capture.md` is what asks a session to run the capture after a route changes. It fires on route and page files rather than on every component, so a shared component changing every screen fires nothing and the operator runs the capture by hand.

## Verify script

The web layer's `scripts/verify.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build. Stack adapters may override if their typecheck or build differs.
