chore(client): scrub style={[...]} on DOM-leaf primitives + ESLint guardrail (#239) #384

Merged
james merged 1 commit from 239-style-array-scrub into main 2026-06-29 18:25:46 +00:00
Owner

Closes #239

React DOM's setValueForStyles for-in-iterates the style prop and writes node.style[key]; a style array (or RNW passing a nested array through certain Link/Pressable merges) iterates numeric indices and throws CSSStyleProperties doesn't have an indexed property setter for '0' — the real crash from PR #209. Most array sites flatten fine, but the failures are subtle and context-dependent, so the arrays are land mines.

What changed

  • 613 style / contentContainerStyle array sites on DOM-leaf primitives (View/Text/Pressable/TextInput/Image/ScrollView) migrated to merged objects: [E1, E2]{ ...(E1), ...(E2) }. Order-preserving (later spreads win); spreading a falsy conditional is a no-op. Style-shape only — no visual change. Done with a one-shot TS-compiler-API codemod (run, then removed — not committed).
  • eslint.config.mjs: a no-restricted-syntax rule (error) flags style={[...]} on the leaf primitives, with a message explaining the crash, pointing at #239 / PR #209, and the merged-object pattern. Opt-out via eslint-disable for a genuine animated/Reanimated style array.
  • CONTRIBUTING.md: a Conventions note on the rule + pattern.
  • Left alone: KeyboardAwareScrollView / FlatList arrays (non-leaf, flatten reliably); there are no animated-style arrays in the repo.

Verification

  • typecheck / lint (0 errors) / 172 tests / export:web (Exported: dist) / semgrep 0 — all pass.
  • Spot-check note: a handful of sites spread a variable (e.g. { ...(textStyle), ... }). All current callers pass a single style object, so this is correct today; if a caller ever passed an array as that prop the merge would misbehave — worth an eye in review, but no such caller exists.
  • Not verifiable headlessly: visual spot-check of the heaviest screens (account, experience) recommended (client vitest is node-env).

Merge note

Overlaps #236 (PR #382) on apps/client/app/server-setup.tsx. Merge #382 first, then rebase this branch — the only conflict is the 8 style sites in server-setup.tsx.

🤖 Generated with Claude Code

Closes #239 React DOM's `setValueForStyles` for-in-iterates the `style` prop and writes `node.style[key]`; a style **array** (or RNW passing a nested array through certain Link/Pressable merges) iterates numeric indices and throws `CSSStyleProperties doesn't have an indexed property setter for '0'` — the real crash from PR #209. Most array sites flatten fine, but the failures are subtle and context-dependent, so the arrays are land mines. ## What changed - **613 `style` / `contentContainerStyle` array sites** on DOM-leaf primitives (`View`/`Text`/`Pressable`/`TextInput`/`Image`/`ScrollView`) migrated to merged objects: `[E1, E2]` → `{ ...(E1), ...(E2) }`. Order-preserving (later spreads win); spreading a falsy conditional is a no-op. **Style-shape only — no visual change.** Done with a one-shot TS-compiler-API codemod (run, then removed — not committed). - **`eslint.config.mjs`**: a `no-restricted-syntax` rule (error) flags `style={[...]}` on the leaf primitives, with a message explaining the crash, pointing at #239 / PR #209, and the merged-object pattern. Opt-out via `eslint-disable` for a genuine animated/Reanimated style array. - **`CONTRIBUTING.md`**: a Conventions note on the rule + pattern. - Left alone: `KeyboardAwareScrollView` / `FlatList` arrays (non-leaf, flatten reliably); there are no animated-style arrays in the repo. ## Verification - typecheck / lint (0 errors) / 172 tests / `export:web` (Exported: dist) / semgrep 0 — all pass. - **Spot-check note**: a handful of sites spread a *variable* (e.g. `{ ...(textStyle), ... }`). All current callers pass a single style object, so this is correct today; if a caller ever passed an array as that prop the merge would misbehave — worth an eye in review, but no such caller exists. - **Not verifiable headlessly**: visual spot-check of the heaviest screens (account, experience) recommended (client vitest is node-env). ## Merge note Overlaps **#236** (PR #382) on `apps/client/app/server-setup.tsx`. **Merge #382 first**, then rebase this branch — the only conflict is the 8 style sites in `server-setup.tsx`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chore(client): scrub style={[...]} on DOM-leaf primitives + ESLint guardrail (#239)
All checks were successful
PR / Client (web export smoke) (pull_request) Successful in 3m12s
Secrets / gitleaks (pull_request) Successful in 1m20s
PR / Package age policy (soft) (pull_request) Successful in 1m32s
PR / OSV-Scanner (pull_request) Successful in 2m18s
PR / Test (sqlite) (pull_request) Successful in 3m26s
PR / Test (postgres) (pull_request) Successful in 3m41s
PR / Lint (pull_request) Successful in 2m29s
PR / Coverage (soft) (pull_request) Successful in 3m30s
PR / Trivy (image) (pull_request) Successful in 3m45s
PR / E2E (Playwright) (pull_request) Successful in 4m47s
PR / OpenAPI (pull_request) Successful in 2m51s
PR / Static analysis (pull_request) Successful in 2m52s
PR / Typecheck (pull_request) Successful in 3m29s
PR / Build (pull_request) Successful in 3m34s
PR / pnpm audit (pull_request) Successful in 2m17s
Commits / Conventional Commits (pull_request) Successful in 13s
26ba0c323d
React DOM's setValueForStyles for-in-iterates the style prop and writes
node.style[key]; a style array (or RNW passing a nested array through
certain Link/Pressable merges) iterates numeric indices and throws
"CSSStyleProperties doesn't have an indexed property setter for '0'" — a
real crash seen in PR #209. Most array sites flatten fine but the failures
are subtle, so the arrays are land mines.

- Migrate 613 style/contentContainerStyle array sites on DOM-leaf
  primitives (View/Text/Pressable/TextInput/Image/ScrollView) to merged
  objects: [E1, E2] → { ...(E1), ...(E2) }. Order-preserving (later spreads
  win); spreading a falsy conditional is a no-op. Style-shape only — no
  visual change. KeyboardAwareScrollView/FlatList arrays (non-leaf, flatten
  reliably) left alone; no animated-style arrays exist in the repo.
- eslint.config.mjs: a no-restricted-syntax rule flags style={[...]} on the
  leaf primitives, with a message explaining the crash + pointing at #239
  and the merged-object pattern (eslint-disable for genuine animated arrays).
- CONTRIBUTING.md: a Conventions note on the rule + pattern.

Closes #239

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.6% ≥ 50%
Branches 71.6% ⚠️ ≥ 75%
Functions 81.2% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.6% ✅ | ≥ 50% | | Branches | 71.6% ⚠️ | ≥ 75% | | Functions | 81.2% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james force-pushed 239-style-array-scrub from 26ba0c323d
All checks were successful
PR / Client (web export smoke) (pull_request) Successful in 3m12s
Secrets / gitleaks (pull_request) Successful in 1m20s
PR / Package age policy (soft) (pull_request) Successful in 1m32s
PR / OSV-Scanner (pull_request) Successful in 2m18s
PR / Test (sqlite) (pull_request) Successful in 3m26s
PR / Test (postgres) (pull_request) Successful in 3m41s
PR / Lint (pull_request) Successful in 2m29s
PR / Coverage (soft) (pull_request) Successful in 3m30s
PR / Trivy (image) (pull_request) Successful in 3m45s
PR / E2E (Playwright) (pull_request) Successful in 4m47s
PR / OpenAPI (pull_request) Successful in 2m51s
PR / Static analysis (pull_request) Successful in 2m52s
PR / Typecheck (pull_request) Successful in 3m29s
PR / Build (pull_request) Successful in 3m34s
PR / pnpm audit (pull_request) Successful in 2m17s
Commits / Conventional Commits (pull_request) Successful in 13s
to 1c35e77bfd
Some checks failed
PR / Static analysis (pull_request) Successful in 3m14s
PR / E2E (Playwright) (pull_request) Successful in 5m45s
PR / Client (web export smoke) (pull_request) Successful in 3m21s
PR / pnpm audit (pull_request) Successful in 3m20s
PR / Build (pull_request) Successful in 3m40s
PR / Lint (pull_request) Successful in 3m56s
PR / Package age policy (soft) (pull_request) Successful in 35s
PR / OSV-Scanner (pull_request) Successful in 1m0s
Commits / Conventional Commits (pull_request) Successful in 7s
Secrets / gitleaks (pull_request) Successful in 35s
PR / Test (sqlite) (pull_request) Successful in 4m21s
PR / OpenAPI (pull_request) Successful in 2m59s
PR / Test (postgres) (pull_request) Failing after 4m29s
PR / Typecheck (pull_request) Successful in 3m11s
PR / Coverage (soft) (pull_request) Successful in 1m52s
PR / Trivy (image) (pull_request) Successful in 2m6s
2026-06-29 18:14:21 +00:00
Compare
james merged commit e72419d85e into main 2026-06-29 18:25:46 +00:00
james deleted branch 239-style-array-scrub 2026-06-29 18:25:47 +00:00
Sign in to join this conversation.
No description provided.